#include "../Commands/OneWire.h"

#if FEATURE_DALLAS_HELPER && FEATURE_COMMAND_OWSCAN

# include "../Commands/Common.h"

# include "../Helpers/Dallas1WireHelper.h"
# include "../Helpers/Hardware_GPIO.h"
# include "../Helpers/StringConverter.h"

String Command_OneWire_Owscan(struct EventStruct *event,
                              const char         *Line) {
  int pinnr; bool input; bool output1; bool output; bool warning;

  if (getGpioInfo(event->Par1, pinnr, input, output1, warning) && input) {      // Input pin required
    if (!parseString(Line, 3).isEmpty()) {
      if (getGpioInfo(event->Par2, pinnr, input, output, warning) && !output) { // Output required if specified
        return return_command_failed();                                         // Argument error
      }
    } else {
      event->Par2 = event->Par1;                                                // RX and TX on same pin

      if (!output1) {                                                           // Single pin must be input & output capable
        return return_command_failed();                                         // Argument error
      }
    }
    pinMode(event->Par1, INPUT);

    if (event->Par2 != event->Par1) {
      pinMode(event->Par2, OUTPUT);
    }

    uint8_t addr[8]{};

    Dallas_reset(event->Par1, event->Par2);
    String res;
    res.reserve(80);

    while (Dallas_search(addr, event->Par1, event->Par2)) { // Scan the 1-wire
      res += Dallas_format_address(addr);
      res += '\n';
    }
    return res;
  }

  return return_command_failed();
}

#endif // if FEATURE_DALLAS_HELPER && FEATURE_COMMAND_OWSCAN

#include "../Commands/ExecuteCommand.h"

#include "../../_Plugin_Helper.h"

#include "../Commands/Common.h"
#include "../Commands/InternalCommands.h"

#include "../DataStructs/ESPEasy_EventStruct.h"

#include "../ESPEasyCore/Controller.h"

#include "../Helpers/StringConverter.h"
#include "../Helpers/StringParser.h"

ExecuteCommandArgs::ExecuteCommandArgs(EventValueSource::Enum source,
                                       const char            *Line) :
  _taskIndex(INVALID_TASK_INDEX),
  _source(source),
  _Line(Line),
  _tryPlugin(false),
  _tryInternal(false),
  _tryRemoteConfig(false) {}

ExecuteCommandArgs::ExecuteCommandArgs(EventValueSource::Enum source,
                                       const String         & Line) :
  _taskIndex(INVALID_TASK_INDEX),
  _source(source),
  _Line(Line),
  _tryPlugin(false),
  _tryInternal(false),
  _tryRemoteConfig(false) {}

ExecuteCommandArgs::ExecuteCommandArgs(EventValueSource::Enum source,
                                       String              && Line) :
  _taskIndex(INVALID_TASK_INDEX),
  _source(source),
  _Line(Line),
  _tryPlugin(false),
  _tryInternal(false),
  _tryRemoteConfig(false) {}


ExecuteCommandArgs::ExecuteCommandArgs(
  taskIndex_t            taskIndex,
  EventValueSource::Enum source,
  const char            *Line,
  bool                   tryPlugin,
  bool                   tryInternal,
  bool                   tryRemoteConfig) :
  _taskIndex(taskIndex),
  _source(source),
  _Line(Line),
  _tryPlugin(tryPlugin),
  _tryInternal(tryInternal),
  _tryRemoteConfig(tryRemoteConfig) {}

ExecuteCommandArgs::ExecuteCommandArgs(
  taskIndex_t            taskIndex,
  EventValueSource::Enum source,
  const String         & Line,
  bool                   tryPlugin,
  bool                   tryInternal,
  bool                   tryRemoteConfig) :
  _taskIndex(taskIndex),
  _source(source),
  _Line(Line),
  _tryPlugin(tryPlugin),
  _tryInternal(tryInternal),
  _tryRemoteConfig(tryRemoteConfig) {}

ExecuteCommandArgs::ExecuteCommandArgs(
  taskIndex_t            taskIndex,
  EventValueSource::Enum source,
  String              && Line,
  bool                   tryPlugin,
  bool                   tryInternal,
  bool                   tryRemoteConfig) :
  _taskIndex(taskIndex),
  _source(source),
  _Line(std::move(Line)),
  _tryPlugin(tryPlugin),
  _tryInternal(tryInternal),
  _tryRemoteConfig(tryRemoteConfig) {}


std::list<ExecuteCommandArgs> ExecuteCommand_queue;

bool processExecuteCommandQueue()
{
  bool res = false;

  if (!ExecuteCommand_queue.empty()) {
    auto it = ExecuteCommand_queue.front();

    res = ExecuteCommand(std::move(it), false);
    ExecuteCommand_queue.pop_front();
  }
  return res;
}

// Execute command which may be plugin or internal commands
bool ExecuteCommand_all(ExecuteCommandArgs&& args,
                        bool                      addToQueue)
{
  args._tryPlugin = true;
  args._tryInternal = true;
  args._tryRemoteConfig = false;
  return ExecuteCommand(std::move(args), addToQueue);
}

bool ExecuteCommand_all_config(ExecuteCommandArgs&& args,
                               bool                      addToQueue)
{
  args._tryPlugin = true;
  args._tryInternal = true;
  args._tryRemoteConfig = true;
  return ExecuteCommand(std::move(args), addToQueue);
}

bool ExecuteCommand_plugin_config(ExecuteCommandArgs&& args,
                                  bool                      addToQueue)
{
  args._tryPlugin = true;
  args._tryInternal = false;
  args._tryRemoteConfig = true;
  return ExecuteCommand(std::move(args), addToQueue);
}

bool ExecuteCommand_internal(ExecuteCommandArgs&& args,
                             bool                      addToQueue)
{
  args._tryPlugin = false;
  args._tryInternal = true;
  args._tryRemoteConfig = false;
  return ExecuteCommand(std::move(args), addToQueue);
}

bool ExecuteCommand(taskIndex_t            taskIndex,
                    EventValueSource::Enum source,
                    const char            *Line,
                    bool                   tryPlugin,
                    bool                   tryInternal,
                    bool                   tryRemoteConfig,
                    bool                   addToQueue)
{
  ExecuteCommandArgs args(taskIndex,
                          source,
                          Line,
                          tryPlugin,
                          tryInternal,
                          tryRemoteConfig);
  return ExecuteCommand(std::move(args), addToQueue);
}

bool ExecuteCommand(ExecuteCommandArgs&& args, bool addToQueue)
{
  if (addToQueue) {
    ExecuteCommand_queue.emplace_back(std::move(args));
    return false; // What should be returned here?
  }
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("ExecuteCommand"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  String cmd;

  // We first try internal commands, which should not have a taskIndex set.
  struct EventStruct TempEvent;

  if (!GetArgv(args._Line.c_str(), cmd, 1)) {
    SendStatus(&TempEvent, return_command_failed());
    return false;
  }

  if (args._tryInternal) {
    // Small optimization for events, which happen frequently
    // FIXME TD-er: Make quick check to see if a command is an internal command, so we don't need to try all
    if (cmd.equalsIgnoreCase(F("event"))) {
      args._tryPlugin       = false;
      args._tryRemoteConfig = false;
    }
  }

  TempEvent.Source = args._source;

  #ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLogMove(LOG_LEVEL_DEBUG, concat(F("Command: "), cmd));
    addLog(LOG_LEVEL_DEBUG, args._Line); // for debug purposes add the whole line.
  }
  #endif

  args._Line = parseTemplate(args._Line); // parseTemplate before executing the command

  // Split the arguments into Par1...5 of the event.
  // Do not split it in executeInternalCommand, since that one will be called from the scheduler with pre-set events.
  // FIXME TD-er: Why call this for all commands? The CalculateParam function is quite heavy.
  parseCommandString(&TempEvent, args._Line);

  // FIXME TD-er: This part seems a bit strange.
  // It can't schedule a call to PLUGIN_WRITE.
  // Maybe ExecuteCommand can be scheduled?
  delay(0);

#ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLogMove(LOG_LEVEL_DEBUG, strformat(
                 F("Par1: %d Par2: %d Par3: %d Par4: %d Par5: %d"),
                 TempEvent.Par1,
                 TempEvent.Par2,
                 TempEvent.Par3,
                 TempEvent.Par4,
                 TempEvent.Par5));
  }
#endif // ifndef BUILD_NO_DEBUG


  if (args._tryInternal) {
    InternalCommands internalCommands(cmd.c_str(), &TempEvent, args._Line.c_str());
    bool handled = internalCommands.executeInternalCommand();

    const command_case_data& data = internalCommands.getData();

    if (data.status.length() > 0) {
      delay(0);
      SendStatus(&TempEvent, data.status);
      delay(0);
    }

    if (handled) {
      //      addLog(LOG_LEVEL_INFO, F("executeInternalCommand accepted"));
      return true;
    }
  }

  // When trying a task command, set the task index, even if it is not a valid task index.
  // For example commands from elsewhere may not have a proper task index.
  TempEvent.setTaskIndex(args._taskIndex);
  checkDeviceVTypeForTask(&TempEvent);

  if (args._tryPlugin) {
    // Use a tmp string to call PLUGIN_WRITE, since PluginCall may inadvertenly
    // alter the string.
    String tmpAction(args._Line);
    bool   handled = PluginCall(PLUGIN_WRITE, &TempEvent, tmpAction);

    //    if (handled) addLog(LOG_LEVEL_INFO, F("PLUGIN_WRITE accepted"));

    #ifndef BUILD_NO_DEBUG

    if (!tmpAction.equals(args._Line)) {
      if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
        String log = F("PLUGIN_WRITE altered the string: ");
        log += args._Line;
        log += F(" to: ");
        log += tmpAction;
        addLogMove(LOG_LEVEL_ERROR, log);
      }
    }
    #endif // ifndef BUILD_NO_DEBUG

    if (!handled) {
      // Try a controller
      handled = CPluginCall(CPlugin::Function::CPLUGIN_WRITE, &TempEvent, tmpAction);

      //      if (handled) addLog(LOG_LEVEL_INFO, F("CPLUGIN_WRITE accepted"));
    }

    if (handled) {
      SendStatus(&TempEvent, return_command_success());
      return true;
    }
  }

  if (args._tryRemoteConfig) {
    if (remoteConfig(&TempEvent, args._Line)) {
      SendStatus(&TempEvent, return_command_success());

      //      addLog(LOG_LEVEL_INFO, F("remoteConfig accepted"));

      return true;
    }
  }
  String errorUnknown = concat(F("Command unknown: "), args._Line);
  SendStatus(&TempEvent, errorUnknown);
  addLogMove(LOG_LEVEL_INFO, errorUnknown);
  delay(0);
  return false;
}

#include "../Commands/UPD.h"


#include "../../ESPEasy_common.h"

#include "../Commands/Common.h"
#include "../ESPEasyCore/ESPEasyNetwork.h"
#include "../Globals/NetworkState.h"
#include "../Globals/Settings.h"
#include "../Helpers/Misc.h"
#include "../Helpers/Network.h"
#include "../Helpers/Networking.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringParser.h"

String Command_UDP_Port(struct EventStruct *event, const char *Line)
{
  return Command_GetORSetBool(event, F("UDPPort:"),
                              Line,
                              (bool *)&Settings.UDPPort,
                              1);
}

#if FEATURE_ESPEASY_P2P

const __FlashStringHelper* Command_UDP_Test(struct EventStruct *event, const char *Line)
{
  for (uint8_t x = 0; x < event->Par2; x++)
  {
    String eventName = "Test ";
    eventName += x;
    SendUDPCommand(event->Par1, eventName.c_str(), eventName.length());
  }
  return return_command_success_flashstr();
}

const __FlashStringHelper* Command_UPD_SendTo(struct EventStruct *event, const char *Line)
{
  int destUnit = parseCommandArgumentInt(Line, 1);

  if ((destUnit > 0) && (destUnit < 255))
  {
    String eventName = tolerantParseStringKeepCase(Line, 3);
    stripEscapeCharacters(eventName);
    SendUDPCommand(destUnit, eventName.c_str(), eventName.length());
  }
  return return_command_success_flashstr();
}

#endif // FEATURE_ESPEASY_P2P

const __FlashStringHelper* Command_UDP_SendToUPD(struct EventStruct *event, const char *Line)
{
  return Command_UDP_SendToUPD(event, Line, false);
}

const __FlashStringHelper* Command_UDP_SendToUPDMix(struct EventStruct *event, const char *Line)
{
  return Command_UDP_SendToUPD(event, Line, true);
}

const __FlashStringHelper* Command_UDP_SendToUPD(struct EventStruct *event, const char *Line, const bool handleMix)
{
  if (NetworkConnected()) {
    const String ip   = parseString(Line, 2);
    const int    port = parseCommandArgumentInt(Line, 2, -1);

    if ((port < 0) || (port > 65535)) {
      return return_command_failed_flashstr();
    }

    // FIXME TD-er: This command is not using the tolerance setting
    // tolerantParseStringKeepCase(Line, 4);
    String message;
    std::vector<uint8_t> argument;

    if (handleMix) {
      argument = parseHexTextData(Line, 4);
    } else {
      message = parseStringToEndKeepCase(Line, 4);
    }
    IPAddress UDP_IP;

    if (UDP_IP.fromString(ip)) {
      FeedSW_watchdog();
      portUDP.beginPacket(UDP_IP, port);

      if (handleMix) {
        portUDP.write(&argument[0], argument.size());
      } else {
        #if defined(ESP8266)
        portUDP.write(message.c_str(),                                    message.length());
        #endif // if defined(ESP8266)
        #if defined(ESP32)
        portUDP.write(reinterpret_cast<const uint8_t *>(message.c_str()), message.length());
        #endif // if defined(ESP32)
      }
      portUDP.endPacket();
      FeedSW_watchdog();
      delay(0);
    }
    return return_command_success_flashstr();
  }
  return return_not_connected();
}

#include "../Commands/System.h"

#include "../../ESPEasy_common.h"


#include "../Commands/Common.h"

#include "../Globals/Settings.h"

#include "../Helpers/DeepSleep.h"
#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/Misc.h"
#include "../Helpers/Scheduler.h"

const __FlashStringHelper * Command_System_NoSleep(struct EventStruct *event, const char* Line)
{
	if (event->Par1 > 0)
		Settings.deepSleep_wakeTime = event->Par1; // set deep Sleep awake time
	else Settings.deepSleep_wakeTime = 0;
	return return_command_success_flashstr();
}

const __FlashStringHelper * Command_System_deepSleep(struct EventStruct *event, const char* Line)
{
	if (event->Par1 >= 0) {
		deepSleepStart(event->Par1); // call the second part of the function to avoid check and enable one-shot operation
	}
	return return_command_success_flashstr();
}

const __FlashStringHelper * Command_System_Reboot(struct EventStruct *event, const char* Line)
{
	pinMode(0, INPUT);
	pinMode(2, INPUT);
	pinMode(15, INPUT);
	reboot(IntendedRebootReason_e::CommandReboot);
	return return_command_success_flashstr();
}

#ifdef ESP8266
// Erase the RF calibration partition (4k)
const __FlashStringHelper * Command_System_Erase_RFcal(struct EventStruct *event, const char* Line)
{
	if (clearRFcalPartition()) {
		return F("Cleared RFcal partition. Please reboot!");
	}
	return return_command_failed_flashstr();
}

// Erase the SDK WiFI partition (12k)
const __FlashStringHelper * Command_System_Erase_SDK_WiFiconfig(struct EventStruct *event, const char* Line)
{
	if (clearWiFiSDKpartition()) {
		return F("Cleared SDK WiFi partition. Please reboot!");
	}
	return return_command_failed_flashstr();
}

#endif

#include "../../ESPEasy_common.h"
#include "../Globals/MQTT.h"

#if FEATURE_MQTT



#include "../Commands/Common.h"
#include "../Commands/MQTT.h"

#include "../ESPEasyCore/Controller.h"
#include "../ESPEasyCore/ESPEasy_Log.h"

#include "../Globals/CPlugins.h"
#include "../Globals/ESPEasy_Scheduler.h"
#include "../Globals/Settings.h"

#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/PeriodicalActions.h"
#include "../Helpers/Scheduler.h"
#include "../Helpers/StringConverter.h"


const __FlashStringHelper* Command_MQTT_Publish(struct EventStruct *event, const char *Line) {
  return Command_MQTT_Publish_handler(event, Line, false);
}

const __FlashStringHelper* Command_MQTT_PublishR(struct EventStruct *event, const char *Line) {
  return Command_MQTT_Publish_handler(event, Line, true);
}

const __FlashStringHelper* Command_MQTT_Publish_handler(struct EventStruct *event, const char *Line, const bool forceRetain)
{
  // ToDo TD-er: Not sure about this function, but at least it sends to an existing MQTTclient
  controllerIndex_t enabledMqttController = firstEnabledMQTT_ControllerIndex();

  if (!validControllerIndex(enabledMqttController)) {
    return F("No MQTT controller enabled");
  }

  // Command structure:  Publish,<topic>,<value>
  const String topic = parseStringKeepCase(Line, 2);
  const String value = tolerantParseStringKeepCase(Line, 3);
  # ifndef BUILD_NO_DEBUG
  addLog(LOG_LEVEL_DEBUG, strformat(F("Publish%c: %s:%s"), forceRetain ? 'R' : ' ', topic.c_str(), value.c_str()));
  # endif

  if (!topic.isEmpty()) {

    bool mqtt_retainFlag = forceRetain;
    if (!forceRetain) {
      // Place the ControllerSettings in a scope to free the memory as soon as we got all relevant information.
      MakeControllerSettings(ControllerSettings); //-V522
      if (!AllocatedControllerSettings()) {
        addLog(LOG_LEVEL_ERROR, F("MQTT : Cannot publish, out of RAM"));
        return F("MQTT : Cannot publish, out of RAM");
      }

      LoadControllerSettings(enabledMqttController, *ControllerSettings);
      mqtt_retainFlag = ControllerSettings->mqtt_retainFlag();
    }


    // @giig1967g: if payload starts with '=' then treat it as a Formula and evaluate accordingly
    // The evaluated value is already present in event->Par2
    // FIXME TD-er: Is the evaluated value always present in event->Par2 ?
    // Should it already be evaluated, or should we evaluate it now?

    bool success = false;
    if (value[0] != '=') {
      success = MQTTpublish(enabledMqttController, INVALID_TASK_INDEX, topic.c_str(), value.c_str(), mqtt_retainFlag);
    }
    else {
      success = MQTTpublish(enabledMqttController, INVALID_TASK_INDEX,  topic.c_str(), String(event->Par2).c_str(), mqtt_retainFlag);
    }
    if (success) {
      return return_command_success_flashstr();
    }
  }
  return return_command_failed_flashstr();
}


boolean MQTTsubscribe(controllerIndex_t controller_idx, const char* topic, boolean retained)
{
  if (MQTTclient.subscribe(topic)) {
    Scheduler.setIntervalTimerOverride(SchedulerIntervalTimer_e::TIMER_MQTT, 10); // Make sure the MQTT is being processed as soon as possible.
    scheduleNextMQTTdelayQueue();
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      String log = F("Subscribed to: ");  
      log += topic;
      addLogMove(LOG_LEVEL_INFO, log);
    }
    return true;
  }
  addLog(LOG_LEVEL_ERROR, F("MQTT : subscribe failed"));
  return false;
}

const __FlashStringHelper * Command_MQTT_Subscribe(struct EventStruct *event, const char* Line)
{
  if (MQTTclient.connected() ) {
    // ToDo TD-er: Not sure about this function, but at least it sends to an existing MQTTclient
    controllerIndex_t enabledMqttController = firstEnabledMQTT_ControllerIndex();
    if (validControllerIndex(enabledMqttController)) {
      bool mqtt_retainFlag;
      {
        // Place the ControllerSettings in a scope to free the memory as soon as we got all relevant information.
        MakeControllerSettings(ControllerSettings); //-V522
        if (!AllocatedControllerSettings()) {
          addLog(LOG_LEVEL_ERROR, F("MQTT : Cannot subscribe, out of RAM"));
          return F("MQTT : Cannot subscribe, out of RAM");
        }
        LoadControllerSettings(event->ControllerIndex, *ControllerSettings);
        mqtt_retainFlag = ControllerSettings->mqtt_retainFlag();
      }

      String eventName = Line;
      String topic = eventName.substring(10);
      return return_command_boolean_result_flashstr(
        MQTTsubscribe(enabledMqttController, topic.c_str(), mqtt_retainFlag));
    }
    return F("No MQTT controller enabled");
  }
  return return_not_connected();
}


#endif // if FEATURE_MQTT

#include "../Commands/Timer.h"




#include "../../ESPEasy_common.h"


#include "../Commands/Common.h"

#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../ESPEasyCore/ESPEasyRules.h"

#include "../Globals/ESPEasy_Scheduler.h"

#include "../Helpers/ESPEasy_time_calc.h"
#include "../Helpers/Misc.h"
#include "../Helpers/Scheduler.h"

const __FlashStringHelper * command_setRulesTimer(int msecFromNow, int timerIndex, int recurringCount, bool startImmediately = false) {
  if (msecFromNow < 0)
  {
    addLog(LOG_LEVEL_ERROR, F("TIMER: time must be positive"));
  } else {
    // start new timer when msecFromNow > 0
    // Clear timer when msecFromNow == 0
    if (Scheduler.setRulesTimer(msecFromNow, timerIndex, recurringCount, startImmediately))
    { 
      return return_command_success_flashstr();
    }
  }
  return return_command_failed_flashstr();
}

const __FlashStringHelper * Command_Timer_Set(struct EventStruct *event, const char *Line)
{
  return command_setRulesTimer(
    event->Par2 * 1000, // msec from now
    event->Par1,        // timer index
    0                   // recurringCount
    );
}

const __FlashStringHelper * Command_Timer_Set_ms (struct EventStruct *event, const char* Line)
{
  return command_setRulesTimer(
    event->Par2, // interval
    event->Par1, // timer index
    0            // recurringCount
    );
}

const __FlashStringHelper * Command_Loop_Timer_Set (struct EventStruct *event, const char* Line)
{
  int recurringCount = event->Par3;
  if (recurringCount == 0) {
    // if the optional 3rd parameter is not given, set it to "run always"
    recurringCount = -1;
  }
  return command_setRulesTimer(
    event->Par2 * 1000, // msec from now
    event->Par1,        // timer index
    recurringCount
    );
}

const __FlashStringHelper * Command_Loop_Timer_Set_ms (struct EventStruct *event, const char* Line)
{
  int recurringCount = event->Par3;
  if (recurringCount == 0) {
    // if the optional 3rd parameter is not given, set it to "run always"
    recurringCount = -1;
  }
  return command_setRulesTimer(
    event->Par2, // interval
    event->Par1, // timer index
    recurringCount
    );
}

const __FlashStringHelper * Command_Loop_Timer_SetAndRun (struct EventStruct *event, const char* Line)
{
  int recurringCount = event->Par3;
  if (recurringCount == 0) {
    // if the optional 3rd parameter is not given, set it to "run always"
    recurringCount = -1;
  }
  return command_setRulesTimer(
    event->Par2 * 1000, // msec from now
    event->Par1,        // timer index
    recurringCount,
    true);
}

const __FlashStringHelper * Command_Loop_Timer_SetAndRun_ms (struct EventStruct *event, const char* Line)
{
  int recurringCount = event->Par3;
  if (recurringCount == 0) {
    // if the optional 3rd parameter is not given, set it to "run always"
    recurringCount = -1;
  }
  return command_setRulesTimer(
    event->Par2, // interval
    event->Par1, // timer index
    recurringCount,
    true);
}

const __FlashStringHelper * Command_Timer_Pause(struct EventStruct *event, const char *Line)
{
  if (Scheduler.pause_rules_timer(event->Par1)) {
    String eventName = F("Rules#TimerPause=");
    eventName += event->Par1;
    rulesProcessing(eventName); // TD-er: Process right now
    return return_command_success_flashstr();
  }
  return return_command_failed_flashstr();
}

const __FlashStringHelper * Command_Timer_Resume(struct EventStruct *event, const char *Line)
{
  if (Scheduler.resume_rules_timer(event->Par1)) {
    String eventName = F("Rules#TimerResume=");
    eventName += event->Par1;
    rulesProcessing(eventName); // TD-er: Process right now
    return return_command_success_flashstr();
  }
  return return_command_failed_flashstr();
}

const __FlashStringHelper * Command_Delay(struct EventStruct *event, const char *Line)
{
  delayBackground(event->Par1);
  return return_command_success_flashstr();
}

#include "../Commands/Networks.h"

#include "../../ESPEasy_common.h"
#include "../Commands/Common.h"
#include "../ESPEasyCore/ESPEasyNetwork.h"
#include "../ESPEasyCore/ESPEasyEth.h"
#include "../Globals/NetworkState.h"
#include "../Globals/Settings.h"
#include "../Helpers/StringConverter.h"
#include "../WebServer/AccessControl.h"


#if FEATURE_ETHERNET
#include <ETH.h>
#endif

String Command_AccessInfo_Ls(struct EventStruct *event, const char* Line)
{
  return return_result(event, concat(F("Allowed IP range : "), describeAllowedIPrange()));
}

String Command_AccessInfo_Clear (struct EventStruct *event, const char* Line)
{
  clearAccessBlock();
  return Command_AccessInfo_Ls(event, Line);
}

String Command_DNS (struct EventStruct *event, const char* Line)
{
  return Command_GetORSetIP(event, F("DNS:"), Line, Settings.DNS, NetworkDnsIP(0), 1);
}

String Command_Gateway (struct EventStruct *event, const char* Line)
{
  return Command_GetORSetIP(event, F("Gateway:"), Line, Settings.Gateway, NetworkGatewayIP(),1);
}

String Command_IP (struct EventStruct *event, const char* Line)
{
  return Command_GetORSetIP(event, F("IP:"), Line, Settings.IP, NetworkLocalIP(),1);
}

#if FEATURE_USE_IPV6
String Command_show_all_IP6 (struct EventStruct *event, const char* Line)
{
  // Only get all IPv6 addresses
  IP6Addresses_t addresses = NetworkAllIPv6();
  String res;
  res += '[';
  bool first = true;
  for (auto it = addresses.begin(); it != addresses.end(); ++it)
  {
    if (first) {
      first = false;
    } else {
      res += ',';
    }
    res += wrap_String(it->toString(true), '"');
  }
  res += ']';
  return res;
}
#endif


String Command_Subnet (struct EventStruct *event, const char* Line)
{
  return Command_GetORSetIP(event, F("Subnet:"), Line, Settings.Subnet, NetworkSubnetMask(), 1);
}

#if FEATURE_ETHERNET
String Command_ETH_Phy_Addr (struct EventStruct *event, const char* Line)
{
  return Command_GetORSetInt8_t(event, F("ETH_Phy_Addr:"), Line, reinterpret_cast<int8_t*>(&Settings.ETH_Phy_Addr),1);
}

String Command_ETH_Pin_mdc (struct EventStruct *event, const char* Line)
{
  return Command_GetORSetInt8_t(event, F("ETH_Pin_mdc_cs:"), Line, reinterpret_cast<int8_t*>(&Settings.ETH_Pin_mdc_cs),1);
}

String Command_ETH_Pin_mdio (struct EventStruct *event, const char* Line)
{
  return Command_GetORSetInt8_t(event, F("ETH_Pin_mdio_irq:"), Line, reinterpret_cast<int8_t*>(&Settings.ETH_Pin_mdio_irq),1);
}

String Command_ETH_Pin_power (struct EventStruct *event, const char* Line)
{
  return Command_GetORSetInt8_t(event, F("ETH_Pin_power_rst:"), Line, reinterpret_cast<int8_t*>(&Settings.ETH_Pin_power_rst),1);
}

String Command_ETH_Phy_Type (struct EventStruct *event, const char* Line)
{
  return Command_GetORSetInt8_t(event, F("ETH_Phy_Type:"), Line, reinterpret_cast<int8_t*>(&Settings.ETH_Phy_Type),1);
}

String Command_ETH_Clock_Mode (struct EventStruct *event, const char* Line)
{
  return Command_GetORSetETH(event, 
                             F("ETH_Clock_Mode:"), 
                             toString(Settings.ETH_Clock_Mode),
                             Line, 
                             reinterpret_cast<uint8_t*>(&Settings.ETH_Clock_Mode),
                             1);
}

String Command_ETH_IP (struct EventStruct *event, const char* Line)
{
  return Command_GetORSetIP(event, F("ETH_IP:"), Line, Settings.ETH_IP,ETH.localIP(),1);
}

String Command_ETH_Gateway (struct EventStruct *event, const char* Line)
{
  return Command_GetORSetIP(event, F("ETH_Gateway:"), Line, Settings.ETH_Gateway,ETH.gatewayIP(),1);
}

String Command_ETH_Subnet (struct EventStruct *event, const char* Line)
{
  return Command_GetORSetIP(event, F("ETH_Subnet:"), Line, Settings.ETH_Subnet,ETH.subnetMask(),1);
}

String Command_ETH_DNS (struct EventStruct *event, const char* Line)
{
  return Command_GetORSetIP(event, F("ETH_DNS:"), Line, Settings.ETH_DNS,ETH.dnsIP(),1);
}

String Command_ETH_Wifi_Mode (struct EventStruct *event, const char* Line)
{
  const NetworkMedium_t orig_medium = Settings.NetworkMedium;
  const String result = Command_GetORSetETH(event, 
                             F("NetworkMedium:"), 
                             toString(active_network_medium),
                             Line, 
                             reinterpret_cast<uint8_t*>(&Settings.NetworkMedium), 
                             1);
  if (orig_medium != Settings.NetworkMedium) {
    if (!isValid(Settings.NetworkMedium)) {
      Settings.NetworkMedium = orig_medium;
      return return_command_failed();
    }
    setNetworkMedium(Settings.NetworkMedium);
  }
  
  return result;
}

String Command_ETH_Disconnect (struct EventStruct *event, const char* Line)
{

  ethPower(0);
  delay(400);
//  ethPower(1);
  setNetworkMedium(NetworkMedium_t::Ethernet);
  ETHConnectRelaxed();

  return return_command_success();
}

#endif // if FEATURE_ETHERNET

#include "../Commands/GPIO.h"

#include "../../ESPEasy_common.h"


#include "../../ESPEasy-Globals.h"

#include "../Commands/Common.h"
#include "../DataStructs/PinMode.h"
#include "../ESPEasyCore/Controller.h"
#include "../ESPEasyCore/ESPEasyGPIO.h"
#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../Globals/ESPEasy_Scheduler.h"
#include "../Globals/GlobalMapPortStatus.h"
#include "../Helpers/Audio.h"
#include "../Helpers/Hardware_PWM.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/PortStatus.h"
#include "../Helpers/Numerical.h"

#if FEATURE_I2C_MULTIPLE
#include "../Globals/Settings.h"
#include "../Helpers/Hardware_device_info.h"
#include "../Helpers/I2C_access.h"
#endif // if FEATURE_I2C_MULTIPLE

#if FEATURE_GPIO_USE_ESP8266_WAVEFORM
# include <core_esp8266_waveform.h>
#endif 

// Forward declarations of functions used in this module
// Normally those would be declared in the .h file as private members
// But since these are not part of a class, forward declare them in the .cpp
//void createAndSetPortStatus_Mode_State(uint32_t key, uint8_t newMode, int8_t newState);
const __FlashStringHelper * getPluginIDAndPrefix(char selection, pluginID_t& pluginID, bool& success);
void logErrorGpioOffline(const __FlashStringHelper * prefix, int port);
void logErrorGpioOutOfRange(const __FlashStringHelper * prefix, int port, const char* Line = nullptr);
void logErrorGpioNotOutput(const __FlashStringHelper * prefix, int port);
void logErrorModeOutOfRange(const __FlashStringHelper * prefix, int port);
bool gpio_monitor_helper(int port, struct EventStruct *event, const char* Line);
bool gpio_unmonitor_helper(int port, struct EventStruct *event, const char* Line);
#ifdef USES_P009
bool mcpgpio_range_pattern_helper(struct EventStruct *event, const char* Line, bool isWritePattern);
#endif
#ifdef USES_P019
bool pcfgpio_range_pattern_helper(struct EventStruct *event, const char* Line, bool isWritePattern);
#endif
bool gpio_mode_range_helper(uint8_t pin, uint8_t pinMode, struct EventStruct *event, const char* Line);
#ifdef USES_P019
uint8_t getPcfAddress(uint8_t pin);
#endif
bool setGPIOMode(uint8_t pin, uint8_t mode);
#ifdef USES_P019
bool setPCFMode(uint8_t pin, uint8_t mode);
#endif
#ifdef USES_P009
bool setMCPMode(uint8_t pin, uint8_t mode);
bool mcpgpio_plugin_range_helper(uint8_t pin1, uint8_t pin2, uint16_t &result);
#endif
#ifdef USES_P019
bool pcfgpio_plugin_range_helper(uint8_t pin1, uint8_t pin2, uint16_t &result);
#endif


/*************************************************************************/

const __FlashStringHelper * Command_GPIO_Monitor(struct EventStruct *event, const char *Line)
{
  return return_command_boolean_result_flashstr(gpio_monitor_helper(event->Par2, event, Line));
}

const __FlashStringHelper * Command_GPIO_MonitorRange(struct EventStruct *event, const char *Line)
{
  bool success = true;

  for (uint8_t i = event->Par2; i <= event->Par3; i++) {
    success &= gpio_monitor_helper(i, event, Line);
  }
  return return_command_boolean_result_flashstr(success);
}

bool gpio_monitor_helper(int port, struct EventStruct *event, const char *Line)
{
  pluginID_t pluginID = INVALID_PLUGIN_ID;
  bool success = false;

  // parseString(Line, 2).charAt(0)='g':gpio; ='p':pcf; ='m':mcp
  const __FlashStringHelper * logPrefix = getPluginIDAndPrefix(parseString(Line, 2).charAt(0), pluginID, success);

  if (success && checkValidPortRange(pluginID, port))
  {
    const uint32_t key = createKey(pluginID, port); // WARNING: 'monitor' uses Par2 instead of Par1
    // if (!existPortStatus(key)) globalMapPortStatus[key].mode=PIN_MODE_OUTPUT;
    addMonitorToPort(key);

    int8_t state;

    // giig1967g: Comment next 3 lines to receive an EVENT just after calling the monitor command
    GPIO_Read(pluginID, port, state);
    globalMapPortStatus[key].state = state;

    if (state == -1) { globalMapPortStatus[key].mode = PIN_MODE_OFFLINE; }
    #ifndef BUILD_MINIMAL_OTA
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO, concat(
        logPrefix,
        strformat(F(" port #%d: added to monitor list."), port))); 
    }
    #endif
    String dummy;
    SendStatusOnlyIfNeeded(event, SEARCH_PIN_STATE, key, dummy, 0);

    return true;
  } else {
    logErrorGpioOutOfRange(logPrefix, port, Line);
    return false;
  }
}

const __FlashStringHelper * Command_GPIO_UnMonitor(struct EventStruct *event, const char *Line)
{
  return return_command_boolean_result_flashstr(gpio_unmonitor_helper(event->Par2, event, Line));
}

const __FlashStringHelper * Command_GPIO_UnMonitorRange(struct EventStruct *event, const char *Line)
{
  bool success = true;

  for (uint8_t i = event->Par2; i <= event->Par3; i++) {
    success &= gpio_unmonitor_helper(i, event, Line);
  }
  return return_command_boolean_result_flashstr(success);
}

bool gpio_unmonitor_helper(int port, struct EventStruct *event, const char *Line)
{

  pluginID_t pluginID = INVALID_PLUGIN_ID;
  bool success = false;

  // parseString(Line, 2).charAt(0)='g':gpio; ='p':pcf; ='m':mcp
  const __FlashStringHelper * logPrefix = getPluginIDAndPrefix(parseString(Line, 2).charAt(0), pluginID, success);

  if (success && checkValidPortRange(pluginID, port))
  {
    const uint32_t key = createKey(pluginID, port); // WARNING: 'monitor' uses Par2 instead of Par1
    String dummy;
    SendStatusOnlyIfNeeded(event, SEARCH_PIN_STATE, key, dummy, 0);

    removeMonitorFromPort(key);
    #ifndef BUILD_MINIMAL_OTA
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO, concat(
        logPrefix,
        strformat(F(" port #%d: removed from monitor list."), port)));
    }
    #endif

    return true;
  } else {
    logErrorGpioOutOfRange(logPrefix, port, Line);
    return false;
  }
}

const __FlashStringHelper * Command_GPIO_LongPulse(struct EventStruct *event, const char *Line)
{
  event->Par3 *= 1000;
  event->Par4 *= 1000;
  event->Par5 *= 1000;
  return Command_GPIO_LongPulse_Ms(event, Line);
}

const __FlashStringHelper * Command_GPIO_LongPulse_Ms(struct EventStruct *event, const char *Line)
{
  pluginID_t pluginID = INVALID_PLUGIN_ID;
  bool success = false;

  // Line[0]='l':longpulse; ='p':pcflongpulse; ='m':mcplongpulse
  const __FlashStringHelper * logPrefix = getPluginIDAndPrefix(Line[0], pluginID, success);

  if (success && checkValidPortRange(pluginID, event->Par1))
  {
    // Event parameters
    // Par1: pin nr
    // Par2: state
    // Par3: duration of  state
    // Par4: duration of !state
    // Par5: repeat count (only when Par4 > 0)
    //       -1 = repeat indefinately
    //        0 = only once
    //        N = Repeat N times
    Scheduler.clearGPIOTimer(pluginID, event->Par1);
    const uint32_t key = createKey(pluginID, event->Par1);
    createAndSetPortStatus_Mode_State(key, PIN_MODE_OUTPUT, event->Par2);

    #if FEATURE_GPIO_USE_ESP8266_WAVEFORM
    bool usingWaveForm = 
        event->Par3 > 0 && event->Par3 < 15000 &&
        event->Par4 > 0 && event->Par4 < 15000 &&
        event->Par5 != 0;
        
    if (usingWaveForm) {
      // Preferred is to use the ESP8266 waveform function.
      // Max time high or low is roughly 53 sec @ 80 MHz or half @160 MHz.
      // This is not available on ESP32.

      const uint8_t pin = event->Par1;
      uint32_t timeHighUS = event->Par3 * 1000;
      uint32_t timeLowUS  = event->Par4 * 1000;

      if (event->Par2 == 0) {
        std::swap(timeHighUS, timeLowUS);
      }
      uint32_t runTimeUS = 0;
      if (event->Par5 > 0) {
        runTimeUS = event->Par5 * (timeHighUS + timeLowUS);
        if (event->Par2 == 0) {
          // When having an inverted state repeat-cycle, add some overshoot to return to the original state
          runTimeUS += timeHighUS / 2;
        } else {
          // Must set slightly lower than expected duration as it will be rounded up.
          runTimeUS -= ((timeHighUS + timeLowUS) / 2);
        }
      }

      pinMode(event->Par1, OUTPUT);
      usingWaveForm = startWaveform(
        pin, timeHighUS, timeLowUS, runTimeUS);

      if (event->Par5 > 0 && event->Par2 == 0) {
        // Schedule switching pin back to original state
        Scheduler.setGPIOTimer(
          (runTimeUS / 1000) + 1, // msecFromNow, rounded up
          pluginID,    
          event->Par1,            // Pin/port nr
          !event->Par2,           // pin state
          0,                      // repeatInterval
          0);                     // repeatCount
      }
    }
    #else
    // waveform function not available on ESP32
    const bool usingWaveForm = false;
    #endif

    if (!usingWaveForm) {
      // Par1 = pinnr
      // Par2 = pin state
      // Par3 = timeHigh in msec
      // Par4 = timeLow in msec
      // Par5 = repeat count
      GPIO_Write(pluginID, event->Par1, event->Par2);
      if (event->Par4 > 0 && event->Par5 != 0) {
        // Compute repeat interval

        // Schedule switching pin to given state for repeat
        Scheduler.setGPIOTimer(
          event->Par3,   // msecFromNow
          pluginID,    
          event->Par1,   // Pin/port nr
          event->Par2,   // pin state
          event->Par3,   // repeat interval (high)
          event->Par5,   // repeat count
          event->Par4);  // alternate interval (low)
      } else {
        // Schedule switching pin back to original state
        Scheduler.setGPIOTimer(
          event->Par3,   // msecFromNow
          pluginID,    
          event->Par1,   // Pin/port nr
          !event->Par2,  // pin state
          0, // repeatInterva
          0); // repeatCount
      }
    }


    String log = concat(
      logPrefix, 
      strformat(F(" : port %d. Pulse H:%d"), event->Par1, event->Par3));
    if (event->Par4 > 0 && event->Par5 != 0) {
      log += strformat(F(" L:%d #:%d"), event->Par4, event->Par5);
    }
    log += F(" ms");
    addLog(LOG_LEVEL_INFO, log);
    SendStatusOnlyIfNeeded(event, SEARCH_PIN_STATE, key, log, 0);

    return return_command_success_flashstr();
  } else {
    logErrorGpioOutOfRange(logPrefix, event->Par1, Line);
    return return_command_failed_flashstr();
  }
}

const __FlashStringHelper * Command_GPIO_Status(struct EventStruct *event, const char *Line)
{
  bool sendStatusFlag;
  pluginID_t pluginID;
  int8_t value = -1;

  switch (tolower(parseString(Line, 2).charAt(0)))
  {
    case 'g': // gpio
      pluginID       = PLUGIN_GPIO;
      sendStatusFlag = true;
      break;
#ifdef USES_P009
    case 'm': // mcp
      pluginID       = PLUGIN_MCP;
      value          = GPIO_MCP_Read(event->Par2);
      sendStatusFlag = value == -1;
      break;
#endif
#ifdef USES_P019
    case 'p': // pcf
      pluginID       = PLUGIN_PCF;
      value          = GPIO_PCF_Read(event->Par2);
      sendStatusFlag = value == -1;
      break;
#endif
    default:
      addLog(LOG_LEVEL_ERROR, F("Plugin not included in build"));
      return return_command_failed_flashstr();
  }

  if (!checkValidPortRange(pluginID, event->Par2))
  {
    return return_command_failed_flashstr();
  }
  const uint32_t key = createKey(pluginID, event->Par2); // WARNING: 'status' uses Par2 instead of Par1
  String dummy;
  SendStatusOnlyIfNeeded(event, sendStatusFlag, key, dummy, value);
  return return_command_success_flashstr();
}

const __FlashStringHelper * Command_GPIO_PWM(struct EventStruct *event, const char *Line)
{
  // Par1: GPIO
  // Par2: Duty Cycle
  // Par3: Fade duration
  // Par4: Frequency

  // For now, we only support the internal GPIO pins.
  const __FlashStringHelper * logPrefix = F("GPIO");
  uint32_t frequency = event->Par4;
  uint32_t key       = 0;

  if (set_Gpio_PWM(event->Par1, event->Par2, event->Par3, frequency, key)) {
    String log = strformat(F("PWM  : GPIO: %d duty: %d"), event->Par1, event->Par2);

    if (event->Par3 != 0) {
      log += strformat(F(" Fade: %d ms"), event->Par3);
    }

    if (event->Par4 != 0) {
      log += strformat(F(" f: %d Hz"), frequency);
    }
    addLog(LOG_LEVEL_INFO, log);
    SendStatusOnlyIfNeeded(event, SEARCH_PIN_STATE, key, log, 0);

    // SendStatus(event, getPinStateJSON(SEARCH_PIN_STATE, pluginID, event->Par1, log, 0));

    return return_command_success_flashstr();
  }
  logErrorGpioOutOfRange(logPrefix, event->Par1, Line);
  return return_command_failed_flashstr();
}

const __FlashStringHelper * Command_GPIO_Tone(struct EventStruct *event, const char *Line)
{
  // play a tone on pin par1, with frequency par2 and duration in msec par3.
  unsigned long duration   = event->Par3;
  bool mustScheduleToneOff = false;

  if (duration > 50) {
    duration            = 0;
    mustScheduleToneOff = true;
  }

  if (tone_espEasy(event->Par1, event->Par2, duration)) {
    if (mustScheduleToneOff) {
      // For now, we only support the internal GPIO pins.
      const pluginID_t pluginID = PLUGIN_GPIO;
      Scheduler.setGPIOTimer(event->Par3, pluginID, event->Par1, 0);
    }
    return return_command_success_flashstr();
  }
  return return_command_failed_flashstr();
}

const __FlashStringHelper * Command_GPIO_RTTTL(struct EventStruct *event, const char *Line)
{
  #if FEATURE_RTTTL

  // FIXME: Absolutely no error checking in play_rtttl, until then keep it only in testing
  // play a tune via a RTTTL string, look at https://www.letscontrolit.com/forum/viewtopic.php?f=4&t=343&hilit=speaker&start=10 for
  // more info.

  // First assume 'new' syntax: rtttl,<gpio>,<rtttl string>
  // Difference between 'old' and 'new':
  // Comma between the GPIO argument and the melody
  String melody = parseStringToEndKeepCase(Line, 2);
  melody = melody.substring(melody.indexOf(':'), melody.length());

  melody.replace('-', '#');
  melody.replace('_', '#');

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, strformat(F("RTTTL: pin: %d melody: %s"), event->Par1, melody.c_str()));
  }
  #if FEATURE_ANYRTTTL_LIB && FEATURE_ANYRTTTL_ASYNC
  set_rtttl_melody(melody);
  #endif // if FEATURE_ANYRTTTL_LIB && FEATURE_ANYRTTTL_ASYNC

  if (play_rtttl(event->Par1, melody.c_str())) {
    return return_command_success_flashstr();
  }
  #else // if FEATURE_RTTTL
  #ifndef BUILD_MINIMAL_OTA
  addLog(LOG_LEVEL_ERROR, F("RTTTL: command not included in build"));
  #endif
  #endif // if FEATURE_RTTTL
  return return_command_failed_flashstr();
}

const __FlashStringHelper * Command_GPIO_Pulse(struct EventStruct *event, const char *Line)
{
  const __FlashStringHelper * logPrefix = F("");
  bool   success  = false;
  pluginID_t   pluginID;

  switch (tolower(Line[0]))
  {
    case 'p':                        // pulse or pcfpulse

      if (tolower(Line[1]) == 'u') { // pulse
        pluginID  = PLUGIN_GPIO;
        logPrefix = F("GPIO");
        success   = true;
      } else if (tolower(Line[1]) == 'c') { // pcfpulse
        pluginID  = PLUGIN_PCF;
        logPrefix = F("PCF");
        success   = true;
      }
      break;
    case 'm': // mcp
      pluginID  = PLUGIN_MCP;
      logPrefix = F("MCP");
      success   = true;
      break;
  }

  if (success && checkValidPortRange(pluginID, event->Par1))
  {
    const uint32_t key = createKey(pluginID, event->Par1);

    createAndSetPortStatus_Mode_State(key, PIN_MODE_OUTPUT, event->Par2);
    GPIO_Write(pluginID, event->Par1, event->Par2);

    delay(event->Par3);

    createAndSetPortStatus_Mode_State(key, PIN_MODE_OUTPUT, !event->Par2);
    GPIO_Write(pluginID, event->Par1, !event->Par2);

    String log = logPrefix;
    log += strformat(F(" : port %d. Pulse set for %d ms"), event->Par1, event->Par3);
    addLog(LOG_LEVEL_INFO, log);
    SendStatusOnlyIfNeeded(event, SEARCH_PIN_STATE, key, log, 0);

    return return_command_success_flashstr();
  } else {
    logErrorGpioOutOfRange(logPrefix, event->Par1, Line);
    return return_command_failed_flashstr();
  }
}

const __FlashStringHelper * Command_GPIO_Toggle(struct EventStruct *event, const char *Line)
{
  pluginID_t pluginID = INVALID_PLUGIN_ID;
  bool success = false;

  // Line[0]='g':gpiotoggle; ='p':pcfgpiotoggle; ='m':mcpgpiotoggle
  const __FlashStringHelper * logPrefix = getPluginIDAndPrefix(Line[0], pluginID, success);

  if (success && checkValidPortRange(pluginID, event->Par1))
  {
    const uint32_t key = createKey(pluginID, event->Par1);

    // WARNING: operator [] creates an entry in the map if key does not exist
    // So the next command should be part of each command:
    uint8_t   mode;
    int8_t state;

    auto it = globalMapPortStatus.find(key);

    if (it != globalMapPortStatus.end()) {
      mode  = it->second.mode;
      state = it->second.state;
    } else {
      GPIO_Read(pluginID, event->Par1, state);
      mode = (state == -1) ? PIN_MODE_OFFLINE : PIN_MODE_OUTPUT;
    }

    switch (mode) {
      case PIN_MODE_OUTPUT:
      case PIN_MODE_UNDEFINED:
      {
        createAndSetPortStatus_Mode_State(key, PIN_MODE_OUTPUT, !state);
        GPIO_Write(pluginID, event->Par1, !state);

        String log = logPrefix;
        log += concat(
          F(" toggle"),
          strformat(F(": port#%d: set to %d"), event->Par1, static_cast<int>(!state)));
        addLog(LOG_LEVEL_ERROR, log);
        SendStatusOnlyIfNeeded(event, SEARCH_PIN_STATE, key, log, 0);

        return return_command_success_flashstr();
      }
      case PIN_MODE_OFFLINE:
        logErrorGpioOffline(logPrefix, event->Par1);
        return return_command_failed_flashstr();
      default:
        logErrorGpioNotOutput(logPrefix, event->Par1);
        return return_command_failed_flashstr();
    }
  } else {
    logErrorGpioOutOfRange(logPrefix, event->Par1, Line);
    return return_command_failed_flashstr();
  }
}

const __FlashStringHelper * Command_GPIO(struct EventStruct *event, const char *Line)
{
  pluginID_t pluginID;
  bool success = false;

  // Line[0]='g':gpio; ='p':pcfgpio; ='m':mcpgpio
  const __FlashStringHelper * logPrefix = getPluginIDAndPrefix(Line[0], pluginID, success);

  if (success && checkValidPortRange(pluginID, event->Par1))
  {
    int8_t state = 0;
    uint8_t   mode;

    if (event->Par2 == 2) { // INPUT
      mode = PIN_MODE_INPUT_PULLUP;

      switch (pluginID.value) {
        case PLUGIN_GPIO_INT:
          setInternalGPIOPullupMode(event->Par1);
          state = GPIO_Read_Switch_State(event->Par1, PIN_MODE_INPUT_PULLUP);
          break;
#ifdef USES_P009
        case PLUGIN_MCP_INT:
          setMCPInputAndPullupMode(event->Par1, true);
          GPIO_Read(pluginID, event->Par1, state);
          break;
#endif
#ifdef USES_P019
        case PLUGIN_PCF_INT:
          // PCF8574 specific: only can read 0/low state, so we must send 1
          state = 1;
          break;
#endif
        default:
          addLog(LOG_LEVEL_ERROR, F("Plugin not included in build"));
          return return_command_failed_flashstr();
      }
    } else { // OUTPUT
      mode  = PIN_MODE_OUTPUT;
      state = (event->Par2 == 0) ? 0 : 1;
    }

    const uint32_t key = createKey(pluginID, event->Par1);

    if (globalMapPortStatus[key].mode != PIN_MODE_OFFLINE)
    {
      int8_t currentState;
      GPIO_Read(pluginID, event->Par1, currentState);

      if (currentState == -1) {
        mode  = PIN_MODE_OFFLINE;
        state = -1;
      }

      createAndSetPortStatus_Mode_State(key, mode, state);

      if ((mode == PIN_MODE_OUTPUT) || (pluginID == PLUGIN_PCF)) { GPIO_Write(pluginID, event->Par1, state, mode); }

      const String log = concat(
        logPrefix,
        strformat(F(": port#%d: set to %d"), event->Par1, state));
      addLog(LOG_LEVEL_INFO, log);
      SendStatusOnlyIfNeeded(event, SEARCH_PIN_STATE, key, log, 0);
      return return_command_success_flashstr();
    } else {
      logErrorGpioOffline(logPrefix, event->Par1);
      return return_command_failed_flashstr();
    }
  } else {
    logErrorGpioOutOfRange(logPrefix, event->Par1, Line);
    return return_command_failed_flashstr();
  }
}

void logErrorGpio(const __FlashStringHelper * prefix, int port, const __FlashStringHelper * description)
{
  if (port >= 0) {
    String log = prefix;
    log += concat(F(" : port#"), port);
    log += description;
    addLogMove(LOG_LEVEL_ERROR, log);
  }
}

void logErrorModeOutOfRange(const __FlashStringHelper * prefix, int port)
{
  logErrorGpio(prefix, port, F(" mode selection is incorrect. Valid values are: 0, 1 or 2."));
}

void logErrorGpioOffline(const __FlashStringHelper * prefix, int port)
{
  logErrorGpio(prefix, port, F(" is offline."));
}

void logErrorGpioOutOfRange(const __FlashStringHelper * prefix, int port, const char *Line)
{
  logErrorGpio(prefix, port, F(" is out of range"));
  # ifndef BUILD_NO_DEBUG
  if (port >= 0) {
    if (Line != nullptr) {
      addLog(LOG_LEVEL_DEBUG, Line);
    }
  }
  #endif
}

void logErrorGpioNotOutput(const __FlashStringHelper * prefix, int port)
{
  logErrorGpio(prefix, port, F(" is not an output port"));
}

void createAndSetPortStatus_Mode_State(uint32_t key, uint8_t newMode, int8_t newState)
{
  // WARNING: operator [] creates an entry in the map if key does not exist

  #ifdef ESP32
  switch (newMode) {
    case PIN_MODE_PWM:
    case PIN_MODE_SERVO:
      break;
    default:
      checkAndClearPWM(key);
      break;
  }
  #endif

  auto it = globalMapPortStatus.find(key);
  if (it == globalMapPortStatus.end()) {
#ifdef ESP32
    if (getPluginFromKey(key).value == PLUGIN_GPIO_INT) {
      gpio_reset_pin((gpio_num_t)getPortFromKey(key));
    }
#endif
  }
  // If it doesn't exist, it is now created.
  globalMapPortStatus[key].mode = newMode;
  if (it == globalMapPortStatus.end()) {
    it = globalMapPortStatus.find(key);
  }

  if (it != globalMapPortStatus.end()) {
    // Should always be true, as it would be created if it didn't exist.
    it->second.command = 1; // set to 1 in order to display the status in the PinStatus page

    // only force events if state has changed
    if (it->second.state != newState) {
      it->second.state        = newState;
      it->second.output       = newState;
      it->second.forceEvent   = 1;
      it->second.forceMonitor = 1;
    }
  }
}

const __FlashStringHelper * getPluginIDAndPrefix(char selection, pluginID_t& pluginID, bool& success)
{
  success = true;
  switch (tolower(selection))
  {
    case 'g': // gpio
    case 'l': // longpulse (gpio)
      pluginID  = PLUGIN_GPIO;
      return F("GPIO");
#ifdef USES_P009
    case 'm': // mcp & mcplongpulse
      pluginID  = PLUGIN_MCP;
      return F("MCP");
#endif
#ifdef USES_P019
    case 'p': // pcf & pcflongpulse
      pluginID  = PLUGIN_PCF;
      return F("PCF");
#endif
    default:
      break;
  }
  success = false;
  return F("Plugin not included in build");
}

struct range_pattern_helper_data {
  range_pattern_helper_data() {
    // Make sure the pointer is always set.
    logPrefix = F("GPIO");
  }


  const __FlashStringHelper *  logPrefix;
  uint32_t write = 0;
  uint32_t mask  = 0;

  uint8_t firstPin     = 0;
  uint8_t lastPin      = 0;
  uint8_t numBytes     = 0;
  uint8_t deltaStart   = 0;
  uint8_t numBits      = 0;
  uint8_t firstAddress = 0;
  uint8_t firstBank    = 0;
  uint8_t initVal      = 0;
  bool isMask       = false;
  bool valid        = false;
};


range_pattern_helper_data range_helper_shared(pluginID_t plugin, uint8_t pin1, uint8_t pin2)
{
  range_pattern_helper_data data;

  switch (plugin.value) {
    case PLUGIN_PCF_INT:
      data.logPrefix = F("PCF");
      break;
    case PLUGIN_MCP_INT:
      data.logPrefix = F("MCP");
      break;
  }

  if ((pin2 < pin1) ||
      !checkValidPortRange(plugin, pin1) ||
      !checkValidPortRange(plugin, pin2) ||
      ((pin2 - pin1 + 1) > 16)) {
    if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
      addLog(LOG_LEVEL_ERROR, concat(data.logPrefix, F(": pin numbers out of range.")));
    }
    return data;
  }

  data.firstPin   = ((pin1 - 1) & 0xF8) + 1;
  data.lastPin    = ((pin2 - 1) & 0xF8) + 8;
  data.numBytes   = (data.lastPin - data.firstPin + 1) / 8;
  data.deltaStart = pin1 - data.firstPin;
  data.numBits    = pin2 - pin1 + 1;

  if (plugin == PLUGIN_MCP) {
    data.firstAddress = ((pin1 - 1) / 16) + 0x20;
    data.firstBank    = (((data.firstPin - 1) / 8) + 2) % 2;
    data.initVal      = 2 * data.firstAddress + data.firstBank;
  }

  data.valid = true;
  return data;
}

range_pattern_helper_data range_pattern_helper_shared(pluginID_t plugin, struct EventStruct *event, const char *Line, bool isWritePattern)
{
  range_pattern_helper_data data = range_helper_shared(plugin, event->Par1, event->Par2);

  if (!data.valid) {
    return data;
  }

  if (isWritePattern) {
    data.logPrefix = F("GPIOPattern");
  } else {
    data.logPrefix = F("GPIORange");
  }
  data.valid  = false;
  data.isMask = !parseString(Line, 5).isEmpty();

  if (data.isMask) {
    data.mask  = event->Par4 & ((1 << (data.numBytes * 8)) - 1);
    data.mask &= ((1 << data.numBits) - 1);
    data.mask  = data.mask << data.deltaStart;
  } else {
    data.mask = (1 << data.numBits) - 1;
    data.mask = data.mask << (data.deltaStart);
  }

  if (isWritePattern) {                                         // write pattern is present
    data.write  = event->Par3 & ((1 << (data.numBytes * 8)) - 1); // limit number of bytes
    data.write &= ((1 << data.numBits) - 1);                    // limit to number of bits
    data.write  = data.write << data.deltaStart;                // shift to start from starting pin
  } else {                                                      // write pattern not present
    if (event->Par3 == 0) {
      data.write = 0;
    } else if (event->Par3 == 1) {
      data.write = (1 << data.numBits) - 1;
      data.write = data.write << data.deltaStart;
    } else {
      if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
        addLog(LOG_LEVEL_ERROR, concat(data.logPrefix,  F(": write value must be 0 or 1.")));
      }
      return data;
    }
  }


  data.valid = true;
  return data;
}

/******************************************************************************
** Par1=starting pin
** Par2=ending pin (must be higher of starting pin; and maximum 16 pin per command)
** Par3=write pattern: it's a write pattern. Write 0 or 1.
**                     Example: use decimal number 15 (in binary is 00001111) to set to 1 pin 1,2,3 and 4 and to set to 0 pins 5,6,7,8
**                     if number of bit lower than number of pins, then padded with 0;
**                     if number of bit higher than number of pins, then it's truncated.
** Par4=mask (optional): if not present assume to operate in all pins; if present is used as a mask (1=update, 0=do not update).
**            if number of bit lower than number of pins, then padded with 0;
**            if number of bit higher than number of pins, then it's truncated.
**
**  examples:
**  mcpgpioPattern,1,8,255
**     write pattern = '1101' that will be padded as: '0000001101'
**     mask not present, assume mask = '1111111111'
**  mcpgpioPattern,3,12,13
**     write pattern = '1101' that will be padded as: '0000001101'
**     mask not present, assume mask = '1111111111'
**  mcpgpioPattern,3,12,525
**     write pattern = 525 = '100001101'
**     mask not present, assume mask = '1111111111'
**  mcpgpioPattern,3,12,525,973
**     write pattern = 525 = '100001101'
**     mask = 973 = '1111001101'
**     write pattern after mask = '1000xx11x1' where x indicates that the pin will not be changed
******************************************************************************/
#ifdef USES_P009
const __FlashStringHelper * Command_GPIO_McpGPIOPattern(struct EventStruct *event, const char *Line)
{
  return return_command_boolean_result_flashstr(mcpgpio_range_pattern_helper(event, Line, true));
}
#endif

#ifdef USES_P019
const __FlashStringHelper * Command_GPIO_PcfGPIOPattern(struct EventStruct *event, const char *Line)
{
  return return_command_boolean_result_flashstr(pcfgpio_range_pattern_helper(event, Line, true));
}
#endif

/******************************************************************************
** Par1=starting pin
** Par2=ending pin (must be higher of starting pin; and maximum 16 pin per command)
** Par3=write value: if 0 (or 1) then assume 0 (or 1) for all the pins in the range;
** Par4=mask (optional): if not present assume to operate in all pins; if present is used as a mask (1=update, 0=do not update).
**            if number of bit lower than number of pins, then padded with 0;
**            if number of bit higher than number of pins, then it's truncated.
**
**  examples:
**  mcpgpioRange,1,8,1: set pins 1 to 8 to 1
**  mcpgpioRange,3,12,1: set pins 3 to 12 to 1
**  mcpgpioRange,5,17,0: set pins 5 to 17 to 0
**  mcpgpioRange,3,12,1,525
**     mask = '0100001101'
**     write pattern after mask = 'x1xxxx11x1' where x indicates that the pin will not be changed
**  mcpgpioRange,3,12,1,973
**     mask = 973 = '1111001101'
**     write pattern after mask = '1111xx11x1' where x indicates that the pin will not be changed
******************************************************************************/
#ifdef USES_P009
const __FlashStringHelper * Command_GPIO_McpGPIORange(struct EventStruct *event, const char *Line)
{
  return return_command_boolean_result_flashstr(mcpgpio_range_pattern_helper(event, Line, false));
}
#endif

#ifdef USES_P019
const __FlashStringHelper * Command_GPIO_PcfGPIORange(struct EventStruct *event, const char *Line)
{
  return return_command_boolean_result_flashstr(pcfgpio_range_pattern_helper(event, Line, false));
}
#endif

#ifdef USES_P009
// FIXME TD-er: Function is nearly identical to pcfgpio_range_pattern_helper
bool mcpgpio_range_pattern_helper(struct EventStruct *event, const char *Line, bool isWritePattern)
{
  range_pattern_helper_data data = range_pattern_helper_shared(PLUGIN_MCP, event, Line, isWritePattern);

  if (!data.valid) {
    return false;
  }

  bool   onLine = false;
  for (uint8_t i = 0; i < data.numBytes; i++) {
    uint8_t readValue;
    const uint8_t    currentVal            = data.initVal + i;
    const uint8_t    currentAddress        = static_cast<int>(currentVal / 2);
    uint8_t          currentMask           = (data.mask  >> (8 * i)) & 0xFF;
    const uint8_t    currentInvertedMask   = 0xFF - currentMask;
    const uint8_t    currentWrite          = (data.write >> (8 * i)) & 0xFF;
    const uint8_t    currentGPIORegister   = ((currentVal % 2) == 0) ? MCP23017_GPIOA : MCP23017_GPIOB;
    const uint8_t    currentIOModeRegister = ((currentVal % 2) == 0) ? MCP23017_IODIRA : MCP23017_IODIRB;
    uint8_t    writeGPIOValue        = 0;

    if (GPIO_MCP_ReadRegister(currentAddress, currentIOModeRegister, &readValue)) {
      // set type to output only for the pins of the mask
      uint8_t writeModeValue = (readValue & currentInvertedMask);
      GPIO_MCP_WriteRegister(currentAddress, currentIOModeRegister, writeModeValue);
      GPIO_MCP_ReadRegister(currentAddress, currentGPIORegister, &readValue);

      // write to port
      writeGPIOValue = (readValue & currentInvertedMask) | (currentWrite & data.mask);
      GPIO_MCP_WriteRegister(currentAddress, currentGPIORegister, writeGPIOValue);

      onLine = true;
    } else {
      onLine = false;
    }

    const uint8_t   mode = (onLine) ? PIN_MODE_OUTPUT : PIN_MODE_OFFLINE;
    for (uint8_t j = 0; currentMask != 0 && j < 8; j++) {
      if (currentMask & 1) {  // only for the pins in the mask
        uint8_t currentPin    = data.firstPin + j + 8 * i;
        const uint32_t key = createKey(PLUGIN_MCP, currentPin);

        // state = onLine ? ((writeGPIOValue & uint8_t(pow(2,j))) >> j) : -1;
        const int8_t state = onLine ? ((writeGPIOValue & (1 << j)) >> j) : -1;

        createAndSetPortStatus_Mode_State(key, mode, state);
        const String log = concat(
          data.logPrefix,
          strformat(F(": port#%d: set to %d"), currentPin, state));
        addLog(LOG_LEVEL_INFO, log);
        SendStatusOnlyIfNeeded(event, SEARCH_PIN_STATE, key, log, 0);
      }
      currentMask >>= 1; // Shift the mask 1 position
    }
  }
  return onLine;
}
#endif

#ifdef USES_P019
uint8_t getPcfAddress(uint8_t pin)
{
  uint8_t retValue = static_cast<int>((pin - 1) / 8) + 0x20;

  if (retValue > 0x27) { retValue += 0x10; }
  return retValue;
}

// FIXME TD-er: Function is nearly identical to mcpgpio_range_pattern_helper
bool pcfgpio_range_pattern_helper(struct EventStruct *event, const char *Line, bool isWritePattern)
{
  range_pattern_helper_data data = range_pattern_helper_shared(PLUGIN_PCF, event, Line, isWritePattern);

  if (!data.valid) {
    return false;
  }

  bool   onLine = false;
  for (uint8_t i = 0; i < data.numBytes; i++) {
    uint8_t readValue;
    uint8_t    currentAddress = getPcfAddress(event->Par1 + 8 * i);

    uint8_t currentMask         = (data.mask  >> (8 * i)) & 0xFF;
    uint8_t currentInvertedMask = 0xFF - currentMask;
    uint8_t currentWrite        = (data.write >> (8 * i)) & 0xFF;
    uint8_t writeGPIOValue      = 255;

    onLine = GPIO_PCF_ReadAllPins(currentAddress, &readValue);

    if (onLine) { writeGPIOValue = (readValue & currentInvertedMask) | (currentWrite & data.mask); }

    uint8_t   mode = (onLine) ? PIN_MODE_OUTPUT : PIN_MODE_OFFLINE;
    int8_t state;

    for (uint8_t j = 0; j < 8; j++) {
      uint8_t currentPin    = data.firstPin + j + 8 * i;
      const uint32_t key = createKey(PLUGIN_PCF, currentPin);

      if (currentMask & 1) {  // only for the pins in the mask
        state = onLine ? ((writeGPIOValue & (1 << j)) >> j) : -1;

        createAndSetPortStatus_Mode_State(key, mode, state);
        const String log = concat(
          data.logPrefix,
          strformat(F(": port#%d: set to %d"), currentPin, state));
        addLog(LOG_LEVEL_INFO, log);
        SendStatusOnlyIfNeeded(event, SEARCH_PIN_STATE, key, log, 0);
      } else {
        // set to 1 the INPUT pins and the PIN that have not been initialized yet.
        if (!existPortStatus(key) ||
            (existPortStatus(key) &&
             ((globalMapPortStatus[key].mode == PIN_MODE_INPUT) || (globalMapPortStatus[key].mode == PIN_MODE_INPUT_PULLUP)))) {
          readValue |= (1 << j); // set port j = 1
        }
      }
      currentMask >>= 1; // Shift the mask 1 position
    }

    if (onLine) {
      writeGPIOValue = (readValue & currentInvertedMask) | (currentWrite & data.mask);

      // write to port
      GPIO_PCF_WriteAllPins(currentAddress, writeGPIOValue);
    }
  }
  return onLine;
}
#endif

bool setGPIOMode(uint8_t pin, uint8_t mode)
{
  if (!checkValidPortRange(PLUGIN_GPIO, pin)) {
    return false;
  }
  switch (mode) {
    case PIN_MODE_OUTPUT:
      pinMode(pin, OUTPUT);
      break;
    case PIN_MODE_INPUT_PULLUP:
      setInternalGPIOPullupMode(pin);
      break;
    case PIN_MODE_INPUT:
      pinMode(pin, INPUT);
      break;
  }
  return true;
}

#ifdef USES_P009
bool setMCPMode(uint8_t pin, uint8_t mode)
{
  if (checkValidPortRange(PLUGIN_MCP, pin)) {
    switch (mode) {
      case PIN_MODE_OUTPUT:
        setMCPOutputMode(pin);
        break;
      case PIN_MODE_INPUT_PULLUP:
        setMCPInputAndPullupMode(pin, true);
        break;
      case PIN_MODE_INPUT:
        setMCPInputAndPullupMode(pin, false);
        break;
    }
    return true;
  } else {
    return false;
  }
}
#endif

#ifdef USES_P019
bool setPCFMode(uint8_t pin, uint8_t mode)
{
  if (checkValidPortRange(PLUGIN_PCF, pin)) {
    switch (mode) {
      case PIN_MODE_OUTPUT:
        // do nothing
        break;
      case PIN_MODE_INPUT_PULLUP:
      case PIN_MODE_INPUT:
        setPCFInputMode(pin);
        break;
    }
    return true;
  } else {
    return false;
  }
}
#endif

/***********************************************
 * event->Par1: PIN to be set
 * event->Par2: MODE to be set:
 *             0 = OUTPUT
 *             1 = INPUT PULLUP or INPUT PULLDOWN (only for GPIO16)
 *             2 = INPUT
 **********************************************/
const __FlashStringHelper * Command_GPIO_Mode(struct EventStruct *event, const char *Line)
{
  return return_command_boolean_result_flashstr(gpio_mode_range_helper(event->Par1, event->Par2, event, Line));
}

const __FlashStringHelper * Command_GPIO_ModeRange(struct EventStruct *event, const char *Line)
{
  bool success = true;

  for (uint8_t i = event->Par1; i <= event->Par2; i++) {
    success &= gpio_mode_range_helper(i, event->Par3, event, Line);
  }
  return return_command_boolean_result_flashstr(success);
}

bool gpio_mode_range_helper(uint8_t pin, uint8_t pinMode, struct EventStruct *event, const char *Line)
{
  pluginID_t pluginID = INVALID_PLUGIN_ID;
  bool success = false;

  // Line[0]='g':gpio; ='p':pcfgpio; ='m':mcpgpio
  const __FlashStringHelper * logPrefix = getPluginIDAndPrefix(Line[0], pluginID, success);
  const __FlashStringHelper * logPostfix = F(""); // = new char;

  if (success && checkValidPortRange(pluginID, pin))
  {
    // int8_t state=0;
    uint8_t mode = 255;

    // bool setSuccess=false;

    switch (pinMode) {
      case 0:
        mode       = PIN_MODE_OUTPUT;
        logPostfix = F("OUTPUT");
        break;
      case 1:
        mode       = PIN_MODE_INPUT_PULLUP;
        logPostfix = F("INPUT PULLUP");
        break;
      case 2:
        mode       = PIN_MODE_INPUT;
        logPostfix = F("INPUT");
        break;
    }

    if (mode < 255) {
      switch (pluginID.value) {
        case PLUGIN_GPIO_INT:
          /* setSuccess = */ setGPIOMode(pin, mode);
          break;
#ifdef USES_P019
        case PLUGIN_PCF_INT:
          // set pin = 1 when INPUT
          /* setSuccess = */ setPCFMode(pin, mode);
          break;
#endif
#ifdef USES_P009
        case PLUGIN_MCP_INT:
          /* setSuccess = */ setMCPMode(pin, mode);
          break;
#endif
        default:
          addLog(LOG_LEVEL_ERROR, F("Plugin not included in build"));
          return false;
      }

      const uint32_t key = createKey(pluginID, pin);

      if (globalMapPortStatus[key].mode != PIN_MODE_OFFLINE)
      {
        int8_t currentState;
        GPIO_Read(pluginID, pin, currentState);

        // state = currentState;

        if (currentState == -1) {
          mode = PIN_MODE_OFFLINE;

          // state = -1;
        }

        createAndSetPortStatus_Mode_State(key, mode, currentState);

        String log = logPrefix;
        log += strformat(F(" : port#%d: MODE set to "), pin);
        log += logPostfix;
        log += concat(F(". Value = "), currentState);
        addLog(LOG_LEVEL_INFO, log);
        SendStatusOnlyIfNeeded(event, SEARCH_PIN_STATE, key, log, 0);
        return true;
      } else {
        logErrorGpioOffline(logPrefix, pin);
        return false;
      }
    }
    logErrorModeOutOfRange(logPrefix, pin);
    return false;
  }
  logErrorGpioOutOfRange(logPrefix, pin, Line);
  return false;
}

bool getGPIOPinStateValues(String& str) {
  // parseString(string, 1) = device (gpio,mcpgpio,pcfgpio) that can be shortened to g, m or p
  // parseString(string, 2) = command (pinstate,pinrange)
  // parseString(string, 3) = gpio 1st number or a range separated by '-'
  bool   success = false;
  const String device     = parseString(str, 1);
  const String command    = parseString(str, 2);
  const String gpio_descr = parseString(str, 3);

  if ((command.length() >= 8) && equals(command, F("pinstate")) && (device.length() > 0)) {
    #ifndef BUILD_NO_DEBUG
    String logPrefix;
    #endif
    // returns pin value using syntax: [plugin#xxxxxxx#pinstate#x]
    int32_t par1{};
    const bool validArgument = validIntFromString(gpio_descr, par1);
    #if FEATURE_PINSTATE_EXTENDED
    pluginID_t pluginID = INVALID_PLUGIN_ID;
    #endif // if FEATURE_PINSTATE_EXTENDED

    if (validArgument) {
      switch (device[0]) {
        case 'g':
        {
          #if FEATURE_PINSTATE_EXTENDED
          pluginID  = PLUGIN_GPIO;
          #endif // if FEATURE_PINSTATE_EXTENDED
          str       = digitalRead(par1);
          #ifndef BUILD_NO_DEBUG
          logPrefix = F("GPIO");
          #endif
          success   = true;
          break;
        }

#ifdef USES_P009
        case 'm':
          #if FEATURE_PINSTATE_EXTENDED
          pluginID  = PLUGIN_MCP;
          #endif // if FEATURE_PINSTATE_EXTENDED
          #if FEATURE_I2C_MULTIPLE
          if (getI2CBusCount() > 1) {
            I2CSelectHighClockSpeed(Settings.getI2CInterfacePCFMCP());
          }
          #endif // if FEATURE_I2C_MULTIPLE
          str       = GPIO_MCP_Read(par1);
          #ifndef BUILD_NO_DEBUG
          logPrefix = F("MCP");
          #endif
          success   = true;
          break;
#endif

#ifdef USES_P019
        case 'p':
          #if FEATURE_PINSTATE_EXTENDED
          pluginID  = PLUGIN_PCF;
          #endif // if FEATURE_PINSTATE_EXTENDED
          #if FEATURE_I2C_MULTIPLE
          if (getI2CBusCount() > 1) {
            I2CSelectHighClockSpeed(Settings.getI2CInterfacePCFMCP());
          }
          #endif // if FEATURE_I2C_MULTIPLE
          str       = GPIO_PCF_Read(par1);
          #ifndef BUILD_NO_DEBUG
          logPrefix = F("PCF");
          #endif
          success   = true;
          break;
#endif
        default:
        {
          #if FEATURE_PINSTATE_EXTENDED
          uint32_t plugin = INVALID_PLUGIN_ID.value;
          if (validUIntFromString(device, plugin) && (plugin != INVALID_PLUGIN_ID.value)) { // Valid plugin ID?
            pluginID.value  = plugin;
            #ifndef BUILD_NO_DEBUG
            logPrefix = get_formatted_Plugin_number(pluginID);
            #endif
          } else 
          #endif // if FEATURE_PINSTATE_EXTENDED
          {
            addLog(LOG_LEVEL_ERROR, F("Plugin not included in build"));
            return false;
          }
        }
      }
      #if FEATURE_PINSTATE_EXTENDED
      if (pluginID != INVALID_PLUGIN_ID) {
        const uint32_t key       = createKey(pluginID, par1);
        const auto it            = globalMapPortStatus.find(key);
        const bool notGpioMcpPcf = ((pluginID != PLUGIN_GPIO) && (pluginID != PLUGIN_MCP) && (pluginID != PLUGIN_PCF));

        if (it != globalMapPortStatus.end() && ((it->second.mode == PIN_MODE_PWM) || (it->second.mode == PIN_MODE_SERVO) || notGpioMcpPcf)) {
          // For GPIO/MCP/PCF PWM or SERVO mode get the last set duty cycle or for other plugins get the PWM/SERVO or pinstate
          str     = it->second.getValue();
          success = true;
        }
      }
      #endif // if FEATURE_PINSTATE_EXTENDED
    }

    if (success) {
      #ifndef BUILD_NO_DEBUG
      String log = logPrefix;
      log += strformat(F(" PLUGIN PINSTATE pin =%d; value=%s"), par1, str.c_str());
      addLog(LOG_LEVEL_DEBUG, log);
      #endif // ifndef BUILD_NO_DEBUG
    } else {
      addLog(LOG_LEVEL_ERROR, F(" PLUGIN PINSTATE. Syntax error. Pin parameter is not numeric"));
    }
  } else if ((command.length() >= 8) && equals(command, F("pinrange"))) {
    // returns pin value using syntax: [plugin#xxxxxxx#pinrange#x-y]
    int32_t  par1, par2;
    bool successPar = false;
    int  dashpos    = gpio_descr.indexOf('-');

    if (dashpos != -1) {
      // Found an extra '-' in the 4th param, will split.
      successPar  = validIntFromString(gpio_descr.substring(dashpos + 1), par2);
      successPar &= validIntFromString(gpio_descr.substring(0, dashpos), par1);
    }

    if (successPar) {
      const __FlashStringHelper * logPrefix = F("");

      switch (device[0]) {
#ifdef USES_P009
        case 'm':
        {
          uint16_t tempValue = 0;
          logPrefix = F("MCP");
          success   = mcpgpio_plugin_range_helper(par1, par2, tempValue);
          str       = String(tempValue);
          break;
        }
#endif

#ifdef USES_P019
        case 'p':
        {
          uint16_t tempValue = 0;
          logPrefix = F("PCF");
          success   = pcfgpio_plugin_range_helper(par1, par2, tempValue);
          str       = String(tempValue);
          break;
        }
#endif
        default:
          addLog(LOG_LEVEL_ERROR, F("PLUGIN PINSTATE. Plugin not included in build"));
          return false;

      }

      if (success) {
        #ifndef BUILD_NO_DEBUG
        addLog(LOG_LEVEL_DEBUG, concat(
          logPrefix,
          strformat(F(" PLUGIN RANGE pin start=%d; pin end=%d; value=%s"), par1, par2, str.c_str())));
        #endif // ifndef BUILD_NO_DEBUG
      } else {
        addLog(LOG_LEVEL_ERROR, concat(
          logPrefix,
          strformat(F(" IS OFFLINE. PLUGIN RANGE pin start=%d; pin end=%d; value=%s"), par1, par2, str.c_str())));
      }
    } else {
      addLog(LOG_LEVEL_ERROR, F(" PLUGIN PINRANGE. Syntax error. Pin parameters are not numeric."));
    }
  } else {
    addLog(LOG_LEVEL_ERROR, F("Syntax error. Invalid command. Valid commands are 'pinstate' and 'pinrange'."));
  }

  if (!success) {
    str = '0';
  }
  return success;
}

#ifdef USES_P009
bool mcpgpio_plugin_range_helper(uint8_t pin1, uint8_t pin2, uint16_t& result)
{
  const range_pattern_helper_data data = range_helper_shared(PLUGIN_MCP, pin1, pin2);

  if (!data.valid) {
    return false;
  }

  //  data.logPrefix += F("PluginRead");

  String log;
  bool success = false;
  uint32_t tempResult = 0;

  for (uint8_t i = 0; i < data.numBytes; i++) {
    uint8_t readValue                 = 0;
    const uint8_t currentVal          = data.initVal + i;
    const uint8_t currentAddress      = static_cast<int>(currentVal / 2);
    const uint8_t currentGPIORegister = ((currentVal % 2) == 0) ? MCP23017_GPIOA : MCP23017_GPIOB;

    const bool onLine = GPIO_MCP_ReadRegister(currentAddress, currentGPIORegister, &readValue);

    if (onLine) {
      success = true; // One valid address
      tempResult += (static_cast<uint32_t>(readValue) << (8 * i)); 
    }
  }

  tempResult  = tempResult >> data.deltaStart;
  tempResult &= ((1 << data.numBits) - 1);
  result      = uint16_t(tempResult);

  return success;
}
#endif

#ifdef USES_P019
bool pcfgpio_plugin_range_helper(uint8_t pin1, uint8_t pin2, uint16_t& result)
{
  const range_pattern_helper_data data = range_helper_shared(PLUGIN_PCF, pin1, pin2);

  if (!data.valid) {
    return false;
  }

  //  data.logPrefix += F("PluginRead");

  String log;

  bool success = false;
  uint32_t tempResult = 0;

  for (uint8_t i = 0; i < data.numBytes; i++) {
    uint8_t readValue         = 0;
    const uint8_t currentAddress = getPcfAddress(pin1 + 8 * i);

    const bool onLine = GPIO_PCF_ReadAllPins(currentAddress, &readValue);

    if (onLine) { 
      success = true; // One valid address
      tempResult += (static_cast<uint32_t>(readValue) << (8 * i)); 
    }
  }

  tempResult  = tempResult >> data.deltaStart;
  tempResult &= ((1 << data.numBits) - 1);
  result      = uint16_t(tempResult);

  return success;
}
#endif
#include "../Commands/Provisioning.h"

#if FEATURE_CUSTOM_PROVISIONING

# include "../Commands/Common.h"
# include "../DataTypes/ESPEasyFileType.h"
# include "../DataStructs/ESPEasy_EventStruct.h"
# include "../Helpers/ESPEasy_Storage.h"
# include "../Helpers/Networking.h"
# include "../Helpers/StringConverter.h"

String Command_Provisioning_Dispatcher(struct EventStruct *event,
                                       const char         *Line)
{
  const String cmd = parseString(Line, 2);

  if (equals(cmd, F("config"))) {
    return Command_Provisioning_Config();
  } else
  if (equals(cmd, F("firmware"))) {
    return Command_Provisioning_Firmware(event, Line);
  } else
  # if FEATURE_NOTIFIER
  if (equals(cmd, F("notification"))) {
    return Command_Provisioning_Notification();
  } else
  # endif // if FEATURE_NOTIFIER
  if (equals(cmd, F("provision"))) {
    return Command_Provisioning_Provision();
  } else
  if (equals(cmd, F("rules"))) {
    return Command_Provisioning_Rules(event);
  } else
  if (equals(cmd, F("security"))) {
    return Command_Provisioning_Security();
  }
  return return_command_failed_flashstr();
}

String Command_Provisioning_Config()
{
  return downloadFileType(FileType::CONFIG_DAT);
}

String Command_Provisioning_Security()
{
  return downloadFileType(FileType::SECURITY_DAT);
}

# if FEATURE_NOTIFIER
String Command_Provisioning_Notification()
{
  return downloadFileType(FileType::NOTIFICATION_DAT);
}

# endif // if FEATURE_NOTIFIER

String Command_Provisioning_Provision()
{
  return downloadFileType(FileType::PROVISIONING_DAT);
}

String Command_Provisioning_Rules(struct EventStruct *event)
{
  if ((event->Par2 <= 0) || (event->Par2 > 4)) {
    return F("Provision,Rules: rules index out of range");
  }
  return downloadFileType(FileType::RULES_TXT, event->Par2 - 1);
}

String Command_Provisioning_Firmware(struct EventStruct *event, const char *Line)
{
  // FIXME TD-er: Must only allow to use set prefix in the provisioning settings
  const String url = parseStringToEndKeepCase(Line, 3);
  String error;

  downloadFirmware(url, error); // Events are sent from download handler
  return error;
}

# ifdef PLUGIN_BUILD_MAX_ESP32
void Command_Provisioning_DeprecatedMessage(const String& param) {
  addLog(LOG_LEVEL_ERROR, strformat(F("WARNING: 'Provision%s' is deprecated, change to 'Provision,%s'"), param.c_str(), param.c_str()));
}

String Command_Provisioning_ConfigFallback(struct EventStruct *event, const char *Line)
{
  Command_Provisioning_DeprecatedMessage(F("Config"));
  return Command_Provisioning_Config();
}

String Command_Provisioning_SecurityFallback(struct EventStruct *event, const char *Line)
{
  Command_Provisioning_DeprecatedMessage(F("Security"));
  return Command_Provisioning_Security();
}

#  if FEATURE_NOTIFIER
String Command_Provisioning_NotificationFallback(struct EventStruct *event, const char *Line)
{
  Command_Provisioning_DeprecatedMessage(F("Notification"));
  return Command_Provisioning_Notification();
}

String Command_Provisioning_ProvisionFallback(struct EventStruct *event, const char *Line)
{
  Command_Provisioning_DeprecatedMessage(F("Provision"));
  return Command_Provisioning_Provision();
}

String Command_Provisioning_RulesFallback(struct EventStruct *event, const char *Line)
{
  Command_Provisioning_DeprecatedMessage(F("Rules,<n>"));

  if ((event->Par1 <= 0) || (event->Par1 > 4)) {
    return F("ProvisionRules: rules index out of range");
  }
  return downloadFileType(FileType::RULES_TXT, event->Par1 - 1);
}

String Command_Provisioning_FirmwareFallback(struct EventStruct *event, const char *Line)
{
  Command_Provisioning_DeprecatedMessage(F("Firmware,<Firmware.bin>"));

  // FIXME TD-er: Must only allow to use set prefix in the provisioning settings
  const String url = parseStringToEndKeepCase(Line, 2);
  String error;

  downloadFirmware(url, error);
  return error;
}

#  endif // if FEATURE_NOTIFIER

# endif // ifdef PLUGIN_BUILD_MAX_ESP32

#endif // if FEATURE_CUSTOM_PROVISIONING

#include "../Commands/wd.h"

#ifndef LIMIT_BUILD_SIZE

#include "../Commands/Common.h"

#include "../DataStructs/ESPEasy_EventStruct.h"

#include "../ESPEasyCore/Serial.h"

#include "../Helpers/StringConverter.h"


#include <Wire.h>

const __FlashStringHelper * Command_WD_Config(EventStruct *event, const char* Line)
{
  Wire.beginTransmission(event->Par1);  // address
  Wire.write(event->Par2);              // command
  Wire.write(event->Par3);              // data
  Wire.endTransmission();
  return return_command_success_flashstr();
}

String Command_WD_Read(EventStruct *event, const char* Line)
{
  Wire.beginTransmission(event->Par1);  // address
  Wire.write(0x83);                     // command to set pointer
  Wire.write(event->Par2);              // pointer value
  Wire.endTransmission();
  if ( Wire.requestFrom(static_cast<uint8_t>(event->Par1), static_cast<uint8_t>(1)) == 1 )
  {
    uint8_t value = Wire.read();
    return return_result(
      event, 
      concat(F("I2C Read address "),  formatToHex(event->Par1)) 
    + concat(F(" Value "), formatToHex(value)));
  }
  return return_command_success();
}

#endif
#include "../Commands/InternalCommands_decoder.h"

#include "../DataStructs/TimingStats.h"
#include "../Helpers/StringConverter.h"

// Keep the order of elements in ESPEasy_cmd_e enum
// the same as in the PROGMEM strings below
//
// The first item in the PROGMEM strings below should be an enum
// which is always included in each build as it is used to set an offset
// to compute the final enum value.
//
// Keep the offset used in match_ESPEasy_internal_command in sync
// when adding new commands


const char Internal_commands_ab[] PROGMEM =
  "accessinfo|"
  "asyncevent|"
  "build|"
#ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
  "background|"
#endif // ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
#ifdef USES_C012
  "blynkget|"
#endif // #ifdef USES_C012
#ifdef USES_C015
  "blynkset|"
#endif // #ifdef USES_C015
;

#define Int_cmd_c_offset ESPEasy_cmd_e::clearaccessblock
const char Internal_commands_c[] PROGMEM =
  "clearaccessblock|"
  "clearpassword|"
  "clearrtcram|"
#ifdef ESP8266
  "clearsdkwifi|"
  "clearwifirfcal|"
#endif // #ifdef ESP8266
  "config|"
  "controllerdisable|"
  "controllerenable|"
;

#define Int_cmd_d_offset ESPEasy_cmd_e::datetime
const char Internal_commands_d[] PROGMEM =
  "datetime|"
  "debug|"
  "dec|"
  "deepsleep|"
  "delay|"
#if FEATURE_PLUGIN_PRIORITY
  "disableprioritytask|"
#endif // #if FEATURE_PLUGIN_PRIORITY
  "dns|"
  "dst|"
;

#define Int_cmd_e_offset ESPEasy_cmd_e::erasesdkwifi
const char Internal_commands_e[] PROGMEM =
  "erasesdkwifi|"
  "event|"
  "executerules|"
#if FEATURE_ETHERNET
  "ethphyadr|"
  "ethpinmdc|"
  "ethpinmdio|"
  "ethpinpower|"
  "ethphytype|"
  "ethclockmode|"
  "ethip|"
  "ethgateway|"
  "ethsubnet|"
  "ethdns|"
  "ethdisconnect|"
  "ethwifimode|"
#endif // FEATURE_ETHERNET
;

#define Int_cmd_fghij_offset ESPEasy_cmd_e::factoryreset
const char Internal_commands_fghij[] PROGMEM =
  "factoryreset|"
  "gateway|"
  "gpio|"
  "gpiotoggle|"
  "hiddenssid|"
  "i2cscanner|"
  "inc|"
  "ip|"
#if FEATURE_USE_IPV6
  "ip6|"
#endif
#ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
  "jsonportstatus|"
#endif // ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
;

#if FEATURE_LAT_LONG_VAR_CMD
#define Int_cmd_l_offset ESPEasy_cmd_e::latitude
#else // if FEATURE_LAT_LONG_VAR_CMD
#define Int_cmd_l_offset ESPEasy_cmd_e::let
#endif // if FEATURE_LAT_LONG_VAR_CMD
const char Internal_commands_l[] PROGMEM =
  #if FEATURE_LAT_LONG_VAR_CMD
  "latitude|"
  #endif // if FEATURE_LAT_LONG_VAR_CMD
  "let|"
  #if FEATURE_STRING_VARIABLES
  "letstr|"
  #endif
  "load|"
  "logentry|"
  #if FEATURE_LAT_LONG_VAR_CMD
  "longitude|"
  #endif // if FEATURE_LAT_LONG_VAR_CMD
  "looptimerset|"
  "looptimerset_ms|"
  "looptimersetandrun|"
  "looptimersetandrun_ms|"
  "longpulse|"
  "longpulse_ms|"
#ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
  "logportstatus|"
  "lowmem|"
#endif // ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
;

#define Int_cmd_m_offset ESPEasy_cmd_e::monitor
const char Internal_commands_m[] PROGMEM =
  "monitor|"
  "monitorrange|"
#ifdef USES_P009
  "mcpgpio|"
  "mcpgpiorange|"
  "mcpgpiopattern|"
  "mcpgpiotoggle|"
  "mcplongpulse|"
  "mcplongpulse_ms|"
  "mcpmode|"
  "mcpmoderange|"
  "mcppulse|"
#endif // #ifdef USES_P009
#ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
  "malloc|"
  "meminfo|"
  "meminfodetail|"
#endif // ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
;

#define Int_cmd_no_offset ESPEasy_cmd_e::name
const char Internal_commands_no[] PROGMEM =
  "name|"
  "nosleep|"
#if FEATURE_NOTIFIER
  "notify|"
#endif // #if FEATURE_NOTIFIER
  "ntphost|"
#if FEATURE_DALLAS_HELPER && FEATURE_COMMAND_OWSCAN
  "owscan|"
#endif // if FEATURE_DALLAS_HELPER && FEATURE_COMMAND_OWSCAN
;

#define Int_cmd_p_offset ESPEasy_cmd_e::password
const char Internal_commands_p[] PROGMEM =
  "password|"
#ifdef USES_P019
  "pcfgpio|"
  "pcfgpiorange|"
  "pcfgpiopattern|"
  "pcfgpiotoggle|"
  "pcflongpulse|"
  "pcflongpulse_ms|"
  "pcfmode|"
  "pcfmoderange|"
  "pcfpulse|"
#endif // #ifdef USES_P019
#if FEATURE_POST_TO_HTTP
  "posttohttp|"
#if FEATURE_HTTP_TLS
  "posttohttps|"
#endif // if FEATURE_HTTP_TLS
#endif // #if FEATURE_POST_TO_HTTP
#if FEATURE_CUSTOM_PROVISIONING
  "provision|"
 # ifdef PLUGIN_BUILD_MAX_ESP32 // FIXME DEPRECATED: Fallback for temporary backward compatibility
  "provisionconfig|"
  "provisionsecurity|"
  #  if FEATURE_NOTIFIER
  "provisionnotification|"
  #  endif // #if FEATURE_NOTIFIER
  "provisionprovision|"
  "provisionrules|"
  "provisionfirmware|"
 # endif // #ifdef PLUGIN_BUILD_MAX_ESP32
#endif   // #if FEATURE_CUSTOM_PROVISIONING
  "pulse|"
#if FEATURE_MQTT
  "publish|"
  "publishr|"
#endif // #if FEATURE_MQTT
#if FEATURE_PUT_TO_HTTP
  "puttohttp|"
#if FEATURE_HTTP_TLS
  "puttohttps|"
#endif // if FEATURE_HTTP_TLS
#endif // #if FEATURE_PUT_TO_HTTP
  "pwm|"
;

#define Int_cmd_r_offset ESPEasy_cmd_e::reboot
const char Internal_commands_r[] PROGMEM =
  "reboot|"
  "resetflashwritecounter|"
  "restart|"
  "rtttl|"
  "rules|"
;

#define Int_cmd_s_offset ESPEasy_cmd_e::save
const char Internal_commands_s[] PROGMEM =
  "save|"
  "scheduletaskrun|"
#if FEATURE_SD
  "sdcard|"
  "sdremove|"
#endif // #if FEATURE_SD
#if FEATURE_ESPEASY_P2P
  "sendto|"
#endif // #if FEATURE_ESPEASY_P2P
#if FEATURE_SEND_TO_HTTP
  "sendtohttp|"
#if FEATURE_HTTP_TLS
  "sendtohttps|"
#endif // if FEATURE_HTTP_TLS
#endif // FEATURE_SEND_TO_HTTP
  "sendtoudp|"
  "sendtoudpmix|"
#ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
  "serialfloat|"
#endif // ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
  "settings|"
#if FEATURE_SERVO
  "servo|"
#endif // #if FEATURE_SERVO
  "status|"
  "subnet|"
#if FEATURE_MQTT
  "subscribe|"
#endif // #if FEATURE_MQTT
#ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
  "sysload|"
#endif // ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
;

#define Int_cmd_t_offset ESPEasy_cmd_e::taskclear
const char Internal_commands_t[] PROGMEM =
  "taskclear|"
  "taskclearall|"
  "taskdisable|"
  "taskenable|"
  "taskrun|"
  "taskrunat|"
  "taskvalueset|"
  #if FEATURE_STRING_VARIABLES
  "taskvaluesetderived|"
  "taskvaluesetpresentation|"
  #endif // if FEATURE_STRING_VARIABLES
  "taskvaluetoggle|"
  "taskvaluesetandrun|"
  "timerpause|"
  "timerresume|"
  "timerset|"
  "timerset_ms|"
  "timezone|"
  "tone|"
;

#define Int_cmd_u_offset ESPEasy_cmd_e::udpport
const char Internal_commands_u[] PROGMEM =
  "udpport|"
#if FEATURE_ESPEASY_P2P
  "udptest|"
#endif // #if FEATURE_ESPEASY_P2P
  "unit|"
  "unmonitor|"
  "unmonitorrange|"
  "usentp|"
;

#define Int_cmd_w_offset ESPEasy_cmd_e::wifiallowap
const char Internal_commands_w[] PROGMEM =
  "wifiallowap|"
  "wifiapmode|"
  "wificonnect|"
  "wifidisconnect|"
  "wifikey|"
  "wifikey2|"
  "wifimode|"
  "wifiscan|"
  "wifissid|"
  "wifissid2|"
  "wifistamode|"
#ifndef LIMIT_BUILD_SIZE
  "wdconfig|"
  "wdread|"
#endif // ifndef LIMIT_BUILD_SIZE
;

const char* getInternalCommand_Haystack_Offset(const char firstLetter, int& offset)
{
  const char *haystack = nullptr;

  offset = static_cast<int>(ESPEasy_cmd_e::NotMatched);

  // Keep the offset in sync when adding new commands
  switch (firstLetter)
  {
    case 'a':
    case 'b':
      offset   = 0;
      haystack = Internal_commands_ab;
      break;
    case 'c':
      offset   = static_cast<int>(Int_cmd_c_offset);
      haystack = Internal_commands_c;
      break;
    case 'd':
      offset   = static_cast<int>(Int_cmd_d_offset);
      haystack = Internal_commands_d;
      break;
    case 'e':
      offset   = static_cast<int>(Int_cmd_e_offset);
      haystack = Internal_commands_e;
      break;
    case 'f':
    case 'g':
    case 'h':
    case 'i':
    case 'j':
      offset   = static_cast<int>(Int_cmd_fghij_offset);
      haystack = Internal_commands_fghij;
      break;
    case 'l':
      offset   = static_cast<int>(Int_cmd_l_offset);
      haystack = Internal_commands_l;
      break;
    case 'm':
      offset   = static_cast<int>(Int_cmd_m_offset);
      haystack = Internal_commands_m;
      break;
    case 'n':
    case 'o':
      offset   = static_cast<int>(Int_cmd_no_offset);
      haystack = Internal_commands_no;
      break;
    case 'p':
      offset   = static_cast<int>(Int_cmd_p_offset);
      haystack = Internal_commands_p;
      break;
    case 'r':
      offset   = static_cast<int>(Int_cmd_r_offset);
      haystack = Internal_commands_r;
      break;
    case 's':
      offset   = static_cast<int>(Int_cmd_s_offset);
      haystack = Internal_commands_s;
      break;
    case 't':
      offset   = static_cast<int>(Int_cmd_t_offset);
      haystack = Internal_commands_t;
      break;
    case 'u':
      offset   = static_cast<int>(Int_cmd_u_offset);
      haystack = Internal_commands_u;
      break;
    case 'w':
      offset   = static_cast<int>(Int_cmd_w_offset);
      haystack = Internal_commands_w;
      break;

    default:
      return nullptr;
  }
  return haystack;
}

ESPEasy_cmd_e match_ESPEasy_internal_command(const String& cmd)
{
  START_TIMER;
  ESPEasy_cmd_e res = ESPEasy_cmd_e::NotMatched;

  if (cmd.length() < 2) {
    // No commands less than 2 characters
    return res;
  }

  int offset           = 0;
  const char *haystack = getInternalCommand_Haystack_Offset(cmd[0], offset);

  if (haystack == nullptr) {
/*
    addLog(LOG_LEVEL_ERROR, strformat(
             F("Internal command: No Haystack/offset '%s', offset: %d"),
             cmd.c_str(),
             offset));
*/
    return res;
  }


  if (haystack != nullptr) {
    const int command_i = GetCommandCode(cmd.c_str(), haystack);

    if (command_i != -1) {
      res = static_cast<ESPEasy_cmd_e>(command_i + offset);
    }
/*
    else {
      addLog(LOG_LEVEL_ERROR, strformat(
               F("Internal command: Not found '%s', haystack: %s"),
               cmd.c_str(),
               String(haystack).c_str()));
    }
*/
  }
  STOP_TIMER(COMMAND_DECODE_INTERNAL);
  return res;
}

#ifndef BUILD_NO_DEBUG
bool toString(ESPEasy_cmd_e cmd, String& str)
{
  if (cmd == ESPEasy_cmd_e::NotMatched) {
    return false;
  }
  char c     = 'z';
  bool found = false;
  int  offset;
  const char *haystack = nullptr;

  while (!found && c >= 'a') {
    haystack = getInternalCommand_Haystack_Offset(c, offset);

    if (haystack != nullptr) {
      if (offset <= static_cast<int>(cmd)) {
        found = true;
      }
    }
    --c;
  }

  if (found) {
    const int index = static_cast<int>(cmd) - offset;
/*
    addLog(LOG_LEVEL_INFO, strformat(
             F("Internal command: cmd=%d offset=%d index=%d"),
             static_cast<int>(cmd),
             offset,
             index));
*/

    if ((index >= 0) && (haystack != nullptr)) {
      // Likely long enough to parse any command
      char temp[32]{};
      str = GetTextIndexed(temp, sizeof(temp), index, haystack);
      return !str.isEmpty();
    }
  }
  return false;
}

bool checkAll_internalCommands()
{
  constexpr int last = static_cast<int>(ESPEasy_cmd_e::NotMatched);
  bool no_error      = true;

  for (int i = 0; i < last; ++i) {
    const ESPEasy_cmd_e cmd = static_cast<ESPEasy_cmd_e>(i);
    String cmd_str;

    if (!toString(cmd, cmd_str)) {
      no_error = false;
//      addLog(LOG_LEVEL_ERROR, concat(F("Internal command: no matching string for "), i));
    } else {
      const ESPEasy_cmd_e cmd_found = match_ESPEasy_internal_command(cmd_str);

      if (cmd_found != cmd) {
        if (cmd_str.isEmpty()) {
          addLog(LOG_LEVEL_ERROR, strformat(
                   F("Internal command: mismatch (%d)"), i));
        }
        else {
          addLog(LOG_LEVEL_ERROR, strformat(
                   F("Internal command: mismatch '%s' (%d)"),
                   cmd_str.c_str(),
                   i));
        }
        no_error = false;
      }
    }
  }

  if (no_error) {
    addLog(LOG_LEVEL_INFO, F("Internal command: All checked OK"));
  } 
/*
  else {
    const int index = static_cast<int>(match_ESPEasy_internal_command(F("build")));
    addLog(LOG_LEVEL_ERROR, concat(F("Internal command: index 'build'="), index));
  }
*/
  return no_error;
}

#endif // ifndef BUILD_NO_DEBUG

#include "../Commands/Settings.h"

#include "../../ESPEasy_common.h"

#include "../Commands/Common.h"

#include "../CustomBuild/CompiletimeDefines.h"

#include "../ESPEasyCore/ESPEasyNetwork.h"
#include "../ESPEasyCore/Serial.h"

#include "../Globals/SecuritySettings.h"
#include "../Globals/Settings.h"

#include "../Helpers/ESPEasy_FactoryDefault.h"
#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/Memory.h"
#include "../Helpers/MDNS_Helper.h"
#include "../Helpers/Misc.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringGenerator_System.h"


String Command_Settings_Build(struct EventStruct *event, const char* Line)
{
	if (HasArgv(Line, 2)) {
	  Settings.Build = event->Par1;
	} else {
      return return_result(event, concat(F("Build:"), static_cast<int>(Settings.Build)));
	}
	return return_command_success();
}

String Command_Settings_Unit(struct EventStruct *event, const char* Line)
{
	if (HasArgv(Line, 2)) {
	  Settings.Unit = event->Par1;
	  update_mDNS();
	} else {
      return return_result(event, concat(F("Unit:"), static_cast<int>(Settings.Unit)));
	}
	return return_command_success();
}

String Command_Settings_Name(struct EventStruct *event, const char* Line)
{
	return Command_GetORSetString(event, F("Name:"),
							Line,
							Settings.Name,
							sizeof(Settings.Name),
							1);
}

String Command_Settings_Password(struct EventStruct *event, const char* Line)
{
	return Command_GetORSetString(event, F("Password:"),
				      Line,
				      SecuritySettings.Password,
				      sizeof(SecuritySettings.Password),
				      1
				      );
}

const __FlashStringHelper *  Command_Settings_Password_Clear(struct EventStruct *event, const char* Line)
{
	const String storedPassword = SecuritySettings.getPassword();
	if (storedPassword.length() > 0) {
		// There is a password set, so we must check it.
		const String password = parseStringKeepCase(Line, 2);
		if (!storedPassword.equals(password)) {
			return return_command_failed_flashstr();
		}
        ZERO_FILL(SecuritySettings.Password);
	}
	return return_command_success_flashstr();
}

const __FlashStringHelper * Command_Settings_Save(struct EventStruct *event, const char* Line)
{
	SaveSettings();
	return return_command_success_flashstr();
}

const __FlashStringHelper * Command_Settings_Load(struct EventStruct *event, const char* Line)
{
	LoadSettings();
	return return_command_success_flashstr();
}

const __FlashStringHelper * Command_Settings_Print(struct EventStruct *event, const char* Line)
{
	serialPrintln();

	serialPrintln(F("System Info"));
	serialPrint(F("  IP Address    : ")); serialPrintln(formatIP(NetworkLocalIP()));
	serialPrint(F("  Build         : ")); serialPrintln(String(get_build_nr()) + '/' + getSystemBuildString());
	serialPrint(F("  Name          : ")); serialPrintln(Settings.getName());
	serialPrint(F("  Unit          : ")); serialPrintln(String(static_cast<int>(Settings.Unit)));
	serialPrint(F("  WifiSSID      : ")); serialPrintln(SecuritySettings.WifiSSID);
	serialPrint(F("  WifiKey       : ")); serialPrintln(SecuritySettings.WifiKey);
	serialPrint(F("  WifiSSID2     : ")); serialPrintln(SecuritySettings.WifiSSID2);
	serialPrint(F("  WifiKey2      : ")); serialPrintln(SecuritySettings.WifiKey2);
	serialPrint(F("  Free mem      : ")); serialPrintln(String(FreeMem()));
	return return_see_serial(event);
}

const __FlashStringHelper * Command_Settings_FactoryReset(struct EventStruct *event, const char* Line)
{
	ResetFactory();
	reboot(IntendedRebootReason_e::ResetFactoryCommand);
	return return_command_success_flashstr();
}

#include "../Commands/Common.h"
#include "../Commands/LatitudeLongitude.h"
#include "../Globals/Settings.h"

String Command_Latitude(struct EventStruct *event,
                        const char         *Line) {
  return Command_GetORSetFloatMinMax(event, F("Latitude:"), Line, &Settings.Latitude, 1, -90.0001f, 90.0001f);
}

String Command_Longitude(struct EventStruct *event,
                         const char         *Line) {
  return Command_GetORSetFloatMinMax(event, F("Longitude:"), Line, &Settings.Longitude, 1, -180.0001f, 180.0001f);
}

#include "../Commands/Servo.h"

#include "../Commands/Common.h"
#if FEATURE_SERVO

#include "../DataStructs/EventStructCommandWrapper.h"
#include "../DataStructs/PinMode.h"
#include "../DataStructs/PortStatusStruct.h"
#include "../ESPEasyCore/Controller.h"
#include "../ESPEasyCore/ESPEasyGPIO.h"
#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../Globals/GlobalMapPortStatus.h"
#include "../Helpers/Hardware_PWM.h"
#include "../Helpers/PortStatus.h"
#include "../Helpers/StringConverter.h"

// Needed also here for PlatformIO's library finder as the .h file 
// is in a directory which is excluded in the src_filter
# include <Servo.h>
ServoPinMap_t ServoPinMap;
#endif // if FEATURE_SERVO

const __FlashStringHelper * Command_Servo(struct EventStruct *event, const char *Line)
{
  #if FEATURE_SERVO

  // GPIO number is stored inside event->Par2 instead of event->Par1 as in all the other commands
  // So needs to reload the tempPortStruct.

  // FIXME TD-er: For now only fixed to "P001" even when it is for internal GPIO pins
  pluginID_t pluginID = PLUGIN_GPIO;

  // Par1: Servo ID (obsolete/unused since 2020/11/22)
  // Par2: GPIO pin
  // Par3: angle 0...180 degree
  if (checkValidPortRange(pluginID, event->Par2)) {
    portStatusStruct tempStatus;
    const uint32_t   key = createKey(pluginID, event->Par2); // WARNING: 'servo' uses Par2 instead of Par1
    // WARNING: operator [] creates an entry in the map if key does not exist
    // So the next command should be part of each command:
    tempStatus = globalMapPortStatus[key];

    String log = concat(F("Servo : GPIO "), event->Par2);

    // SPECIAL CASE TO ALLOW SERVO TO BE DETATTCHED AND SAVE POWER.
    if (event->Par3 >= 9000) {
      auto it = ServoPinMap.find(event->Par2);

      if (it != ServoPinMap.end()) {
        it->second.detach();
        # ifdef ESP32
          detachLedChannel(event->Par2);
        # endif // ifdef ESP32
        ServoPinMap.erase(it);
      }

      // Set parameters to make sure the port status will be removed.
      tempStatus.task    = 0;
      tempStatus.monitor = 0;
      tempStatus.command = 0;
      savePortStatus(key, tempStatus);
      addLog(LOG_LEVEL_INFO, concat(log, F(" Servo detached")));
      return return_command_success_flashstr();

    }
    # ifdef ESP32
      // Must keep track of used channels or else cause conflicts with PWM
      int8_t ledChannel = attachLedChannel(event->Par2);
      ServoPinMap[event->Par2].attach(event->Par2, ledChannel);
    # else // ifdef ESP32
      ServoPinMap[event->Par2].attach(event->Par2);
    # endif // ifdef ESP32
    ServoPinMap[event->Par2].write(event->Par3);

    tempStatus.command   = 1; // set to 1 in order to display the status in the PinStatus page
    tempStatus.state     = 1;
    tempStatus.output    = 1;
    tempStatus.dutyCycle = event->Par3;

    // setPinState(PLUGIN_ID_001, event->Par2, PIN_MODE_SERVO, event->Par3);
    tempStatus.mode = PIN_MODE_SERVO;
    savePortStatus(key, tempStatus);
    log += F(" Servo set to ");
    log += event->Par3;
    addLog(LOG_LEVEL_INFO, log);
    SendStatusOnlyIfNeeded(event, SEARCH_PIN_STATE, key, log, 0);

    // SendStatus(event, getPinStateJSON(SEARCH_PIN_STATE, PLUGIN_ID_001, event->Par2, log, 0));
    return return_command_success_flashstr();
  }
  #else // if FEATURE_SERVO
  #ifndef BUILD_MINIMAL_OTA
  addLog(LOG_LEVEL_ERROR, F("SERVO : command not included in build"));
  #endif
  #endif // FEATURE_SERVO
  return return_command_failed_flashstr();
}

#include "../Commands/Common.h"

#include <ctype.h>
#include <IPAddress.h>

#include "../../ESPEasy_common.h"


#include "../ESPEasyCore/ESPEasyWifi.h"
#include "../ESPEasyCore/Serial.h"

#include "../Helpers/Networking.h"
#include "../Helpers/Numerical.h"
#include "../Helpers/StringConverter.h"


// Simple function to return "Ok", to avoid flash string duplication in the firmware.
const __FlashStringHelper * return_command_success_flashstr() { return F("\nOK"); }
const __FlashStringHelper * return_command_failed_flashstr() { return F("\nFailed"); }

const __FlashStringHelper * return_command_boolean_result_flashstr(bool success) 
{
    return success ? return_command_success_flashstr() : return_command_failed_flashstr();
}


String return_command_success()
{
  return return_command_success_flashstr();
}

String return_command_failed()
{
  return return_command_failed_flashstr();
}

const __FlashStringHelper * return_incorrect_nr_arguments() { return F("Too many arguments, try using quotes!"); }
const __FlashStringHelper * return_incorrect_source() { return F("Command not allowed from this source!"); }
const __FlashStringHelper * return_not_connected() { return F("Not connected to WiFi"); }


String return_result(struct EventStruct *event, const String& result)
{
  serialPrintln();
  serialPrintln(result);

  if (event->Source == EventValueSource::Enum::VALUE_SOURCE_SERIAL) {
    return return_command_success();
  }
  return result;
}


const __FlashStringHelper * return_see_serial(struct EventStruct *event)
{
  return (event->Source == EventValueSource::Enum::VALUE_SOURCE_SERIAL)
    ? return_command_success_flashstr() 
    : F("Output sent to serial");
}


String Command_GetORSetIP(struct EventStruct *event,
                          const __FlashStringHelper * targetDescription,
                          const char         *Line,
                          uint8_t               *IP,
                          const IPAddress   & dhcpIP,
                          int                 arg)
{
  bool hasArgument = false;
  {
    // Check if command is valid. Leave in separate scope to delete the TmpStr1
    String TmpStr1;

    if (GetArgv(Line, TmpStr1, arg + 1)) {
      hasArgument = true;

      if (!str2ip(TmpStr1, IP)) {
        return return_result(event, concat(F("Invalid parameter: "), TmpStr1));
      }
    }
  }

  if (!hasArgument) {
    String result = targetDescription;

    if (useStaticIP()) {
      result += formatIP(IP);
    } else {
      result += formatIP(dhcpIP);
      result += F("(DHCP)");
    }
    return return_result(event, result);
  }
  return return_command_success();
}

String Command_GetORSetString(struct EventStruct *event,
                              const __FlashStringHelper * targetDescription,
                              const char         *Line,
                              char               *target,
                              size_t              len,
                              int                 arg
                              )
{
  bool hasArgument = false;
  {
    // Check if command is valid. Leave in separate scope to delete the TmpStr1
    String TmpStr1;

    if (GetArgv(Line, TmpStr1, arg + 1)) {
      hasArgument = true;

      if (TmpStr1.length() > len) {
        String result = concat(targetDescription, F(" is too large. max size is "));
        result += len;
        return return_result(event, result);
      }
      safe_strncpy(target, TmpStr1, len);
    }
  }

  if (hasArgument) {
    String result = targetDescription;
    result += target;
    return return_result(event, result);
  }
  return return_command_success();
}

String Command_GetORSetBool(struct EventStruct *event,
                            const __FlashStringHelper * targetDescription,
                            const char         *Line,
                            bool               *value,
                            int                 arg)
{
  bool hasArgument = false;
  {
    // Check if command is valid. Leave in separate scope to delete the TmpStr1
    String TmpStr1;

    if (GetArgv(Line, TmpStr1, arg + 1)) {
      hasArgument = true;
      TmpStr1.toLowerCase();

      int32_t tmp_int = 0;
      if (validIntFromString(TmpStr1, tmp_int)) {
        *value = tmp_int > 0;
      }
      else if (equals(TmpStr1, F("on"))) { *value = true; }
      else if (equals(TmpStr1, F("true"))) { *value = true; }
      else if (equals(TmpStr1, F("off"))) { *value = false; }
      else if (equals(TmpStr1, F("false"))) { *value = false; }
    }
  }

  if (hasArgument) {
    return return_result(event, concat(targetDescription, boolToString(*value)));
  }
  return return_command_success();
}

#if FEATURE_ETHERNET
String Command_GetORSetETH(struct EventStruct *event,
                            const __FlashStringHelper * targetDescription,
                            const __FlashStringHelper * valueToString,
                            const char         *Line,
                            uint8_t            *value,
                            int                 arg)
{
  bool hasArgument = false;
  {
    // Check if command is valid. Leave in separate scope to delete the TmpStr1
    String TmpStr1;

    if (GetArgv(Line, TmpStr1, arg + 1)) {
      hasArgument = true;
      TmpStr1.toLowerCase();

      int32_t tmp_int = 0;
      if (validIntFromString(TmpStr1, tmp_int)) {
        *value = static_cast<uint8_t>(tmp_int);
      }

      // FIXME TD-er: This should not be in a generic function, but rather pre-processed in the command itself


      // WiFi/Eth mode
      else if (equals(TmpStr1, F("wifi"))) { *value = 0; }
      else if (equals(TmpStr1, F("ethernet"))) { *value = 1; }

      // ETH clockMode
      else if (TmpStr1.startsWith(F("ext"))) { *value = 0; }
      else if (TmpStr1.indexOf(F("gpio0"))  != -1) { *value = 1; }
      else if (TmpStr1.indexOf(F("gpio16")) != -1) { *value = 2; }
      else if (TmpStr1.indexOf(F("gpio17")) != -1) { *value = 3; }
    }
  }

  String result = targetDescription;
  if (hasArgument) {
    result += *value;
  } else {
    result += valueToString;
  }
  return return_result(event, result);
}
#endif

String Command_GetORSetInt8_t(struct EventStruct *event,
                            const __FlashStringHelper * targetDescription,
                            const char         *Line,
                            int8_t             *value,
                            int                 arg)
{
  bool hasArgument = false;
  {
    // Check if command is valid. Leave in separate scope to delete the TmpStr1
    String TmpStr1;

    if (GetArgv(Line, TmpStr1, arg + 1)) {
      hasArgument = true;
      TmpStr1.toLowerCase();

      int32_t tmp_int = 0;
      if (validIntFromString(TmpStr1, tmp_int)) {
        *value = static_cast<int8_t>(tmp_int);
      }
    }
  }

  if (hasArgument) {
    String result = targetDescription;
    result += *value;
    return return_result(event, result);
  }
  return return_command_success();
}

String Command_GetORSetFloatMinMax(struct EventStruct        *event,
                                   const __FlashStringHelper *targetDescription,
                                   const char                *Line,
                                   float                     *value,
                                   int                        arg,
                                   float                      _min,
                                   float                      _max)
{
  {
    // Check if command is valid. Leave in separate scope to delete the TmpStr1
    String TmpStr1;

    if (GetArgv(Line, TmpStr1, arg + 1)) {
      TmpStr1.toLowerCase();

      float tmp_float{};

      if (validFloatFromString(TmpStr1, tmp_float) &&
          definitelyGreaterThan(tmp_float, _min) &&
          definitelyLessThan(tmp_float, _max)) {
        *value = tmp_float;
      } else {
        return return_command_failed();
      }
    }
  }

  return return_result(event, concat(targetDescription, toString(*value)));
}

#include "../Commands/Rules.h"


#include "../../ESPEasy_common.h"


#include "../Commands/Common.h"

#include "../DataTypes/EventValueSource.h"

#include "../ESPEasyCore/Controller.h"
#include "../ESPEasyCore/ESPEasyRules.h"

#include "../Globals/EventQueue.h"
#include "../Globals/ExtraTaskSettings.h"
#include "../Globals/RulesCalculate.h"
#include "../Globals/RuntimeData.h"
#include "../Globals/Settings.h"

#include "../Helpers/Misc.h"
#include "../Helpers/StringConverter.h"

const __FlashStringHelper * Command_Rules_Execute(struct EventStruct *event, const char *Line)
{
  String filename;

  if (GetArgv(Line, filename, 2)) {
    String event;
    rulesProcessingFile(filename, event);
  }
  return return_command_success_flashstr();
}

String Command_Rules_UseRules(struct EventStruct *event, const char *Line)
{
  return Command_GetORSetBool(event, F("Rules:"),
                              Line,
                              (bool *)&Settings.UseRules,
                              1);
}

const __FlashStringHelper * Command_Rules_Async_Events(struct EventStruct *event, const char *Line)
{
  if (Settings.UseRules) {
    String eventName = parseStringToEndKeepCase(Line, 2);

    eventName.replace('$', '#');
    eventQueue.addMove(std::move(eventName));
  }
  return return_command_success_flashstr();
}

const __FlashStringHelper * Command_Rules_Events(struct EventStruct *event, const char *Line)
{
  if (Settings.UseRules) {
    const bool executeImmediately =
      SourceNeedsStatusUpdate(event->Source) ||
      event->Source == EventValueSource::Enum::VALUE_SOURCE_RULES ||
      event->Source == EventValueSource::Enum::VALUE_SOURCE_RULES_RESTRICTED;

    String eventName = parseStringToEndKeepCase(Line, 2);

    eventName.replace('$', '#');
    if (executeImmediately) {
      rulesProcessing(eventName); // TD-er: Process right now
    } else {
      eventQueue.addMove(std::move(eventName));
    }
  }
  return return_command_success_flashstr();
}

const __FlashStringHelper * Command_Rules_Let(struct EventStruct *event, const char *Line)
{
  String varName;
  String TmpStr1;
  GetArgv(Line, varName, 2);

  if (!varName.isEmpty() &&
      ExtraTaskSettings.checkInvalidCharInNames(varName.c_str()) && GetArgv(Line, TmpStr1, 3)) {
    ESPEASY_RULES_FLOAT_TYPE result{};

    if (!isError(Calculate(TmpStr1, result))) {
      setCustomFloatVar(varName, result);
      return return_command_success_flashstr();
    }
  }
  return return_command_failed_flashstr();
}

#if FEATURE_STRING_VARIABLES
const __FlashStringHelper * Command_Rules_LetStr(struct EventStruct *event, const char *Line)
{
  String varName;
  String TmpStr1;
  GetArgv(Line, varName, 2);

  if (!varName.isEmpty() &&
      ExtraTaskSettings.checkInvalidCharInNames(varName.c_str()) && GetArgv(Line, TmpStr1, 3)) {
    String result = parseTemplate(TmpStr1);

    setCustomStringVar(varName, result);
    return return_command_success_flashstr();
  }
  return return_command_failed_flashstr();
}
#endif // if FEATURE_STRING_VARIABLES

const __FlashStringHelper * Command_Rules_IncDec(struct EventStruct *event, const char *Line, const ESPEASY_RULES_FLOAT_TYPE factor)
{
  String varName;
  String TmpStr1;
  ESPEASY_RULES_FLOAT_TYPE result = 1;
  GetArgv(Line, varName, 2);

  if (!varName.isEmpty()) {
    if (GetArgv(Line, TmpStr1, 3) && isError(Calculate(TmpStr1, result))) {
      return return_command_failed_flashstr();
    }
    setCustomFloatVar(varName, getCustomFloatVar(varName) + (result * factor));
    return return_command_success_flashstr();
  }
  return return_command_failed_flashstr();
}

const __FlashStringHelper * Command_Rules_Inc(struct EventStruct *event, const char *Line)
{
  return Command_Rules_IncDec(event, Line, 1.0);
}

const __FlashStringHelper * Command_Rules_Dec(struct EventStruct *event, const char *Line)
{
  return Command_Rules_IncDec(event, Line, -1.0);
}

#include "../Commands/InternalCommands.h"

#include "../../ESPEasy_common.h"

#include "../../_Plugin_Helper.h"
#include "../Globals/Settings.h"

#if FEATURE_BLYNK
# include "../Commands/Blynk.h"
# include "../Commands/Blynk_c015.h"
#endif // if FEATURE_BLYNK

#include "../Commands/Common.h"
#include "../Commands/Controller.h"
#include "../Commands/Diagnostic.h"
#include "../Commands/GPIO.h"
#include "../Commands/HTTP.h"
#include "../Commands/InternalCommands_decoder.h"
#include "../Commands/i2c.h"

#if FEATURE_LAT_LONG_VAR_CMD
#include "../Commands/LatitudeLongitude.h"
#endif // if FEATURE_LAT_LONG_VAR_CMD

#if FEATURE_MQTT
# include "../Commands/MQTT.h"
#endif // if FEATURE_MQTT

#include "../Commands/Networks.h"
#if FEATURE_NOTIFIER
# include "../Commands/Notifications.h"
#endif // if FEATURE_NOTIFIER
#if FEATURE_DALLAS_HELPER && FEATURE_COMMAND_OWSCAN
#include "../Commands/OneWire.h"
#endif // if FEATURE_DALLAS_HELPER && FEATURE_COMMAND_OWSCAN
#include "../Commands/Provisioning.h"
#include "../Commands/RTC.h"
#include "../Commands/Rules.h"
#include "../Commands/SDCARD.h"
#include "../Commands/Settings.h"
#if FEATURE_SERVO
# include "../Commands/Servo.h"
#endif // if FEATURE_SERVO
#include "../Commands/System.h"
#include "../Commands/Tasks.h"
#include "../Commands/Time.h"
#include "../Commands/Timer.h"
#include "../Commands/UPD.h"
#include "../Commands/wd.h"
#include "../Commands/WiFi.h"

#include "../DataStructs/TimingStats.h"

#include "../ESPEasyCore/ESPEasy_Log.h"

#include "../Helpers/Misc.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringParser.h"


bool checkNrArguments(const char *cmd, const String& Line, int nrArguments) {
  if (nrArguments < 0) { return true; }

  // 0 arguments means argument on pos1 is valid (the command) and argpos 2 should not be there.
  if (HasArgv(Line.c_str(), nrArguments + 2)) {
    #ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
      String log;

      if (reserve_special(log, 128)) {
        log += F("Too many arguments: cmd=");
        log += cmd;

        if (nrArguments < 1) {
          log += Line;
        } else {
          // Check for one more argument than allowed, since we apparently have one.
          bool done = false;
          int  i    = 1;

          while (!done) {
            String parameter;

            if (i == nrArguments) {
              parameter = tolerantParseStringKeepCase(Line, i + 1);
            } else {
              parameter = parseStringKeepCase(Line, i + 1);
            }
            done = parameter.isEmpty();

            if (!done) {
              if (i <= nrArguments) {
                if (Settings.TolerantLastArgParse() && (i == nrArguments)) {
                  log += F(" (fixed)");
                }
                log += F(" Arg");
              } else {
                log += F(" ExtraArg");
              }
              log += i;
              log += '=';
              log += parameter;
            }
            ++i;
          }
        }
        log += F(" lineLength=");
        log += Line.length();
        addLogMove(LOG_LEVEL_ERROR, log);
      }
      addLogMove(LOG_LEVEL_ERROR, strformat(F("Line: _%s_"), Line.c_str()));

      addLogMove(LOG_LEVEL_ERROR, concat(Settings.TolerantLastArgParse() ?
                                         F("Command executed, but may fail.") : F("Command not executed!"),
                                         F(" See: https://github.com/letscontrolit/ESPEasy/issues/2724")));
    }
    #endif // ifndef BUILD_NO_DEBUG

    if (Settings.TolerantLastArgParse()) {
      return true;
    }
    return false;
  }
  return true;
}

bool checkSourceFlags(EventValueSource::Enum source, EventValueSourceGroup::Enum group) {
  if (EventValueSource::partOfGroup(source, group)) {
    return true;
  }
  addLog(LOG_LEVEL_ERROR, return_incorrect_source());
  return false;
}

command_case_data::command_case_data(const char *cmd, struct EventStruct *event, const char *line) :
  cmd(cmd), event(event), line(line)
{
  cmd_lc = cmd;
  cmd_lc.toLowerCase();
}

InternalCommands::InternalCommands(const char *cmd, struct EventStruct *event, const char *line)
  : _data(cmd, event, line) {}


// Wrapper to reduce generated code by macro
bool InternalCommands::do_command_case_all(command_function_fs pFunc,
                                           int                 nrArguments)
{
  return do_command_case(_data, pFunc, nrArguments, EventValueSourceGroup::Enum::ALL);
}

bool InternalCommands::do_command_case_all(command_function pFunc,
                                           int              nrArguments)
{
  return do_command_case(_data, pFunc, nrArguments, EventValueSourceGroup::Enum::ALL);
}

// Wrapper to reduce generated code by macro
bool InternalCommands::do_command_case_all_restricted(command_function_fs pFunc,
                                                      int                 nrArguments)
{
  return do_command_case(_data,  pFunc, nrArguments, EventValueSourceGroup::Enum::RESTRICTED);
}

bool InternalCommands::do_command_case_all_restricted(command_function pFunc,
                                                      int              nrArguments)
{
  return do_command_case(_data,  pFunc, nrArguments, EventValueSourceGroup::Enum::RESTRICTED);
}

bool do_command_case_check(command_case_data         & data,
                           int                         nrArguments,
                           EventValueSourceGroup::Enum group)
{
  // The data struct is re-used on each attempt to process an internal command.
  // Re-initialize the only two members that may have been altered by a previous call.
  data.retval = false;
  free_string(data.status);

  if (!checkSourceFlags(data.event->Source, group)) {
    data.status = return_incorrect_source();
    return false;
  }

  // FIXME TD-er: Do not check nr arguments from MQTT source.
  // See https://github.com/letscontrolit/ESPEasy/issues/3344
  // C005 does recreate command partly from topic and published message
  // e.g. ESP_Easy/Bathroom_pir_env/GPIO/14 with data 0 or 1
  // This only allows for 2 parameters, but some commands need more arguments (default to "0")
  const bool mustCheckNrArguments = data.event->Source != EventValueSource::Enum::VALUE_SOURCE_MQTT;

  if (mustCheckNrArguments) {
    if (!checkNrArguments(data.cmd, data.line, nrArguments)) {
      data.status = return_incorrect_nr_arguments();

      // data.retval = false;
      return true;    // Command is handled
    }
  }
  data.retval = true; // Mark the command should be executed.
  return true;        // Command is handled
}

bool InternalCommands::do_command_case(command_case_data         & data,
                                       command_function_fs         pFunc,
                                       int                         nrArguments,
                                       EventValueSourceGroup::Enum group)
{
  if (do_command_case_check(data, nrArguments, group)) {
    // It has been handled, check if we need to execute it.
    // FIXME TD-er: Must change command function signature to use const String&
    START_TIMER;
    data.status = pFunc(data.event, data.line.c_str());
    STOP_TIMER(COMMAND_EXEC_INTERNAL);
    return true;
  }
  return false;
}

bool InternalCommands::do_command_case(command_case_data         & data,
                                       command_function            pFunc,
                                       int                         nrArguments,
                                       EventValueSourceGroup::Enum group)
{
  if (do_command_case_check(data, nrArguments, group)) {
    // It has been handled, check if we need to execute it.
    // FIXME TD-er: Must change command function signature to use const String&
    START_TIMER;
    data.status = pFunc(data.event, data.line.c_str());
    STOP_TIMER(COMMAND_EXEC_INTERNAL);
    return true;
  }
  return false;
}

bool InternalCommands::executeInternalCommand()
{
  // Simple macro to match command to function call.

  // EventValueSourceGroup::Enum::ALL
  #define COMMAND_CASE_A(C, NARGS) \
  do_command_case_all(&C, NARGS); break;

  // EventValueSourceGroup::Enum::RESTRICTED
  #define COMMAND_CASE_R(C, NARGS) \
   do_command_case_all_restricted(&C, NARGS); break;


  const ESPEasy_cmd_e cmd = match_ESPEasy_internal_command(_data.cmd_lc);

  _data.retval = false;

  if (cmd == ESPEasy_cmd_e::NotMatched) {
    return false;
  }

  // FIXME TD-er: Should we execute command when number of arguments is wrong?

  // FIXME TD-er: must determine nr arguments where NARGS is set to -1
  switch (cmd) {
    case ESPEasy_cmd_e::accessinfo:                 COMMAND_CASE_A(Command_AccessInfo_Ls,       0); // Network Command
    case ESPEasy_cmd_e::asyncevent:                 COMMAND_CASE_A(Command_Rules_Async_Events, -1); // Rule.h
#ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
    case ESPEasy_cmd_e::background:                 COMMAND_CASE_R(Command_Background, 1);          // Diagnostic.h
#endif // ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
#ifdef USES_C012
    case ESPEasy_cmd_e::blynkget:                   COMMAND_CASE_A(Command_Blynk_Get, -1);
#endif // ifdef USES_C012
#ifdef USES_C015
    case ESPEasy_cmd_e::blynkset:                   COMMAND_CASE_R(Command_Blynk_Set, -1);
#endif // ifdef USES_C015
    case ESPEasy_cmd_e::build:                      COMMAND_CASE_A(Command_Settings_Build, 1);      // Settings.h
    case ESPEasy_cmd_e::clearaccessblock:           COMMAND_CASE_R(Command_AccessInfo_Clear,   0);  // Network Command
    case ESPEasy_cmd_e::clearpassword:              COMMAND_CASE_R(Command_Settings_Password_Clear, 1);      // Settings.h
    case ESPEasy_cmd_e::clearrtcram:                COMMAND_CASE_R(Command_RTC_Clear,          0);           // RTC.h
#ifdef ESP8266
    case ESPEasy_cmd_e::clearsdkwifi:               COMMAND_CASE_R(Command_System_Erase_SDK_WiFiconfig,  0); // System.h
    case ESPEasy_cmd_e::clearwifirfcal:             COMMAND_CASE_R(Command_System_Erase_RFcal,  0);          // System.h
#endif // ifdef ESP8266
    case ESPEasy_cmd_e::config:                     COMMAND_CASE_R(Command_Task_RemoteConfig, -1);           // Tasks.h
    case ESPEasy_cmd_e::controllerdisable:          COMMAND_CASE_R(Command_Controller_Disable, 1);           // Controller.h
    case ESPEasy_cmd_e::controllerenable:           COMMAND_CASE_R(Command_Controller_Enable,  1);           // Controller.h
    case ESPEasy_cmd_e::datetime:                   COMMAND_CASE_R(Command_DateTime,             2);         // Time.h
    case ESPEasy_cmd_e::debug:                      COMMAND_CASE_R(Command_Debug,                1);         // Diagnostic.h
    case ESPEasy_cmd_e::dec:                        COMMAND_CASE_A(Command_Rules_Dec,           -1);         // Rules.h
    case ESPEasy_cmd_e::deepsleep:                  COMMAND_CASE_R(Command_System_deepSleep,     1);         // System.h
    case ESPEasy_cmd_e::delay:                      COMMAND_CASE_R(Command_Delay,                1);         // Timers.h
#if FEATURE_PLUGIN_PRIORITY
    case ESPEasy_cmd_e::disableprioritytask:        COMMAND_CASE_R(Command_PriorityTask_Disable, 1);         // Tasks.h
#endif // if FEATURE_PLUGIN_PRIORITY
    case ESPEasy_cmd_e::dns:                        COMMAND_CASE_R(Command_DNS,                  1);         // Network Command
    case ESPEasy_cmd_e::dst:                        COMMAND_CASE_R(Command_DST,                  1);         // Time.h
#if FEATURE_ETHERNET
    case ESPEasy_cmd_e::ethphyadr:                  COMMAND_CASE_R(Command_ETH_Phy_Addr,   1);               // Network Command
    case ESPEasy_cmd_e::ethpinmdc:                  COMMAND_CASE_R(Command_ETH_Pin_mdc,    1);               // Network Command
    case ESPEasy_cmd_e::ethpinmdio:                 COMMAND_CASE_R(Command_ETH_Pin_mdio,   1);               // Network Command
    case ESPEasy_cmd_e::ethpinpower:                COMMAND_CASE_R(Command_ETH_Pin_power,  1);               // Network Command
    case ESPEasy_cmd_e::ethphytype:                 COMMAND_CASE_R(Command_ETH_Phy_Type,   1);               // Network Command
    case ESPEasy_cmd_e::ethclockmode:               COMMAND_CASE_R(Command_ETH_Clock_Mode, 1);               // Network Command
    case ESPEasy_cmd_e::ethip:                      COMMAND_CASE_R(Command_ETH_IP,         1);               // Network Command
    case ESPEasy_cmd_e::ethgateway:                 COMMAND_CASE_R(Command_ETH_Gateway,    1);               // Network Command
    case ESPEasy_cmd_e::ethsubnet:                  COMMAND_CASE_R(Command_ETH_Subnet,     1);               // Network Command
    case ESPEasy_cmd_e::ethdns:                     COMMAND_CASE_R(Command_ETH_DNS,        1);               // Network Command
    case ESPEasy_cmd_e::ethdisconnect:              COMMAND_CASE_A(Command_ETH_Disconnect, 0);               // Network Command
    case ESPEasy_cmd_e::ethwifimode:                COMMAND_CASE_R(Command_ETH_Wifi_Mode,  1);               // Network Command
#endif // FEATURE_ETHERNET
    case ESPEasy_cmd_e::erasesdkwifi:               COMMAND_CASE_R(Command_WiFi_Erase,     0);               // WiFi.h
    case ESPEasy_cmd_e::event:                      COMMAND_CASE_A(Command_Rules_Events,  -1);               // Rule.h
    case ESPEasy_cmd_e::executerules:               COMMAND_CASE_A(Command_Rules_Execute, -1);               // Rule.h
    case ESPEasy_cmd_e::factoryreset:               COMMAND_CASE_R(Command_Settings_FactoryReset, 0);        // Settings.h
    case ESPEasy_cmd_e::gateway:                    COMMAND_CASE_R(Command_Gateway,     1);                  // Network Command
    case ESPEasy_cmd_e::gpio:                       COMMAND_CASE_A(Command_GPIO,        2);                  // Gpio.h
    case ESPEasy_cmd_e::gpiotoggle:                 COMMAND_CASE_A(Command_GPIO_Toggle, 1);                  // Gpio.h
    case ESPEasy_cmd_e::hiddenssid:                 COMMAND_CASE_R(Command_Wifi_HiddenSSID, 1);              // wifi.h
    case ESPEasy_cmd_e::i2cscanner:                 COMMAND_CASE_R(Command_i2c_Scanner, -1);                 // i2c.h
    case ESPEasy_cmd_e::inc:                        COMMAND_CASE_A(Command_Rules_Inc,   -1);                 // Rules.h
    case ESPEasy_cmd_e::ip:                         COMMAND_CASE_R(Command_IP,           1);                 // Network Command
#if FEATURE_USE_IPV6
    case ESPEasy_cmd_e::ip6:                        COMMAND_CASE_A(Command_show_all_IP6,           0);                 // Network Command
#endif
#ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
    case ESPEasy_cmd_e::jsonportstatus:             COMMAND_CASE_A(Command_JSONPortStatus, -1);              // Diagnostic.h
#endif // ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
    #if FEATURE_LAT_LONG_VAR_CMD
    case ESPEasy_cmd_e::latitude:                   COMMAND_CASE_A(Command_Latitude,               -1);      // LatitudeLongitude.h
    #endif // if FEATURE_LAT_LONG_VAR_CMD
    case ESPEasy_cmd_e::let:                        COMMAND_CASE_A(Command_Rules_Let,               2);      // Rules.h
    #if FEATURE_STRING_VARIABLES
    case ESPEasy_cmd_e::letstr:                     COMMAND_CASE_A(Command_Rules_LetStr,            2);      // Rules.h
    #endif // if FEATURE_STRING_VARIABLES
    case ESPEasy_cmd_e::load:                       COMMAND_CASE_A(Command_Settings_Load,           0);      // Settings.h
    case ESPEasy_cmd_e::logentry:                   COMMAND_CASE_A(Command_logentry,               -1);      // Diagnostic.h
    #if FEATURE_LAT_LONG_VAR_CMD
    case ESPEasy_cmd_e::longitude:                  COMMAND_CASE_A(Command_Longitude,              -1);      // LatitudeLongitude.h
    #endif // if FEATURE_LAT_LONG_VAR_CMD
    case ESPEasy_cmd_e::looptimerset:               COMMAND_CASE_A(Command_Loop_Timer_Set,          3);      // Timers.h
    case ESPEasy_cmd_e::looptimerset_ms:            COMMAND_CASE_A(Command_Loop_Timer_Set_ms,       3);      // Timers.h
    case ESPEasy_cmd_e::looptimersetandrun:         COMMAND_CASE_A(Command_Loop_Timer_SetAndRun,    3);      // Timers.h
    case ESPEasy_cmd_e::looptimersetandrun_ms:      COMMAND_CASE_A(Command_Loop_Timer_SetAndRun_ms, 3);      // Timers.h
    case ESPEasy_cmd_e::longpulse:                  COMMAND_CASE_A(Command_GPIO_LongPulse,          5);      // GPIO.h
    case ESPEasy_cmd_e::longpulse_ms:               COMMAND_CASE_A(Command_GPIO_LongPulse_Ms,       5);      // GPIO.h
#ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
    case ESPEasy_cmd_e::logportstatus:              COMMAND_CASE_A(Command_logPortStatus,     0);            // Diagnostic.h
    case ESPEasy_cmd_e::lowmem:                     COMMAND_CASE_A(Command_Lowmem,            0);            // Diagnostic.h
#endif // ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
#ifdef USES_P009
    case ESPEasy_cmd_e::mcpgpio:                    COMMAND_CASE_A(Command_GPIO,              2);            // Gpio.h
    case ESPEasy_cmd_e::mcpgpiorange:               COMMAND_CASE_A(Command_GPIO_McpGPIORange, -1);           // Gpio.h
    case ESPEasy_cmd_e::mcpgpiopattern:             COMMAND_CASE_A(Command_GPIO_McpGPIOPattern, -1);         // Gpio.h
    case ESPEasy_cmd_e::mcpgpiotoggle:              COMMAND_CASE_A(Command_GPIO_Toggle,       1);            // Gpio.h
    case ESPEasy_cmd_e::mcplongpulse:               COMMAND_CASE_A(Command_GPIO_LongPulse,    3);            // GPIO.h
    case ESPEasy_cmd_e::mcplongpulse_ms:            COMMAND_CASE_A(Command_GPIO_LongPulse_Ms, 3);            // GPIO.h
    case ESPEasy_cmd_e::mcpmode:                    COMMAND_CASE_A(Command_GPIO_Mode,         2);            // Gpio.h
    case ESPEasy_cmd_e::mcpmoderange:               COMMAND_CASE_A(Command_GPIO_ModeRange,    3);            // Gpio.h
    case ESPEasy_cmd_e::mcppulse:                   COMMAND_CASE_A(Command_GPIO_Pulse,        3);            // GPIO.h
#endif // ifdef USES_P009
    case ESPEasy_cmd_e::monitor:                    COMMAND_CASE_A(Command_GPIO_Monitor,      2);            // GPIO.h
    case ESPEasy_cmd_e::monitorrange:               COMMAND_CASE_A(Command_GPIO_MonitorRange, 3);            // GPIO.h
#ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
    case ESPEasy_cmd_e::malloc:                     COMMAND_CASE_A(Command_Malloc,         1);               // Diagnostic.h
    case ESPEasy_cmd_e::meminfo:                    COMMAND_CASE_A(Command_MemInfo,        0);               // Diagnostic.h
    case ESPEasy_cmd_e::meminfodetail:              COMMAND_CASE_A(Command_MemInfo_detail, 0);               // Diagnostic.h
#endif // ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
    case ESPEasy_cmd_e::name:                       COMMAND_CASE_R(Command_Settings_Name,        1);         // Settings.h
    case ESPEasy_cmd_e::nosleep:                    COMMAND_CASE_R(Command_System_NoSleep,       1);         // System.h
#if FEATURE_NOTIFIER
    case ESPEasy_cmd_e::notify:                     COMMAND_CASE_R(Command_Notifications_Notify, -1);        // Notifications.h
#endif // if FEATURE_NOTIFIER
    case ESPEasy_cmd_e::ntphost:                    COMMAND_CASE_R(Command_NTPHost,              1);         // Time.h
#if FEATURE_DALLAS_HELPER && FEATURE_COMMAND_OWSCAN
    case ESPEasy_cmd_e::owscan:                     COMMAND_CASE_R(Command_OneWire_Owscan,       -1);         // OneWire.h
#endif // if FEATURE_DALLAS_HELPER && FEATURE_COMMAND_OWSCAN
#ifdef USES_P019
    case ESPEasy_cmd_e::pcfgpio:                    COMMAND_CASE_A(Command_GPIO,                 2);         // Gpio.h
    case ESPEasy_cmd_e::pcfgpiorange:               COMMAND_CASE_A(Command_GPIO_PcfGPIORange,   -1);         // Gpio.h
    case ESPEasy_cmd_e::pcfgpiopattern:             COMMAND_CASE_A(Command_GPIO_PcfGPIOPattern, -1);         // Gpio.h
    case ESPEasy_cmd_e::pcfgpiotoggle:              COMMAND_CASE_A(Command_GPIO_Toggle,          1);         // Gpio.h
    case ESPEasy_cmd_e::pcflongpulse:               COMMAND_CASE_A(Command_GPIO_LongPulse,       3);         // GPIO.h
    case ESPEasy_cmd_e::pcflongpulse_ms:            COMMAND_CASE_A(Command_GPIO_LongPulse_Ms,    3);         // GPIO.h
    case ESPEasy_cmd_e::pcfmode:                    COMMAND_CASE_A(Command_GPIO_Mode,            2);         // Gpio.h
    case ESPEasy_cmd_e::pcfmoderange:               COMMAND_CASE_A(Command_GPIO_ModeRange,       3);         // Gpio.h   ************
    case ESPEasy_cmd_e::pcfpulse:                   COMMAND_CASE_A(Command_GPIO_Pulse,           3);         // GPIO.h
#endif // ifdef USES_P019
    case ESPEasy_cmd_e::password:                   COMMAND_CASE_R(Command_Settings_Password, 1);            // Settings.h
#if FEATURE_POST_TO_HTTP
    case ESPEasy_cmd_e::posttohttp:                 COMMAND_CASE_A(Command_HTTP_PostToHTTP,  -1);            // HTTP.h
#if FEATURE_HTTP_TLS
    case ESPEasy_cmd_e::posttohttps:                COMMAND_CASE_A(Command_HTTP_PostToHTTPS, -1);            // HTTP.h
#endif // if FEATURE_HTTP_TLS
#endif // if FEATURE_POST_TO_HTTP
#if FEATURE_CUSTOM_PROVISIONING
    case ESPEasy_cmd_e::provision:                  COMMAND_CASE_A(Command_Provisioning_Dispatcher, -1);     // Provisioning.h
# ifdef PLUGIN_BUILD_MAX_ESP32

    // FIXME DEPRECATED: Fallback for temporary backward compatibility
    case ESPEasy_cmd_e::provisionconfig:            COMMAND_CASE_A(Command_Provisioning_ConfigFallback,       0); // Provisioning.h
    case ESPEasy_cmd_e::provisionsecurity:          COMMAND_CASE_A(Command_Provisioning_SecurityFallback,     0); // Provisioning.h
#  if FEATURE_NOTIFIER
    case ESPEasy_cmd_e::provisionnotification:      COMMAND_CASE_A(Command_Provisioning_NotificationFallback, 0); // Provisioning.h
#  endif // if FEATURE_NOTIFIER
    case ESPEasy_cmd_e::provisionprovision:         COMMAND_CASE_A(Command_Provisioning_ProvisionFallback,    0); // Provisioning.h
    case ESPEasy_cmd_e::provisionrules:             COMMAND_CASE_A(Command_Provisioning_RulesFallback,        1); // Provisioning.h
    case ESPEasy_cmd_e::provisionfirmware:          COMMAND_CASE_A(Command_Provisioning_FirmwareFallback,     1); // Provisioning.h
# endif // ifdef PLUGIN_BUILD_MAX_ESP32
#endif // if FEATURE_CUSTOM_PROVISIONING
    case ESPEasy_cmd_e::pulse:                      COMMAND_CASE_A(Command_GPIO_Pulse,        3);                 // GPIO.h
#if FEATURE_MQTT
    case ESPEasy_cmd_e::publish:                    COMMAND_CASE_A(Command_MQTT_Publish,     -1);                 // MQTT.h
    case ESPEasy_cmd_e::publishr:                   COMMAND_CASE_A(Command_MQTT_PublishR,    -1);                 // MQTT.h
#endif // if FEATURE_MQTT
#if FEATURE_PUT_TO_HTTP
    case ESPEasy_cmd_e::puttohttp:                  COMMAND_CASE_A(Command_HTTP_PutToHTTP,  -1);                  // HTTP.h
#if FEATURE_HTTP_TLS
    case ESPEasy_cmd_e::puttohttps:                 COMMAND_CASE_A(Command_HTTP_PutToHTTPS, -1);                  // HTTP.h
#endif // if FEATURE_HTTP_TLS
#endif // if FEATURE_PUT_TO_HTTP
    case ESPEasy_cmd_e::pwm:                        COMMAND_CASE_A(Command_GPIO_PWM,          4);                 // GPIO.h
    case ESPEasy_cmd_e::reboot:                     COMMAND_CASE_A(Command_System_Reboot,              0);        // System.h
    case ESPEasy_cmd_e::resetflashwritecounter:     COMMAND_CASE_A(Command_RTC_resetFlashWriteCounter, 0);        // RTC.h
    case ESPEasy_cmd_e::restart:                    COMMAND_CASE_A(Command_System_Reboot,              0);        // System.h
    case ESPEasy_cmd_e::rtttl:                      COMMAND_CASE_A(Command_GPIO_RTTTL,                -1);        // GPIO.h
    case ESPEasy_cmd_e::rules:                      COMMAND_CASE_A(Command_Rules_UseRules,             1);        // Rule.h
    case ESPEasy_cmd_e::save:                       COMMAND_CASE_R(Command_Settings_Save, 0);                     // Settings.h
    case ESPEasy_cmd_e::scheduletaskrun:            COMMAND_CASE_A(Command_ScheduleTask_Run, 2);                  // Tasks.h

#if FEATURE_SD
    case ESPEasy_cmd_e::sdcard:                     COMMAND_CASE_R(Command_SD_LS,         0);                     // SDCARDS.h
    case ESPEasy_cmd_e::sdremove:                   COMMAND_CASE_R(Command_SD_Remove,     1);                     // SDCARDS.h
#endif // if FEATURE_SD

#if FEATURE_ESPEASY_P2P

    // FIXME TD-er: These send commands, can we determine the nr of arguments?
    case ESPEasy_cmd_e::sendto:                     COMMAND_CASE_A(Command_UPD_SendTo,      2);      // UDP.h
#endif // if FEATURE_ESPEASY_P2P
#if FEATURE_SEND_TO_HTTP
    case ESPEasy_cmd_e::sendtohttp:                 COMMAND_CASE_A(Command_HTTP_SendToHTTP, 3);      // HTTP.h
#if FEATURE_HTTP_TLS
    case ESPEasy_cmd_e::sendtohttps:                COMMAND_CASE_A(Command_HTTP_SendToHTTPS, 3);     // HTTP.h
#endif // if FEATURE_HTTP_TLS
#endif // FEATURE_SEND_TO_HTTP
    case ESPEasy_cmd_e::sendtoudp:                  COMMAND_CASE_A(Command_UDP_SendToUPD,   3);      // UDP.h
    case ESPEasy_cmd_e::sendtoudpmix:               COMMAND_CASE_A(Command_UDP_SendToUPDMix, 3);     // UDP.h
#ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
    case ESPEasy_cmd_e::serialfloat:                COMMAND_CASE_R(Command_SerialFloat,    0);       // Diagnostic.h
#endif // ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
    case ESPEasy_cmd_e::settings:                   COMMAND_CASE_R(Command_Settings_Print, 0);       // Settings.h
#if FEATURE_SERVO
    case ESPEasy_cmd_e::servo:                      COMMAND_CASE_A(Command_Servo,          3);       // Servo.h
#endif // if FEATURE_SERVO

    case ESPEasy_cmd_e::status:                     COMMAND_CASE_A(Command_GPIO_Status,          2); // GPIO.h
    case ESPEasy_cmd_e::subnet:                     COMMAND_CASE_R(Command_Subnet, 1);               // Network Command
#if FEATURE_MQTT
    case ESPEasy_cmd_e::subscribe:                  COMMAND_CASE_A(Command_MQTT_Subscribe, 1);       // MQTT.h
#endif // if FEATURE_MQTT
#ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
    case ESPEasy_cmd_e::sysload:                    COMMAND_CASE_A(Command_SysLoad,        0);       // Diagnostic.h
#endif // ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
    case ESPEasy_cmd_e::taskclear:                  COMMAND_CASE_R(Command_Task_Clear,    1);        // Tasks.h
    case ESPEasy_cmd_e::taskclearall:               COMMAND_CASE_R(Command_Task_ClearAll, 0);        // Tasks.h
    case ESPEasy_cmd_e::taskdisable:                COMMAND_CASE_R(Command_Task_Disable,  1);        // Tasks.h
    case ESPEasy_cmd_e::taskenable:                 COMMAND_CASE_R(Command_Task_Enable,   1);        // Tasks.h
    case ESPEasy_cmd_e::taskrun:                    COMMAND_CASE_A(Command_Task_Run,            1);  // Tasks.h
    case ESPEasy_cmd_e::taskrunat:                  COMMAND_CASE_A(Command_Task_Run,            2);  // Tasks.h
    case ESPEasy_cmd_e::taskvalueset:               COMMAND_CASE_A(Command_Task_ValueSet,       3);  // Tasks.h
    #if FEATURE_STRING_VARIABLES
    case ESPEasy_cmd_e::taskvaluesetderived:        COMMAND_CASE_A(Command_Task_ValueSetDerived,      5); // Tasks.h
    case ESPEasy_cmd_e::taskvaluesetpresentation:   COMMAND_CASE_A(Command_Task_ValueSetPresentation, 3); // Tasks.h
    #endif // if FEATURE_STRING_VARIABLES
    case ESPEasy_cmd_e::taskvaluetoggle:            COMMAND_CASE_A(Command_Task_ValueToggle,    2);  // Tasks.h
    case ESPEasy_cmd_e::taskvaluesetandrun:         COMMAND_CASE_A(Command_Task_ValueSetAndRun, 3);  // Tasks.h
    case ESPEasy_cmd_e::timerpause:                 COMMAND_CASE_A(Command_Timer_Pause,  1);         // Timers.h
    case ESPEasy_cmd_e::timerresume:                COMMAND_CASE_A(Command_Timer_Resume, 1);         // Timers.h
    case ESPEasy_cmd_e::timerset:                   COMMAND_CASE_A(Command_Timer_Set,    2);         // Timers.h
    case ESPEasy_cmd_e::timerset_ms:                COMMAND_CASE_A(Command_Timer_Set_ms, 2);         // Timers.h
    case ESPEasy_cmd_e::timezone:                   COMMAND_CASE_R(Command_TimeZone, 1);             // Time.h
    case ESPEasy_cmd_e::tone:                       COMMAND_CASE_A(Command_GPIO_Tone, 3);            // GPIO.h
    case ESPEasy_cmd_e::udpport:                    COMMAND_CASE_R(Command_UDP_Port,      1);        // UDP.h
#if FEATURE_ESPEASY_P2P
    case ESPEasy_cmd_e::udptest:                    COMMAND_CASE_R(Command_UDP_Test,      2);        // UDP.h
#endif // if FEATURE_ESPEASY_P2P
    case ESPEasy_cmd_e::unit:                       COMMAND_CASE_R(Command_Settings_Unit, 1);        // Settings.h
    case ESPEasy_cmd_e::unmonitor:                  COMMAND_CASE_A(Command_GPIO_UnMonitor, 2);       // GPIO.h
    case ESPEasy_cmd_e::unmonitorrange:             COMMAND_CASE_A(Command_GPIO_UnMonitorRange, 3);  // GPIO.h
    case ESPEasy_cmd_e::usentp:                     COMMAND_CASE_R(Command_useNTP, 1);               // Time.h
#ifndef LIMIT_BUILD_SIZE
    case ESPEasy_cmd_e::wdconfig:                   COMMAND_CASE_R(Command_WD_Config, 3);            // WD.h
    case ESPEasy_cmd_e::wdread:                     COMMAND_CASE_R(Command_WD_Read,   2);            // WD.h
#endif // ifndef LIMIT_BUILD_SIZE

    case ESPEasy_cmd_e::wifiallowap:                COMMAND_CASE_R(Command_Wifi_AllowAP,    0);      // WiFi.h
    case ESPEasy_cmd_e::wifiapmode:                 COMMAND_CASE_R(Command_Wifi_APMode,     0);      // WiFi.h
    case ESPEasy_cmd_e::wificonnect:                COMMAND_CASE_A(Command_Wifi_Connect,    0);      // WiFi.h
    case ESPEasy_cmd_e::wifidisconnect:             COMMAND_CASE_A(Command_Wifi_Disconnect, 0);      // WiFi.h
    case ESPEasy_cmd_e::wifikey:                    COMMAND_CASE_R(Command_Wifi_Key,        1);      // WiFi.h
    case ESPEasy_cmd_e::wifikey2:                   COMMAND_CASE_R(Command_Wifi_Key2,       1);      // WiFi.h
    case ESPEasy_cmd_e::wifimode:                   COMMAND_CASE_R(Command_Wifi_Mode,       1);      // WiFi.h
    case ESPEasy_cmd_e::wifiscan:                   COMMAND_CASE_R(Command_Wifi_Scan,       0);      // WiFi.h
    case ESPEasy_cmd_e::wifissid:                   COMMAND_CASE_R(Command_Wifi_SSID,       1);      // WiFi.h
    case ESPEasy_cmd_e::wifissid2:                  COMMAND_CASE_R(Command_Wifi_SSID2,      1);      // WiFi.h
    case ESPEasy_cmd_e::wifistamode:                COMMAND_CASE_R(Command_Wifi_STAMode,    0);      // WiFi.h


    case ESPEasy_cmd_e::NotMatched:
      return false;

      // Do not add default: here
      // The compiler will then warn when a command is not included
  }

  #undef COMMAND_CASE_R
  #undef COMMAND_CASE_A
  return _data.retval;
}

#include "../Commands/Tasks.h"


#include "../../ESPEasy_common.h"
#include "../../_Plugin_Helper.h"

#include "../Commands/Common.h"

#include "../DataStructs/TimingStats.h"

#include "../ESPEasyCore/Controller.h"
#include "../ESPEasyCore/Serial.h"

#include "../Globals/ESPEasy_Scheduler.h"
#include "../Globals/RulesCalculate.h"
#include "../Globals/RuntimeData.h"
#include "../Globals/Settings.h"

#include "../Helpers/ESPEasy_UnitOfMeasure.h"
#include "../Helpers/Misc.h"
#include "../Helpers/Numerical.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringParser.h"

// taskIndex = (event->Par1 - 1);   Par1 is here for 1 ... TASKS_MAX
bool validTaskIndexVar(struct EventStruct *event, taskIndex_t& taskIndex)
{
  if (event == nullptr) { return false; }

  if (event->Par1 <= 0) { return false; }
  const taskIndex_t tmp_taskIndex = static_cast<taskIndex_t>(event->Par1 - 1);

  if (!validTaskIndex(tmp_taskIndex)) { return false; }

  taskIndex = tmp_taskIndex;

  return true;
}

// taskIndex = (event->Par1 - 1);   Par1 is here for 1 ... TASKS_MAX
// varNr = event->Par2 - 1;
bool validTaskVars(struct EventStruct *event, taskIndex_t& taskIndex, unsigned int& varNr)
{
  if (event == nullptr) { return false; }

  taskIndex_t tmp_taskIndex;
  if (!validTaskIndexVar(event, tmp_taskIndex)) { return false; }

  varNr = 0;

  if (event->Par2 > 0 && event->Par2 <= VARS_PER_TASK) {
    varNr = event->Par2 - 1;
    taskIndex = tmp_taskIndex;
    return true;
  }

  return false;
}

bool validateAndParseTaskIndexArguments(struct EventStruct * event, const char *Line, taskIndex_t &taskIndex)
{
  if (!validTaskIndexVar(event, taskIndex)) {
    String taskName;
    taskIndex_t tmpTaskIndex = taskIndex;
    if ((event->Par1 <= 0 || event->Par1 >= INVALID_TASK_INDEX) && GetArgv(Line, taskName, 2)) {
      tmpTaskIndex = findTaskIndexByName(taskName, true);
      if (tmpTaskIndex != INVALID_TASK_INDEX) {
        event->Par1 = tmpTaskIndex + 1;
      }
    }
    return validTaskIndexVar(event, taskIndex);
  }
  return true;
}

/**
 * parse TaskName/TaskValue when not numeric for task name and value name and validate values
 */
bool validateAndParseTaskValueArguments(struct EventStruct * event, const char *Line, taskIndex_t &taskIndex, unsigned int &varNr)
{
  if (!validTaskVars(event, taskIndex, varNr) || (event->Par2 <= 0 || event->Par2 >= VARS_PER_TASK))  // Extra check required because of shortcutting in validTaskVars()
  { 
    String taskName;
    taskIndex_t tmpTaskIndex = taskIndex;
    if ((event->Par1 <= 0 || event->Par1 >= INVALID_TASK_INDEX) && GetArgv(Line, taskName, 2)) {
      tmpTaskIndex = findTaskIndexByName(taskName, true);
      if (tmpTaskIndex != INVALID_TASK_INDEX) {
        event->Par1 = tmpTaskIndex + 1;
      }
    }
    String valueName;
    if ((event->Par2 <= 0 || event->Par2 >= VARS_PER_TASK) && event->Par1 - 1 != INVALID_TASK_INDEX && GetArgv(Line, valueName, 3))
    {
      uint8_t tmpVarNr = findDeviceValueIndexByName(valueName, event->Par1 - 1);
      if (tmpVarNr != VARS_PER_TASK) {
        event->Par2 = tmpVarNr + 1;
      }
    }
    if (!validTaskVars(event, taskIndex, varNr)) return false; 
  }

  return true;
}

const __FlashStringHelper * taskValueSet(struct EventStruct *event, const char *Line, taskIndex_t& taskIndex, bool& success)
{
  String TmpStr1;
  unsigned int varNr;

  if (!validateAndParseTaskValueArguments(event, Line, taskIndex, varNr)) {
    success = false;
    return F("INVALID_PARAMETERS");
  }
  if (!Settings.AllowTaskValueSetAllPlugins() && 
      getPluginID_from_TaskIndex(taskIndex).value != 33) { // PluginID 33 = Dummy Device
    success = false;
    return F("NOT_A_DUMMY_TASK");
  }
  if (!Settings.TaskDeviceEnabled[taskIndex]) {
    success = false;
    return F("TASK_NOT_ENABLED");
  }
  const uint8_t valueCount = getValueCountForTask(taskIndex);
  if (valueCount <= varNr) {
    success = false;
    return F("INVALID_VAR_INDEX");
  }

  EventStruct tmpEvent(taskIndex);
  if (GetArgv(Line, TmpStr1, 4)) {
    const Sensor_VType sensorType = tmpEvent.getSensorType();

    // FIXME TD-er: Must check if the value has to be computed and not convert to double when sensor type is 64 bit int.

    // Perform calculation with float result.
    ESPEASY_RULES_FLOAT_TYPE result{};

    if (isError(Calculate(TmpStr1, result))) {
      success = false;
      return F("CALCULATION_ERROR");
    }
    #ifndef BUILD_NO_DEBUG
    addLog(LOG_LEVEL_INFO, strformat(
      F("taskValueSet: %s  taskindex: %d varNr: %d result: %f type: %d"),
      Line,
      taskIndex,
      varNr, result, sensorType));
    #endif

    UserVar.set(taskIndex, varNr, result, sensorType);
  } else  {
    // TODO: Get Task description and var name
    serialPrintln(formatUserVarNoCheck(&tmpEvent, varNr));
  }
  success = true;
  return return_command_success_flashstr();
}

const __FlashStringHelper * Command_Task_Clear(struct EventStruct *event, const char *Line)
{
  taskIndex_t  taskIndex;

  if (!validateAndParseTaskIndexArguments(event, Line, taskIndex)) {
    return F("INVALID_PARAMETERS"); 
  }

  taskClear(taskIndex, true);
  return return_command_success_flashstr();
}

const __FlashStringHelper * Command_Task_ClearAll(struct EventStruct *event, const char *Line)
{
  for (taskIndex_t t = 0; t < TASKS_MAX; t++) {
    taskClear(t, false);
  }
  return return_command_success_flashstr();
}

const __FlashStringHelper * Command_Task_EnableDisable(struct EventStruct *event, bool enable, const char *Line)
{
  taskIndex_t  taskIndex;
  if (validateAndParseTaskIndexArguments(event, Line, taskIndex)) {
    // This is a command so no guarantee the taskIndex is correct in the event
    event->setTaskIndex(taskIndex);

    #if FEATURE_PLUGIN_PRIORITY
    if (Settings.isPriorityTask(event->TaskIndex)) {
      return return_command_failed_flashstr();
    }
    #endif // if FEATURE_PLUGIN_PRIORITY
    return return_command_boolean_result_flashstr(setTaskEnableStatus(event, enable));
  }
  return F("INVALID_PARAMETERS");
}

#if FEATURE_PLUGIN_PRIORITY
const __FlashStringHelper * Command_PriorityTask_DisableTask(struct EventStruct *event, const char *Line)
{
  taskIndex_t  taskIndex;

  if (validateAndParseTaskIndexArguments(event, Line, taskIndex)) {
    // This is a command so no guarantee the taskIndex is correct in the event
    event->setTaskIndex(taskIndex);

    if (Settings.isPowerManagerTask(event->TaskIndex))
    {
      Settings.setPowerManagerTask(event->TaskIndex, false);
      return return_command_success_flashstr();
    }
    // Handle other Priotiry task options
    Settings.setTaskEnableReadonly(event->TaskIndex, false);
    return return_command_failed_flashstr();
  }
  return F("INVALID_PARAMETERS");
}
#endif // if FEATURE_PLUGIN_PRIORITY

const __FlashStringHelper * Command_Task_Disable(struct EventStruct *event, const char *Line)
{
  return Command_Task_EnableDisable(event, false, Line);
}

const __FlashStringHelper * Command_Task_Enable(struct EventStruct *event, const char *Line)
{
  return Command_Task_EnableDisable(event, true, Line);
}

#if FEATURE_PLUGIN_PRIORITY
const __FlashStringHelper * Command_PriorityTask_Disable(struct EventStruct *event, const char *Line)
{
  return Command_PriorityTask_DisableTask(event, Line);
}
#endif

const __FlashStringHelper * Command_Task_ValueSet(struct EventStruct *event, const char *Line)
{
  taskIndex_t taskIndex;
  bool success;
  return taskValueSet(event, Line, taskIndex, success);
}

#if FEATURE_STRING_VARIABLES
const __FlashStringHelper * Command_Task_ValueSetDerived(struct EventStruct *event, const char *Line)
{
  return taskValueSetString(event, Line, F(TASK_VALUE_DERIVED_PREFIX_TEMPLATE), F(TASK_VALUE_UOM_PREFIX_TEMPLATE), F(TASK_VALUE_VTYPE_PREFIX_TEMPLATE));
}

const __FlashStringHelper * Command_Task_ValueSetPresentation(struct EventStruct *event, const char *Line)
{
  return taskValueSetString(event, Line, F(TASK_VALUE_PRESENTATION_PREFIX_TEMPLATE));
}

const __FlashStringHelper * taskValueSetString(struct EventStruct *event, const char *Line, const __FlashStringHelper * storageTemplate, const __FlashStringHelper * uomTemplate, const __FlashStringHelper * vTypeTemplate)
{
  String taskName;
  taskIndex_t tmpTaskIndex = INVALID_TASK_INDEX;
  if ((event->Par1 <= 0 || event->Par1 >= INVALID_TASK_INDEX) && GetArgv(Line, taskName, 2)) {
    tmpTaskIndex = findTaskIndexByName(taskName, true);
    if (tmpTaskIndex != INVALID_TASK_INDEX) {
      event->Par1 = tmpTaskIndex + 1;
    }
  }
  String valueName;
  const bool hasValueName = GetArgv(Line, valueName, 3);
  if ((event->Par2 <= 0 || event->Par2 >= VARS_PER_TASK) && event->Par1 - 1 != INVALID_TASK_INDEX && hasValueName)
  {
    uint8_t tmpVarNr = findDeviceValueIndexByName(valueName, event->Par1 - 1);
    if (tmpVarNr != VARS_PER_TASK) {
      event->Par2 = tmpVarNr + 1;
    }
  }

  if (event->Par1 > 0 && validTaskIndex(event->Par1 - 1)) {
    taskName = getTaskDeviceName(event->Par1 - 1); // Taskname must be valid
  }

  if (!hasValueName || (event->Par1 > 0 && validTaskIndex(event->Par1 - 1) && event->Par2 > 0 && validTaskVarIndex(event->Par2 - 1))) {
    valueName = getTaskValueName(static_cast<taskIndex_t>(event->Par1 - 1), event->Par2 - 1); // Convert numeric var index into name
  }
  // addLog(LOG_LEVEL_INFO, strformat(F("TaskValueSetStorage: task: %s (%d), value: %s (%d), Line: %s"), taskName.c_str(), event->Par1, valueName.c_str(), event->Par2, Line)); // FIXME

  String argument;
  if (!taskName.isEmpty() && !valueName.isEmpty() && GetArgv(Line, argument, 4)) {
    taskName.toLowerCase();
    String orgValueName(valueName);
    valueName.toLowerCase();
    String key = strformat(storageTemplate, taskName.c_str(), valueName.c_str());
    const String key2 = strformat(F(TASK_VALUE_NAME_PREFIX_TEMPLATE), taskName.c_str(), valueName.c_str());
    setCustomStringVar(key, argument);
    if (getCustomStringVar(key2).isEmpty() || argument.isEmpty()) {
      setCustomStringVar(key2, argument.isEmpty() ? EMPTY_STRING : orgValueName);
    }
    if (uomTemplate != nullptr) { // We have a Unit of Measure template
      if (GetArgv(Line, argument, 5)) { // check for extra argument holding UoM
        key = strformat(uomTemplate, taskName.c_str(), valueName.c_str());
        if (!argument.isEmpty()) {
          #if FEATURE_TASKVALUE_UNIT_OF_MEASURE

          uint8_t i = 1; // Index 0 is empty/None
          String uom = toUnitOfMeasureName(i);
          while (i < 255 && !uom.isEmpty()) {
            if (argument.equalsIgnoreCase(uom)) {
              setCustomStringVar(key, uom);
              argument.clear();
              break;
            }
            ++i;
            uom = toUnitOfMeasureName(i);
          }
          if (!argument.isEmpty()) 
          #endif // if FEATURE_TASKVALUE_UNIT_OF_MEASURE
          {
            setCustomStringVar(key, argument);
          }
        } else {
          clearCustomStringVar(key);
        }
      }
    }
    if (vTypeTemplate != nullptr) { // We have a ValueType template
      if (GetArgv(Line, argument, 6)) { // check for extra argument holding VType
        key = strformat(vTypeTemplate, taskName.c_str(), valueName.c_str());
        if (!argument.isEmpty()) {
          constexpr uint8_t maxVType = static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_NOT_SET);

          for (uint8_t i = 0; i < maxVType; ++i) {
            const Sensor_VType vt = static_cast<Sensor_VType>(i);

            if ((getValueCountFromSensorType(vt, false) == 1) &&
                argument.equalsIgnoreCase(getSensorTypeLabel(vt))) {
              setCustomStringVar(key, getSensorTypeLabel(vt)); // Set 'official' VType label
              break;
            }
          }
        } else {
          clearCustomStringVar(key);
        }
      }
    }
    // addLog(LOG_LEVEL_INFO, strformat(F("TaskValueSetStorage: key: %s, argument: %s"), key.c_str(), argument.c_str())); // FIXME
    return return_command_success_flashstr();
  }
  return return_command_failed_flashstr(); // taskValueSet(event, Line, taskIndex, success);
}
#endif

const __FlashStringHelper * Command_Task_ValueToggle(struct EventStruct *event, const char *Line)
{
  taskIndex_t  taskIndex;
  unsigned int varNr;

  if (!validateAndParseTaskValueArguments(event, Line, taskIndex, varNr)) {
    return F("INVALID_PARAMETERS");
  }
  if (!Settings.TaskDeviceEnabled[taskIndex]) {
    return F("TASK_NOT_ENABLED");
  }

  EventStruct tmpEvent(taskIndex);
  const Sensor_VType sensorType = tmpEvent.getSensorType();
  const int    result       = lround(UserVar.getAsDouble(taskIndex, varNr, sensorType));
  if ((result == 0) || (result == 1)) {
    UserVar.set(taskIndex, varNr, (result == 0) ? 1.0 : 0.0, sensorType);
  }
  return return_command_success_flashstr();
}

const __FlashStringHelper * Command_Task_ValueSetAndRun(struct EventStruct *event, const char *Line)
{
  taskIndex_t taskIndex;
  bool success;
  const __FlashStringHelper * returnvalue = taskValueSet(event, Line, taskIndex, success);
  if (success)
  {
    struct EventStruct TempEvent(taskIndex);
    TempEvent.Source = event->Source;
    SensorSendTask(&TempEvent);

    return return_command_success_flashstr();
  }
  return returnvalue;
}

const __FlashStringHelper * Command_ScheduleTask_Run(struct EventStruct *event, const char* Line)
{
  taskIndex_t  taskIndex;

  if (!validateAndParseTaskIndexArguments(event, Line, taskIndex) || event->Par2 < 0) {
    return F("INVALID_PARAMETERS");
  }
  if (!Settings.TaskDeviceEnabled[taskIndex]) {
    return F("TASK_NOT_ENABLED");
  }

  uint32_t msecFromNow = 0;
  String par3;
  if (GetArgv(Line, par3, 3)) {
    if (validUIntFromString(par3, msecFromNow)) {
      Scheduler.schedule_task_device_timer(taskIndex, millis() + msecFromNow);
      return return_command_success_flashstr();
    }
  }
  return F("INVALID_PARAMETERS");  
}

const __FlashStringHelper * Command_Task_Run(struct EventStruct *event, const char *Line)
{
  taskIndex_t  taskIndex;

  if (!validateAndParseTaskIndexArguments(event, Line, taskIndex) || event->Par2 < 0) {
    return F("INVALID_PARAMETERS");
  }
  if (!Settings.TaskDeviceEnabled[taskIndex]) {
    return F("TASK_NOT_ENABLED");
  }
  uint32_t unixTime = 0;
  String par3;
  if (GetArgv(Line, par3, 3)) {
    validUIntFromString(par3, unixTime);
  }

  struct EventStruct TempEvent(taskIndex);
  TempEvent.Source = event->Source;
  SensorSendTask(&TempEvent, unixTime);

  return return_command_success_flashstr();
}

const __FlashStringHelper * Command_Task_RemoteConfig(struct EventStruct *event, const char *Line)
{
  struct EventStruct TempEvent(event->TaskIndex);
  String request = Line;

  // FIXME TD-er: Should we call ExecuteCommand here? The command is not parsed like any other call.
  remoteConfig(&TempEvent, request);
  return return_command_success_flashstr();
}

#include "../Commands/SDCARD.h"

#include "../../ESPEasy_common.h"
#include "../Commands/Common.h"


#if FEATURE_SD

#include "../ESPEasyCore/Serial.h"
#include "../Globals/Settings.h"
#include "../Helpers/StringConverter.h"

#include <SD.h>


void printDirectory(fs::File dir, int numTabs)
{
  while (true) {
    fs::File entry = dir.openNextFile();

    if (!entry) {
      // no more files
      break;
    }

    for (uint8_t i = 0; i < numTabs; i++) {
      serialPrint("\t");
    }
    serialPrint(entry.name());

    if (entry.isDirectory()) {
      serialPrintln("/");
      printDirectory(entry, numTabs + 1);
    } else {
      // files have sizes, directories do not
      serialPrint("\t\t");
      serialPrintln(String(entry.size(), DEC));
    }
    entry.close();
  }
}


const __FlashStringHelper * Command_SD_LS(struct EventStruct *event, const char* Line)
{
  fs::File root = SD.open("/");
  root.rewindDirectory();
  printDirectory(root, 0);
  root.close();
  return return_see_serial(event);
}

String Command_SD_Remove(struct EventStruct *event, const char* Line)
{
  // FIXME TD-er: This one is not using parseString* function
  String fname = Line;
  fname = fname.substring(9);
  SD.remove((char*)fname.c_str());
  return return_result(event, concat(F("Removing:"), fname));
}
#endif

#include "../Commands/RTC.h"

#include "../../ESPEasy_common.h"


#include "../Commands/Common.h"

#include "../DataStructs/RTCStruct.h"

#include "../Globals/RTC.h"

#include "../Helpers/ESPEasyRTC.h"


const __FlashStringHelper * Command_RTC_Clear(struct EventStruct *event, const char* Line)
{
	initRTC();
	return return_command_success_flashstr();
}

const __FlashStringHelper * Command_RTC_resetFlashWriteCounter(struct EventStruct *event, const char* Line)
{
	RTC.flashDayCounter = 0;
	return return_command_success_flashstr();
}

#include "../Commands/Time.h"


#include "../../ESPEasy_common.h"


#include "../Commands/Common.h"

#include "../DataStructs/ESPEasy_EventStruct.h"

#include "../ESPEasyCore/Serial.h"

#include "../Globals/ESPEasy_time.h"
#include "../Globals/Settings.h"

#include "../Helpers/ESPEasy_time_calc.h"
#include "../Helpers/StringConverter.h"


String Command_NTPHost(struct EventStruct *event, const char *Line)
{
  return Command_GetORSetString(event, F("NTPHost:"),
                                Line,
                                Settings.NTPHost,
                                sizeof(Settings.NTPHost),
                                1);
}

String Command_useNTP(struct EventStruct *event, const char *Line)
{
  if (HasArgv(Line, 2)) {
    Settings.UseNTP(event->Par1);
  } else {
    return return_result(event, concat(F("UseNTP:"), boolToString(Settings.UseNTP())));
  }
  return return_command_success();
}

String Command_TimeZone(struct EventStruct *event, const char *Line)
{
  if (HasArgv(Line, 2)) {
    Settings.TimeZone = event->Par1;
  } else {
    return return_result(event, concat(F("TimeZone:"), static_cast<int>(Settings.TimeZone)));
  }
  return return_command_success();
}

String Command_DST(struct EventStruct *event, const char *Line)
{
  if (HasArgv(Line, 2)) {
    Settings.DST = event->Par1;
  } else  {
    return return_result(event, concat(F("DST:"),  boolToString(Settings.DST)));
  }
  return return_command_success();
}

String Command_DateTime(struct EventStruct *event, const char *Line)
{
  String TmpStr1;

  if (GetArgv(Line, TmpStr1, 2)) {
    struct tm newtime;
    int yr = 1970;
    int mnth = 1;
    int d = 1;
    sscanf(TmpStr1.c_str(), "%4d-%2d-%2d", &yr, &mnth, &d);
    newtime.tm_year = yr - 1900;
    newtime.tm_mon  = mnth - 1; // tm_mon starts at 0
    newtime.tm_mday = d;

    int h = 0;
    int m = 0;
    int s = 0;

    if (GetArgv(Line, TmpStr1, 3)) {
      sscanf(TmpStr1.c_str(), "%2d:%2d:%2d", &h, &m, &s);
    }
    newtime.tm_hour = h;
    newtime.tm_min  = m;
    newtime.tm_sec  = s;

    // Please note the time set in this command is in UTC time, not local time.
    node_time.setExternalTimeSource(makeTime(newtime), timeSource_t::Manual_set);
  } else  {
    // serialPrintln();
    return return_result(event, concat(F("Datetime:"), node_time.getDateTimeString('-', ':', ' ')));
  }
  return return_command_success();
}
#include "../Commands/Controller.h"


#include "../../ESPEasy_common.h"


#include "../Commands/Common.h"

#include "../DataStructs/ESPEasy_EventStruct.h"

#include "../DataTypes/ControllerIndex.h"

#include "../Globals/CPlugins.h"

#include "../Helpers/Misc.h"

//      controllerIndex = (event->Par1 - 1);   Par1 is here for 1 ... CONTROLLER_MAX
bool validControllerVar(struct EventStruct *event, controllerIndex_t& controllerIndex)
{
  if (event->Par1 <= 0) { return false; }
  controllerIndex = static_cast<controllerIndex_t>(event->Par1 - 1);
  return validControllerIndex(controllerIndex);
}

const __FlashStringHelper * Command_Controller_Disable(struct EventStruct *event, const char *Line)
{
  controllerIndex_t controllerIndex;
  return return_command_boolean_result_flashstr(validControllerVar(event, controllerIndex) && setControllerEnableStatus(controllerIndex, false));
}

const __FlashStringHelper * Command_Controller_Enable(struct EventStruct *event, const char *Line)
{
  controllerIndex_t controllerIndex;
  return return_command_boolean_result_flashstr(validControllerVar(event, controllerIndex) && setControllerEnableStatus(controllerIndex, true));
}

#include "../Commands/Notifications.h"

#if FEATURE_NOTIFIER

#include "../Commands/Common.h"

#include "../../ESPEasy_common.h"
#include "../DataTypes/ESPEasy_plugin_functions.h"
#include "../Globals/ESPEasy_Scheduler.h"
#include "../Globals/Settings.h"
#include "../Globals/NPlugins.h"
#include "../Helpers/StringConverter.h"


const __FlashStringHelper * Command_Notifications_Notify(struct EventStruct *event, const char* Line)
{
	String message;
	String subject;
	GetArgv(Line, message, 3);
	GetArgv(Line, subject, 4);

	if (event->Par1 > 0) {
		int index = event->Par1 - 1;
		if (Settings.NotificationEnabled[index] && Settings.Notification[index] != INVALID_N_PLUGIN_ID.value) {
			nprotocolIndex_t NotificationProtocolIndex = 
			  getNProtocolIndex(npluginID_t::toPluginID(Settings.Notification[index]));
			if (validNProtocolIndex(NotificationProtocolIndex )) {
				struct EventStruct TempEvent(event->TaskIndex);
				// TempEvent.NotificationProtocolIndex = NotificationProtocolIndex;
				TempEvent.NotificationIndex = index;
				TempEvent.String1 = message;
				TempEvent.String2 = subject;
				Scheduler.schedule_notification_event_timer(NotificationProtocolIndex, NPlugin::Function::NPLUGIN_NOTIFY, std::move(TempEvent));
			}
		}
	}
	return return_command_success_flashstr();
}

#endif
#include "../Commands/Diagnostic.h"


#include "../Commands/Common.h"

#include "../DataStructs/PortStatusStruct.h"

#include "../DataTypes/SettingsType.h"

#include "../ESPEasyCore/ESPEasy_backgroundtasks.h"
#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../ESPEasyCore/Serial.h"

#include "../Globals/Device.h"
#include "../Globals/ExtraTaskSettings.h"
#include "../Globals/GlobalMapPortStatus.h"
#include "../Globals/SecuritySettings.h"
#include "../Globals/Settings.h"
#include "../Globals/Statistics.h"

#include "../Helpers/Convert.h"
#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/ESPEasy_time_calc.h"
#include "../Helpers/Misc.h"
#include "../Helpers/_Plugin_init.h"
#include "../Helpers/PortStatus.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringParser.h"

#include <map>
#include <stdint.h>


#ifndef BUILD_MINIMAL_OTA
bool showSettingsFileLayout = false;
#endif // ifndef BUILD_MINIMAL_OTA

#ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
String Command_Lowmem(struct EventStruct *event, const char *Line)
{
  String result;

  result += lowestRAM;
  result += F(" : ");
  result += lowestRAMfunction;
  return return_result(event, result);
}

const __FlashStringHelper * Command_Malloc(struct EventStruct *event, const char *Line)
{
  char *ramtest;
  int size = parseCommandArgumentInt(Line, 1);

  ramtest = (char *)malloc(size);

  if (ramtest == nullptr) { return return_command_failed_flashstr(); }
  free(ramtest);
  return return_command_success_flashstr();
}

String Command_SysLoad(struct EventStruct *event, const char *Line)
{
  String result = toString(getCPUload(), 2);

  result += F("% (LC=");
  result += getLoopCountPerSec();
  result += ')';
  return return_result(event, result);
}

const __FlashStringHelper * Command_SerialFloat(struct EventStruct *event, const char *Line)
{
  pinMode(1, INPUT);
  pinMode(3, INPUT);
  delay(60000);
  return return_command_success_flashstr();
}

const __FlashStringHelper * Command_MemInfo(struct EventStruct *event, const char *Line)
{
  serialPrint(F("SecurityStruct         | "));
  serialPrintln(String(sizeof(SecuritySettings)));
  serialPrint(F("SettingsStruct         | "));
  serialPrintln(String(sizeof(Settings)));
  serialPrint(F("ExtraTaskSettingsStruct| "));
  serialPrintln(String(sizeof(ExtraTaskSettings)));
  serialPrint(F("DeviceStruct           | "));
  serialPrintln(String(getNrBuiltInDeviceIndex()));
  return return_see_serial(event);
}

const __FlashStringHelper * Command_MemInfo_detail(struct EventStruct *event, const char *Line)
{
#ifndef BUILD_MINIMAL_OTA
  showSettingsFileLayout = true;
  Command_MemInfo(event, Line);

  for (int st = 0; st < static_cast<int>(SettingsType::Enum::SettingsType_MAX); ++st) {
    SettingsType::SettingsType::Enum settingsType = static_cast<SettingsType::Enum>(st);
    int max_index, offset, max_size;
    int struct_size = 0;
    serialPrintln();
    if (SettingsType::getSettingsParameters(settingsType, 0, max_index, offset, max_size, struct_size))
    {
      serialPrint(SettingsType::getSettingsTypeString(settingsType));
      serialPrintln(F(" | start | end | max_size | struct_size"));
      serialPrintln(F("--- | --- | --- | --- | ---"));

      for (int i = 0; i < max_index; ++i) {
        if (SettingsType::getSettingsParameters(settingsType, i, offset, max_size))
        {
          serialPrintln(strformat(
            F("%d|%d|%d|%d|%d"),
            i,
            offset,
            offset + max_size - 1,
            max_size,
            struct_size
          ));
        }
      }
    }
  }
  return return_see_serial(event);
  #else
  return return_command_failed_flashstr();
  #endif // ifndef BUILD_MINIMAL_OTA
}

const __FlashStringHelper * Command_Background(struct EventStruct *event, const char *Line)
{
  unsigned long timer = millis() + parseCommandArgumentInt(Line, 1);

  serialPrintln(F("start"));

  while (!timeOutReached(timer)) {
    backgroundtasks();
  }
  serialPrintln(F("end"));
  return return_see_serial(event);
}
#endif // BUILD_NO_DIAGNOSTIC_COMMANDS

const __FlashStringHelper * Command_Debug(struct EventStruct *event, const char *Line)
{
  if (HasArgv(Line, 2)) {
    setLogLevelFor(LOG_TO_SERIAL, parseCommandArgumentInt(Line, 1));
  } else  {
    serialPrintln();
    serialPrint(F("Serial debug level: "));
    serialPrintln(String(Settings.SerialLogLevel));
  }
  return return_see_serial(event);
}

String Command_logentry(struct EventStruct *event, const char *Line)
{
  uint8_t level = LOG_LEVEL_INFO;
  // An extra optional parameter to set log level.
  if (event->Par2 > LOG_LEVEL_NONE && event->Par2 <= 
  # ifndef BUILD_NO_DEBUG
    LOG_LEVEL_DEBUG_MORE
  #else
    LOG_LEVEL_INFO
  #endif
    ) { level = event->Par2; }
  String res = tolerantParseStringKeepCase(Line, 2);
  addLog(level, res);
  return res;
}

#ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
const __FlashStringHelper * Command_JSONPortStatus(struct EventStruct *event, const char *Line)
{
  addLog(LOG_LEVEL_INFO, F("JSON Port Status: Command not implemented yet."));
  return return_command_success_flashstr();
}

void createLogPortStatus(std::map<uint32_t, portStatusStruct>::iterator it)
{  
  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO, strformat(
      F("PortStatus detail: Port=%u State=%d Output=%d Mode=%u Task=%u Monitor=%u Command=%d Init=%d PreviousTask=%d"),
      getPortFromKey(it->first),
      it->second.getValue(),
      it->second.output,
      it->second.mode,
      it->second.task,
      it->second.monitor,
      it->second.command,
      it->second.init,
      it->second.previousTask));
  }
}

void debugPortStatus(std::map<uint32_t, portStatusStruct>::iterator it)
{
  createLogPortStatus(it);
}


const __FlashStringHelper * Command_logPortStatus(struct EventStruct *event, const char *Line)
{
  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO, concat(F("PortStatus structure: Called from=Rules Count="), static_cast<int>(globalMapPortStatus.size())));
  }

  for (auto it = globalMapPortStatus.begin(); it != globalMapPortStatus.end(); ++it) {
    debugPortStatus(it);
  }

  return return_command_success_flashstr();
}
#endif // BUILD_NO_DIAGNOSTIC_COMMANDS

#include "../Commands/Blynk.h"

#ifdef USES_C012

#include "../Commands/Common.h"
#include "../DataStructs/ESPEasy_EventStruct.h"
#include "../ESPEasyCore/ESPEasy_backgroundtasks.h"
#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../Globals/Settings.h"
#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/ESPEasy_time_calc.h"
#include "../Helpers/_CPlugin_Helper.h"

#include "../../ESPEasy_fdwdecl.h"


controllerIndex_t firstEnabledBlynk_ControllerIndex() {
  for (controllerIndex_t i = 0; i < CONTROLLER_MAX; ++i) {
    protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(i);

    if (validProtocolIndex(ProtocolIndex)) {
      const cpluginID_t number = getCPluginID_from_ProtocolIndex(ProtocolIndex);

      if ((number == 12) && Settings.ControllerEnabled[i]) {
        return i;
      }
    }
  }
  return INVALID_CONTROLLER_INDEX;
}

const __FlashStringHelper * Command_Blynk_Get(struct EventStruct *event, const char *Line)
{
  controllerIndex_t first_enabled_blynk_controller = firstEnabledBlynk_ControllerIndex();

  if (!validControllerIndex(first_enabled_blynk_controller)) {
    return F("Controller not enabled");
  } else {
    // FIXME TD-er: This one is not using parseString* function
    String strLine = Line;
    strLine = strLine.substring(9);
    int index = strLine.indexOf(',');

    if (index > 0)
    {
      int index           = strLine.lastIndexOf(',');
      String blynkcommand = strLine.substring(index + 1);
      float  value        = 0;

      if (Blynk_get(blynkcommand, first_enabled_blynk_controller, &value))
      {
        UserVar.setFloat((event->Par1 - 1), event->Par2 - 1, value);
      }
      else {
        return F("Error getting data");
      }
    }
    else
    {
      if (!Blynk_get(strLine, first_enabled_blynk_controller, nullptr))
      {
        return F("Error getting data");
      }
    }
  }
  return return_command_success_flashstr();
}

bool Blynk_get(const String& command, controllerIndex_t controllerIndex, float *data)
{
  bool MustCheckReply = false;
  String hostname, pass;
  unsigned int ClientTimeout = 0;
  WiFiClient client;

  {
    // Place ControllerSettings in its own scope, as it is quite big.
    MakeControllerSettings(ControllerSettings); //-V522
    if (!AllocatedControllerSettings()) {
      addLog(LOG_LEVEL_ERROR, F("Blynk : Cannot run GET, out of RAM"));
      return false;
    }

    LoadControllerSettings(controllerIndex, *ControllerSettings);
    MustCheckReply = ControllerSettings->MustCheckReply;
    hostname = ControllerSettings->getHost();
    pass = getControllerPass(controllerIndex, *ControllerSettings);
    ClientTimeout = ControllerSettings->ClientTimeout;

    if (pass.isEmpty()) {
      addLog(LOG_LEVEL_ERROR, F("Blynk : No password set"));
      return false;
    }

    if (!try_connect_host(/* CPLUGIN_ID_012 */ 12, client, *ControllerSettings)) {
      return false;
    }
  }

  // We now create a URI for the request
  {
    // Place this stack allocated array in its own scope, as it is quite big.
    char request[300] = { 0 };
    sprintf_P(request,
              PSTR("GET /%s/%s HTTP/1.1\r\n Host: %s \r\n Connection: close\r\n\r\n"),
              pass.c_str(),
              command.c_str(),
              hostname.c_str());
#ifndef BUILD_NO_DEBUG
    addLog(LOG_LEVEL_DEBUG, request);
#endif
    client.print(request);
  }
  bool success = !MustCheckReply;

  if (MustCheckReply || data) {
    unsigned long timer = millis() + ClientTimeout;

    while (!client_available(client) && !timeOutReached(timer)) {
      delay(1);
    }

    #ifndef BUILD_NO_DEBUG
    char log[80] = { 0 };
    #endif
    timer = millis() + 1500;

    // Read all the lines of the reply from server and log them
    while (client_available(client) && !success && !timeOutReached(timer)) {
      String line;
      safeReadStringUntil(client, line, '\n');
      #ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_DEBUG_MORE, line);
      #endif

      // success ?
      if (equals(line.substring(0, 15), F("HTTP/1.1 200 OK"))) {
        #ifndef BUILD_NO_DEBUG
        strcpy_P(log, PSTR("HTTP : Success"));
        #endif

        if (!data) { success = true; }
      }
      #ifndef BUILD_NO_DEBUG
      else if (equals(line.substring(0, 24), F("HTTP/1.1 400 Bad Request"))) {
        strcpy_P(log, PSTR("HTTP : Unauthorized"));
      }
      else if (equals(line.substring(0, 25), F("HTTP/1.1 401 Unauthorized"))) {
        strcpy_P(log, PSTR("HTTP : Unauthorized"));
      }
      addLog(LOG_LEVEL_DEBUG, log);
      #endif

      // data only
      if (data && line.startsWith("["))
      {
        String strValue = line;
        uint8_t   pos      = strValue.indexOf('"', 2);
        strValue = strValue.substring(2, pos);
        strValue.trim();
        *data   = 0.0f;
        validFloatFromString(strValue, *data);
        success = true;

        char value_char[5] = { 0 };
        strValue.toCharArray(value_char, 5);
        #ifndef BUILD_NO_DEBUG
        sprintf_P(log, PSTR("Blynk get - %s => %s"), command.c_str(), value_char);
        addLog(LOG_LEVEL_DEBUG, log);
        #endif
      }
      delay(0);
    }
  }
  #ifndef BUILD_NO_DEBUG
  addLog(LOG_LEVEL_DEBUG, F("HTTP : closing connection (012)"));
  #endif

  client.PR_9453_FLUSH_TO_CLEAR();
  client.stop();

  // important - backgroundtasks - free mem
  unsigned long timer = millis() + 10;

  while (!timeOutReached(timer)) {
    backgroundtasks();
  }

  return success;
}

#endif // ifdef USES_C012

#include "../Commands/HTTP.h"

#include "../../ESPEasy_common.h"

#include "../Commands/Common.h"

#include "../DataStructs/ControllerSettingsStruct.h"
#include "../DataStructs/SettingsStruct.h"

#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../ESPEasyCore/ESPEasyNetwork.h"

#include "../Globals/Settings.h"

#include "../Helpers/_CPlugin_Helper.h"
#include "../Helpers/Misc.h"
#include "../Helpers/Networking.h"
#include "../Helpers/StringParser.h"

#if FEATURE_SEND_TO_HTTP || FEATURE_POST_TO_HTTP || FEATURE_PUT_TO_HTTP
const __FlashStringHelper* httpEmitToHTTP(struct EventStruct        *event,
                                          const __FlashStringHelper *logIdentifier,
                                          const __FlashStringHelper *HttpMethod,
                                          const char                *Line,
                                          const int                  timeout,
                                          const bool                 waitForAck,
                                          const bool                 useHeader,
                                          const bool                 useBody,
                                          const bool                 useHttps)
{
  if (NetworkConnected()) {
    String   user, pass, host, file, path, header, postBody;
    uint16_t port;
    uint8_t  idx;

    const String arg1 = parseStringKeepCase(Line, 2);

    if (arg1.indexOf(F("://")) != -1) {
      // Full url given
      path = splitURL(arg1, user, pass, host, port, file);
      idx  = 3;

      if (useHeader || useBody) {
        path = parseStringKeepCase(path, 1); // Only first part is path when having header or body
      }
    } else {
      // Command arguments are split into: host, port, url
      if (!splitUserPass_HostPortString(
            arg1,
            user,
            pass,
            host,
            port))
      {
        return return_command_failed_flashstr();
      }

      const int port_arg = event->Par2;

      if ((port_arg > 0) && (port_arg < 65536)) {
        port = port_arg;
      } else {
        if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
          addLogMove(LOG_LEVEL_ERROR, strformat(F("%s: Invalid port argument: %d will use: %d"),
                                                FsP(logIdentifier), port_arg, port));
        }
      }

      if (useHeader || useBody) {
        path = parseStringKeepCase(Line, 4);
      } else {
        path = parseStringToEndKeepCase(Line, 4);
      }
      idx = 5;
    }

    if (useHeader) {
      header = parseStringKeepCase(Line, idx);
      idx++;
    }

    if (useBody) {
      postBody = parseStringKeepCase(Line, idx);
    }
    # ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      addLogMove(LOG_LEVEL_DEBUG, strformat(F("%s: Host: %s port: %d path: %s"),
                                            FsP(logIdentifier), host.c_str(), port, path.c_str()));
    }
    # endif // ifndef BUILD_NO_DEBUG

    #if FEATURE_HTTP_TLS
    TLS_types tlsType = TLS_types::NoTLS;
    if (useHttps) {
      tlsType = TLS_types::TLS_insecure;
      // TODO: ?Parse Line for type of TLS to use?
    }
    #endif // if FEATURE_HTTP_TLS

    int httpCode = -1;
    send_via_http(
      logIdentifier,
      timeout,
      user,
      pass,
      host,
      port,
      path,
      HttpMethod,
      header,
      postBody,
      httpCode,
      waitForAck
      #if FEATURE_HTTP_TLS
      , tlsType
      #endif // if FEATURE_HTTP_TLS
     );

    if ((httpCode >= 100) && (httpCode < 300)) {
      return return_command_success_flashstr();
    }
  } else {
    addLog(LOG_LEVEL_ERROR, concat(logIdentifier, F(": Not connected to network")));
  }
  return return_command_failed_flashstr();
}

#endif // if FEATURE_SEND_TO_HTTP || FEATURE_POST_TO_HTTP || FEATURE_PUT_TO_HTTP

#if FEATURE_SEND_TO_HTTP

// syntax 1: SendToHttp,<[<user>:<password>@]<host>,<port>,<path>
// syntax 2: SendToHttp,http://<[<user>:<password>@]<host>[:<port>]/<path>
const __FlashStringHelper* Command_HTTP_SendToHTTP(struct EventStruct *event, const char *Line)
{
  // Some servers don't give an ack.
  // For these it is adviced to uncheck to wait for an acknowledgement.
  // However the default timeout of 4000 msec is then way too long
  // FIXME TD-er: Make sendToHttp timeout a setting.
  const int timeout = Settings.SendToHttp_ack()
         ? CONTROLLER_CLIENTTIMEOUT_MAX : 1000;

  return httpEmitToHTTP(event, F("SendToHTTP"), F("GET"), Line, timeout, Settings.SendToHttp_ack(), false, false, false);
}

#if FEATURE_HTTP_TLS

// syntax 1: SendToHttpS,<[<user>:<password>@]<host>,<port>,<path>
// syntax 2: SendToHttpS,http://<[<user>:<password>@]<host>[:<port>]/<path>
const __FlashStringHelper* Command_HTTP_SendToHTTPS(struct EventStruct *event, const char *Line)
{
  // Some servers don't give an ack.
  // For these it is adviced to uncheck to wait for an acknowledgement.
  // However the default timeout of 4000 msec is then way too long
  // FIXME TD-er: Make sendToHttp timeout a setting.
  const int timeout = Settings.SendToHttp_ack()
         ? CONTROLLER_CLIENTTIMEOUT_MAX : 1000;

  return httpEmitToHTTP(event, F("SendToHTTPS"), F("GET"), Line, timeout, Settings.SendToHttp_ack(), false, false, true);
}
#endif // if FEATURE_HTTP_TLS

#endif // FEATURE_SEND_TO_HTTP

#if FEATURE_POST_TO_HTTP

// syntax 1: PostToHttp,<[<user>:<password>@]<host>,<port>,<path>,<header>,<body>
// syntax 2: PostToHttp,http://<[<user>:<password>@]<host>[:<port>]/<path>,<header>,<body>
const __FlashStringHelper* Command_HTTP_PostToHTTP(struct EventStruct *event, const char *Line)
{
  // FIXME tonhuisman: Make postToHttp timeout a setting, now using a somewhat sensible default
  const int timeout = CONTROLLER_CLIENTTIMEOUT_MAX;

  // FIXME tonhuisman: make PostToHttp_ack a setting, using SendToHttp_ack for now...

  return httpEmitToHTTP(event, F("PostToHTTP"), F("POST"), Line, timeout, Settings.SendToHttp_ack(), true, true, false);
}

#if FEATURE_HTTP_TLS

// syntax 1: PostToHttpS,<[<user>:<password>@]<host>,<port>,<path>,<header>,<body>
// syntax 2: PostToHttpS,http://<[<user>:<password>@]<host>[:<port>]/<path>,<header>,<body>
const __FlashStringHelper* Command_HTTP_PostToHTTPS(struct EventStruct *event, const char *Line)
{
  // FIXME tonhuisman: Make postToHttp timeout a setting, now using a somewhat sensible default
  const int timeout = CONTROLLER_CLIENTTIMEOUT_MAX;

  // FIXME tonhuisman: make PostToHttp_ack a setting, using SendToHttp_ack for now...

  return httpEmitToHTTP(event, F("PostToHTTPS"), F("POST"), Line, timeout, Settings.SendToHttp_ack(), true, true, true);
}
#endif // if FEATURE_HTTP_TLS

#endif // if FEATURE_POST_TO_HTTP

#if FEATURE_PUT_TO_HTTP

// syntax 1: PutToHttp,<[<user>:<password>@]<host>,<port>,<path>,<header>,<body>
// syntax 2: PutToHttp,http://<[<user>:<password>@]<host>[:<port>]/<path>,<header>,<body>
const __FlashStringHelper* Command_HTTP_PutToHTTP(struct EventStruct *event, const char *Line)
{
  // FIXME tonhuisman: Make putToHttp timeout a setting, now using a somewhat sensible default
  const int timeout = CONTROLLER_CLIENTTIMEOUT_MAX;

  // FIXME tonhuisman: make PutToHttp_ack a setting, using SendToHttp_ack for now...

  return httpEmitToHTTP(event, F("PutToHTTP"), F("PUT"), Line, timeout, Settings.SendToHttp_ack(), true, true, false);
}

#if FEATURE_HTTP_TLS

// syntax 1: PutToHttpS,<[<user>:<password>@]<host>,<port>,<path>,<header>,<body>
// syntax 2: PutToHttpS,http://<[<user>:<password>@]<host>[:<port>]/<path>,<header>,<body>
const __FlashStringHelper* Command_HTTP_PutToHTTPS(struct EventStruct *event, const char *Line)
{
  // FIXME tonhuisman: Make putToHttp timeout a setting, now using a somewhat sensible default
  const int timeout = CONTROLLER_CLIENTTIMEOUT_MAX;

  // FIXME tonhuisman: make PutToHttp_ack a setting, using SendToHttp_ack for now...

  return httpEmitToHTTP(event, F("PutToHTTPS"), F("PUT"), Line, timeout, Settings.SendToHttp_ack(), true, true, true);
}
#endif // if FEATURE_HTTP_TLS

#endif // if FEATURE_PUT_TO_HTTP

#include "../Commands/WiFi.h"

#include "../../ESPEasy_common.h"

#include "../Commands/Common.h"

#include "../DataStructs/ESPEasy_EventStruct.h"

#include "../ESPEasyCore/ESPEasyWifi.h"
#include "../ESPEasyCore/Serial.h"

#include "../Globals/ESPEasyWiFiEvent.h"
#include "../Globals/RTC.h"
#include "../Globals/SecuritySettings.h"
#include "../Globals/Settings.h"

#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/StringConverter.h"


#define WIFI_MODE_MAX (WiFiMode_t)4



String Command_Wifi_SSID(struct EventStruct *event, const char *Line)
{
  return Command_GetORSetString(event, F("Wifi SSID:"),
                                Line,
                                SecuritySettings.WifiSSID,
                                sizeof(SecuritySettings.WifiSSID),
                                1);
}

String Command_Wifi_Key(struct EventStruct *event, const char *Line)
{
  return Command_GetORSetString(event, F("Wifi Key:"),
                                Line,
                                SecuritySettings.WifiKey,
                                sizeof(SecuritySettings.WifiKey),
                                1);
}

String Command_Wifi_SSID2(struct EventStruct *event, const char *Line)
{
  return Command_GetORSetString(event, F("Wifi2 SSID:"),
                                Line,
                                SecuritySettings.WifiSSID2,
                                sizeof(SecuritySettings.WifiSSID2),
                                1);
}

String Command_Wifi_Key2(struct EventStruct *event, const char *Line)
{
  return Command_GetORSetString(event, F("Wifi2 Key:"),
                                Line,
                                SecuritySettings.WifiKey2,
                                sizeof(SecuritySettings.WifiKey2),
                                1);
}

String Command_Wifi_HiddenSSID(struct EventStruct *event, const char *Line)
{
  bool   includeHiddenSSID = Settings.IncludeHiddenSSID();
  String result            = Command_GetORSetBool(event, F("Include Hidden SSID:"),
                                                  Line,
                                                  (bool *)&includeHiddenSSID,
                                                  1);

  if (Settings.IncludeHiddenSSID() != includeHiddenSSID) { // Update if changed
    Settings.IncludeHiddenSSID(includeHiddenSSID);
  }
  return result;
}

const __FlashStringHelper* Command_Wifi_Scan(struct EventStruct *event, const char *Line)
{
  WiFiScan_log_to_serial();
  return return_command_success_flashstr();
}

const __FlashStringHelper* Command_Wifi_Connect(struct EventStruct *event, const char *Line)
{
  WiFiEventData.wifiConnectAttemptNeeded = true;
  return return_command_success_flashstr();
}

const __FlashStringHelper* Command_Wifi_Disconnect(struct EventStruct *event, const char *Line)
{
  RTC.clearLastWiFi(); // Force a WiFi scan
  WifiDisconnect();

  return return_command_success_flashstr();
}

const __FlashStringHelper* Command_Wifi_APMode(struct EventStruct *event, const char *Line)
{
  setAP(true);
  return return_command_success_flashstr();
}

const __FlashStringHelper* Command_Wifi_STAMode(struct EventStruct *event, const char *Line)
{
  setSTA(true);
  return return_command_success_flashstr();
}

String Command_Wifi_Mode(struct EventStruct *event, const char *Line)
{
  String TmpStr1;

  if (GetArgv(Line, TmpStr1, 2)) {
    WiFiMode_t mode = WIFI_MODE_MAX;

    if ((event->Par1 > 0) && (event->Par1 < WIFI_MODE_MAX)) {
      mode = static_cast<WiFiMode_t>(event->Par1 - 1);
    } else {
      TmpStr1.toLowerCase();

      if (strcmp_P(TmpStr1.c_str(), PSTR("off")) == 0) { mode = WIFI_OFF; }
      else if (strcmp_P(TmpStr1.c_str(), PSTR("sta")) == 0) { mode = WIFI_STA; }
      else if (strcmp_P(TmpStr1.c_str(), PSTR("ap")) == 0) { mode = WIFI_AP; }
      else if (strcmp_P(TmpStr1.c_str(), PSTR("ap+sta")) == 0) { mode = WIFI_AP_STA; }
    }

    if ((mode >= WIFI_OFF) && (mode < WIFI_MODE_MAX)) {
      setWifiMode(mode);
    } else {
      return return_result(event, F("Wifi Mode: invalid arguments"));
    }
  } else {
    return return_result(event, concat(F("WiFi Mode:"),  getWifiModeString(WiFi.getMode())));
  }
  return return_command_success();
}

const __FlashStringHelper* Command_Wifi_AllowAP(struct EventStruct *event, const char *Line)
{
  Settings.DoNotStartAP(false);
  return return_command_success_flashstr();
}

// FIXME: TD-er This is not an erase, but actually storing the current settings
// in the wifi settings of the core library
const __FlashStringHelper* Command_WiFi_Erase(struct EventStruct *event, const char *Line)
{
  return Erase_WiFi_Calibration() 
    ? return_command_success_flashstr()
    : return_command_failed_flashstr();
}

#include "../Commands/i2c.h"

#include "../Commands/Common.h"
#include "../ESPEasyCore/Serial.h"

#include "../Globals/I2Cdev.h"
#include "../Globals/Settings.h"

#include "../Helpers/Hardware_I2C.h"
#include "../Helpers/StringConverter.h"

#include "../../ESPEasy_common.h"

void i2c_scanI2Cbus(bool dbg, int8_t channel, uint8_t i2cBus) {
  uint8_t error, address;

  #if FEATURE_I2CMULTIPLEXER

  if (-1 == channel) {
    serialPrintln(concat(F("Standard I2C bus "), i2cBus));
  } else {
    serialPrintln(concat(F("Multiplexer channel "), channel));
  }
  #endif // if FEATURE_I2CMULTIPLEXER

  for (address = 1; address <= 127; address++) {
    Wire.beginTransmission(address);
    error = Wire.endTransmission();

    if (error == 0) {
      serialPrintln(strformat(F("I2C  : Found 0x%02x"), address));
    } else if ((error == 4) || dbg) {
      serialPrintln(strformat(F("I2C  : Error %d at 0x%02x"), error, address));
    }
  }
}

const __FlashStringHelper* Command_i2c_Scanner(struct EventStruct *event, const char *Line)
{
  const bool dbg = equals(parseString(Line, 2), F("1"));

  #if !FEATURE_I2C_MULTIPLE
  const uint8_t i2cBus = 0;
  #else // if !FEATURE_I2C_MULTIPLE

  for (uint8_t i2cBus = 0; i2cBus < getI2CBusCount(); ++i2cBus)
  #endif // if !FEATURE_I2C_MULTIPLE
  {
    if (Settings.isI2CEnabled(i2cBus)) {
      I2CSelect_Max100kHz_ClockSpeed(i2cBus); // Scan bus using low speed

      i2c_scanI2Cbus(dbg, -1, i2cBus);        // Base I2C bus

      #if FEATURE_I2CMULTIPLEXER

      if (isI2CMultiplexerEnabled(i2cBus)) {
        uint8_t mux_max = I2CMultiplexerMaxChannels(i2cBus);

        for (int8_t channel = 0; channel < mux_max; ++channel) {
          I2CMultiplexerSelect(i2cBus, channel);
          i2c_scanI2Cbus(dbg, channel, i2cBus); // Multiplexer I2C bus
        }
        I2CMultiplexerOff(0);
      }
      #endif // if FEATURE_I2CMULTIPLEXER
    } else {
      serialPrintln(strformat(F("I2C %d: Not enabled."), i2cBus));
    }
  }
  I2CSelectHighClockSpeed(0); // By default the bus is in standard speed
  return return_see_serial(event);
}

#include "../ControllerQueue/C016_queue_element.h"

#ifdef USES_C016

# include "../DataStructs/ESPEasy_EventStruct.h"
# include "../Globals/Plugins.h"
# include "../Globals/RuntimeData.h"
# include "../Helpers/_Plugin_SensorTypeHelper.h"
# include "../Helpers/ESPEasy_math.h"

C016_queue_element::C016_queue_element() :  sensorType(
    Sensor_VType::SENSOR_TYPE_NONE) {
  _timestamp      = 0;
  _controller_idx = 0;
  _taskIndex      = INVALID_TASK_INDEX;
  values.clear();
}

C016_queue_element::C016_queue_element(C016_queue_element&& other)
  : sensorType(other.sensorType)
  , valueCount(other.valueCount)
{
  _timestamp      = other._timestamp;
  _controller_idx = other._controller_idx;
  _taskIndex      = other._taskIndex;
  values          = other.values;
}

C016_queue_element::C016_queue_element(const struct EventStruct *event, uint8_t value_count) :
  unixTime(event->timestamp_sec),
  sensorType(event->sensorType),
  valueCount(value_count)
{
  _controller_idx = event->ControllerIndex;
  _taskIndex      = event->TaskIndex;
  values.clear();
  const TaskValues_Data_t* data = UserVar.getRawTaskValues_Data(event->TaskIndex);

  if (data != nullptr) {
    for (uint8_t i = 0; i < value_count; ++i) {
      values.copyValue(*data, i, sensorType);
    }
  }
}

C016_queue_element& C016_queue_element::operator=(C016_queue_element&& other) {
  _timestamp      = other._timestamp;
  _taskIndex      = other._taskIndex;
  _controller_idx = other._controller_idx;
  sensorType      = other.sensorType;
  valueCount      = other.valueCount;
  unixTime        = other.unixTime;
  values          = other.values;

  return *this;
}

size_t C016_queue_element::getSize() const {
  return sizeof(*this);
}

bool C016_queue_element::isDuplicate(const Queue_element_base& other) const {
  const C016_queue_element& oth = static_cast<const C016_queue_element&>(other);

  if ((oth._controller_idx != _controller_idx) ||
      (oth._taskIndex != _taskIndex) ||
      (oth.sensorType != sensorType) ||
      (oth.valueCount != valueCount)) {
    return false;
  }

  for (uint8_t i = 0; i < valueCount; ++i) {
    if (isFloatOutputDataType(sensorType)) {
      if (!essentiallyEqual(oth.values.getFloat(i), values.getFloat(i))) {
        return false;
      }
    } else {
      if (oth.values.getUint32(i) != values.getUint32(i)) {
        return false;
      }
    }
  }
  return true;
}

C016_binary_element C016_queue_element::getBinary() const {
  C016_binary_element element;

  element.unixTime   = unixTime;
  element.TaskIndex  = _taskIndex;
  element.sensorType = sensorType;
  element.valueCount = valueCount;
  element.values     = values;

  // It makes no sense to keep the controller index when storing it.
  // re-purpose it to store the pluginID
  element.pluginID = getPluginID_from_TaskIndex(_taskIndex);

  return element;
}

#endif // ifdef USES_C016

#include "../ControllerQueue/SimpleQueueElement_formatted_Strings.h"

#include "../DataStructs/ESPEasy_EventStruct.h"
#include "../Helpers/StringConverter.h"

#include "../../_Plugin_Helper.h"


SimpleQueueElement_formatted_Strings::SimpleQueueElement_formatted_Strings(struct EventStruct *event) :
  idx(event->idx),
  sensorType(event->sensorType),
  valuesSent(0)
{
  _controller_idx = event->ControllerIndex;
  _taskIndex = event->TaskIndex;

  valueCount = getValueCountForTask(_taskIndex);

  for (uint8_t i = 0; i < valueCount; ++i) {
    move_special(txt[i], formatUserVarNoCheck(event, i));
  }
}

SimpleQueueElement_formatted_Strings::SimpleQueueElement_formatted_Strings(const struct EventStruct *event, uint8_t value_count) :
  idx(event->idx),
  sensorType(event->sensorType),
  valuesSent(0),
  valueCount(value_count) {
  _controller_idx = event->ControllerIndex;
  _taskIndex      = event->TaskIndex;
}

SimpleQueueElement_formatted_Strings::SimpleQueueElement_formatted_Strings(SimpleQueueElement_formatted_Strings&& rval)
  : idx(rval.idx),
  sensorType(rval.sensorType),
  valuesSent(rval.valuesSent), valueCount(rval.valueCount)
{
  _timestamp      = rval._timestamp;
  _controller_idx = rval._controller_idx;
  _taskIndex      = rval._taskIndex;

  for (uint8_t i = 0; i < VARS_PER_TASK; ++i) {
    move_special(txt[i], std::move(rval.txt[i]));
  }
}

SimpleQueueElement_formatted_Strings& SimpleQueueElement_formatted_Strings::operator=(SimpleQueueElement_formatted_Strings&& rval) {
  idx             = rval.idx;
  _timestamp      = rval._timestamp;
  _taskIndex      = rval._taskIndex;
  _controller_idx = rval._controller_idx;
  sensorType      = rval.sensorType;
  valuesSent      = rval.valuesSent;
  valueCount      = rval.valueCount;

  for (size_t i = 0; i < VARS_PER_TASK; ++i) {
    move_special(txt[i], std::move(rval.txt[i]));
  }
  return *this;
}

bool SimpleQueueElement_formatted_Strings::checkDone(bool succesfull) const {
  if (succesfull) { ++valuesSent; }
  return valuesSent >= valueCount || valuesSent >= VARS_PER_TASK;
}

size_t SimpleQueueElement_formatted_Strings::getSize() const {
  size_t total = sizeof(*this);

  for (int i = 0; i < VARS_PER_TASK; ++i) {
    total += txt[i].length();
  }
  return total;
}

bool SimpleQueueElement_formatted_Strings::isDuplicate(const Queue_element_base& rval) const {
  const SimpleQueueElement_formatted_Strings& oth = static_cast<const SimpleQueueElement_formatted_Strings&>(rval);

  if ((oth._controller_idx != _controller_idx) ||
      (oth._taskIndex != _taskIndex) ||
      (oth.sensorType != sensorType) ||
      (oth.valueCount != valueCount) ||
      (oth.idx != idx)) {
    return false;
  }

  for (uint8_t i = 0; i < VARS_PER_TASK; ++i) {
    if (oth.txt[i] != txt[i]) {
      return false;
    }
  }
  return true;
}

#include "../ControllerQueue/ControllerDelayHandlerStruct.h"


ControllerDelayHandlerStruct::ControllerDelayHandlerStruct() :
  lastSend(0),
  minTimeBetweenMessages(CONTROLLER_DELAY_QUEUE_DELAY_DFLT),
  expire_timeout(0),
  max_queue_depth(CONTROLLER_DELAY_QUEUE_DEPTH_DFLT),
  attempt(0),
  max_retries(CONTROLLER_DELAY_QUEUE_RETRY_DFLT),
  delete_oldest(false),
  must_check_reply(false),
  deduplicate(false),
  useLocalSystemTime(false) {}

bool ControllerDelayHandlerStruct::cacheControllerSettings(controllerIndex_t ControllerIndex)
{
  MakeControllerSettings(ControllerSettings);

  if (!AllocatedControllerSettings()) {
    return false;
  }
  LoadControllerSettings(ControllerIndex, *ControllerSettings);
  cacheControllerSettings(*ControllerSettings);
  return true;
}

void ControllerDelayHandlerStruct::cacheControllerSettings(const ControllerSettingsStruct& settings) {
  minTimeBetweenMessages = settings.MinimalTimeBetweenMessages;
  max_queue_depth        = settings.MaxQueueDepth;
  max_retries            = settings.MaxRetry;
  delete_oldest          = settings.DeleteOldest;
  must_check_reply       = settings.MustCheckReply;
  deduplicate            = settings.deduplicate();
  useLocalSystemTime     = settings.useLocalSystemTime();

  if (settings.allowExpire()) {
    expire_timeout = max_queue_depth * max_retries * (minTimeBetweenMessages + settings.ClientTimeout);

    if (expire_timeout < CONTROLLER_QUEUE_MINIMAL_EXPIRE_TIME) {
      expire_timeout = CONTROLLER_QUEUE_MINIMAL_EXPIRE_TIME;
    }
  } else {
    expire_timeout = 0;
  }

  // Set some sound limits when not configured
  if (max_queue_depth == 0) { max_queue_depth = CONTROLLER_DELAY_QUEUE_DEPTH_DFLT; }

  if (max_retries == 0) { max_retries = CONTROLLER_DELAY_QUEUE_RETRY_DFLT; }

  if (minTimeBetweenMessages == 0) { minTimeBetweenMessages = CONTROLLER_DELAY_QUEUE_DELAY_DFLT; }

  // No less than 10 msec between messages.
  if (minTimeBetweenMessages < 10) { minTimeBetweenMessages = 10; }
}

bool ControllerDelayHandlerStruct::readyToProcess(const Queue_element_base& element) const {
  const protocolIndex_t protocolIndex = getProtocolIndex_from_ControllerIndex(element._controller_idx);

  if (protocolIndex == INVALID_PROTOCOL_INDEX) {
    return false;
  }

  if (getProtocolStruct(protocolIndex).needsNetwork) {
    return NetworkConnected(10);
  }
  return true;
}

bool ControllerDelayHandlerStruct::queueFull(controllerIndex_t controller_idx) const {
  if (sendQueue.size() >= max_queue_depth) { return true; }

  // Number of elements is not exceeding the limit, check memory
  int freeHeap = FreeMem();
  {
    /*
      #ifdef USE_SECOND_HEAP
    const int freeHeap2 = FreeMem2ndHeap();

    if (freeHeap2 < freeHeap) {
      freeHeap = freeHeap2;
    }
      #endif // ifdef USE_SECOND_HEAP
      */
  }

#ifdef ESP32
  if (freeHeap > 50000) 
#else
  if (freeHeap > 5000) 
#endif
  {
    return false; // Memory is not an issue.
  }
#ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    String log = F("Controller-");
    log += controller_idx + 1;
    log += F(" : Memory used: ");
    log += getQueueMemorySize();
    log += F(" bytes ");
    log += sendQueue.size();
    log += F(" items ");
    log += freeHeap;
    log += F(" free");
    addLogMove(LOG_LEVEL_DEBUG, log);
  }
#endif // ifndef BUILD_NO_DEBUG
  return true;
}

// Return true if message is already present in the queue
bool ControllerDelayHandlerStruct::isDuplicate(const Queue_element_base& element) const {
  // Some controllers may receive duplicate messages, due to lost acknowledgement
  // This is actually the same message, so this should not be processed.
  if (!unitLastMessageCount.isNew(element.getUnitMessageCount())) {
    return true;
  }

  // The unit message count is still stored to make sure a new one with the same count
  // is considered a duplicate, even when the queue is empty.
  unitLastMessageCount.add(element.getUnitMessageCount());

  // the setting 'deduplicate' does look at the content of the message and only compares it to messages in the queue.
  if (deduplicate && !sendQueue.empty()) {
    // Use reverse iterator here, as it is more likely a duplicate is added shortly after another.
    auto it = sendQueue.rbegin(); // Same as back()

    for (; it != sendQueue.rend(); ++it) {
      if (element.isDuplicate(*(it->get()))) {
#ifndef BUILD_NO_DEBUG

        if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
          const cpluginID_t cpluginID = getCPluginID_from_ControllerIndex(it->get()->_controller_idx);
          addLogMove(LOG_LEVEL_DEBUG, concat(get_formatted_Controller_number(cpluginID), F(" : Remove duplicate")));
        }
#endif // ifndef BUILD_NO_DEBUG
        return true;
      }
    }
  }
  return false;
}

// Try to add to the queue, if permitted by "delete_oldest"
// Return true when item was added, or skipped as it was considered a duplicate
bool ControllerDelayHandlerStruct::addToQueue(std::unique_ptr<Queue_element_base>element) {
  if (!element) { 
    return false;
  }
  if (isDuplicate(*element)) {
    return true;
  }

  if (delete_oldest) {
    // Force add to the queue.
    // If max buffer is reached, the oldest in the queue (first to be served) will be removed.
    while (queueFull(element->_controller_idx)) {
      sendQueue.pop_front();
      attempt = 0;
    }
  }

  if (!queueFull(element->_controller_idx)) {
    #ifdef USE_SECOND_HEAP
    // Do not store in 2nd heap, std::list cannot handle 2nd heap well
    HeapSelectDram ephemeral;
    #endif // ifdef USE_SECOND_HEAP

    sendQueue.push_back(std::move(element));

    return true;
  }
#ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    const cpluginID_t cpluginID = getCPluginID_from_ControllerIndex((*element)._controller_idx);
    addLogMove(LOG_LEVEL_DEBUG, concat(get_formatted_Controller_number(cpluginID), F(" : queue full")));
  }
#endif // ifndef BUILD_NO_DEBUG
  return false;
}

// Get the next element.
// Remove front element when max_retries is reached.
Queue_element_base * ControllerDelayHandlerStruct::getNext() {
  if (sendQueue.empty()) { return nullptr; }

  if (attempt > max_retries) {
    sendQueue.pop_front();
    attempt = 0;
  }

  if (expire_timeout != 0) {
    bool done = false;

    while (!done && !sendQueue.empty()) {
      if ((sendQueue.front().get() != nullptr) && (timePassedSince(sendQueue.front()->_timestamp) < static_cast<long>(expire_timeout))) {
        done = true;
      } else {
        sendQueue.pop_front();
        attempt = 0;
      }
    }
  }

  if (sendQueue.empty()) { return nullptr; }
  return sendQueue.front().get();
}

// Mark as processed and return time to schedule for next process.
// Return 0 when nothing to process.
// @param remove_from_queue indicates whether the elements should be removed from the queue.
unsigned long ControllerDelayHandlerStruct::markProcessed(bool remove_from_queue) {
  if (sendQueue.empty()) { return 0; }

  if (remove_from_queue) {
    sendQueue.pop_front();
    attempt  = 0;
    lastSend = millis();
  } else {
    ++attempt;
  }
  return getNextScheduleTime();
}

unsigned long ControllerDelayHandlerStruct::getNextScheduleTime() const {
  if (sendQueue.empty()) { return 0; }
  unsigned long nextTime = lastSend + minTimeBetweenMessages;

  if (timePassedSince(nextTime) > 0) {
    nextTime = millis();
  }

  if (nextTime == 0) { nextTime = 1; // Just to make sure it will be executed
  }
  return nextTime;
}

// Set the "lastSend" to "now" + some additional delay.
// This will cause the next schedule time to be delayed to
// msecFromNow + minTimeBetweenMessages
void ControllerDelayHandlerStruct::setAdditionalDelay(unsigned long msecFromNow) {
  lastSend = millis() + msecFromNow;
}

size_t ControllerDelayHandlerStruct::getQueueMemorySize() const {
  size_t totalSize = 0;

  for (auto it = sendQueue.begin(); it != sendQueue.end(); ++it) {
    if (it->get() != nullptr) {
      totalSize += it->get()->getSize();
    }
  }
  return totalSize;
}

void ControllerDelayHandlerStruct::process(
  cpluginID_t                        cpluginID,
  do_process_function                func,
  TimingStatsElements                timerstats_id,
  SchedulerIntervalTimer_e timerID) 
{
  Queue_element_base *element(static_cast<Queue_element_base *>(getNext()));

  if (element == nullptr) { return; }

  if (readyToProcess(*element)) {
    MakeControllerSettings(ControllerSettings);

    if (AllocatedControllerSettings()) {
      LoadControllerSettings(element->_controller_idx, *ControllerSettings);
      cacheControllerSettings(*ControllerSettings);
      START_TIMER;
      markProcessed(func(cpluginID, *element, *ControllerSettings));
      #if FEATURE_TIMING_STATS
      STOP_TIMER_VAR(timerstats_id);
      #endif
    }
  }
  Scheduler.scheduleNextDelayQueue(timerID, getNextScheduleTime());
}

#include "../ControllerQueue/DelayQueueElements.h"

#include "../DataStructs/ControllerSettingsStruct.h"
#include "../DataStructs/TimingStats.h"
#include "../Globals/ESPEasy_Scheduler.h"
#include "../Helpers/PeriodicalActions.h"

#if FEATURE_MQTT
ControllerDelayHandlerStruct *MQTTDelayHandler = nullptr;

bool init_mqtt_delay_queue(controllerIndex_t ControllerIndex, String& pubname, bool& retainFlag) {
  // Make sure the controller is re-connecting with the current settings.
  MQTTDisconnect();

  MakeControllerSettings(ControllerSettings); // -V522

  if (!AllocatedControllerSettings()) {
    return false;
  }
  LoadControllerSettings(ControllerIndex, *ControllerSettings);

  if (MQTTDelayHandler == nullptr) {
    # ifdef USE_SECOND_HEAP
    HeapSelectDram ephemeral;
    # endif // ifdef USE_SECOND_HEAP

    MQTTDelayHandler = new (std::nothrow) ControllerDelayHandlerStruct;
  }

  if (MQTTDelayHandler == nullptr) {
    return false;
  }
  MQTTDelayHandler->cacheControllerSettings(*ControllerSettings);
  pubname    = ControllerSettings->Publish;
  retainFlag = ControllerSettings->mqtt_retainFlag();
  Scheduler.setIntervalTimerOverride(SchedulerIntervalTimer_e::TIMER_MQTT, 10); // Make sure the MQTT is being processed as soon
                                                                                          // as possible.
  scheduleNextMQTTdelayQueue();
  return true;
}

void exit_mqtt_delay_queue() {
  if (MQTTDelayHandler != nullptr) {
    MQTTDisconnect();
    delete MQTTDelayHandler;
    MQTTDelayHandler = nullptr;
  }
}

#endif // if FEATURE_MQTT


/*********************************************************************************************\
* C001_queue_element for queueing requests for C001.
\*********************************************************************************************/
#ifdef USES_C001
DEFINE_Cxxx_DELAY_QUEUE_MACRO_CPP(00, 1) // -V522
#endif // ifdef USES_C001

/*********************************************************************************************\
* C003_queue_element for queueing requests for C003 Nodo Telnet.
\*********************************************************************************************/
#ifdef USES_C003
DEFINE_Cxxx_DELAY_QUEUE_MACRO_CPP(00, 3) // -V522
#endif // ifdef USES_C003

#ifdef USES_C004
DEFINE_Cxxx_DELAY_QUEUE_MACRO_CPP(00, 4) // -V522
#endif // ifdef USES_C004

#ifdef USES_C007
DEFINE_Cxxx_DELAY_QUEUE_MACRO_CPP(00, 7) // -V522
#endif // ifdef USES_C007


/*********************************************************************************************\
* C008_queue_element for queueing requests for 008: Generic HTTP
* Using SimpleQueueElement_formatted_Strings
\*********************************************************************************************/
#ifdef USES_C008
DEFINE_Cxxx_DELAY_QUEUE_MACRO_CPP(00, 8) // -V522
#endif // ifdef USES_C008

#ifdef USES_C009
DEFINE_Cxxx_DELAY_QUEUE_MACRO_CPP(00, 9) // -V522
#endif // ifdef USES_C009


/*********************************************************************************************\
* C010_queue_element for queueing requests for 010: Generic UDP
* Using SimpleQueueElement_formatted_Strings
\*********************************************************************************************/
#ifdef USES_C010
DEFINE_Cxxx_DELAY_QUEUE_MACRO_CPP(0, 10) // -V522
#endif // ifdef USES_C010


/*********************************************************************************************\
* C011_queue_element for queueing requests for 011: Generic HTTP Advanced
\*********************************************************************************************/
#ifdef USES_C011
DEFINE_Cxxx_DELAY_QUEUE_MACRO_CPP(0, 11) // -V522
#endif // ifdef USES_C011


/*********************************************************************************************\
* C012_queue_element for queueing requests for 012: Blynk
* Using SimpleQueueElement_formatted_Strings
\*********************************************************************************************/
#ifdef USES_C012
DEFINE_Cxxx_DELAY_QUEUE_MACRO_CPP(0, 12) // -V522
#endif // ifdef USES_C012

/*
 #ifdef USES_C013
   DEFINE_Cxxx_DELAY_QUEUE_MACRO_CPP(0, 13)  // -V522
 #endif
 */

/*
 #ifdef USES_C014
   DEFINE_Cxxx_DELAY_QUEUE_MACRO_CPP(0, 14)  // -V522
 #endif
 */


#ifdef USES_C015
DEFINE_Cxxx_DELAY_QUEUE_MACRO_CPP(0, 15) // -V522
#endif // ifdef USES_C015


#ifdef USES_C016
DEFINE_Cxxx_DELAY_QUEUE_MACRO_CPP(0, 16) // -V522
#endif // ifdef USES_C016


#ifdef USES_C017
DEFINE_Cxxx_DELAY_QUEUE_MACRO_CPP(0, 17) // -V522
#endif // ifdef USES_C017

#ifdef USES_C018
DEFINE_Cxxx_DELAY_QUEUE_MACRO_CPP(0, 18) // -V522
#endif // ifdef USES_C018


/*
 #ifdef USES_C019
   DEFINE_Cxxx_DELAY_QUEUE_MACRO_CPP(0, 19)  // -V522
 #endif
 */

/*
 #ifdef USES_C020
   DEFINE_Cxxx_DELAY_QUEUE_MACRO_CPP(0, 20)  // -V522
 #endif
 */

/*
 #ifdef USES_C021
   DEFINE_Cxxx_DELAY_QUEUE_MACRO_CPP(0, 21)  // -V522
 #endif
 */

/*
 #ifdef USES_C022
   DEFINE_Cxxx_DELAY_QUEUE_MACRO_CPP(0, 22)  // -V522
 #endif
 */

/*
 #ifdef USES_C023
   DEFINE_Cxxx_DELAY_QUEUE_MACRO_CPP(0, 23)  // -V522
 #endif
 */

/*
 #ifdef USES_C024
   DEFINE_Cxxx_DELAY_QUEUE_MACRO_CPP(0, 24)  // -V522
 #endif
 */

/*
 #ifdef USES_C025
   DEFINE_Cxxx_DELAY_QUEUE_MACRO_CPP(0, 25)  // -V522
 #endif
 */


// When extending this, search for EXTEND_CONTROLLER_IDS
// in the code to find all places that need to be updated too.

#include "../ControllerQueue/Queue_element_base.h"

Queue_element_base::Queue_element_base() :
  _controller_idx(INVALID_CONTROLLER_INDEX),
  _taskIndex(INVALID_TASK_INDEX),
  _call_PLUGIN_PROCESS_CONTROLLER_DATA(false),
  _processByController(false)
{
  _timestamp = millis();
}

Queue_element_base::~Queue_element_base() {}

#include "../ControllerQueue/C011_queue_element.h"

#include "../DataStructs/ESPEasy_EventStruct.h"

#ifdef USES_C011


C011_queue_element::C011_queue_element(const struct EventStruct *event) :
  idx(event->idx),
  sensorType(event->sensorType)
{
  _controller_idx = event->ControllerIndex;
  _taskIndex = event->TaskIndex;
}

size_t C011_queue_element::getSize() const {
  size_t total = sizeof(*this);

  total += uri.length();
  total += HttpMethod.length();
  total += header.length();
  total += postStr.length();
  return total;
}

bool C011_queue_element::isDuplicate(const Queue_element_base& other) const {
  const C011_queue_element& oth = static_cast<const C011_queue_element&>(other);

  if ((oth._controller_idx != _controller_idx) ||
      (oth._taskIndex != _taskIndex) ||
      (oth.sensorType != sensorType) ||
      (oth.idx != idx)) {
    return false;
  }
  return oth.uri.equals(uri) &&
         oth.HttpMethod.equals(HttpMethod) &&
         oth.header.equals(header) &&
         oth.postStr.equals(postStr);
}

#endif // ifdef USES_C011

#include "../ControllerQueue/C018_queue_element.h"

#ifdef USES_C018

# include "../DataStructs/ESPEasy_EventStruct.h"
# include "../DataStructs/UnitMessageCount.h"

# include "../ESPEasyCore/ESPEasy_Log.h"

# include "../Helpers/_CPlugin_LoRa_TTN_helper.h"
# include "../Helpers/StringConverter.h"

C018_queue_element::C018_queue_element(struct EventStruct *event, uint8_t sampleSetCount)
{
  _controller_idx = event->ControllerIndex;
  _taskIndex      = event->TaskIndex;
  # if FEATURE_PACKED_RAW_DATA
  move_special(packed, getPackedFromPlugin(event, sampleSetCount));

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO, concat(F("C018 queue element: "), packed));
  }
  # endif // if FEATURE_PACKED_RAW_DATA
}

size_t C018_queue_element::getSize() const {
  return sizeof(*this) + packed.length();
}

bool C018_queue_element::isDuplicate(const Queue_element_base& other) const {
  const C018_queue_element& oth = static_cast<const C018_queue_element&>(other);

  if ((oth._controller_idx != _controller_idx) ||
      (oth._taskIndex != _taskIndex) ||
      (oth.packed != packed)) {
    return false;
  }
  return true;
}

#endif // ifdef USES_C018

#include "../ControllerQueue/MQTT_queue_element.h"

#if FEATURE_MQTT

#include "../Helpers/StringConverter.h"

MQTT_queue_element::MQTT_queue_element(int ctrl_idx,
                                       taskIndex_t TaskIndex,
                                       const String& topic, const String& payload,
                                       bool retained, bool callbackTask) :
  _retained(retained)
{
  _controller_idx                      = ctrl_idx;
  _taskIndex                           = TaskIndex;
  _call_PLUGIN_PROCESS_CONTROLLER_DATA = callbackTask;

  // Copy in the scope of the constructor, so we might store it in the 2nd heap
  move_special(_topic, String(topic));
  move_special(_payload, String(payload));

  removeEmptyTopics();
}

MQTT_queue_element::MQTT_queue_element(int         ctrl_idx,
                                       taskIndex_t TaskIndex,
                                       String   && topic,
                                       String   && payload,
                                       bool        retained,
                                       bool        callbackTask)
  : _retained(retained)
{
  _controller_idx                      = ctrl_idx;
  _taskIndex                           = TaskIndex;
  _call_PLUGIN_PROCESS_CONTROLLER_DATA = callbackTask;

  // Copy in the scope of the constructor, so we might store it in the 2nd heap
  move_special(_topic, std::move(topic));
  move_special(_payload, std::move(payload));
  removeEmptyTopics();
}

size_t MQTT_queue_element::getSize() const {
  return sizeof(*this) + _topic.length() + _payload.length();
}

bool MQTT_queue_element::isDuplicate(const Queue_element_base& other) const {
  if (_call_PLUGIN_PROCESS_CONTROLLER_DATA || other._call_PLUGIN_PROCESS_CONTROLLER_DATA) {
    return false;
  }
  const MQTT_queue_element& oth = static_cast<const MQTT_queue_element&>(other);

  // TD-er: We do not compare the taskindex.
  // If it were to make a difference, the topic would be different.
  if ((oth._controller_idx != _controller_idx) ||
      (oth._retained != _retained) ||
      (oth._topic != _topic) ||
      (oth._payload != _payload)) {
    return false;
  }
  return true;
}

void MQTT_queue_element::removeEmptyTopics() {
  // some parts of the topic may have been replaced by empty strings,
  // or "/status" may have been appended to a topic ending with a "/"
  // Get rid of "//"
  while (_topic.indexOf(F("//")) != -1) {
    _topic.replace(F("//"), F("/"));
  }
}

#endif // if FEATURE_MQTT

#include "../ControllerQueue/C015_queue_element.h"

#include "../DataStructs/ESPEasy_EventStruct.h"

#ifdef USES_C015

#include "../Helpers/StringConverter.h"

C015_queue_element::C015_queue_element(C015_queue_element&& other)
  : idx(other.idx)
  , valuesSent(other.valuesSent)
  , valueCount(other.valueCount)
{
  _timestamp      = other._timestamp;
  _controller_idx = other._controller_idx;
  _taskIndex      = other._taskIndex;

  for (uint8_t i = 0; i < VARS_PER_TASK; ++i) {
    move_special(txt[i], std::move(other.txt[i]));
    vPin[i] = other.vPin[i];
  }
}

C015_queue_element::C015_queue_element(const struct EventStruct *event, uint8_t value_count) :
  idx(event->idx),
  valuesSent(0),
  valueCount(value_count) {
  _controller_idx = event->ControllerIndex;
  _taskIndex      = event->TaskIndex;
}

C015_queue_element& C015_queue_element::operator=(C015_queue_element&& other) {
  idx             = other.idx;
  _timestamp      = other._timestamp;
  _taskIndex      = other._taskIndex;
  _controller_idx = other._controller_idx;
  valuesSent      = other.valuesSent;
  valueCount      = other.valueCount;

  for (uint8_t i = 0; i < VARS_PER_TASK; ++i) {
    move_special(txt[i], std::move(other.txt[i]));
    vPin[i] = other.vPin[i];
  }
  return *this;
}

bool C015_queue_element::checkDone(bool succesfull) const {
  if (succesfull) { ++valuesSent; }
  return valuesSent >= valueCount || valuesSent >= VARS_PER_TASK;
}

size_t C015_queue_element::getSize() const {
  size_t total = sizeof(*this);

  for (int i = 0; i < VARS_PER_TASK; ++i) {
    total += txt[i].length();
  }
  return total;
}

bool C015_queue_element::isDuplicate(const Queue_element_base& other) const {
  const C015_queue_element& oth = static_cast<const C015_queue_element&>(other);

  if ((oth._controller_idx != _controller_idx) ||
      (oth._taskIndex != _taskIndex) ||
      (oth.valueCount != valueCount) ||
      (oth.idx != idx)) {
    return false;
  }

  for (uint8_t i = 0; i < VARS_PER_TASK; ++i) {
    if (oth.txt[i] != txt[i]) {
      return false;
    }

    if (oth.vPin[i] != vPin[i]) {
      return false;
    }
  }
  return true;
}

#endif // ifdef USES_C015

#include "../ControllerQueue/SimpleQueueElement_string_only.h"

#include "../Helpers/StringConverter.h"


simple_queue_element_string_only::simple_queue_element_string_only(int ctrl_idx, taskIndex_t TaskIndex,  String&& req)
{
  _controller_idx = ctrl_idx;
  _taskIndex      = TaskIndex;
  move_special(txt, std::move(req));
}

size_t simple_queue_element_string_only::getSize() const {
  return sizeof(*this) + txt.length();
}

bool simple_queue_element_string_only::isDuplicate(const Queue_element_base& other) const {
  const simple_queue_element_string_only& oth = static_cast<const simple_queue_element_string_only&>(other);

  if ((oth._controller_idx != _controller_idx) ||
      (oth._taskIndex != _taskIndex) ||
      (oth.txt != txt)) {
    return false;
  }
  return true;
}

#include "../DataStructs/SchedulerTimerID.h"

#include "../Helpers/Misc.h"

SchedulerTimerID::SchedulerTimerID(SchedulerTimerType_e timerType)
{
  set4BitToUL(mixed_id, 0, static_cast<uint32_t>(timerType));
}

void SchedulerTimerID::setTimerType(SchedulerTimerType_e timerType)
{
  set4BitToUL(mixed_id, 0, static_cast<uint32_t>(timerType));
}

SchedulerTimerType_e SchedulerTimerID::getTimerType() const
{
  return static_cast<SchedulerTimerType_e>(get4BitFromUL(mixed_id, 0));
}

uint32_t SchedulerTimerID::getId() const
{
  return mixed_id >> 4;
}

#include "../DataStructs/RulesEventCache.h"

#include "../DataStructs/TimingStats.h"
#include "../Helpers/RulesMatcher.h"
#include "../Helpers/StringConverter.h"


RulesEventCache_element::RulesEventCache_element(
  const String& filename, size_t pos, const String& event, const String& action)
    : _filename(filename), _posInFile(pos), _event(event), _action(action)
  {}


RulesEventCache_element::RulesEventCache_element(
  const String& filename, size_t pos, String&& event, String&& action) :
  _posInFile(pos)
{
  move_special(_filename, String(filename));
  move_special(_event, std::move(event));
  move_special(_action, std::move(action));  
}


void RulesEventCache::clear()
{
  _eventCache.clear();
  _initialized = false;
}

void RulesEventCache::initialize()
{
  _initialized = true;
}

bool RulesEventCache::addLine(const String& line, const String& filename, size_t pos)
{
  String event, action;

  if (getEventFromRulesLine(line, event, action)) {
    // Do not emplace on the 2nd heap
    # ifdef USE_SECOND_HEAP
    HeapSelectDram ephemeral;
    # endif // ifdef USE_SECOND_HEAP
    #ifdef ESP32
    reserve_special(event, event.length());
    reserve_special(action, action.length());
    #endif

    _eventCache.emplace_back(filename, pos, std::move(event), std::move(action));
    return true;
  }
  return false;
}

RulesEventCache_vector::const_iterator RulesEventCache::findMatchingRule(const String& event, bool optimize)
{
  RulesEventCache_vector::iterator it   = _eventCache.begin();
//  RulesEventCache_vector::iterator prev = _eventCache.end();

  // FIXME TD-er: Disable optimize as it has some side effects.
  // For example, matching a specific event first and then a more generic one is perfectly normal to do.
  // But this optimization will then put the generic one in front as it will be matched more often.
  // Thus it will never match the more specific one anymore.


  for (; it != _eventCache.end(); ++it)
  {
    START_TIMER
    const bool match = ruleMatch(event, it->_event);
    STOP_TIMER(RULES_MATCH);

    if (match) {
      /*
      if (optimize) {
        it->_nrTimesMatched++;

        if (prev != _eventCache.end()) {
          // Check to see if we need to place this one more to the front of the vector
          // to speed up parsing.
          if (prev->_nrTimesMatched < it->_nrTimesMatched) {
            std::swap(*prev, *it);
            return prev;
          }
        }
      }
      */
      return it;
    }
/*
    if (optimize) {
      if (prev == _eventCache.end()) {
        prev = it;
      }
      else if (prev->_nrTimesMatched > it->_nrTimesMatched) {
        // Found one that's having a lower match rate
        prev = it;
      }
    }
    */
  }
  return it;
}

#include "../DataStructs/FactoryDefault_WiFi_NVS.h"

#ifdef ESP32

# include "../Globals/Settings.h"
# include "../Globals/SecuritySettings.h"
# include "../Helpers/StringConverter.h"

// Max. 15 char keys for ESPEasy Factory Default marked keys
# define FACTORY_DEFAULT_NVS_SSID1_KEY      "WIFI_SSID1"
# define FACTORY_DEFAULT_NVS_WPA_PASS1_KEY  "WIFI_PASS1"
# define FACTORY_DEFAULT_NVS_SSID2_KEY      "WIFI_SSID2"
# define FACTORY_DEFAULT_NVS_WPA_PASS2_KEY  "WIFI_PASS2"
# define FACTORY_DEFAULT_NVS_AP_PASS_KEY    "WIFI_AP_PASS"
# define FACTORY_DEFAULT_NVS_WIFI_FLAGS_KEY "WIFI_Flags"


void FactoryDefault_WiFi_NVS::fromSettings() {
  bits.IncludeHiddenSSID              = Settings.IncludeHiddenSSID();
  bits.ApDontForceSetup               = Settings.ApDontForceSetup();
  bits.DoNotStartAP                   = Settings.DoNotStartAP();
  bits.ForceWiFi_bg_mode              = Settings.ForceWiFi_bg_mode();
  bits.WiFiRestart_connection_lost    = Settings.WiFiRestart_connection_lost();
  bits.WifiNoneSleep                  = Settings.WifiNoneSleep();
  bits.gratuitousARP                  = Settings.gratuitousARP();
  bits.UseMaxTXpowerForSending        = Settings.UseMaxTXpowerForSending();
  bits.UseLastWiFiFromRTC             = Settings.UseLastWiFiFromRTC();
  bits.WaitWiFiConnect                = Settings.WaitWiFiConnect();
  bits.SDK_WiFi_autoreconnect         = Settings.SDK_WiFi_autoreconnect();
  bits.HiddenSSID_SlowConnectPerBSSID = Settings.HiddenSSID_SlowConnectPerBSSID();
  bits.EnableIPv6                     = Settings.EnableIPv6();
  bits.PassiveWiFiScan                = Settings.PassiveWiFiScan();
}

void FactoryDefault_WiFi_NVS::applyToSettings() const {
  Settings.IncludeHiddenSSID(bits.IncludeHiddenSSID);
  Settings.ApDontForceSetup(bits.ApDontForceSetup);
  Settings.DoNotStartAP(bits.DoNotStartAP);
  Settings.ForceWiFi_bg_mode(bits.ForceWiFi_bg_mode);
  Settings.WiFiRestart_connection_lost(bits.WiFiRestart_connection_lost);
  Settings.WifiNoneSleep(bits.WifiNoneSleep);
  Settings.gratuitousARP(bits.gratuitousARP);
  Settings.UseMaxTXpowerForSending(bits.UseMaxTXpowerForSending);
  Settings.UseLastWiFiFromRTC(bits.UseLastWiFiFromRTC);
  Settings.WaitWiFiConnect(bits.WaitWiFiConnect);
  Settings.SDK_WiFi_autoreconnect(bits.SDK_WiFi_autoreconnect);
  Settings.HiddenSSID_SlowConnectPerBSSID(bits.HiddenSSID_SlowConnectPerBSSID);
  Settings.EnableIPv6(bits.EnableIPv6);
  Settings.PassiveWiFiScan(bits.PassiveWiFiScan);
}

struct FactoryDefault_WiFi_NVS_securityPrefs {
  FactoryDefault_WiFi_NVS_securityPrefs(const __FlashStringHelper *pref,
                                        char                      *dest,
                                        size_t                     size)
    : _pref(pref), _dest(dest), _size(size) {}

  const __FlashStringHelper *_pref;
  char                      *_dest;
  size_t                     _size;
};

const FactoryDefault_WiFi_NVS_securityPrefs _WiFi_NVS_securityPrefs_values[] = {
  { F(FACTORY_DEFAULT_NVS_SSID1_KEY),     SecuritySettings.WifiSSID,  sizeof(SecuritySettings.WifiSSID)  },
  { F(FACTORY_DEFAULT_NVS_WPA_PASS1_KEY), SecuritySettings.WifiKey,   sizeof(SecuritySettings.WifiKey)   },
  { F(FACTORY_DEFAULT_NVS_SSID2_KEY),     SecuritySettings.WifiSSID2, sizeof(SecuritySettings.WifiSSID2) },
  { F(FACTORY_DEFAULT_NVS_WPA_PASS2_KEY), SecuritySettings.WifiKey2,  sizeof(SecuritySettings.WifiKey2)  },
  { F(FACTORY_DEFAULT_NVS_AP_PASS_KEY),   SecuritySettings.WifiAPKey, sizeof(SecuritySettings.WifiAPKey) }
};


bool FactoryDefault_WiFi_NVS::applyToSettings_from_NVS(ESPEasy_NVS_Helper& preferences) {
  String tmp;
  constexpr unsigned nr__WiFi_NVS_securityPrefs_values = NR_ELEMENTS(_WiFi_NVS_securityPrefs_values);

  for (unsigned i = 0; i < nr__WiFi_NVS_securityPrefs_values; ++i) {
    if (preferences.getPreference(_WiFi_NVS_securityPrefs_values[i]._pref, tmp)) {
      safe_strncpy(_WiFi_NVS_securityPrefs_values[i]._dest, tmp, _WiFi_NVS_securityPrefs_values[i]._size);
    }
  }

  if (!preferences.getPreference(F(FACTORY_DEFAULT_NVS_WIFI_FLAGS_KEY), data)) {
    return false;
  }

  applyToSettings();
  return true;
}

void FactoryDefault_WiFi_NVS::fromSettings_to_NVS(ESPEasy_NVS_Helper& preferences) {
  fromSettings();
  preferences.setPreference(F(FACTORY_DEFAULT_NVS_WIFI_FLAGS_KEY), data);

  // Store WiFi credentials
  constexpr unsigned nr__WiFi_NVS_securityPrefs_values = NR_ELEMENTS(_WiFi_NVS_securityPrefs_values);

  for (unsigned i = 0; i < nr__WiFi_NVS_securityPrefs_values; ++i) {
    preferences.setPreference(_WiFi_NVS_securityPrefs_values[i]._pref, String(_WiFi_NVS_securityPrefs_values[i]._dest));
  }
}

void FactoryDefault_WiFi_NVS::clear_from_NVS(ESPEasy_NVS_Helper& preferences) {
  constexpr unsigned nr__WiFi_NVS_securityPrefs_values = NR_ELEMENTS(_WiFi_NVS_securityPrefs_values);

  for (unsigned i = 0; i < nr__WiFi_NVS_securityPrefs_values; ++i) {
    preferences.remove(_WiFi_NVS_securityPrefs_values[i]._pref);
  }
  preferences.remove(F(FACTORY_DEFAULT_NVS_WIFI_FLAGS_KEY));
}

#endif // ifdef ESP32

#include "../DataStructs/Scheduler_GPIOTimerID.h"

#include "../DataStructs/PinMode.h"

GPIOTimerID::GPIOTimerID(uint8_t GPIOType, uint8_t pinNumber, int Par1) :
  SchedulerTimerID(SchedulerTimerType_e::GPIO_timer)
{
  setId((Par1 << 16) + (pinNumber << 8) + GPIOType);
}


#ifndef BUILD_NO_DEBUG
String GPIOTimerID::decode() const
{
  String result;

  switch (getGPIO_type())
  {
    case GPIO_TYPE_INTERNAL:
      result += F("int");
      break;
    case GPIO_TYPE_MCP:
      result += F("MCP");
      break;
    case GPIO_TYPE_PCF:
      result += F("PCF");
      break;
    default:
      result += '?';
      break;
  }
  result += F(" pin: ");
  result += getPinNumber();
  result += F(" state: ");
  result += getPinStateValue();
  return result;
}

#endif // ifndef BUILD_NO_DEBUG

#include "../DataStructs/FactoryDefaultPref.h"

#include "../../ESPEasy_common.h"

ResetFactoryDefaultPreference_struct::ResetFactoryDefaultPreference_struct() : _preference(0)
{}

ResetFactoryDefaultPreference_struct::ResetFactoryDefaultPreference_struct(uint32_t preference) : _preference(preference)
{}

void ResetFactoryDefaultPreference_struct::set(uint32_t preference)
{
  _preference = preference;
}

#ifdef ESP32

// Max. 15 char keys for ESPEasy Factory Default marked keys
# define FACTORY_DEFAULT_NVS_PREF_KEY       "FacDefPref"

bool ResetFactoryDefaultPreference_struct::init(ESPEasy_NVS_Helper& preferences)
{
  if (preferences.begin(F(FACTORY_DEFAULT_NVS_NAMESPACE))) {
    return from_NVS(preferences);
  }
  return false;
}

bool ResetFactoryDefaultPreference_struct::from_NVS(ESPEasy_NVS_Helper& preferences)
{
  return preferences.getPreference(F(FACTORY_DEFAULT_NVS_PREF_KEY), _preference);
}

void ResetFactoryDefaultPreference_struct::to_NVS(ESPEasy_NVS_Helper& preferences) const
{
  preferences.setPreference(F(FACTORY_DEFAULT_NVS_PREF_KEY), _preference);
}

#endif // ifdef ESP32


/*
   DeviceModel ResetFactoryDefaultPreference_struct::getDeviceModel() const {
   return static_cast<DeviceModel>(_preference & 0xFF);
   }

   void ResetFactoryDefaultPreference_struct::setDeviceModel(DeviceModel model) {
   _preference &= ~(0xFF); // set DeviceModel bits to 0
   _preference |= static_cast<uint32_t>(model);
   }

   bool ResetFactoryDefaultPreference_struct::keepWiFi() const {
   return bitRead(_preference, 9);
   }

   void ResetFactoryDefaultPreference_struct::keepWiFi(bool keep) {
   bitWrite(_preference, 9, keep);
   }

   bool ResetFactoryDefaultPreference_struct::keepNTP() const {
   return bitRead(_preference, 10);
   }

   void ResetFactoryDefaultPreference_struct::keepNTP(bool keep) {
   bitWrite(_preference, 10, keep);
   }

   bool ResetFactoryDefaultPreference_struct::keepNetwork() const {
   return bitRead(_preference, 11);
   }

   void ResetFactoryDefaultPreference_struct::keepNetwork(bool keep) {
   bitWrite(_preference, 11, keep);
   }

   bool ResetFactoryDefaultPreference_struct::keepLogConsoleSettings() const {
   return bitRead(_preference, 12);
   }

   void ResetFactoryDefaultPreference_struct::keepLogConsoleSettings(bool keep) {
   bitWrite(_preference, 12, keep);
   }

   bool ResetFactoryDefaultPreference_struct::keepUnitName() const {
   return bitRead(_preference, 13);
   }

   void ResetFactoryDefaultPreference_struct::keepUnitName(bool keep) {
   bitWrite(_preference, 13, keep);
   }

   // filenr = 0...3 for files rules1.txt ... rules4.txt
   bool ResetFactoryDefaultPreference_struct::fetchRulesTXT(int filenr) const {
   return bitRead(_preference, 14 + filenr);
   }

   void ResetFactoryDefaultPreference_struct::fetchRulesTXT(int filenr, bool fetch) {
   bitWrite(_preference, 14 + filenr, fetch);
   }

   bool ResetFactoryDefaultPreference_struct::fetchNotificationDat() const {
   return bitRead(_preference, 18);
   }

   void ResetFactoryDefaultPreference_struct::fetchNotificationDat(bool fetch) {
   bitWrite(_preference, 18, fetch);
   }

   bool ResetFactoryDefaultPreference_struct::fetchSecurityDat() const {
   return bitRead(_preference, 19);
   }

   void ResetFactoryDefaultPreference_struct::fetchSecurityDat(bool fetch) {
   bitWrite(_preference, 19, fetch);
   }

   bool ResetFactoryDefaultPreference_struct::fetchConfigDat() const {
   return bitRead(_preference, 20);
   }

   void ResetFactoryDefaultPreference_struct::fetchConfigDat(bool fetch) {
   bitWrite(_preference, 20, fetch);
   }

   bool ResetFactoryDefaultPreference_struct::deleteFirst() const {
   return bitRead(_preference, 21);
   }

   void ResetFactoryDefaultPreference_struct::deleteFirst(bool checked) {
   bitWrite(_preference, 21, checked);
   }

   bool ResetFactoryDefaultPreference_struct::delete_Bak_Files() const {
   return bitRead(_preference, 23);
   }

   void ResetFactoryDefaultPreference_struct::delete_Bak_Files(bool checked) {
   bitWrite(_preference, 23, checked);
   }

   bool ResetFactoryDefaultPreference_struct::saveURL() const {
   return bitRead(_preference, 22);
   }

   void ResetFactoryDefaultPreference_struct::saveURL(bool checked) {
   bitWrite(_preference, 22, checked);
   }

   bool ResetFactoryDefaultPreference_struct::storeCredentials() const {
   return bitRead(_preference, 24);
   }

   void ResetFactoryDefaultPreference_struct::storeCredentials(bool checked) {
   bitWrite(_preference, 24, checked);
   }

   bool ResetFactoryDefaultPreference_struct::fetchProvisioningDat() const {
   return bitRead(_preference, 25);
   }

   void ResetFactoryDefaultPreference_struct::fetchProvisioningDat(bool checked) {
   bitWrite(_preference, 25, checked);
   }


   uint32_t ResetFactoryDefaultPreference_struct::getPreference() {
   return _preference;
   }
 */

#include "../DataStructs/ESPEasy_EventStruct.h"

#include "../../ESPEasy_common.h"

#include "../CustomBuild/ESPEasyLimits.h"
#include "../DataTypes/EventValueSource.h"
#include "../Globals/Plugins.h"
#include "../Globals/CPlugins.h"
#include "../Globals/NPlugins.h"

#include "../../_Plugin_Helper.h"

EventStruct::EventStruct(taskIndex_t taskIndex) :
  TaskIndex(taskIndex), BaseVarIndex(taskIndex * VARS_PER_TASK)
{
  if (taskIndex >= INVALID_TASK_INDEX) {
    BaseVarIndex = 0;
  }
}

void EventStruct::deep_copy(const struct EventStruct& other) {
  this->operator=(other);
}

void EventStruct::deep_copy(const struct EventStruct *other) {
  if (other != nullptr) {
    deep_copy(*other);
  }
}

void EventStruct::setTaskIndex(taskIndex_t taskIndex) {
  TaskIndex = taskIndex;

  if (TaskIndex < INVALID_TASK_INDEX) {
    BaseVarIndex = taskIndex * VARS_PER_TASK;
  }
  sensorType = Sensor_VType::SENSOR_TYPE_NOT_SET;
}

void EventStruct::clear() {
  *this = EventStruct();
}

Sensor_VType EventStruct::getSensorType() {
  const int tmp_idx = idx;

  checkDeviceVTypeForTask(this);
  idx = tmp_idx;
  return sensorType;
}

int64_t EventStruct::getTimestamp_as_systemMicros() const
{
  if (timestamp_sec == 0) 
  return  getMicros64();

  // FIXME TD-er: What to do when system time has not been set?
  int64_t res = node_time.Unixtime_to_systemMicros(timestamp_sec, timestamp_frac);
  if (res < 0) {
    // Unix time was from before we booted
    // FIXME TD-er: What to do now?
    return  getMicros64();
  }
  return res;
}

void EventStruct::setUnixTimeTimestamp()
{
  timestamp_sec = node_time.getUnixTime(timestamp_frac);
}

void EventStruct::setLocalTimeTimestamp()
{
  timestamp_sec = node_time.getLocalUnixTime(timestamp_frac);
}
#include "../DataStructs/PinMode.h"

constexpr pluginID_t P001_PLUGIN_ID{PLUGIN_GPIO_INT};
constexpr pluginID_t P009_PLUGIN_ID{PLUGIN_MCP_INT};
constexpr pluginID_t P019_PLUGIN_ID{PLUGIN_PCF_INT};


const pluginID_t PLUGIN_GPIO = P001_PLUGIN_ID;
const pluginID_t PLUGIN_MCP  = P009_PLUGIN_ID;
const pluginID_t PLUGIN_PCF  = P019_PLUGIN_ID;

#include "../DataStructs/Modbus.h"

#if FEATURE_MODBUS

#include "../DataStructs/ControllerSettingsStruct.h"
#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../Helpers/StringConverter.h"


Modbus::Modbus() : ModbusClient(nullptr), errcnt(0), timeout(0),
  TXRXstate(MODBUS_IDLE), RXavailable(0), payLoad(0) {}

Modbus::~Modbus() {
  if (ModbusClient) {
    ModbusClient->PR_9453_FLUSH_TO_CLEAR();
    ModbusClient->stop();
    delete (ModbusClient);
    delay(1);
    ModbusClient = nullptr;
  }
}

bool Modbus::begin(uint8_t function, uint8_t ModbusID, uint16_t ModbusRegister,  MODBUS_registerTypes_t type, char *IPaddress)
{
  currentRegister = ModbusRegister;
  currentFunction = function;
  incomingValue   = type;
  resultReceived  = false;
  if (ModbusClient) {
    ModbusClient->PR_9453_FLUSH_TO_CLEAR();
    ModbusClient->stop();
    delete (ModbusClient);
    delay(1);
    ModbusClient = nullptr;
  }
  ModbusClient    = new (std::nothrow) WiFiClient();
  if (ModbusClient == nullptr) {
    return false;
  }
  ModbusClient->setNoDelay(true);
  ModbusClient->setTimeout(CONTROLLER_CLIENTTIMEOUT_DFLT);
  timeout = millis();
  ModbusClient->PR_9453_FLUSH_TO_CLEAR();

  if (ModbusClient->connected()) {
    #ifndef BUILD_NO_DEBUG
    LogString += F(" already connected. ");
    #endif
  } else {
    #ifndef BUILD_NO_DEBUG
    LogString += F("connect: ");
    LogString += IPaddress;
    #endif

    if (ModbusClient->connect(IPaddress, 502) != 1) {
      #ifndef BUILD_NO_DEBUG
      LogString += F(" fail. ");
      #endif
      TXRXstate  = MODBUS_IDLE;
      errcnt++;

      #ifndef BUILD_NO_DEBUG
      if (LogString.length() > 1) { addLog(LOG_LEVEL_DEBUG, LogString); }
      #endif
      return false;
    } else {
      // Make sure no stale connection is left
      ModbusClient->stop();
    }
  }
  #ifndef BUILD_NO_DEBUG
  LogString += F(" OK, sending read request: ");
  #endif

  sendBuffer[6] = ModbusID;
  sendBuffer[7] = function;
  sendBuffer[8] = (ModbusRegister >> 8);
  sendBuffer[9] = (ModbusRegister & 0x00ff);

  if ((incomingValue == signed64) || (incomingValue == unsigned64)) {
    sendBuffer[11] = 4;
  }

  if ((incomingValue == signed32) || (incomingValue == unsigned32)) {
    sendBuffer[11] = 2;
  }

  if ((incomingValue == signed16) || (incomingValue == unsigned16)) {
    sendBuffer[11] = 1;
  }
  ModbusClient->PR_9453_FLUSH_TO_CLEAR();
  ModbusClient->write(&sendBuffer[0], sizeof(sendBuffer));

  #ifndef BUILD_NO_DEBUG
  for (unsigned int i = 0; i < sizeof(sendBuffer); i++) {
    LogString += ((unsigned int)(sendBuffer[i]));
    LogString += ' ';
  }
  #endif
  TXRXstate = MODBUS_RECEIVE;
  
  #ifndef BUILD_NO_DEBUG
  if (LogString.length() > 1) { addLog(LOG_LEVEL_DEBUG, LogString); }
  #endif
  return true;
}

bool Modbus::handle() {
  unsigned int RXavailable = 0;

  #ifndef BUILD_NO_DEBUG
  free_string(LogString);
  #endif
  int64_t rxValue = 0;

  switch (TXRXstate) {
    case MODBUS_IDLE:

      // clean up;
      if (ModbusClient) {
        ModbusClient->PR_9453_FLUSH_TO_CLEAR();
        ModbusClient->stop();
        delete (ModbusClient);
        delay(1);
        ModbusClient = nullptr;
      }
      break;

    case MODBUS_RECEIVE:

      if  (hasTimeout()) { break; }

      if  (ModbusClient->available() < 9) { break; }

      #ifndef BUILD_NO_DEBUG
      LogString += F("reading bytes: ");
      #endif

      for (int a = 0; a < 9; a++) {
        payLoad    = ModbusClient->read();
        #ifndef BUILD_NO_DEBUG
        LogString += (payLoad);
        LogString += ' ';
        #endif
      }
      #ifndef BUILD_NO_DEBUG
      LogString += F("> ");
      #endif

      if (payLoad > 8) {
        #ifndef BUILD_NO_DEBUG
        LogString += F("Payload too large !? ");
        #endif
        errcnt++;
        TXRXstate = MODBUS_IDLE;
      }
      // FIXME TD-er: Missing break?

    case MODBUS_RECEIVE_PAYLOAD:

      if  (hasTimeout()) { break; }
      RXavailable = ModbusClient->available();

      if (payLoad != RXavailable) {
        TXRXstate = MODBUS_RECEIVE_PAYLOAD;
        break;
      }

      for (unsigned int i = 0; i < RXavailable; i++) {
        rxValue = rxValue << 8;
        char a = ModbusClient->read();
        rxValue    = rxValue | a;
        #ifndef BUILD_NO_DEBUG
        LogString += static_cast<int>(a);  
        LogString += ' ';
        #endif
      }

      switch (incomingValue) {
        case signed16:
          result = (int16_t)rxValue;
          break;
        case unsigned16:
          result = (uint16_t)rxValue;
          break;
        case signed32:
          result = (int32_t)rxValue;
          break;
        case unsigned32:
          result = (uint32_t)rxValue;
          break;
        case signed64:
          result = (int64_t)rxValue;
          break;
        case unsigned64:
          result = (uint64_t)rxValue;
          break;
      }

      #ifndef BUILD_NO_DEBUG
      LogString += F("value: "); 
      LogString += result;
      #endif

      // if ((systemTimePresent()) && (hour() == 0)) errcnt = 0;

      TXRXstate = MODBUS_IDLE;

      resultReceived = true;
      break;

    default:
      #ifndef BUILD_NO_DEBUG
      LogString += F("default. ");
      #endif
      TXRXstate  = MODBUS_IDLE;
      break;
  }
  #ifndef BUILD_NO_DEBUG
  if (LogString.length() > 1) { addLog(LOG_LEVEL_DEBUG, LogString); }
  #endif
  return true;
}

bool Modbus::hasTimeout()
{
  if   ((millis() - timeout) > 10000) { // too many bytes or timeout
    #ifndef BUILD_NO_DEBUG
    LogString += F("Modbus RX timeout. "); 
    LogString += String(ModbusClient->available());
    #endif
    errcnt++;
    TXRXstate = MODBUS_IDLE;
    return true;
  }
  return false;
}

// tryread can be called in a round robin fashion. It will initiate a read if Modbus is idle and update the result once it is available.
// subsequent calls (if Modbus is busy etc. ) will return false and not update the result.
// Use to read multiple values non blocking in an re-entrant function. Not tested yet.
bool Modbus::tryRead(uint8_t ModbusID, uint16_t M_register,  MODBUS_registerTypes_t type, char *IPaddress, ESPEASY_RULES_FLOAT_TYPE& result) {
  if (isBusy()) { return false; // not done yet
  }

  if (available()) {
    if ((currentFunction == MODBUS_FUNCTION_READ) && (currentRegister == M_register)) {
      result = read(); // result belongs to this request.
      return true;
    }
  } else {
    begin(MODBUS_FUNCTION_READ, ModbusID, M_register, type, IPaddress); // idle and no result -> begin read request
  }
  return false;
}

#endif // FEATURE_MODBUS

#include "../DataStructs/tcp_cleanup.h"



#if defined(ESP8266)

struct tcp_pcb;
extern struct tcp_pcb* tcp_tw_pcbs;
extern "C" void tcp_abort (struct tcp_pcb* pcb);

void tcpCleanup()
{

     while(tcp_tw_pcbs!=nullptr)
    {
      tcp_abort(tcp_tw_pcbs);
    }

 }
#endif
#include "../DataStructs/Scheduler_PluginTaskTimerID.h"

#include "../Globals/Plugins.h"
#include "../Helpers/Misc.h"

PluginTaskTimerID::PluginTaskTimerID(taskIndex_t       taskIndex,
                                     int               Par1,
                                     PluginFunctions_e function) :
  SchedulerTimerID(SchedulerTimerType_e::PLUGIN_TASKTIMER_IN_e)
{
  constexpr unsigned nrBitsTaskIndex      = NR_BITS(TASKS_MAX);
  constexpr unsigned mask_taskIndex       = MASK_BITS(nrBitsTaskIndex);
  constexpr unsigned nrBitsPluginFunction = NrBitsPluginFunctions;
  constexpr unsigned mask_function        = MASK_BITS(nrBitsPluginFunction);

  if (validTaskIndex(taskIndex)) {
    setId((taskIndex & mask_taskIndex) |
         ((function & mask_function) << nrBitsTaskIndex) |
         (Par1 << (nrBitsTaskIndex + nrBitsPluginFunction)));
  }
}

taskIndex_t PluginTaskTimerID::getTaskIndex() const
{
  constexpr unsigned nrBitsTaskIndex = NR_BITS(TASKS_MAX);
  constexpr unsigned mask_taskIndex  = MASK_BITS(nrBitsTaskIndex);

  return static_cast<taskIndex_t>(getId() & mask_taskIndex);
}

PluginFunctions_e PluginTaskTimerID::getFunction() const
{
  constexpr unsigned nrBitsTaskIndex      = NR_BITS(TASKS_MAX);
  constexpr unsigned nrBitsPluginFunction = NrBitsPluginFunctions;
  constexpr unsigned mask_function        = MASK_BITS(nrBitsPluginFunction);

  return static_cast<PluginFunctions_e>((getId() >> nrBitsTaskIndex) & mask_function);
}

#ifndef BUILD_NO_DEBUG
String PluginTaskTimerID::decode() const
{
  const taskIndex_t taskIndex = getTaskIndex();

  if (validTaskIndex(taskIndex)) {
    return getTaskDeviceName(taskIndex);
  }
  return String(getId());
}

#endif // ifndef BUILD_NO_DEBUG

#include "../DataStructs/ProvisioningStruct.h"

#if FEATURE_CUSTOM_PROVISIONING

# include "../Helpers/StringConverter.h"
# include "../Helpers/Hardware.h"

ProvisioningStruct::ProvisioningStruct() {
  _allBits = 0u;
}

void ProvisioningStruct::validate() {
  ZERO_TERMINATE(user);
  ZERO_TERMINATE(pass);
  ZERO_TERMINATE(url);
}

bool ProvisioningStruct::matchingFlashSize() const
{
  return modelMatchingFlashSize(ResetFactoryDefaultPreference.getDeviceModel());
}

bool ProvisioningStruct::setUser(const String& username)
{
  return safe_strncpy(user, username, sizeof(user));
}

bool ProvisioningStruct::setPass(const String& password)
{
  return safe_strncpy(pass, password, sizeof(pass));
}

bool ProvisioningStruct::setUrl(const String& url_str)
{
  return safe_strncpy(url, url_str, sizeof(url));
}

bool ProvisioningStruct::fetchFileTypeAllowed(FileType::Enum filetype, unsigned int filenr) const
{
  switch (filetype) {
    case FileType::CONFIG_DAT:       return allowedFlags.allowFetchConfigDat;
    case FileType::SECURITY_DAT:     return allowedFlags.allowFetchSecurityDat;
    case FileType::NOTIFICATION_DAT: return allowedFlags.allowFetchNotificationDat;
    case FileType::RULES_TXT:        return (filenr < RULESETS_MAX) && bitRead(allowedFlags.allowFetchRules, filenr);
    case FileType::PROVISIONING_DAT: return allowedFlags.allowFetchProvisioningDat;

    case FileType::MAX_FILETYPE:
      break;
  }
  return false;
}

void ProvisioningStruct::setFetchFileTypeAllowed(FileType::Enum filetype, unsigned int filenr, bool checked)
{
  switch (filetype) {
    case FileType::CONFIG_DAT:       allowedFlags.allowFetchConfigDat       = checked; break;
    case FileType::SECURITY_DAT:     allowedFlags.allowFetchSecurityDat     = checked; break;
    case FileType::NOTIFICATION_DAT: allowedFlags.allowFetchNotificationDat = checked; break;
    case FileType::PROVISIONING_DAT: allowedFlags.allowFetchProvisioningDat = checked; break;
    case FileType::RULES_TXT:

      if (filenr < RULESETS_MAX) {
        bitWrite(allowedFlags.allowFetchRules, filenr, checked);
      }
      break;

    case FileType::MAX_FILETYPE:
      break;
  }
}

#endif // if FEATURE_CUSTOM_PROVISIONING

#include "../DataStructs/PortStatusStruct.h"

#include "../DataStructs/PinMode.h"


portStatusStruct::portStatusStruct() : state(-1), output(-1), command(0), init(0), not_used(0), mode(0), task(0), monitor(0), forceMonitor(0),
  forceEvent(0), previousTask(-1), x(INVALID_DEVICE_INDEX) {}

uint16_t portStatusStruct::getDutyCycle() const
{
  if (mode == PIN_MODE_PWM) {
    return dutyCycle;
  }
  return 0;
}

int16_t portStatusStruct::getValue() const
{
  if (mode == PIN_MODE_PWM || mode == PIN_MODE_SERVO)
    return dutyCycle;
  return state;
}

#include "../DataStructs/PluginStats_Config.h"


#if FEATURE_PLUGIN_STATS

PluginStats_Config_t::PluginStats_Config_t(const PluginStats_Config_t& other)
{
  setStored(other.getStored());
}

PluginStats_Config_t & PluginStats_Config_t::operator=(const PluginStats_Config_t& other)
{
  setStored(other.getStored());
  return *this;
}

#endif // if FEATURE_PLUGIN_STATS

#include "../DataStructs/FactoryDefault_Network_NVS.h"

#ifdef ESP32

# include "../Globals/Settings.h"
# include "../Helpers/StringConverter.h"

// Max. 15 char keys for ESPEasy Factory Default marked keys
# define FACTORY_DEFAULT_NVS_WIFI_IP_KEY       "WiFI_IP"
# ifdef FEATURE_ETHERNET
#  define FACTORY_DEFAULT_NVS_ETH_IP_KEY       "ETH_IP"
#  define FACTORY_DEFAULT_NVS_ETH_HW_CONF_KEY  "ETH_HW_CONF"
# endif // ifdef FEATURE_ETHERNET

bool FactoryDefault_Network_NVS::applyToSettings_from_NVS(ESPEasy_NVS_Helper& preferences)
{
  bool res = false;

  uint8_t *write = reinterpret_cast<uint8_t *>(&Settings);

  if (preferences.getPreference(F(FACTORY_DEFAULT_NVS_WIFI_IP_KEY), IP_data, sizeof(IP_data))) {
    // TD-er: This data is stored in sequence, so we can do a single memcpy call
    constexpr unsigned int offset = offsetof(SettingsStruct, IP);
    memcpy(write + offset, IP_data, sizeof(IP_data));
    res = true;
  }
# ifdef FEATURE_ETHERNET

  if (preferences.getPreference(F(FACTORY_DEFAULT_NVS_ETH_IP_KEY), IP_data, sizeof(IP_data))) {
    constexpr unsigned int offset = offsetof(SettingsStruct, ETH_IP);

    memcpy(write + offset, IP_data, sizeof(IP_data));
    res = true;
  }

  if (preferences.getPreference(F(FACTORY_DEFAULT_NVS_ETH_HW_CONF_KEY), ETH_HW_conf)) {
    Settings.ETH_Phy_Addr   = bits.ETH_Phy_Addr;
    Settings.ETH_Pin_mdc_cs    = bits.ETH_Pin_mdc_cs;
    Settings.ETH_Pin_mdio_irq   = bits.ETH_Pin_mdio_irq;
    Settings.ETH_Pin_power_rst  = bits.ETH_Pin_power_rst;
    Settings.ETH_Phy_Type   = static_cast<EthPhyType_t>(bits.ETH_Phy_Type);
    Settings.ETH_Clock_Mode = static_cast<EthClockMode_t>(bits.ETH_Clock_Mode);
    Settings.NetworkMedium  = static_cast<NetworkMedium_t>(bits.NetworkMedium);
    res                     = true;
  }
# endif // ifdef FEATURE_ETHERNET
  return res;
}

void FactoryDefault_Network_NVS::fromSettings_to_NVS(ESPEasy_NVS_Helper& preferences)
{
  const uint8_t *read = reinterpret_cast<const uint8_t *>(&Settings);

  {
    constexpr unsigned int offset = offsetof(SettingsStruct, IP);
    memcpy(IP_data, read + offset, sizeof(IP_data));
    preferences.setPreference(F(FACTORY_DEFAULT_NVS_WIFI_IP_KEY), IP_data, sizeof(IP_data));
  }
# ifdef FEATURE_ETHERNET
  {
    constexpr unsigned int offset = offsetof(SettingsStruct, ETH_IP);
    memcpy(IP_data, read + offset, sizeof(IP_data));
    preferences.setPreference(F(FACTORY_DEFAULT_NVS_ETH_IP_KEY), IP_data, sizeof(IP_data));
  }
  {
    bits.ETH_Phy_Addr   = Settings.ETH_Phy_Addr;
    bits.ETH_Pin_mdc_cs    = Settings.ETH_Pin_mdc_cs;
    bits.ETH_Pin_mdio_irq   = Settings.ETH_Pin_mdio_irq;
    bits.ETH_Pin_power_rst  = Settings.ETH_Pin_power_rst;
    bits.ETH_Phy_Type   = static_cast<uint8_t>(Settings.ETH_Phy_Type);
    bits.ETH_Clock_Mode = static_cast<uint8_t>(Settings.ETH_Clock_Mode);
    bits.NetworkMedium  = static_cast<uint8_t>(Settings.NetworkMedium);
    preferences.setPreference(F(FACTORY_DEFAULT_NVS_ETH_HW_CONF_KEY), ETH_HW_conf);
  }
# endif // ifdef FEATURE_ETHERNET
}

void FactoryDefault_Network_NVS::clear_from_NVS(ESPEasy_NVS_Helper& preferences)
{
  preferences.remove(F(FACTORY_DEFAULT_NVS_WIFI_IP_KEY));
# ifdef FEATURE_ETHERNET
  preferences.remove(F(FACTORY_DEFAULT_NVS_ETH_IP_KEY));
  preferences.remove(F(FACTORY_DEFAULT_NVS_ETH_IP_KEY));
# endif // ifdef FEATURE_ETHERNET
}

#endif  // ifdef ESP32

#include "../DataStructs/SystemTimerStruct.h"

#include "../DataStructs/ESPEasy_EventStruct.h"
#include "../ESPEasyCore/ESPEasy_Log.h"


// Rules Timer use
// ***************
systemTimerStruct::systemTimerStruct(int recurringCount, unsigned long msecFromNow, unsigned int timerIndex, int alternateInterval) :
  _recurringCount(recurringCount), _interval(msecFromNow), _timerIndex(timerIndex), _remainder(0), _loopCount(1), _alternateInterval(
    alternateInterval)
{
  if ((recurringCount > 0) && !hasAlternateInterval()) {
    // Will run with _recurringCount == 0, so must subtract one when setting the value.
    _recurringCount--;
  }

  if (msecFromNow == 0) {
    // Create a new timer which should be "scheduled" now to clear up any data
    _recurringCount = 0; // Do not reschedule
    _loopCount      = 0; // Do not execute
    #ifndef BUILD_MINIMAL_OTA
    addLog(LOG_LEVEL_INFO, F("TIMER: disable timer"));
    #endif
  }

  if (hasAlternateInterval()) {
    // Need to double the recurring count, or at least set it to 1 to make sure the alternate interval is also ran at least once.
    if (_recurringCount > 0) {
      _recurringCount *= 2;
    } else if (_recurringCount == 0) {
      _recurringCount = 1;
    }
  }
}

struct EventStruct systemTimerStruct::toEvent() const {
  struct EventStruct TempEvent(TaskIndex);

  TempEvent.Par1 = _recurringCount;
  TempEvent.Par2 = _interval;
  TempEvent.Par3 = _timerIndex;
  TempEvent.Par4 = _remainder;
  TempEvent.Par5 = _loopCount;
  return TempEvent;
}

void systemTimerStruct::fromEvent(taskIndex_t taskIndex,
                                  int         Par1,
                                  int         Par2,
                                  int         Par3,
                                  int         Par4,
                                  int         Par5)
{
  TaskIndex       = taskIndex;
  _recurringCount = Par1;
  _interval       = Par2;
  _timerIndex     = Par3;
  _remainder      = Par4;
  _loopCount      = Par5;
}

void systemTimerStruct::markNextRecurring() {
  toggleAlternateState(); // Will only toggle if it has an alternate state

  if (_recurringCount > 0) {
    // This is a timer with a limited number of runs, so decrease its value.
    _recurringCount--;
  }

  if (_loopCount > 0) {
    // This one should be executed, so increase the count.
    _loopCount++;
  }
}

void systemTimerStruct::toggleAlternateState() {
  if (hasAlternateInterval()) {
    _alternateState = !_alternateState;
  } else {
    _alternateState = false;
  }
}

#include "../DataStructs/UserVarStruct.h"

#include "../DataStructs/ESPEasy_EventStruct.h"
#include "../DataStructs/TimingStats.h"
#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../Globals/Cache.h"
#include "../Globals/Plugins.h"
#include "../Globals/RulesCalculate.h"
#include "../Helpers/_Plugin_SensorTypeHelper.h"
#include "../Helpers/CRC_functions.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringParser.h"



void UserVarStruct::clear()
{
  for (size_t i = 0; i < TASKS_MAX; ++i) {
    _rawData[i].clear();
  }
  _computed.clear();
#ifndef LIMIT_BUILD_SIZE
  _preprocessedFormula.clear();
#endif // ifndef LIMIT_BUILD_SIZE
  _prevValue.clear();
}

float UserVarStruct::operator[](unsigned int index) const
{
  const unsigned int taskIndex = index / VARS_PER_TASK;
  const unsigned int varNr     = index % VARS_PER_TASK;

  constexpr bool raw = false;

  const TaskValues_Data_t *data = getRawOrComputed(taskIndex, varNr, Sensor_VType::SENSOR_TYPE_QUAD, raw);

  if (data != nullptr) {
    return data->getFloat(varNr);
  } else {
    static float errorvalue = NAN;
#ifndef LIMIT_BUILD_SIZE
    addLog(LOG_LEVEL_ERROR, F("UserVar index out of range"));
#endif
    return errorvalue;
  }
}

unsigned long UserVarStruct::getSensorTypeLong(taskIndex_t taskIndex, bool raw) const
{
  const TaskValues_Data_t *data = getRawOrComputed(taskIndex, 0, Sensor_VType::SENSOR_TYPE_ULONG, raw);

  if (data != nullptr) {
    return data->getSensorTypeLong();
  }
  return 0u;
}

void UserVarStruct::setSensorTypeLong(taskIndex_t taskIndex, unsigned long value)
{
  if (validTaskIndex(taskIndex)) {
    if (Cache.hasFormula(taskIndex, 0)) {
      const ESPEASY_RULES_FLOAT_TYPE tmp = value;
      applyFormulaAndSet(taskIndex, 0, tmp, Sensor_VType::SENSOR_TYPE_ULONG);
    } else {
      _rawData[taskIndex].setSensorTypeLong(value);
    }
  }
}

#if FEATURE_EXTENDED_TASK_VALUE_TYPES

int32_t UserVarStruct::getInt32(taskIndex_t    taskIndex,
                                taskVarIndex_t varNr,
                                bool           raw) const
{
  const TaskValues_Data_t *data = getRawOrComputed(taskIndex, varNr, Sensor_VType::SENSOR_TYPE_INT32_QUAD, raw);

  if (data != nullptr) {
    return data->getInt32(varNr);
  }
  return 0;
}

void UserVarStruct::setInt32(taskIndex_t    taskIndex,
                             taskVarIndex_t varNr,
                             int32_t        value)
{
  if (validTaskIndex(taskIndex)) {
    if (Cache.hasFormula(taskIndex, varNr)) {
      const ESPEASY_RULES_FLOAT_TYPE tmp = value;
      applyFormulaAndSet(taskIndex, varNr, tmp, Sensor_VType::SENSOR_TYPE_INT32_QUAD);
    } else {
      _rawData[taskIndex].setInt32(varNr, value);
    }
  }
}

#endif // if FEATURE_EXTENDED_TASK_VALUE_TYPES

uint32_t UserVarStruct::getUint32(taskIndex_t taskIndex, taskVarIndex_t varNr, bool raw) const
{
#if FEATURE_EXTENDED_TASK_VALUE_TYPES
  const TaskValues_Data_t *data = getRawOrComputed(taskIndex, varNr, Sensor_VType::SENSOR_TYPE_UINT32_QUAD, raw);
#else // if FEATURE_EXTENDED_TASK_VALUE_TYPES
  const TaskValues_Data_t *data = getRawOrComputed(taskIndex, varNr, Sensor_VType::SENSOR_TYPE_NOT_SET, true);
#endif // if FEATURE_EXTENDED_TASK_VALUE_TYPES

  if (data != nullptr) {
    return data->getUint32(varNr);
  }
  return 0u;
}

void UserVarStruct::setUint32(taskIndex_t taskIndex, taskVarIndex_t varNr, uint32_t value)
{
  if (validTaskIndex(taskIndex)) {
    // setUInt32 is used to read taskvalues back from RTC
    // If FEATURE_EXTENDED_TASK_VALUE_TYPES is not enabled, this function will never be used for anything else
#if FEATURE_EXTENDED_TASK_VALUE_TYPES

    if (Cache.hasFormula(taskIndex, varNr)) {
      const ESPEASY_RULES_FLOAT_TYPE tmp = value;
      applyFormulaAndSet(taskIndex, varNr, tmp, Sensor_VType::SENSOR_TYPE_UINT32_QUAD);
    } else
#endif // if FEATURE_EXTENDED_TASK_VALUE_TYPES
    {
      _rawData[taskIndex].setUint32(varNr, value);
    }
  }
}

#if FEATURE_EXTENDED_TASK_VALUE_TYPES

int64_t UserVarStruct::getInt64(taskIndex_t    taskIndex,
                                taskVarIndex_t varNr,
                                bool           raw) const
{
  const TaskValues_Data_t *data = getRawOrComputed(taskIndex, varNr, Sensor_VType::SENSOR_TYPE_INT64_DUAL, raw);

  if (data != nullptr) {
    return data->getInt64(varNr);
  }
  return 0;
}

void UserVarStruct::setInt64(taskIndex_t    taskIndex,
                             taskVarIndex_t varNr,
                             int64_t        value)
{
  if (validTaskIndex(taskIndex)) {
    if (Cache.hasFormula(taskIndex, varNr)) {
      const ESPEASY_RULES_FLOAT_TYPE tmp = value;

      if (applyFormulaAndSet(taskIndex, varNr, tmp, Sensor_VType::SENSOR_TYPE_INT64_DUAL)) {
        // Apply anyway so we don't loose resolution in the raw value
        _rawData[taskIndex].setInt64(varNr, value);
      }
    } else {
      _rawData[taskIndex].setInt64(varNr, value);
    }
  }
}

uint64_t UserVarStruct::getUint64(taskIndex_t    taskIndex,
                                  taskVarIndex_t varNr,
                                  bool           raw) const
{
  const TaskValues_Data_t *data = getRawOrComputed(taskIndex, varNr, Sensor_VType::SENSOR_TYPE_UINT64_DUAL, raw);

  if (data != nullptr) {
    return data->getUint64(varNr);
  }
  return 0u;
}

void UserVarStruct::setUint64(taskIndex_t    taskIndex,
                              taskVarIndex_t varNr,
                              uint64_t       value)
{
  if (validTaskIndex(taskIndex)) {
    if (Cache.hasFormula(taskIndex, varNr)) {
      const ESPEASY_RULES_FLOAT_TYPE tmp = value;

      if (applyFormulaAndSet(taskIndex, varNr, tmp, Sensor_VType::SENSOR_TYPE_UINT64_DUAL)) {
        // Apply anyway so we don't loose resolution in the raw value
        _rawData[taskIndex].setUint64(varNr, value);
      }
    } else {
      _rawData[taskIndex].setUint64(varNr, value);
    }
  }
}

#endif // if FEATURE_EXTENDED_TASK_VALUE_TYPES

float UserVarStruct::getFloat(taskIndex_t    taskIndex,
                              taskVarIndex_t varNr,
                              bool           raw) const
{
  const TaskValues_Data_t *data = getRawOrComputed(taskIndex, varNr, Sensor_VType::SENSOR_TYPE_QUAD, raw);

  if (data != nullptr) {
    return data->getFloat(varNr);
  }
  return 0.0f;
}

void UserVarStruct::setFloat(taskIndex_t    taskIndex,
                             taskVarIndex_t varNr,
                             float          value)
{
  if (validTaskIndex(taskIndex)) {
    if (Cache.hasFormula(taskIndex, varNr)) {
      const ESPEASY_RULES_FLOAT_TYPE tmp = value;
      applyFormulaAndSet(taskIndex, varNr, tmp, Sensor_VType::SENSOR_TYPE_QUAD);
    } else {
      _rawData[taskIndex].setFloat(varNr, value);
    }
  }
}

#if FEATURE_EXTENDED_TASK_VALUE_TYPES
# if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
double UserVarStruct::getDouble(taskIndex_t taskIndex,
                                taskVarIndex_t varNr, bool raw) const
{
  const TaskValues_Data_t *data = getRawOrComputed(taskIndex, varNr, Sensor_VType::SENSOR_TYPE_DOUBLE_DUAL, raw);

  if (data != nullptr) {
    return data->getDouble(varNr);
  }
  return 0.0;
}

void UserVarStruct::setDouble(taskIndex_t    taskIndex,
                              taskVarIndex_t varNr,
                              double         value)
{
  if (validTaskIndex(taskIndex)) {
    if (Cache.hasFormula(taskIndex, varNr)) {
      applyFormulaAndSet(taskIndex, varNr, value, Sensor_VType::SENSOR_TYPE_DOUBLE_DUAL);
    } else {
      _rawData[taskIndex].setDouble(varNr, value);
    }
  }
}

# endif // if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
#endif  // if FEATURE_EXTENDED_TASK_VALUE_TYPES

ESPEASY_RULES_FLOAT_TYPE UserVarStruct::getAsDouble(taskIndex_t    taskIndex,
                                                    taskVarIndex_t varNr,
                                                    Sensor_VType   sensorType,
                                                    bool           raw) const
{
  const TaskValues_Data_t *data = getRawOrComputed(taskIndex, varNr, sensorType, raw);

  if (data != nullptr) {
    return data->getAsDouble(varNr, sensorType);
  }
  return 0.0;
}

String UserVarStruct::getAsString(taskIndex_t taskIndex, taskVarIndex_t varNr, Sensor_VType  sensorType, uint8_t nrDecimals, bool raw) const
{
  const TaskValues_Data_t *data = getRawOrComputed(taskIndex, varNr, sensorType, raw);

  if (data != nullptr) {
    if (nrDecimals == 255) {
      // TD-er: Should we use the set nr of decimals here, or not round at all?
      // See: https://github.com/letscontrolit/ESPEasy/issues/3721#issuecomment-889649437
      nrDecimals = Cache.getTaskDeviceValueDecimals(taskIndex, varNr);
    }

    return data->getAsString(varNr, sensorType, nrDecimals);
  }
  return EMPTY_STRING;
}

void UserVarStruct::set(taskIndex_t taskIndex, taskVarIndex_t varNr, const ESPEASY_RULES_FLOAT_TYPE& value, Sensor_VType sensorType)
{
  applyFormulaAndSet(taskIndex, varNr, value, sensorType);
}

bool UserVarStruct::isValid(taskIndex_t    taskIndex,
                            taskVarIndex_t varNr,
                            Sensor_VType   sensorType,
                            bool           raw) const
{
  const TaskValues_Data_t *data = getRawOrComputed(taskIndex, varNr, sensorType, raw);

  if (data != nullptr) {
    return data->isValid(varNr, sensorType);
  }
  return false;
}

uint8_t * UserVarStruct::get(size_t& sizeInBytes)
{
  constexpr size_t size_rawData = TASKS_MAX * sizeof(TaskValues_Data_t);

  sizeInBytes = size_rawData;
  return reinterpret_cast<uint8_t *>(&_rawData[0]);
}

const TaskValues_Data_t * UserVarStruct::getRawTaskValues_Data(taskIndex_t taskIndex) const
{
  if (validTaskIndex(taskIndex)) {
    return &_rawData[taskIndex];
  }
  return nullptr;
}

TaskValues_Data_t * UserVarStruct::getRawTaskValues_Data(taskIndex_t taskIndex)
{
  if (validTaskIndex(taskIndex)) {
    return &_rawData[taskIndex];
  }
  return nullptr;
}

uint32_t UserVarStruct::compute_CRC32() const
{
  const uint8_t   *buffer       = reinterpret_cast<const uint8_t *>(&_rawData[0]);
  constexpr size_t size_rawData = TASKS_MAX * sizeof(TaskValues_Data_t);

  return calc_CRC32(buffer, size_rawData);
}

void UserVarStruct::clear_computed(taskIndex_t taskIndex)
{
  auto it = _computed.find(taskIndex);

  if (it != _computed.end()) {
    _computed.erase(it);
  }

  for (taskVarIndex_t varNr = 0; validTaskVarIndex(varNr); ++varNr) {
    const uint16_t key = makeWord(taskIndex, varNr);
#ifndef LIMIT_BUILD_SIZE
    {
      auto it            = _preprocessedFormula.find(key);

      if (it != _preprocessedFormula.end()) {
        _preprocessedFormula.erase(it);
      }
    }
#endif // ifndef LIMIT_BUILD_SIZE
    {
      auto it            = _prevValue.find(key);

      if (it != _prevValue.end()) {
        _prevValue.erase(it);
      }
    }
  }
}

void UserVarStruct::markPluginRead(taskIndex_t taskIndex)
{
  struct EventStruct TempEvent(taskIndex);
  for (taskVarIndex_t varNr = 0; validTaskVarIndex(varNr); ++varNr) {
    if (Cache.hasFormula_with_prevValue(taskIndex, varNr)) {
      const uint16_t key = makeWord(taskIndex, varNr);
      _prevValue[key] = formatUserVarNoCheck(&TempEvent, varNr);
    }
  }
}

const TaskValues_Data_t * UserVarStruct::getRawOrComputed(
  taskIndex_t    taskIndex,
  taskVarIndex_t varNr,
  Sensor_VType   sensorType,
  bool           raw) const
{
  if (!raw && Cache.hasFormula(taskIndex, varNr)) {
    auto it = _computed.find(taskIndex);

    if ((it == _computed.end()) || !it->second.isSet(varNr)) {
      // Try to compute values which do have a formula but not yet a 'computed' value cached.
      // FIXME TD-er: This may yield unexpected results when formula contains references to %pvalue%


      // Should not apply set nr. of decimals when calculating a formula
      const uint8_t nrDecimals = 254;
      const String value   = getAsString(taskIndex, varNr, sensorType, nrDecimals, true);

      constexpr bool applyNow = true;

      if (applyFormula(taskIndex, varNr, value, sensorType, applyNow)) {
        it = _computed.find(taskIndex);
      }
    }

    if (it != _computed.end()) {
      if (it->second.isSet(varNr)) {
        return &(it->second.values);
      }
    }
  }
  return getRawTaskValues_Data(taskIndex);
}

bool UserVarStruct::applyFormula(taskIndex_t    taskIndex,
                                 taskVarIndex_t varNr,
                                 const String & value,
                                 Sensor_VType   sensorType,
                                 bool           applyNow) const
{
  if (!validTaskIndex(taskIndex) ||
      !validTaskVarIndex(varNr) ||
      (sensorType == Sensor_VType::SENSOR_TYPE_NOT_SET))
  {
    return false;
  }

  const bool formula_has_prevvalue = Cache.hasFormula_with_prevValue(taskIndex, varNr);

  if (!applyNow && !formula_has_prevvalue) {
    // Must check whether we can delay calculations until it is read for the first time.
    auto it = _computed.find(taskIndex);

    if (it != _computed.end()) {
      // Make sure it will apply formula when the value is actually read
      it->second.clear(varNr);
    }
    return true;
  }


  String formula = getPreprocessedFormula(taskIndex, varNr);
  bool   res     = true;

  if (!formula.isEmpty()
      #if FEATURE_STRING_VARIABLES
      && formula[1] != TASK_VALUE_PRESENTATION_PREFIX_CHAR
      #endif // FEATURE_STRING_VARIABLES
     ) // Ignore display-formula
  {
    START_TIMER;

    formula.replace(F("%value%"), value);

    // TD-er: Should we use the set nr of decimals here, or not round at all?
    // See: https://github.com/letscontrolit/ESPEasy/issues/3721#issuecomment-889649437
    if (formula_has_prevvalue) {
      const String prev_str = getPreviousValue(taskIndex, varNr, sensorType);
      formula.replace(F("%pvalue%"), prev_str.isEmpty() ? value : prev_str);
      /*
      addLog(LOG_LEVEL_INFO, 
        strformat(
          F("pvalue: %s, value: %s, formula: %s"), 
          prev_str.c_str(), 
          value.c_str(),
          formula.c_str()));
      */
    }

    ESPEASY_RULES_FLOAT_TYPE result{};

    if (!isError(Calculate_preProcessed(parseTemplate(formula), result))) {
      _computed[taskIndex].set(varNr, result, sensorType);
    } else {
      // FIXME TD-er: What to do now? Just copy the raw value, set error value or don't update?
      res = false;
    }

    STOP_TIMER(COMPUTE_FORMULA_STATS);
  }
  return res;
}

bool UserVarStruct::applyFormulaAndSet(taskIndex_t                     taskIndex,
                                       taskVarIndex_t                  varNr,
                                       const ESPEASY_RULES_FLOAT_TYPE& value,
                                       Sensor_VType                    sensorType)
{
  if (!Cache.hasFormula(taskIndex, varNr)
      #if FEATURE_STRING_VARIABLES
      || Cache.getTaskDeviceFormula(taskIndex, varNr)[1] == TASK_VALUE_PRESENTATION_PREFIX_CHAR
      #endif // if FEATURE_STRING_VARIABLES
     ) {
    _rawData[taskIndex].set(varNr, value, sensorType);
    return true;
  }

  // Use a temporary TaskValues_Data_t object to have uniform formatting
  TaskValues_Data_t tmp;

  tmp.set(varNr, value, sensorType);

  // Should not apply set nr. of decimals when calculating a formula
  const uint8_t nrDecimals = 254;
  const String  value_str  = tmp.getAsString(varNr, sensorType, nrDecimals);

  constexpr bool applyNow = false;

  if (applyFormula(taskIndex, varNr, value_str, sensorType, applyNow)) {
    _rawData[taskIndex].set(varNr, value, sensorType);
    return true;
  }
  return false;
}

String UserVarStruct::getPreprocessedFormula(taskIndex_t taskIndex, taskVarIndex_t varNr) const
{
  if (!Cache.hasFormula(taskIndex, varNr)) {
    return EMPTY_STRING;
  }

#ifndef LIMIT_BUILD_SIZE
  const uint16_t key = makeWord(taskIndex, varNr);
  auto it            = _preprocessedFormula.find(key);

  if (it == _preprocessedFormula.end()) {
    _preprocessedFormula.emplace(
      std::make_pair(
        key, 
        RulesCalculate_t::preProces(Cache.getTaskDeviceFormula(taskIndex, varNr))
        ));
  }
  return _preprocessedFormula[key];
#else // ifndef LIMIT_BUILD_SIZE
  return RulesCalculate_t::preProces(Cache.getTaskDeviceFormula(taskIndex, varNr));
#endif // ifndef LIMIT_BUILD_SIZE
}

String UserVarStruct::getPreviousValue(taskIndex_t taskIndex, taskVarIndex_t varNr, Sensor_VType sensorType) const
{
  /*
     if (!Cache.hasFormula_with_prevValue(taskIndex, varNr)) {
     // Should not happen.

     }
   */

  const uint16_t key = makeWord(taskIndex, varNr);
  auto it            = _prevValue.find(key);

  if (it != _prevValue.end()) {
    return it->second;
  }

  // Probably the first run, so just return the current value

  // Do not call getAsString here as this will result in stack overflow.
  return EMPTY_STRING;
}

#include "../DataStructs/Scheduler_SystemEventQueueTimerID.h"

#include "../Globals/CPlugins.h"
#include "../Globals/Plugins.h"


SystemEventQueueTimerID::SystemEventQueueTimerID(SchedulerPluginPtrType_e ptr_type, uint8_t Index, uint8_t Function) :
  SchedulerTimerID(SchedulerTimerType_e::SystemEventQueue)
{
  setId((static_cast<uint32_t>(ptr_type) << 16) +
       (Index << 8) +
       Function);
}


#ifndef BUILD_NO_DEBUG
String SystemEventQueueTimerID::decode() const
{
  const SchedulerPluginPtrType_e ptr_type = getPtrType();
  const uint8_t index                     = getIndex();
  String result;

  result += toString(ptr_type);
  result += ',';

  if (ptr_type == SchedulerPluginPtrType_e::ControllerPlugin) {
    result += getCPluginNameFromProtocolIndex(index);
  } else if (ptr_type == SchedulerPluginPtrType_e::TaskPlugin) {
    const deviceIndex_t dev_index = deviceIndex_t::toDeviceIndex(index);
    result += getPluginNameFromDeviceIndex(dev_index);
  } else {
    result += (index + 1);
  }
  result += ',';
  result += getFunction();
  return result;
}

#endif // ifndef BUILD_NO_DEBUG

#include "../DataStructs/RTCStruct.h"

  void RTCStruct::init() {
    ID1 = 0xAA;
    ID2 = 0x55;
    clearLastWiFi();
    factoryResetCounter = 0;
    deepSleepState = 0;
    bootFailedCount = 0;
    flashDayCounter = 0;
    lastWiFiSettingsIndex = 0;
    flashCounter = 0;
    bootCounter = 0;
    lastMixedSchedulerId = 0;
    unused1 = 0;
    unused2 = 0;
    lastSysTime = 0;
  }

  void RTCStruct::clearLastWiFi() {
    for (uint8_t i = 0; i < 6; ++i) {
      lastBSSID[i] = 0;
    }
    lastWiFiChannel = 0;
    lastWiFiSettingsIndex = 0;
  }

  bool RTCStruct::lastWiFi_set() const {
    return lastBSSID[0] != 0 && lastWiFiChannel != 0 && lastWiFiSettingsIndex != 0;
  }

#include "../DataStructs/ExtraTaskSettingsStruct.h"

#include "../../ESPEasy_common.h"

#include "../DataStructs/PluginStats_Config.h"

#include "../Helpers/Misc.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringGenerator_Plugin.h"

#define EXTRA_TASK_SETTINGS_VERSION 1


ExtraTaskSettingsStruct::ExtraTaskSettingsStruct()
{
  memset(this, 0, sizeof(ExtraTaskSettingsStruct));
  TaskIndex = INVALID_TASK_INDEX;
  version = EXTRA_TASK_SETTINGS_VERSION;
  for (int i = 0; i < VARS_PER_TASK; ++i) {
    TaskDeviceValueDecimals[i] = 2;
    TaskDeviceErrorValue[i] = NAN;
  }
}

void ExtraTaskSettingsStruct::clear() {
  // Need to make sure every byte between the members is also zero
  // Otherwise the checksum will fail and settings will be saved too often.
  memset(this, 0, sizeof(ExtraTaskSettingsStruct));
  TaskIndex = INVALID_TASK_INDEX;
  //dummy1 = 0;
  version = EXTRA_TASK_SETTINGS_VERSION;
  for (int i = 0; i < VARS_PER_TASK; ++i) {
    TaskDeviceValueDecimals[i] = 2;
    TaskDeviceErrorValue[i] = NAN;
  }
}

void ExtraTaskSettingsStruct::validate() {
  ZERO_TERMINATE(TaskDeviceName);

  for (uint8_t i = 0; i < VARS_PER_TASK; ++i) {
    ZERO_TERMINATE(TaskDeviceFormula[i]);
    ZERO_TERMINATE(TaskDeviceValueNames[i]);
  }

  if (dummy1 != 0) {
    // FIXME TD-er: This check was added to add the version for allowing to make transitions on the data.
    // If we've been using this for a while, we no longer need to check for the value of this dummy and we can re-use it for something else.
    dummy1  = 0;
    version = 0;
  }

  if (version != EXTRA_TASK_SETTINGS_VERSION) {
    if (version < 1) {
      // Need to initialize the newly added fields
      for (uint8_t i = 0; i < VARS_PER_TASK; ++i) {
        setIgnoreRangeCheck(i);
        TaskDeviceErrorValue[i] = NAN;
        VariousBits[i]          = 0u;
      }
    }
    version = EXTRA_TASK_SETTINGS_VERSION;
  }
}

ChecksumType ExtraTaskSettingsStruct::computeChecksum() const {
  return ChecksumType(reinterpret_cast<const uint8_t *>(this), sizeof(ExtraTaskSettingsStruct));
}

bool ExtraTaskSettingsStruct::checkUniqueValueNames() const {
  for (int i = 0; i < (VARS_PER_TASK - 1); ++i) {
    for (int j = i; j < VARS_PER_TASK; ++j) {
      if ((i != j) && (TaskDeviceValueNames[i][0] != 0)) {
        if (strcasecmp(TaskDeviceValueNames[i], TaskDeviceValueNames[j]) == 0) {
          return false;
        }
      }
    }
  }
  return true;
}

void ExtraTaskSettingsStruct::clearUnusedValueNames(uint8_t usedVars) {
  for (uint8_t i = usedVars; i < VARS_PER_TASK; ++i) {
    ZERO_FILL(TaskDeviceFormula[i]);
    ZERO_FILL(TaskDeviceValueNames[i]);
    TaskDeviceValueDecimals[i] = 2;
    setIgnoreRangeCheck(i);
    TaskDeviceErrorValue[i] = NAN;
    VariousBits[i]          = 0;
  }
}

bool ExtraTaskSettingsStruct::checkInvalidCharInNames(const char *name) const {
  int pos = 0;

  while (*(name + pos) != 0) {
    if (!validCharForNames(*(name + pos))) { return false; }
    ++pos;
  }
  return true;
}

bool ExtraTaskSettingsStruct::checkInvalidCharInNames() const {
  if (!checkInvalidCharInNames(&TaskDeviceName[0])) { return false; }

  for (int i = 0; i < VARS_PER_TASK; ++i) {
    if (!checkInvalidCharInNames(&TaskDeviceValueNames[i][0])) { return false; }
  }
  return true;
}

String ExtraTaskSettingsStruct::getInvalidCharsForNames() {
  return F(",-+/*=^%!#[]{}()");
}

bool ExtraTaskSettingsStruct::validCharForNames(char c) {
  return c != ' ' && getInvalidCharsForNames().indexOf(c) == -1;
}

void ExtraTaskSettingsStruct::setTaskDeviceValueName(taskVarIndex_t taskVarIndex, const String& str)
{
  if (validTaskVarIndex(taskVarIndex)) {
    safe_strncpy(
      TaskDeviceValueNames[taskVarIndex],
      str,
      sizeof(TaskDeviceValueNames[taskVarIndex]));
  }
}

void ExtraTaskSettingsStruct::setTaskDeviceValueName(taskVarIndex_t taskVarIndex, const __FlashStringHelper * str)
{
  setTaskDeviceValueName(taskVarIndex, String(str));
}

void ExtraTaskSettingsStruct::clearTaskDeviceValueName(taskVarIndex_t taskVarIndex)
{
  if (validTaskVarIndex(taskVarIndex)) {
    ZERO_FILL(TaskDeviceValueNames[taskVarIndex]);
  }
}

void ExtraTaskSettingsStruct::clearDefaultTaskDeviceValueNames()
{
  for (int i = 0; i < VARS_PER_TASK; ++i) {
    if (isDefaultTaskVarName(i)) {
      clearTaskDeviceValueName(i);
    }
  }
}

void ExtraTaskSettingsStruct::setAllowedRange(taskVarIndex_t taskVarIndex, const float& minValue, const float& maxValue)
{
  if (validTaskVarIndex(taskVarIndex)) {
    if (minValue > maxValue) {
      TaskDeviceMinValue[taskVarIndex] = maxValue;
      TaskDeviceMaxValue[taskVarIndex] = minValue;
    } else {
      TaskDeviceMinValue[taskVarIndex] = minValue;
      TaskDeviceMaxValue[taskVarIndex] = maxValue;
    }
  }
}

void ExtraTaskSettingsStruct::setIgnoreRangeCheck(taskVarIndex_t taskVarIndex)
{
  if (validTaskVarIndex(taskVarIndex)) {
    // Clear range to indicate no range check should be done.
    TaskDeviceMinValue[taskVarIndex] = 0.0f;
    TaskDeviceMaxValue[taskVarIndex] = 0.0f;
  }
}

bool ExtraTaskSettingsStruct::ignoreRangeCheck(taskVarIndex_t taskVarIndex) const
{
  if (validTaskVarIndex(taskVarIndex)) {
    return essentiallyEqual(TaskDeviceMinValue[taskVarIndex], TaskDeviceMaxValue[taskVarIndex]);
  }
  return true;
}

bool ExtraTaskSettingsStruct::valueInAllowedRange(taskVarIndex_t taskVarIndex, const float& value) const
{
  if (ignoreRangeCheck(taskVarIndex)) { return true; }

  if (validTaskVarIndex(taskVarIndex)) {
    return definitelyLessThan(value, TaskDeviceMaxValue[taskVarIndex]) ||
           definitelyGreaterThan(value, TaskDeviceMinValue[taskVarIndex]);
  }
  #ifndef BUILD_NO_DEBUG
  addLog(LOG_LEVEL_ERROR, F("Programming error: invalid taskVarIndex"));
  #endif // ifndef BUILD_NO_DEBUG
  return false;
}

float ExtraTaskSettingsStruct::checkAllowedRange(taskVarIndex_t taskVarIndex, const float& value) const
{
  if (!valueInAllowedRange(taskVarIndex, value)) {
    if (validTaskVarIndex(taskVarIndex)) {
      return TaskDeviceErrorValue[taskVarIndex];
    }
  }
  return value;
}

#if FEATURE_PLUGIN_STATS

// Plugin Stats is now only a single bit, but this may later changed into a combobox with some options.
// Thus leave 8 bits for the plugin stats options.

bool ExtraTaskSettingsStruct::enabledPluginStats(taskVarIndex_t taskVarIndex) const
{
  if (!validTaskVarIndex(taskVarIndex)) { return false; }
  return bitRead(VariousBits[taskVarIndex], 0);
}

void ExtraTaskSettingsStruct::enablePluginStats(taskVarIndex_t taskVarIndex, bool enabled)
{
  if (validTaskVarIndex(taskVarIndex)) {
    bitWrite(VariousBits[taskVarIndex], 0, enabled);
  }
}

bool ExtraTaskSettingsStruct::anyEnabledPluginStats() const
{
  for (uint8_t i = 0; i < VARS_PER_TASK; ++i) {
    if (enabledPluginStats(i)) { return true; }
  }
  return false;
}

PluginStats_Config_t ExtraTaskSettingsStruct::getPluginStatsConfig(taskVarIndex_t taskVarIndex) const
{
  if (!validTaskVarIndex(taskVarIndex)) { return PluginStats_Config_t(); }

  PluginStats_Config_t res(get8BitFromUL(VariousBits[taskVarIndex], 0));
  return res;
}

void ExtraTaskSettingsStruct::setPluginStatsConfig(taskVarIndex_t taskVarIndex, PluginStats_Config_t config)
{
  if (validTaskVarIndex(taskVarIndex)) {
    uint8_t value = config.getStoredBits();
    bitWrite(value, 1, bitRead(VariousBits[taskVarIndex], 1));
    set8BitToUL(VariousBits[taskVarIndex], 0, value);
  }
}


#endif // if FEATURE_PLUGIN_STATS

bool ExtraTaskSettingsStruct::isDefaultTaskVarName(taskVarIndex_t taskVarIndex) const
{
  if (!validTaskVarIndex(taskVarIndex)) { return false; }
  return bitRead(VariousBits[taskVarIndex], 1);
}

void ExtraTaskSettingsStruct::isDefaultTaskVarName(taskVarIndex_t taskVarIndex, bool isDefault)
{
  if (validTaskVarIndex(taskVarIndex)) {
    bitWrite(VariousBits[taskVarIndex], 1, isDefault);
  }
}

#if FEATURE_TASKVALUE_UNIT_OF_MEASURE
uint8_t ExtraTaskSettingsStruct::getTaskVarUnitOfMeasure(taskVarIndex_t taskVarIndex) const {
  if (!validTaskVarIndex(taskVarIndex)) { return 0u; }
  return get8BitFromUL(VariousBits[taskVarIndex], 8);
}

void ExtraTaskSettingsStruct::setTaskVarUnitOfMeasure(taskVarIndex_t taskVarIndex,
                                                      uint8_t        unitOfMeasure) {
  if (validTaskVarIndex(taskVarIndex)) {
    set8BitToUL(VariousBits[taskVarIndex], 8, unitOfMeasure);
  }
}
#endif // if FEATURE_TASKVALUE_UNIT_OF_MEASURE

#if FEATURE_CUSTOM_TASKVAR_VTYPE
uint8_t ExtraTaskSettingsStruct::getTaskVarCustomVType(taskVarIndex_t taskVarIndex) const {
  if (!validTaskVarIndex(taskVarIndex)) { return 0u; }
  return get8BitFromUL(VariousBits[taskVarIndex], 16);
}

void ExtraTaskSettingsStruct::setTaskVarCustomVType(taskVarIndex_t taskVarIndex,
                                                    uint8_t        customVType) {
  if (validTaskVarIndex(taskVarIndex)) {
    set8BitToUL(VariousBits[taskVarIndex], 16, customVType);
  }
}
#endif // if FEATURE_CUSTOM_TASKVAR_VTYPE

#if FEATURE_MQTT_STATE_CLASS
uint8_t ExtraTaskSettingsStruct::getTaskVarStateClass(taskVarIndex_t taskVarIndex) const {
  if (!validTaskVarIndex(taskVarIndex)) { return 0u; }
  return get3BitFromUL(VariousBits[taskVarIndex], 24); // 3 Bits 24, 25, 26
}

void ExtraTaskSettingsStruct::setTaskVarStateClass(taskVarIndex_t taskVarIndex,
                                                   uint8_t        stateClass) {
  if (validTaskVarIndex(taskVarIndex)) {
    set3BitToUL(VariousBits[taskVarIndex], 24, stateClass); // 3 Bits 24, 25, 26
  }
}
#endif // if FEATURE_MQTT_STATE_CLASS


void ExtraTaskSettingsStruct::populateDeviceValueNamesSeq(
  const __FlashStringHelper *valuename,
  size_t                     nrValues,
  uint8_t                    defaultDecimals,
  bool                       displayString)
{
  for (byte i = 0; i < VARS_PER_TASK; ++i) {
    if (i < nrValues) {
      safe_strncpy(
        TaskDeviceValueNames[i],
        Plugin_valuename(valuename, i, displayString),
        sizeof(TaskDeviceValueNames[i]));
      TaskDeviceValueDecimals[i] = defaultDecimals;
    } else {
      ZERO_FILL(TaskDeviceValueNames[i]);
    }
  }
}

#include "../DataStructs/ProtocolStruct.h"

ProtocolStruct::ProtocolStruct() :
    defaultPort(0), usesMQTT(false), usesAccount(false), usesPassword(false),
    usesTemplate(false), usesID(false), Custom(false), usesHost(true), usesPort(true),
    usesQueue(true), usesCheckReply(true), usesTimeout(true), usesSampleSets(false), 
    usesExtCreds(false), needsNetwork(true), allowsExpire(true), allowLocalSystemTime(false)
  #if FEATURE_MQTT_TLS
  , usesTLS(false)
  #endif
    {}

#include "../DataStructs/PluginStats_array.h"

#if FEATURE_PLUGIN_STATS

# include "../../_Plugin_Helper.h"

# include "../Globals/TimeZone.h"

# include "../Helpers/ESPEasy_math.h"
# include "../Helpers/Memory.h"

# include "../WebServer/Chart_JS.h"
# if FEATURE_TASKVALUE_UNIT_OF_MEASURE
#  include "../Helpers/ESPEasy_UnitOfMeasure.h"
# endif // if FEATURE_TASKVALUE_UNIT_OF_MEASURE

PluginStats_array::~PluginStats_array()
{
  for (size_t i = 0; i < VARS_PER_TASK; ++i) {
    if (_plugin_stats[i] != nullptr) {
      delete _plugin_stats[i];
      _plugin_stats[i] = nullptr;
    }
  }

  if (_plugin_stats_timestamps != nullptr) {
    free(_plugin_stats_timestamps);
    _plugin_stats_timestamps = nullptr;
  }
}

void PluginStats_array::initPluginStats(taskIndex_t taskIndex, taskVarIndex_t taskVarIndex)
{
  if (taskVarIndex < VARS_PER_TASK) {
    delete _plugin_stats[taskVarIndex];
    _plugin_stats[taskVarIndex] = nullptr;

    if (!hasStats()) {
      if (_plugin_stats_timestamps != nullptr) {
        free(_plugin_stats_timestamps);
        _plugin_stats_timestamps = nullptr;
      }
    }

    if (ExtraTaskSettings.enabledPluginStats(taskVarIndex)) {
      // Try to allocate in PSRAM or 2nd heap if possible
      constexpr unsigned size = sizeof(PluginStats);
      void *ptr               = special_calloc(1, size);

      if (ptr == nullptr) { _plugin_stats[taskVarIndex] = nullptr; }
      else {
        _plugin_stats[taskVarIndex] = new (ptr) PluginStats(
          ExtraTaskSettings.TaskDeviceValueDecimals[taskVarIndex],
          ExtraTaskSettings.TaskDeviceErrorValue[taskVarIndex]);
      }


      if (_plugin_stats[taskVarIndex] != nullptr) {
        # if FEATURE_TASKVALUE_UNIT_OF_MEASURE
        const uint8_t uomIndex = ExtraTaskSettings.getTaskVarUnitOfMeasure(taskVarIndex);
        String label(ExtraTaskSettings.TaskDeviceValueNames[taskVarIndex]);

        if (uomIndex != 0) {
          label = strformat(F("%s (%s)"), ExtraTaskSettings.TaskDeviceValueNames[taskVarIndex], toUnitOfMeasureName(uomIndex).c_str());
        }
        _plugin_stats[taskVarIndex]->setLabel(label);
        # else // if FEATURE_TASKVALUE_UNIT_OF_MEASURE
        _plugin_stats[taskVarIndex]->setLabel(ExtraTaskSettings.TaskDeviceValueNames[taskVarIndex]);
        # endif // if FEATURE_TASKVALUE_UNIT_OF_MEASURE
        # if FEATURE_CHART_JS
        const __FlashStringHelper *colors[] = { F("#A52422"), F("#BEA57D"), F("#0F4C5C"), F("#A4BAB7") };
        _plugin_stats[taskVarIndex]->_ChartJS_dataset_config.color         = colors[taskVarIndex];
        _plugin_stats[taskVarIndex]->_ChartJS_dataset_config.displayConfig = ExtraTaskSettings.getPluginStatsConfig(taskVarIndex);
        # endif // if FEATURE_CHART_JS

        if (_plugin_stats_timestamps != nullptr) {
          _plugin_stats[taskVarIndex]->setPluginStats_timestamp(_plugin_stats_timestamps);
        }
      }
    }
  }

  if (hasStats()) {
    if (_plugin_stats_timestamps == nullptr) {
      // Try to allocate in PSRAM if possible
      constexpr unsigned size = sizeof(PluginStats_timestamp);
      void *ptr               = special_calloc(1, size);

      if (ptr != nullptr) {
        // TODO TD-er: Let the task decide whether we need 1/50 sec resolution or 1/10
        // 1/10 sec resolution allows for ~13.6 years without overflow
        _plugin_stats_timestamps = new (ptr) PluginStats_timestamp(true);
      }

      for (size_t i = 0; i < VARS_PER_TASK; ++i) {
        _plugin_stats[taskVarIndex]->setPluginStats_timestamp(_plugin_stats_timestamps);
      }
    }
  }
}

void PluginStats_array::clearPluginStats(taskVarIndex_t taskVarIndex)
{
  if (taskVarIndex < VARS_PER_TASK) {
    if (_plugin_stats[taskVarIndex] != nullptr) {
      delete _plugin_stats[taskVarIndex];
      _plugin_stats[taskVarIndex] = nullptr;
    }
  }

  if (!hasStats()) {
    if (_plugin_stats_timestamps != nullptr) {
      free(_plugin_stats_timestamps);
      _plugin_stats_timestamps = nullptr;
    }
  }
}

void PluginStats_array::processTimeSet(const double& time_offset)
{
  if (_plugin_stats_timestamps != nullptr) {
    _plugin_stats_timestamps->processTimeSet(time_offset);
  }

  // Also update timestamps of peaks
  for (taskVarIndex_t taskVarIndex = 0; taskVarIndex < VARS_PER_TASK; ++taskVarIndex) {
    PluginStats *stats = getPluginStats(taskVarIndex);

    if (stats != nullptr) {
      stats->processTimeSet(time_offset);
    }
  }
}

bool PluginStats_array::hasStats() const
{
  for (size_t i = 0; i < VARS_PER_TASK; ++i) {
    if (_plugin_stats[i] != nullptr) { return true; }
  }
  return false;
}

bool PluginStats_array::hasPeaks() const
{
  for (size_t i = 0; i < VARS_PER_TASK; ++i) {
    if ((_plugin_stats[i] != nullptr) && _plugin_stats[i]->hasPeaks()) {
      return true;
    }
  }
  return false;
}

size_t PluginStats_array::nrSamplesPresent() const
{
  for (size_t i = 0; i < VARS_PER_TASK; ++i) {
    if (_plugin_stats[i] != nullptr) {
      return _plugin_stats[i]->getNrSamples();
    }
  }
  return 0;
}

size_t PluginStats_array::nrPluginStats() const
{
  size_t res{};

  for (size_t i = 0; i < VARS_PER_TASK; ++i) {
    if (_plugin_stats[i] != nullptr) {
      ++res;
    }
  }
  return res;
}

uint32_t PluginStats_array::getFullPeriodInSec(uint32_t& time_frac) const
{
  if (_plugin_stats_timestamps == nullptr) {
    time_frac = 0u;
    return 0u;
  }
  return _plugin_stats_timestamps->getFullPeriodInSec(time_frac);
}

void PluginStats_array::pushPluginStatsValues(
  struct EventStruct *event,
  bool                trackPeaks,
  bool                onlyUpdateTimestampWhenSame)
{
  if (validTaskIndex(event->TaskIndex)) {
    const uint8_t valueCount = getValueCountForTask(event->TaskIndex);

    if (valueCount > 0) {
      const Sensor_VType sensorType = event->getSensorType();

      const int64_t timestamp_sysmicros = event->getTimestamp_as_systemMicros();

      if (onlyUpdateTimestampWhenSame && (_plugin_stats_timestamps != nullptr)) {
        // When only updating the timestamp of the last entry,
        // we should look at the last 2 entries to see if they are the same.
        bool   isSame = true;
        size_t i      = 0;

        while (isSame && i < valueCount) {
          if (_plugin_stats[i] != nullptr) {
            const float value = UserVar.getAsDouble(event->TaskIndex, i, sensorType);

            if (!_plugin_stats[i]->matchesLastTwoEntries(value)) {
              isSame = false;
            }
          }
          ++i;
        }

        if (isSame) {
          _plugin_stats_timestamps->updateLast(timestamp_sysmicros);
          return;
        }
      }

      if (_plugin_stats_timestamps != nullptr) {
        _plugin_stats_timestamps->push(timestamp_sysmicros);
      }

      for (size_t i = 0; i < valueCount; ++i) {
        if (_plugin_stats[i] != nullptr) {
          const float value = UserVar.getAsDouble(event->TaskIndex, i, sensorType);
          _plugin_stats[i]->push(value);

          if (trackPeaks) {
            _plugin_stats[i]->trackPeak(value, timestamp_sysmicros);
          }
        }
      }
    }
  }
}

bool PluginStats_array::plugin_get_config_value_base(struct EventStruct *event,
                                                     String            & string) const
{
  // Full value name is something like "taskvaluename.avg"
  const String fullValueName = parseString(string, 1);
  const String valueName     = parseString(fullValueName, 1, '.');

  const uint8_t valueCount = getValueCountForTask(event->TaskIndex);

  for (taskVarIndex_t i = 0; i < valueCount; i++)
  {
    if (_plugin_stats[i] != nullptr) {
      // Check case insensitive, since the user entered value name can have any case.
      if (valueName.equalsIgnoreCase(Cache.getTaskDeviceValueName(event->TaskIndex, i)))
      {
        return _plugin_stats[i]->plugin_get_config_value_base(event, string);
      }
    }
  }
  return false;
}

bool PluginStats_array::plugin_write_base(struct EventStruct *event, const String& string)
{
  bool success     = false;
  const String cmd = parseString(string, 1);                // command

  const bool resetPeaks   = equals(cmd, F("resetpeaks"));   // Command: "taskname.resetPeaks"
  const bool clearSamples = equals(cmd, F("clearsamples")); // Command: "taskname.clearSamples"

  if (resetPeaks || clearSamples) {
    for (size_t i = 0; i < VARS_PER_TASK; ++i) {
      if (_plugin_stats[i] != nullptr) {
        if (resetPeaks) {
          success = true;
          _plugin_stats[i]->resetPeaks();
        }

        if (clearSamples) {
          success = true;
          _plugin_stats[i]->clearSamples();
        }
      }
    }
  }
  return success;
}

bool PluginStats_array::webformLoad_show_stats(struct EventStruct *event, bool showTaskValues) const
{
  bool somethingAdded = false;

  uint32_t time_frac{};
  const uint32_t duration  = getFullPeriodInSec(time_frac);
  const uint32_t nrSamples = nrSamplesPresent();

  if ((duration > 0) && (nrSamples > 1)) {
    const uint32_t duration_millis = unix_time_frac_to_millis(time_frac);
    addRowLabel(F("Total Duration"));
    addHtml(strformat(
              F("%s.%03u (%u.%03u sec)"),
              secondsToDayHourMinuteSecond(duration).c_str(),
              duration_millis,
              duration,
              duration_millis));
    const float duration_f = static_cast<float>(duration) + (duration_millis / 1000.0f);
    addRowLabel(F("Total Nr Samples"));
    addHtmlInt(nrSamples);
    addRowLabel(F("Avg Rate"));
    addHtmlFloat(duration_f / static_cast<float>(nrSamples - 1), 2);
    addUnit(F("sec/sample"));
    addFormSeparator(4);
    somethingAdded = true;
  }

  if (showTaskValues) {
    for (size_t i = 0; i < VARS_PER_TASK; ++i) {
      if (_plugin_stats[i] != nullptr) {
        if (_plugin_stats[i]->webformLoad_show_stats(event)) {
          somethingAdded = true;
        }
      }
    }
  }
  return somethingAdded;
}

# if FEATURE_CHART_JS
void PluginStats_array::plot_ChartJS(bool onlyJSON) const
{
  const size_t nrSamples = nrSamplesPresent();

  if (nrSamples == 0) { return; }

  // Chart Header
  {
    ChartJS_options_scales scales;
    {
      ChartJS_options_scale scaleOption(F("x"));

      if (_plugin_stats_timestamps != nullptr) {
        scaleOption.scaleType = F("time");
      }
      scales.add(scaleOption);
    }

    for (size_t i = 0; i < VARS_PER_TASK; ++i) {
      if (_plugin_stats[i] != nullptr) {
        ChartJS_options_scale scaleOption(
          _plugin_stats[i]->_ChartJS_dataset_config.displayConfig,
          _plugin_stats[i]->getLabel());
        scaleOption.axisTitle.color = _plugin_stats[i]->_ChartJS_dataset_config.color;
        scales.add(scaleOption);

        _plugin_stats[i]->_ChartJS_dataset_config.axisID = scaleOption.axisID;
      }
    }

    scales.update_Yaxis_TickCount();

    const bool enableZoom = true;

    add_ChartJS_chart_header(
      F("line"),
      F("TaskStatsChart"),
      {},
      500 + (70 * (scales.nr_Y_scales() - 1)),
      500,
      scales.toString(),
      enableZoom,
      nrSamples,
      onlyJSON);
  }


  // Add labels
  addHtml(F("\"labels\":["));

  for (size_t i = 0; i < nrSamples; ++i) {
    if (i != 0) {
      addHtml(',');
    }

    if (_plugin_stats_timestamps != nullptr) {
      struct tm ts;
      uint32_t  unix_time_frac{};
      const uint32_t unixtime_sec    = node_time.systemMicros_to_Unixtime((*_plugin_stats_timestamps)[i], unix_time_frac);
      const uint32_t local_timestamp = time_zone.toLocal(unixtime_sec);
      breakTime(local_timestamp, ts);
      addHtml('"');
      addHtml(formatDateTimeString(ts));
      addHtml(strformat(F(".%03u"), unix_time_frac_to_millis(unix_time_frac)));
      addHtml('"');
    } else {
      addHtmlInt(i);
    }
  }
  addHtml(F("],\n\"datasets\":["));


  // Data sets
  bool first = true;

  for (size_t i = 0; i < VARS_PER_TASK; ++i) {
    if (_plugin_stats[i] != nullptr) {
      if (!first) {
        addHtml(',');
      }
      first = false;
      _plugin_stats[i]->plot_ChartJS_dataset();
    }
  }
  add_ChartJS_chart_footer(onlyJSON);
}

void PluginStats_array::plot_ChartJS_scatter(
  taskVarIndex_t                values_X_axis_index,
  taskVarIndex_t                values_Y_axis_index,
  const __FlashStringHelper    *id,
  const ChartJS_title         & chartTitle,
  const ChartJS_dataset_config& datasetConfig,
  int                           width,
  int                           height,
  bool                          showAverage,
  const String                & options,
  bool                          onlyJSON) const
{
  const PluginStats *stats_X = getPluginStats(values_X_axis_index);
  const PluginStats *stats_Y = getPluginStats(values_Y_axis_index);

  if ((stats_X == nullptr) || (stats_Y == nullptr)) {
    return;
  }

  if ((stats_X->getNrSamples() < 2) || (stats_Y->getNrSamples() < 2)) {
    return;
  }

  String axisOptions;

  {
    ChartJS_options_scales scales;
    scales.add({ F("x"), stats_X->getLabel() });
    scales.add({ F("y"), stats_Y->getLabel() });
    axisOptions = scales.toString();
  }


  const size_t nrSamples  = stats_X->getNrSamples();
  const bool   enableZoom = false;

  add_ChartJS_chart_header(
    F("scatter"),
    id,
    chartTitle,
    width,
    height,
    axisOptions,
    enableZoom,
    nrSamples,
    onlyJSON);

  // Add labels, which will be shown in a tooltip when hovering with the mouse over a point.
  addHtml(F("\"labels\":["));

  for (size_t i = 0; i < nrSamples; ++i) {
    if (i != 0) {
      addHtml(',');
    }
    addHtmlInt(i);
  }
  addHtml(F("],\n\"datasets\":["));

  // Long/Lat Coordinates
  add_ChartJS_dataset_header(datasetConfig);

  // Add scatter data
  for (size_t i = 0; i < nrSamples; ++i) {
    const float valX = (*stats_X)[i];
    const float valY = (*stats_Y)[i];
    add_ChartJS_scatter_data_point(valX, valY, 6);
  }

  add_ChartJS_dataset_footer(F("\"showLine\":true"));

  if (showAverage) {
    // Add single point showing the average
    addHtml(',');
    add_ChartJS_dataset_header(
    {
      F("Average"),
      F("#0F4C5C") });

    {
      const float valX = stats_X->getSampleAvg();
      const float valY = stats_Y->getSampleAvg();
      add_ChartJS_scatter_data_point(valX, valY, 6);
    }
    add_ChartJS_dataset_footer(F("\"pointRadius\":6,\"pointHoverRadius\":10"));
  }
  add_ChartJS_chart_footer(onlyJSON);
}

# endif // if FEATURE_CHART_JS


PluginStats * PluginStats_array::getPluginStats(taskVarIndex_t taskVarIndex) const
{
  if ((taskVarIndex < VARS_PER_TASK)) {
    return _plugin_stats[taskVarIndex];
  }
  return nullptr;
}

PluginStats * PluginStats_array::getPluginStats(taskVarIndex_t taskVarIndex)
{
  if ((taskVarIndex < VARS_PER_TASK)) {
    return _plugin_stats[taskVarIndex];
  }
  return nullptr;
}

#endif // if FEATURE_PLUGIN_STATS

#include "../DataStructs/ESPEasyControllerCache_CSV_dumper.h"

#if FEATURE_RTC_CACHE_STORAGE


# include "../Globals/C016_ControllerCache.h"
# include "../Globals/MQTT.h"

# include "../../_Plugin_Helper.h"

void ESPEasyControllerCache_CSV_element::markBegin()
{
  line.clear();

  if ((endFileNr != 0) && (endPos != 0)) {
    startFileNr = endFileNr;
    startPos    = endPos;
  } else {
    startPos = ControllerCache.getPeekFilePos(startFileNr);
  }
}

void ESPEasyControllerCache_CSV_element::markEnd()
{
  endPos = ControllerCache.getPeekFilePos(endFileNr);
}

ESPEasyControllerCache_CSV_dumper::ESPEasyControllerCache_CSV_dumper(bool joinTimestamp, bool onlySetTasks, char separator, Target target)
  : _joinTimestamp(joinTimestamp), _onlySetTasks(onlySetTasks), _separator(separator), _target(target)
{
  // Initialize arrays
  const String sep_zero = String(separator);

  constexpr size_t nrTaskValues = VARS_PER_TASK * TASKS_MAX;

  for (size_t i = 0; i < nrTaskValues; ++i) {
    _csv_values[i] = sep_zero;
    _nrDecimals[i] = Cache.getTaskDeviceValueDecimals(i / VARS_PER_TASK, i % VARS_PER_TASK);
  }

  for (size_t task = 0; validTaskIndex(task); ++task) {
    _includeTask[task] = _onlySetTasks ? validPluginID(Settings.getPluginID_for_task(task)) : true;
  }

  if (_target == Target::CSV_file) {
    // First backup the peek file positions.
    _backup_peekFilePos = ControllerCache.getPeekFilePos(_backup_peekFileNr);

    // Set peek file position to first entry:
    ControllerCache.setPeekFilePos(0, 0);
  }
  C016_flush();
}

ESPEasyControllerCache_CSV_dumper::~ESPEasyControllerCache_CSV_dumper() {
  if ((_backup_peekFileNr != 0) && (_backup_peekFilePos != 0)) {
    // Restore peek file positions.
    ControllerCache.setPeekFilePos(_backup_peekFileNr, _backup_peekFilePos);
  }
}

uint32_t ESPEasyControllerCache_CSV_dumper::writeToTarget(const String& str, bool send) const {
  if (send) {
    if (_target == Target::CSV_file) {
      addHtml(str);
    } else {
      MQTTclient.write((const uint8_t*)str.c_str(), str.length());
    }
  }
  return str.length();
}

uint32_t ESPEasyControllerCache_CSV_dumper::writeToTarget(const char& c, bool send) const {
  if (send) {
    if (_target == Target::CSV_file) {
      addHtml(c);
    } else {
      MQTTclient.write(static_cast<uint8_t>(c));
    }
  }
  return 1;
}

size_t ESPEasyControllerCache_CSV_dumper::generateCSVHeader(bool send) const
{
  size_t count = 0;

  // CSV header
  String header(F("UNIX timestamp;UTC timestamp"));

  if (_joinTimestamp) {
    // Add column with nr of joined samples
    header += F(";nrJoinedSamples");
  }

  // TaskIndex and Plugin ID will be a list of numbers when lines are joined.
  header += F(";taskindex;plugin ID");

  if (_separator != ';') { header.replace(';', _separator); }
  count += writeToTarget(header, send);

  for (taskIndex_t i = 0; i < TASKS_MAX; ++i) {
    if (_includeTask[i]) {
      for (int j = 0; j < VARS_PER_TASK; ++j) {
        count += writeToTarget(_separator, send);
        count += writeToTarget(getTaskDeviceName(i), send);
        count += writeToTarget('#', send);
        count += writeToTarget(getTaskValueName(i, j), send);
      }
    }
  }

  if (_target == Target::CSV_file) {
    count += writeToTarget('\r', send);
    count += writeToTarget('\n', send);
  }

  return count;
}

bool ESPEasyControllerCache_CSV_dumper::createCSVLine()
{
  _outputLine.markBegin();


  uint32_t lastTimestamp   = 0;
  uint32_t csv_values_left = 0;

  _taskIndex_str.clear();
  _pluginID_str.clear();


  // Fetch samples from Cache Controller bin files.
  if (_element_processed) {
    if (!C016_getTaskSample(_element)) {
      return !_outputLine.line.isEmpty();
    }
    _outputLine.markEnd();
    _element_processed = false;
  }

  while (!_element_processed) {
    if (!_joinTimestamp || (lastTimestamp != static_cast<uint32_t>(_element.unixTime))) {
      // Flush the collected CSV values
      if (csv_values_left > 0) {
        flushValuesLeft(csv_values_left);
        return !_outputLine.line.isEmpty();
      }

      // Start writing a new line in the CSV file
      // Begin with the non taskvalues
      _outputLine.line += _element.unixTime;
      _outputLine.line += _separator;
      struct tm ts;
      breakTime(_element.unixTime, ts);
      _outputLine.line += formatDateTimeString(ts);

      if (!_joinTimestamp) {
        _outputLine.line += _separator;
        _outputLine.line += _element.TaskIndex;
        _outputLine.line += _separator;
        _outputLine.line += _element.pluginID.value;
      }

      lastTimestamp = static_cast<uint32_t>(_element.unixTime);
    }

    if (validTaskIndex(_element.TaskIndex)) {
      // Collect the task values for this row in the CSV
      size_t valindex = _element.TaskIndex * VARS_PER_TASK;

      for (size_t i = 0; i < VARS_PER_TASK; ++i) {
        _csv_values[valindex] = _separator;

        if (isFloatOutputDataType(_element.sensorType)) {
          const float value = _element.values.getFloat(i);

          if (essentiallyZero(value)) {
            _csv_values[valindex] += '0';
          } else {
            _csv_values[valindex] += toString(value, static_cast<unsigned int>(_nrDecimals[valindex]));
          }
        } else {
          const String formatted = _element.values.getAsString(i, _element.sensorType, _nrDecimals[valindex]);

          if (formatted.isEmpty()) {
            _csv_values[valindex] += '0';
          }
          else {
            _csv_values[valindex] += formatted;
          }
        }
        ++valindex;
      }

      if (_joinTimestamp) {
        if (!_taskIndex_str.isEmpty()) { _taskIndex_str += '/'; }

        if (!_pluginID_str.isEmpty()) { _pluginID_str += '/'; }

        _taskIndex_str += _element.TaskIndex;
        _pluginID_str  += _element.pluginID.value;
      }
      ++csv_values_left;
    }
    _outputLine.markEnd();
    _element_processed = !C016_getTaskSample(_element);
  }

  if (csv_values_left > 0) {
    flushValuesLeft(csv_values_left);
  }
  return !_outputLine.line.isEmpty();
}

void ESPEasyControllerCache_CSV_dumper::setPeekFilePos(int peekFileNr, int peekReadPos)
{
  ControllerCache.setPeekFilePos(peekFileNr, peekReadPos);
  _element_processed = true;
}

void ESPEasyControllerCache_CSV_dumper::flushValuesLeft(uint32_t csv_values_left)
{
  if (_joinTimestamp) {
    _outputLine.line += _separator;
    _outputLine.line += csv_values_left; // Add column with nr of joined samples
    _outputLine.line += _separator;
    _outputLine.line += _taskIndex_str;
    _outputLine.line += _separator;
    _outputLine.line += _pluginID_str;
  }

  constexpr size_t nrTaskValues = VARS_PER_TASK * TASKS_MAX;

  for (size_t i = 0; i < nrTaskValues; ++i) {
    if (_includeTask[i / VARS_PER_TASK]) {
      _outputLine.line += _csv_values[i];
    }
  }

  if (_target == Target::CSV_file) {
    _outputLine.line += '\r';
    _outputLine.line += '\n';
  }
}

#endif // if FEATURE_RTC_CACHE_STORAGE

#include "../DataStructs/WiFiEventData.h"

#include "../ESPEasyCore/ESPEasy_Log.h"

#include "../Globals/RTC.h"
#include "../Globals/Settings.h"
#include "../Globals/WiFi_AP_Candidates.h"

#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/Networking.h"


#define WIFI_RECONNECT_WAIT                  30000  // in milliSeconds

#define CONNECT_TIMEOUT_MAX                  4000   // in milliSeconds


#if FEATURE_USE_IPV6
#include <esp_netif.h>

// -----------------------------------------------------------------------------------------------------------------------
// ---------------------------------------------------- Private functions ------------------------------------------------
// -----------------------------------------------------------------------------------------------------------------------

esp_netif_t* get_esp_interface_netif(esp_interface_t interface);
#endif



bool WiFiEventData_t::WiFiConnectAllowed() const {
  if (WiFi.status() == WL_IDLE_STATUS) {
    // FIXME TD-er: What to do now? Set a timer?
    //return false;
    if (last_wifi_connect_attempt_moment.isSet() && 
       !last_wifi_connect_attempt_moment.timeoutReached(WIFI_PROCESS_EVENTS_TIMEOUT)) {
      return false;
    }
  }
  if (!wifiConnectAttemptNeeded) return false;
  if (intent_to_reboot) return false;
  if (wifiSetupConnect) return true;
  if (wifiConnectInProgress) {
    if (last_wifi_connect_attempt_moment.isSet() && 
       !last_wifi_connect_attempt_moment.timeoutReached(WIFI_PROCESS_EVENTS_TIMEOUT)) {
      return false;
    }
  } 
  if (lastDisconnectMoment.isSet()) {
    // TODO TD-er: Make this time more dynamic.
    if (!lastDisconnectMoment.timeoutReached(1000)) {
      return false;
    }
  }
  return true;
}

bool WiFiEventData_t::unprocessedWifiEvents() const {
  if (processedConnect && processedDisconnect && processedGotIP && processedDHCPTimeout
#if FEATURE_USE_IPV6
      && processedGotIP6
#endif
  )
  {
    return false;
  }
  if (!processedConnect) {
    if (lastConnectMoment.isSet() && lastConnectMoment.timeoutReached(WIFI_PROCESS_EVENTS_TIMEOUT)) {
      return false;
    }
  }
  if (!processedGotIP) {
    if (lastGetIPmoment.isSet() && lastGetIPmoment.timeoutReached(WIFI_PROCESS_EVENTS_TIMEOUT)) {
      return false;
    }
  }
  if (!processedDisconnect) {
    if (lastDisconnectMoment.isSet() && lastDisconnectMoment.timeoutReached(WIFI_PROCESS_EVENTS_TIMEOUT)) {
      return false;
    }
  }
  if (!processedDHCPTimeout) {
    return false;
  }
  return true;
}

void WiFiEventData_t::clearAll() {
  markWiFiTurnOn();
  lastGetScanMoment.clear();
  last_wifi_connect_attempt_moment.clear();
  timerAPstart.clear();

  lastWiFiResetMoment.setNow();
  wifi_TX_pwr = 0;
  usedChannel = 0;
}

void WiFiEventData_t::markWiFiTurnOn() {
  setWiFiDisconnected();
//  lastDisconnectMoment.clear();
  lastConnectMoment.clear();
  lastGetIPmoment.clear();
  wifi_considered_stable    = false;
  
  clear_processed_flags();
}

void WiFiEventData_t::clear_processed_flags() {
  // Mark all flags to default to prevent handling old events.
  WiFi.scanDelete();
  processedConnect          = true;
  processedDisconnect       = true;
  processedGotIP            = true;
  #if FEATURE_USE_IPV6
  processedGotIP6           = true;
  #endif
  processedDHCPTimeout      = true;
  processedConnectAPmode    = true;
  processedDisconnectAPmode = true;
  processedScanDone         = true;
  wifiConnectAttemptNeeded  = true;
  wifiConnectInProgress     = false;
  processingDisconnect.clear();
  dns0_cache = IPAddress();
  dns1_cache = IPAddress();
}

void WiFiEventData_t::markWiFiBegin() {
  markWiFiTurnOn();
  last_wifi_connect_attempt_moment.setNow();
  wifiConnectInProgress  = true;
  usedChannel = 0;
  ++wifi_connect_attempt;
  if (!timerAPstart.isSet()) {
    timerAPstart.setMillisFromNow(3 * WIFI_RECONNECT_WAIT);
  }
}


void WiFiEventData_t::setWiFiDisconnected() {
  wifiStatus            = ESPEASY_WIFI_DISCONNECTED;
  last_wifi_connect_attempt_moment.clear();
  wifiConnectInProgress = false;
#if FEATURE_ESPEASY_P2P
  updateUDPport(true);
#endif  

}

void WiFiEventData_t::setWiFiGotIP() {
  bitSet(wifiStatus, ESPEASY_WIFI_GOT_IP);
  processedGotIP = true;
  if (valid_DNS_address(WiFi.dnsIP(0))) {
    dns0_cache = WiFi.dnsIP(0);
  }
  if (valid_DNS_address(WiFi.dnsIP(1))) {
    dns1_cache = WiFi.dnsIP(1);
  }
}

void WiFiEventData_t::setWiFiConnected() {
  bitSet(wifiStatus, ESPEASY_WIFI_CONNECTED);
  processedConnect = true;
}

void WiFiEventData_t::setWiFiServicesInitialized() {
  if (!unprocessedWifiEvents() && WiFiConnected() && WiFiGotIP()) {
    # ifndef BUILD_NO_DEBUG
    addLog(LOG_LEVEL_DEBUG, F("WiFi : WiFi services initialized"));
    #endif
    bitSet(wifiStatus, ESPEASY_WIFI_SERVICES_INITIALIZED);
    wifiConnectInProgress = false;
    wifiConnectAttemptNeeded = false;
    dns0_cache = WiFi.dnsIP(0);
    dns1_cache = WiFi.dnsIP(1);

#if FEATURE_ESPEASY_P2P
    updateUDPport(false);
#endif  
  }
}

void WiFiEventData_t::markGotIP() {
  lastGetIPmoment.setNow();

  // Create the 'got IP event' so mark the wifiStatus to not have the got IP flag set
  // This also implies the services are not fully initialized.
  bitClear(wifiStatus, ESPEASY_WIFI_GOT_IP);
  bitClear(wifiStatus, ESPEASY_WIFI_SERVICES_INITIALIZED);
  processedGotIP = false;
}

#if FEATURE_USE_IPV6
  void WiFiEventData_t::markGotIPv6(const IPAddress& ip6) {
    processedGotIP6 = false;
    unprocessed_IP6 = ip6;
  }
#endif


void WiFiEventData_t::markLostIP() {
  bitClear(wifiStatus, ESPEASY_WIFI_GOT_IP);
  bitClear(wifiStatus, ESPEASY_WIFI_SERVICES_INITIALIZED);
}

void WiFiEventData_t::markDisconnect(WiFiDisconnectReason reason) {
/*
  #if defined(ESP32)
  if ((WiFi.getMode() & WIFI_MODE_STA) == 0) return;
  #else // if defined(ESP32)
  if ((WiFi.getMode() & WIFI_STA) == 0) return;
  #endif // if defined(ESP32)
*/
  lastDisconnectMoment.setNow();
  usedChannel = 0;

  if (last_wifi_connect_attempt_moment.isSet() && !lastConnectMoment.isSet()) {
    // There was an unsuccessful connection attempt
    lastConnectedDuration_us = last_wifi_connect_attempt_moment.timeDiff(lastDisconnectMoment);
  } else {
    if (last_wifi_connect_attempt_moment.isSet())
      lastConnectedDuration_us = lastConnectMoment.timeDiff(lastDisconnectMoment);
    else 
      lastConnectedDuration_us = 0;
  }
  lastDisconnectReason = reason;
  processedDisconnect  = false;
  wifiConnectInProgress = false;
}

void WiFiEventData_t::markConnected(const String& ssid, const uint8_t bssid[6], uint8_t channel) {
  usedChannel = channel;
  lastConnectMoment.setNow();
  processedConnect    = false;
  channel_changed     = RTC.lastWiFiChannel != channel;
  last_ssid           = ssid;
  bssid_changed       = false;
  auth_mode           = WiFi_AP_Candidates.getCurrent().enc_type;

  RTC.lastWiFiChannel = channel;
  for (uint8_t i = 0; i < 6; ++i) {
    if (RTC.lastBSSID[i] != bssid[i]) {
      bssid_changed    = true;
      RTC.lastBSSID[i] = bssid[i];
    }
  }
#if FEATURE_USE_IPV6
  if (Settings.EnableIPv6()) {
    WiFi.enableIPv6(true);
  }
#endif
}

void WiFiEventData_t::markConnectedAPmode(const uint8_t mac[6]) {
  lastMacConnectedAPmode = mac;
  processedConnectAPmode = false;
}

void WiFiEventData_t::markDisconnectedAPmode(const uint8_t mac[6]) {
  lastMacDisconnectedAPmode = mac;
  processedDisconnectAPmode = false;
}



String WiFiEventData_t::ESPeasyWifiStatusToString() const {
  String log;
  if (WiFiDisconnected()) {
    log = F("DISCONNECTED");
  } else {
    if (WiFiConnected()) {
      log += F("Conn. ");
    }
    if (WiFiGotIP()) {
      log += F("IP ");
    }
    if (WiFiServicesInitialized()) {
      log += F("Init");
    }
  }
  return log;
}


uint32_t WiFiEventData_t::getSuggestedTimeout(int index, uint32_t minimum_timeout) const {
  auto it = connectDurations.find(index);
  if (it == connectDurations.end()) {
    return 3 * minimum_timeout;
  }
  const uint32_t res = 3 * it->second;
  return constrain(res, minimum_timeout, CONNECT_TIMEOUT_MAX);
}
#include "../DataStructs/ControllerSettingsStruct.h"

#include "../../ESPEasy_common.h"

#include "../CustomBuild/ESPEasyLimits.h"
#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../ESPEasyCore/ESPEasyNetwork.h"
#include "../Helpers/Misc.h"
#include "../Helpers/Networking.h"
#include "../Helpers/StringConverter.h"


#include <IPAddress.h>
#include <WString.h>
#include <WiFiClient.h>
#include <WiFiUdp.h>


ControllerSettingsStruct::ControllerSettingsStruct()
{
  memset(this, 0, sizeof(ControllerSettingsStruct));
  safe_strncpy(ClientID, F(CONTROLLER_DEFAULT_CLIENTID), sizeof(ClientID));
}

void ControllerSettingsStruct::reset() {
  // Need to make sure every byte between the members is also zero
  // Otherwise the checksum will fail and settings will be saved too often.
  memset(this, 0, sizeof(ControllerSettingsStruct));

  UseDNS                                        = DEFAULT_SERVER_USEDNS;
  Port                                          = DEFAULT_PORT;
  MinimalTimeBetweenMessages                    = CONTROLLER_DELAY_QUEUE_DELAY_DFLT;
  MaxQueueDepth                                 = CONTROLLER_DELAY_QUEUE_DEPTH_DFLT;
  MaxRetry                                      = CONTROLLER_DELAY_QUEUE_RETRY_DFLT;
  DeleteOldest                                  = DEFAULT_CONTROLLER_DELETE_OLDEST;
  ClientTimeout                                 = CONTROLLER_CLIENTTIMEOUT_DFLT;
  MustCheckReply                                = DEFAULT_CONTROLLER_MUST_CHECK_REPLY;
  SampleSetInitiator                            = INVALID_TASK_INDEX;
  KeepAliveTime                                 = CONTROLLER_KEEP_ALIVE_TIME_DFLT;
  VariousBits1.mqtt_cleanSession                = 0;
  VariousBits1.mqtt_not_sendLWT                 = 0;
  VariousBits1.mqtt_not_willRetain              = 0;
  VariousBits1.mqtt_uniqueMQTTclientIdReconnect = 0;
  VariousBits1.mqtt_retainFlag                  = 0;
  VariousBits1.useExtendedCredentials           = 0;
  VariousBits1.sendBinary                       = 0;
  VariousBits1.allowExpire                      = 0;
  VariousBits1.deduplicate                      = 0;
  VariousBits1.useLocalSystemTime               = 0;
  VariousBits1.TLStype                          = 0;
  VariousBits1.mqttAutoDiscovery                = 0;

  safe_strncpy(ClientID, F(CONTROLLER_DEFAULT_CLIENTID), sizeof(ClientID));
}

bool ControllerSettingsStruct::isSet() const {
  if (UseDNS) {
    return HostName[0] != 0;
  }
  return ipSet();
}

void ControllerSettingsStruct::validate() {
  if (Port > 65535) { Port = 0; }

  if ((MinimalTimeBetweenMessages < 1) ||  (MinimalTimeBetweenMessages > CONTROLLER_DELAY_QUEUE_DELAY_MAX)) {
    MinimalTimeBetweenMessages = CONTROLLER_DELAY_QUEUE_DELAY_DFLT;
  }

  if (MaxQueueDepth > CONTROLLER_DELAY_QUEUE_DEPTH_MAX) { MaxQueueDepth = CONTROLLER_DELAY_QUEUE_DEPTH_DFLT; }

  if (MaxRetry > CONTROLLER_DELAY_QUEUE_RETRY_MAX) { MaxRetry = CONTROLLER_DELAY_QUEUE_RETRY_MAX; }

  if (MaxQueueDepth == 0) { MaxQueueDepth = CONTROLLER_DELAY_QUEUE_DEPTH_DFLT; }

  if (MaxRetry == 0) { MaxRetry = CONTROLLER_DELAY_QUEUE_RETRY_DFLT; }

  if ((ClientTimeout < 10) || (ClientTimeout > CONTROLLER_CLIENTTIMEOUT_MAX)) {
    ClientTimeout = CONTROLLER_CLIENTTIMEOUT_DFLT;
  }
  ZERO_TERMINATE(HostName);
  ZERO_TERMINATE(Publish);
  ZERO_TERMINATE(Subscribe);
  ZERO_TERMINATE(MQTTLwtTopic);
  ZERO_TERMINATE(LWTMessageConnect);
  ZERO_TERMINATE(LWTMessageDisconnect);
  ZERO_TERMINATE(ClientID);
  ZERO_TERMINATE(MqttAutoDiscoveryTopic);
  ZERO_TERMINATE(MqttAutoDiscoveryConfig);
  ZERO_TERMINATE(MqttAutoDiscoveryTrigger);


  #if FEATURE_MQTT
    #if FEATURE_MQTT_TLS
    if (TLStype() == TLS_types::NoTLS) {
      if (Port == 8883) {
        Port = 1883;
        addLog(LOG_LEVEL_ERROR, F("Not using TLS, but port set to secure 8883. Use port 1883 instead"));
      }
    } else {
      if (Port == 1883) {
        Port = 8883;
        addLog(LOG_LEVEL_ERROR, F("Using TLS, but port set to insecure port 1883. Use port 8883 instead"));
      }
    }
    #else
    if (Port == 8883) {
      // No TLS support, so when switching builds, make sure it can still work.
      Port = 1883;
      addLog(LOG_LEVEL_ERROR, F("Not using TLS, but port set to secure 8883. Use port 1883 instead"));
    }
    #endif
  #endif

}

String ControllerSettingsStruct::getHost() const {
  if (UseDNS) {
    return HostName;
  }
  return formatIP(getIP());
}

void ControllerSettingsStruct::setHostname(const String& controllerhostname) {
  safe_strncpy(HostName, controllerhostname.c_str(), sizeof(HostName));
  updateIPcache();
}

bool ControllerSettingsStruct::checkHostReachable(bool quick) {
  if (!isSet()) {
    // No IP/hostname set
    return false;
  }

  if (!NetworkConnected(10)) {
    return false; // Not connected, so no use in wasting time to connect to a host.
  }
  delay(0);       // Make sure the Watchdog will not trigger a reset.

  if (quick && ipSet()) { return true; }

  if (UseDNS) {
    if (!updateIPcache()) {
      return false;
    }
  }
  return hostReachable(getIP());
}

#if FEATURE_HTTP_CLIENT
bool ControllerSettingsStruct::connectToHost(WiFiClient& client) {
  if (!checkHostReachable(true)) {
    return false; // Host not reachable
  }
  uint8_t retry     = 2;
  bool    connected = false;

  while (retry > 0 && !connected) {
    --retry;
    connected = connectClient(client, getIP(), Port, ClientTimeout);

    if (connected) { return true; }

    if (!checkHostReachable(false)) {
      return false;
    }
  }
  return false;
}

#endif // FEATURE_HTTP_CLIENT

bool ControllerSettingsStruct::beginPacket(WiFiUDP& client) {
  if (!checkHostReachable(true)) {
    return false; // Host not reachable
  }
  uint8_t retry = 2;

  while (retry > 0) {
    --retry;
    FeedSW_watchdog();

    if (client.beginPacket(getIP(), Port) == 1) {
      return true;
    }

    if (!checkHostReachable(false)) {
      return false;
    }
    delay(10);
  }
  return false;
}

String ControllerSettingsStruct::getHostPortString() const {
  String result = getHost();

  result += ':';
  result += Port;
  return result;
}

bool ControllerSettingsStruct::ipSet() const {
  for (uint8_t i = 0; i < 4; ++i) {
    if (IP[i] != 0) { return true; }
  }
  return false;
}

bool ControllerSettingsStruct::updateIPcache() {
  if (!UseDNS) {
    return true;
  }

  if (!NetworkConnected()) { return false; }
  IPAddress tmpIP;

  if (resolveHostByName(HostName, tmpIP, ClientTimeout)) {
    for (uint8_t x = 0; x < 4; x++) {
      IP[x] = tmpIP[x];
    }
    return true;
  }
  return false;
}

/*
bool ControllerSettingsStruct::mqtt_cleanSession() const
{
   return bitRead(VariousFlags, 1);
   }

   void ControllerSettingsStruct::mqtt_cleanSession(bool value)
   {
   bitWrite(VariousFlags, 1, value);
   }

   bool ControllerSettingsStruct::mqtt_sendLWT() const
   {
   return !bitRead(VariousFlags, 2);
   }

   void ControllerSettingsStruct::mqtt_sendLWT(bool value)
   {
   bitWrite(VariousFlags, 2, !value);
   }

   bool ControllerSettingsStruct::mqtt_willRetain() const
   {
   return !bitRead(VariousFlags, 3);
   }

   void ControllerSettingsStruct::mqtt_willRetain(bool value)
   {
   bitWrite(VariousFlags, 3, !value);
   }

   bool ControllerSettingsStruct::mqtt_uniqueMQTTclientIdReconnect() const
   {
   return bitRead(VariousFlags, 4);
   }

   void ControllerSettingsStruct::mqtt_uniqueMQTTclientIdReconnect(bool value)
   {
   bitWrite(VariousFlags, 4, value);
   }

   bool ControllerSettingsStruct::mqtt_retainFlag() const
   {
   return bitRead(VariousFlags, 5);
   }

   void ControllerSettingsStruct::mqtt_retainFlag(bool value)
   {
   bitWrite(VariousFlags, 5, value);
   }

   bool ControllerSettingsStruct::useExtendedCredentials() const
   {
   return bitRead(VariousFlags, 6);
   }

   void ControllerSettingsStruct::useExtendedCredentials(bool value)
   {
   bitWrite(VariousFlags, 6, value);
   }

   bool ControllerSettingsStruct::sendBinary() const
   {
   return bitRead(VariousFlags, 7);
   }

   void ControllerSettingsStruct::sendBinary(bool value)
   {
   bitWrite(VariousFlags, 7, value);
   }

   bool ControllerSettingsStruct::allowExpire() const
   {
   return bitRead(VariousFlags, 9);
   }

   void ControllerSettingsStruct::allowExpire(bool value)
   {
   bitWrite(VariousFlags, 9, value);
   }

   bool ControllerSettingsStruct::deduplicate() const
   {
   return bitRead(VariousFlags, 10);
   }

   void ControllerSettingsStruct::deduplicate(bool value)
   {
   bitWrite(VariousFlags, 10, value);
   }

   bool ControllerSettingsStruct::useLocalSystemTime() const
   {
   return bitRead(VariousFlags, 11);
   }

   void ControllerSettingsStruct::useLocalSystemTime(bool value)
   {
  bitWrite(VariousFlags, 11, value);
}
*/

#if FEATURE_MQTT_TLS || FEATURE_HTTP_TLS
String ControllerSettingsStruct::getCertificateFilename() const
{
  return getCertificateFilename(TLStype());
}

String ControllerSettingsStruct::getCertificateFilename(TLS_types tls_type) const
{
  String certFile = HostName;
  if (certFile.isEmpty()) {
    certFile = F("<HostName>");
  }

  switch (tls_type) {
    case TLS_types::NoTLS:
    case TLS_types::TLS_insecure:
      return EMPTY_STRING;
/*
    case TLS_types::TLS_PSK:
      certFile += F(".psk");
      break;
*/
/*
    case TLS_types::TLS_CA_CLI_CERT:
      certFile += F(".caclicert");
      break;
*/
    case TLS_types::TLS_CA_CERT:
      certFile += F(".cacert");
      break;
    case TLS_types::TLS_FINGERPRINT:
      certFile += F(".fp");
      break;
  }

  // Only use the last 29 bytes of the filename
  if (certFile.length() > 28) {
    certFile = certFile.substring(certFile.length() - 28);
  }
  
  return certFile;
}
#endif // #if FEATURE_MQTT_TLS || FEATURE_HTTP_TLS

#include "../DataStructs/PluginTaskData_base.h"

#include "../DataStructs/ESPEasy_EventStruct.h"

#include "../Globals/RuntimeData.h"

#include "../Helpers/StringConverter.h"

#include "../WebServer/Chart_JS.h"
#include "../WebServer/HTML_wrappers.h"


PluginTaskData_base::PluginTaskData_base()
  : _taskdata_pluginID(INVALID_PLUGIN_ID)
#if FEATURE_PLUGIN_STATS
  , _plugin_stats_array(nullptr)
#endif // if FEATURE_PLUGIN_STATS
{}


PluginTaskData_base::~PluginTaskData_base()  {
#if FEATURE_PLUGIN_STATS
  delete _plugin_stats_array;
  _plugin_stats_array = nullptr;
#endif // if FEATURE_PLUGIN_STATS
}

bool PluginTaskData_base::hasPluginStats() const {
#if FEATURE_PLUGIN_STATS

  if (_plugin_stats_array != nullptr) {
    return _plugin_stats_array->hasStats();
  }
#endif // if FEATURE_PLUGIN_STATS
  return false;
}

bool PluginTaskData_base::hasPeaks() const {
#if FEATURE_PLUGIN_STATS

  if (_plugin_stats_array != nullptr) {
    return _plugin_stats_array->hasPeaks();
  }
#endif // if FEATURE_PLUGIN_STATS
  return false;
}

size_t PluginTaskData_base::nrSamplesPresent() const {
#if FEATURE_PLUGIN_STATS

  if (_plugin_stats_array != nullptr) {
    return _plugin_stats_array->nrSamplesPresent();
  }
#endif // if FEATURE_PLUGIN_STATS
  return 0;
}

#if FEATURE_PLUGIN_STATS
void PluginTaskData_base::initPluginStats(taskIndex_t taskIndex, taskVarIndex_t taskVarIndex)
{
  if (taskVarIndex < VARS_PER_TASK) {
    if (_plugin_stats_array == nullptr) {
      constexpr unsigned size = sizeof(PluginStats_array);
      void *ptr               = special_calloc(1, size);

      if (ptr != nullptr) {
        _plugin_stats_array = new (ptr) PluginStats_array();
      }
    }

    if (_plugin_stats_array != nullptr) {
      _plugin_stats_array->initPluginStats(taskIndex, taskVarIndex);
    }
  }
}

void PluginTaskData_base::clearPluginStats(taskVarIndex_t taskVarIndex)
{
  if ((taskVarIndex < VARS_PER_TASK) && _plugin_stats_array) {
    _plugin_stats_array->clearPluginStats(taskVarIndex);

    if (!_plugin_stats_array->hasStats()) {
      delete _plugin_stats_array;
      _plugin_stats_array = nullptr;
    }
  }
}

void PluginTaskData_base::processTimeSet(const double& time_offset)
{
  if (_plugin_stats_array != nullptr) {
    _plugin_stats_array->processTimeSet(time_offset);
  }
}

#endif // if FEATURE_PLUGIN_STATS

void PluginTaskData_base::pushPluginStatsValues(struct EventStruct *event,
                                                bool                trackPeaks,
                                                bool                onlyUpdateTimestampWhenSame)
{
#if FEATURE_PLUGIN_STATS

  if (_plugin_stats_array != nullptr) {
    _plugin_stats_array->pushPluginStatsValues(event, trackPeaks, onlyUpdateTimestampWhenSame);
  }
#endif // if FEATURE_PLUGIN_STATS
}

bool PluginTaskData_base::plugin_get_config_value_base(struct EventStruct *event,
                                                       String            & string) const
{
#if FEATURE_PLUGIN_STATS

  if (_plugin_stats_array != nullptr) {
    return _plugin_stats_array->plugin_get_config_value_base(event, string);
  }
#endif // if FEATURE_PLUGIN_STATS
  return false;
}

bool PluginTaskData_base::plugin_write_base(struct EventStruct *event,
                                            const String      & string)
{
#if FEATURE_PLUGIN_STATS

  if (_plugin_stats_array != nullptr) {
    return _plugin_stats_array->plugin_write_base(event, string);
  }
#endif // if FEATURE_PLUGIN_STATS
  return false;
}

#if FEATURE_PLUGIN_STATS
bool PluginTaskData_base::webformLoad_show_stats(struct EventStruct *event) const
{
  if (_plugin_stats_array != nullptr) {
    return _plugin_stats_array->webformLoad_show_stats(event);
  }
  return false;
}

# if FEATURE_CHART_JS
void PluginTaskData_base::plot_ChartJS(bool onlyJSON) const
{
  if (_plugin_stats_array != nullptr) {
    _plugin_stats_array->plot_ChartJS(onlyJSON);
  }
}

void PluginTaskData_base::plot_ChartJS_scatter(
  taskVarIndex_t                values_X_axis_index,
  taskVarIndex_t                values_Y_axis_index,
  const __FlashStringHelper    *id,
  const ChartJS_title         & chartTitle,
  const ChartJS_dataset_config& datasetConfig,
  int                           width,
  int                           height,
  bool                          showAverage,
  const String                & options,
  bool                          onlyJSON) const
{
  if (_plugin_stats_array != nullptr) {
    _plugin_stats_array->plot_ChartJS_scatter(
      values_X_axis_index,
      values_Y_axis_index,
      id,
      chartTitle,
      datasetConfig,
      width,
      height,
      showAverage,
      options,
      onlyJSON);
  }
}

# endif // if FEATURE_CHART_JS


PluginStats * PluginTaskData_base::getPluginStats(taskVarIndex_t taskVarIndex) const
{
  if (_plugin_stats_array != nullptr) {
    return _plugin_stats_array->getPluginStats(taskVarIndex);
  }
  return nullptr;
}

PluginStats * PluginTaskData_base::getPluginStats(taskVarIndex_t taskVarIndex)
{
  if (_plugin_stats_array != nullptr) {
    return _plugin_stats_array->getPluginStats(taskVarIndex);
  }
  return nullptr;
}

#endif // if FEATURE_PLUGIN_STATS

#include "../DataStructs/Caches.h"

#include "../Globals/Device.h"
#include "../Globals/ExtraTaskSettings.h"
#include "../Globals/Settings.h"
#include "../Globals/WiFi_AP_Candidates.h"

#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/StringConverter.h"

#ifdef PLUGIN_USES_SERIAL
# include <ESPeasySerial.h>
#endif // ifdef PLUGIN_USES_SERIAL

void Caches::clearAllCaches()
{
  clearAllButTaskCaches();
  clearAllTaskCaches();
}

void Caches::clearAllButTaskCaches() {
  clearFileCaches();
  WiFi_AP_Candidates.clearCache();
  rulesHelper.closeAllFiles();
}

void Caches::clearAllTaskCaches() {
  taskIndexName.clear();
  taskIndexValueName.clear();
  extraTaskSettings_cache.clear();
  updateActiveTaskUseSerial0();
}

void Caches::clearTaskCache(taskIndex_t TaskIndex) {
  clearTaskIndexFromMaps(TaskIndex);

  auto it = extraTaskSettings_cache.find(TaskIndex);

  if (it != extraTaskSettings_cache.end()) {
    extraTaskSettings_cache.erase(it);
  }
  updateActiveTaskUseSerial0();
}

void Caches::clearFileCaches()
{
  fileExistsMap.clear();
  fileCacheClearMoment = 0;
}

bool Caches::matchChecksumExtraTaskSettings(taskIndex_t TaskIndex, const ChecksumType& checksum) const
{
  if (validTaskIndex(TaskIndex)) {
    auto it = extraTaskSettings_cache.find(TaskIndex);

    if (it != extraTaskSettings_cache.end()) {
      return checksum == it->second.md5checksum;
    }
  }
  return false;
}

void Caches::updateActiveTaskUseSerial0() {
  activeTaskUseSerial0 = false;
#ifdef PLUGIN_USES_SERIAL

  if (getDeviceCount() <= 0) {
    return;
  }

  // Check to see if a task is enabled and using the pins we also use for receiving commands.
  // We're now receiving only from Serial0, so check if an enabled task is also using it.
  for (taskIndex_t task = 0; task < TASKS_MAX; ++task)
  {
    const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(task);

    if (Settings.TaskDeviceEnabled[task] && validDeviceIndex(DeviceIndex)) {
      if ((Device[DeviceIndex].Type == DEVICE_TYPE_SERIAL) ||
          (Device[DeviceIndex].Type == DEVICE_TYPE_SERIAL_PLUS1)) {
        const ESPEasySerialPort port = ESPeasySerialType::getSerialType(
          static_cast<ESPEasySerialPort>(Settings.TaskDevicePort[task]),
          Settings.TaskDevicePin1[task],
          Settings.TaskDevicePin2[task]);

        // FIXME TD-er: Must not check for conflict with serial0, but for conflict with ESPEasy_Console.
        # ifdef ESP32

        if (port == ESPEasySerialPort::serial0)
        {
          activeTaskUseSerial0 = true;
        }
        # endif // ifdef ESP32
        # ifdef ESP8266

        if ((port == ESPEasySerialPort::serial0_swap) ||
            (port == ESPEasySerialPort::serial0))
        {
          activeTaskUseSerial0 = true;
        }
        # endif // ifdef ESP8266
      }
    }
  }
#endif // ifdef PLUGIN_USES_SERIAL
}

uint8_t Caches::getTaskDeviceValueDecimals(taskIndex_t TaskIndex, uint8_t rel_index)
{
  if (validTaskIndex(TaskIndex) && (rel_index < VARS_PER_TASK)) {
    auto it = getExtraTaskSettings(TaskIndex);

    if (it != extraTaskSettings_cache.end()) {
      return it->second.decimals[rel_index];
    }
  }
  return 0;
}

String Caches::getTaskDeviceName(taskIndex_t TaskIndex)
{
  if (validTaskIndex(TaskIndex)) {
    auto it = getExtraTaskSettings(TaskIndex);

    if (it != extraTaskSettings_cache.end()) {
      return it->second.TaskDeviceName;
    }
  }
  return EMPTY_STRING;
}

String Caches::getTaskDeviceValueName(taskIndex_t TaskIndex, uint8_t rel_index)
{
  if (validTaskIndex(TaskIndex) && (rel_index < VARS_PER_TASK)) {
  #ifdef ESP8266

    // ESP8266 uses SPIFFS, which is fast enough to read the task settings
    // Also RAM is limited, so don't waste it on caching where it is not needed.
    LoadTaskSettings(TaskIndex);
    return ExtraTaskSettings.TaskDeviceValueNames[rel_index];
  #endif // ifdef ESP8266
  #ifdef ESP32

    auto it = getExtraTaskSettings(TaskIndex);

    if (it != extraTaskSettings_cache.end()) {
      return it->second.TaskDeviceValueNames[rel_index];
    }
    #endif // ifdef ESP32
  }

  return EMPTY_STRING;
}

bool Caches::hasFormula(taskIndex_t TaskIndex, uint8_t rel_index)
{
  if (validTaskIndex(TaskIndex) && (rel_index < VARS_PER_TASK)) {
    // Just a quick test to see if we do have a formula present.
    // Task Formula are not used very often, so we will probably almost always have to return an empty string.
    auto it = getExtraTaskSettings(TaskIndex);

    if (it != extraTaskSettings_cache.end()) {
      return bitRead(it->second.hasFormula, 2 * rel_index);
    }
  }
  return false;
}

bool Caches::hasFormula(taskIndex_t TaskIndex)
{
  if (validTaskIndex(TaskIndex)) {
    // Just a quick test to see if we do have a formula present.
    // Task Formula are not used very often, so we will probably almost always have to return an empty string.
    auto it = getExtraTaskSettings(TaskIndex);

    if (it != extraTaskSettings_cache.end()) {
      return it->second.hasFormula != 0;
    }
  }
  return false;
}

bool Caches::hasFormula_with_prevValue(taskIndex_t TaskIndex, uint8_t rel_index)
{
  if (validTaskIndex(TaskIndex) && (rel_index < VARS_PER_TASK)) {
    // Just a quick test to see if we do have a formula present which requires %pvalue%.
    auto it = getExtraTaskSettings(TaskIndex);

    if (it != extraTaskSettings_cache.end()) {
      return bitRead(it->second.hasFormula, 2 * rel_index + 1);
    }
  }
  return false;
}

String Caches::getTaskDeviceFormula(taskIndex_t TaskIndex, uint8_t rel_index)
{
  if ((rel_index < VARS_PER_TASK) && hasFormula(TaskIndex, rel_index)) {
    #ifdef ESP32
    auto it = getExtraTaskSettings(TaskIndex);

    if (it != extraTaskSettings_cache.end()) {
      return it->second.TaskDeviceFormula[rel_index];
    }
    #else // ifdef ESP32
    LoadTaskSettings(TaskIndex);
    return ExtraTaskSettings.TaskDeviceFormula[rel_index];
    #endif // ifdef ESP32
  }
  return EMPTY_STRING;
}

long Caches::getTaskDevicePluginConfigLong(taskIndex_t TaskIndex, uint8_t rel_index)
{
  if (validTaskIndex(TaskIndex) && (rel_index < PLUGIN_EXTRACONFIGVAR_MAX)) {
    #ifdef ESP8266
    LoadTaskSettings(TaskIndex);
    return ExtraTaskSettings.TaskDevicePluginConfigLong[rel_index];
    #endif // ifdef ESP8266
    #ifdef ESP32
    auto it = getExtraTaskSettings(TaskIndex);

    if (it != extraTaskSettings_cache.end()) {
      if (bitRead(it->second.TaskDevicePluginConfigLong_index_used, rel_index) == 0)
      {
        return 0;
      }
      auto it_long = it->second.long_values.find(ExtraTaskSettings_cache_t::make_Long_ValuesIndex(rel_index));

      if (it_long != it->second.long_values.end()) {
        return it_long->second;
      }

      // Should not get here, since the long_values map should be in sync with the index_used bitmap.
    }
    #endif // ifdef ESP32
  }

  return 0;
}

int16_t Caches::getTaskDevicePluginConfig(taskIndex_t TaskIndex, uint8_t rel_index)
{
  if (validTaskIndex(TaskIndex) && (rel_index < PLUGIN_EXTRACONFIGVAR_MAX)) {
    #ifdef ESP8266
    LoadTaskSettings(TaskIndex);
    return ExtraTaskSettings.TaskDevicePluginConfig[rel_index];
    #endif // ifdef ESP8266
    #ifdef ESP32
    auto it = getExtraTaskSettings(TaskIndex);

    if (it != extraTaskSettings_cache.end()) {
      if (bitRead(it->second.TaskDevicePluginConfig_index_used, rel_index) == 0)
      {
        return 0;
      }
      auto it_long = it->second.long_values.find(ExtraTaskSettings_cache_t::make_Uint16_ValuesIndex(rel_index));

      if (it_long != it->second.long_values.end()) {
        return it_long->second;
      }

      // Should not get here, since the long_values map should be in sync with the index_used bitmap.
    }
    #endif // ifdef ESP32
  }

  return 0;
}

#if FEATURE_PLUGIN_STATS
bool Caches::enabledPluginStats(taskIndex_t TaskIndex, uint8_t rel_index)
{
  if (validTaskIndex(TaskIndex) && (rel_index < VARS_PER_TASK)) {
    auto it = getExtraTaskSettings(TaskIndex);

    if (it != extraTaskSettings_cache.end()) {
      return it->second.pluginStatsConfig[rel_index].isEnabled();
    }
  }
  return false;
}

PluginStats_Config_t Caches::getPluginStatsConfig(taskIndex_t TaskIndex, taskVarIndex_t taskVarIndex)
{
  if (validTaskIndex(TaskIndex) && (taskVarIndex < VARS_PER_TASK)) {
    auto it = getExtraTaskSettings(TaskIndex);

    if (it != extraTaskSettings_cache.end()) {
      return it->second.pluginStatsConfig[taskVarIndex];
    }
  }
  return PluginStats_Config_t(0);
}

#endif // if FEATURE_PLUGIN_STATS

#if FEATURE_TASKVALUE_UNIT_OF_MEASURE
uint8_t Caches::getTaskVarUnitOfMeasure(taskIndex_t    TaskIndex,
                                        taskVarIndex_t taskVarIndex) {
  if (validTaskIndex(TaskIndex) && (validTaskVarIndex(taskVarIndex))) {
    auto it = getExtraTaskSettings(TaskIndex);

    if (it != extraTaskSettings_cache.end()) {
      return it->second.unitOfMeasure[taskVarIndex];
    }
  }
  return 0;
}

#endif // if FEATURE_TASKVALUE_UNIT_OF_MEASURE

#if FEATURE_CUSTOM_TASKVAR_VTYPE
uint8_t Caches::getTaskVarCustomVType(taskIndex_t    TaskIndex,
                                      taskVarIndex_t taskVarIndex) {
  if (validTaskIndex(TaskIndex) && (validTaskVarIndex(taskVarIndex))) {
    auto it = getExtraTaskSettings(TaskIndex);

    if (it != extraTaskSettings_cache.end()) {
      return it->second.customVType[taskVarIndex];
    }
  }
  return 0;
}

#endif // if FEATURE_CUSTOM_TASKVAR_VTYPE

#if FEATURE_MQTT_STATE_CLASS
uint8_t Caches::getTaskVarStateClass(taskIndex_t    TaskIndex,
                                     taskVarIndex_t taskVarIndex) {
  if (validTaskIndex(TaskIndex) && (validTaskVarIndex(taskVarIndex))) {
    auto it = getExtraTaskSettings(TaskIndex);

    if (it != extraTaskSettings_cache.end()) {
      return it->second.mqttStateClass[taskVarIndex];
    }
  }
  return 0;
}

#endif // if FEATURE_MQTT_STATE_CLASS

void Caches::updateExtraTaskSettingsCache()
{
  const taskIndex_t TaskIndex = ExtraTaskSettings.TaskIndex;

  if (validTaskIndex(TaskIndex)) {
    ExtraTaskSettings_cache_t tmp;

    auto it = extraTaskSettings_cache.find(TaskIndex);

    if (it != extraTaskSettings_cache.end()) {
      // We need to keep the original checksum, from when loaded from storage
      tmp.md5checksum                = it->second.md5checksum;
      tmp.defaultTaskDeviceValueName = it->second.defaultTaskDeviceValueName;

      // Now clear it so we can create a fresh copy.
      extraTaskSettings_cache.erase(it);
      clearTaskIndexFromMaps(TaskIndex);
    }
    move_special(tmp.TaskDeviceName, String(ExtraTaskSettings.TaskDeviceName));

    for (size_t i = 0; i < VARS_PER_TASK; ++i) {
      #ifdef ESP32
      move_special(tmp.TaskDeviceValueNames[i], String(ExtraTaskSettings.TaskDeviceValueNames[i]));
      #endif // ifdef ESP32

      if (ExtraTaskSettings.TaskDeviceFormula[i][0] != 0) {
        bitSet(tmp.hasFormula, 2 * i);
        String formula(ExtraTaskSettings.TaskDeviceFormula[i]);

        if (formula.indexOf(F("%pvalue%")) != -1) {
          bitSet(tmp.hasFormula, 2 * i + 1);
        }


        #ifdef ESP32
        tmp.TaskDeviceFormula[i] = std::move(formula);
        #endif // ifdef ESP32
      }
      tmp.decimals[i] = ExtraTaskSettings.TaskDeviceValueDecimals[i];
      #if FEATURE_PLUGIN_STATS

      tmp.pluginStatsConfig[i] = ExtraTaskSettings.getPluginStatsConfig(i);
      #endif // if FEATURE_PLUGIN_STATS

      #if FEATURE_TASKVALUE_UNIT_OF_MEASURE
      tmp.unitOfMeasure[i] = ExtraTaskSettings.getTaskVarUnitOfMeasure(i);
      #endif // if FEATURE_TASKVALUE_UNIT_OF_MEASURE
      #if FEATURE_CUSTOM_TASKVAR_VTYPE
      tmp.customVType[i] = ExtraTaskSettings.getTaskVarCustomVType(i);
      #endif // if FEATURE_CUSTOM_TASKVAR_VTYPE
      #if FEATURE_MQTT_STATE_CLASS
      tmp.mqttStateClass[i] = ExtraTaskSettings.getTaskVarStateClass(i);
      #endif // if FEATURE_MQTT_STATE_CLASS
    }
    #ifdef ESP32
    tmp.TaskDevicePluginConfigLong_index_used = 0;
    tmp.TaskDevicePluginConfig_index_used     = 0;
    tmp.long_values.clear();

    for (size_t i = 0; i < PLUGIN_EXTRACONFIGVAR_MAX; ++i) {
      if (ExtraTaskSettings.TaskDevicePluginConfigLong[i] != 0) {
        bitSet(tmp.TaskDevicePluginConfigLong_index_used, i);
        tmp.long_values[ExtraTaskSettings_cache_t::make_Long_ValuesIndex(i)] = ExtraTaskSettings.TaskDevicePluginConfigLong[i];
      }

      if (ExtraTaskSettings.TaskDevicePluginConfig[i] != 0) {
        bitSet(tmp.TaskDevicePluginConfig_index_used, i);
        tmp.long_values[ExtraTaskSettings_cache_t::make_Uint16_ValuesIndex(i)] = ExtraTaskSettings.TaskDevicePluginConfig[i];
      }
    }
    #endif // ifdef ESP32

    extraTaskSettings_cache.emplace(std::make_pair(TaskIndex, std::move(tmp)));
  }
}

void Caches::updateExtraTaskSettingsCache_afterLoad_Save()
{
  if (!validTaskIndex(ExtraTaskSettings.TaskIndex)) {
    return;
  }

  // First update all other values
  updateExtraTaskSettingsCache();

  // Iterator has changed
  auto it = extraTaskSettings_cache.find(ExtraTaskSettings.TaskIndex);

  if (it != extraTaskSettings_cache.end()) {
    for (size_t i = 0; i < VARS_PER_TASK; ++i) {
      bitWrite(it->second.defaultTaskDeviceValueName, i, ExtraTaskSettings.isDefaultTaskVarName(i));
    }
    it->second.md5checksum = ExtraTaskSettings.computeChecksum();
  }
}

ExtraTaskSettingsMap::const_iterator Caches::getExtraTaskSettings(taskIndex_t TaskIndex)
{
  if (validTaskIndex(TaskIndex)) {
    auto it = extraTaskSettings_cache.find(TaskIndex);

    if (it == extraTaskSettings_cache.end()) {
      ExtraTaskSettings.clear(); // Force reload so the cache is filled
      LoadTaskSettings(TaskIndex);
      it = extraTaskSettings_cache.find(TaskIndex);
    }
    return it;
  }
  return extraTaskSettings_cache.end();
}

void Caches::clearTaskIndexFromMaps(taskIndex_t TaskIndex)
{
  {
    auto it = taskIndexName.begin();

    for (; it != taskIndexName.end();) {
      if (it->second == TaskIndex) {
        it = taskIndexName.erase(it);
      } else {
        ++it;
      }
    }
  }
  {
    const String searchstr = String('#') + TaskIndex;
    auto it                = taskIndexValueName.begin();

    for (; it != taskIndexValueName.end();) {
      if (it->first.endsWith(searchstr)) {
        it = taskIndexValueName.erase(it);
      } else {
        ++it;
      }
    }
  }
}

  #ifdef ESP32
bool Caches::getControllerSettings(controllerIndex_t index,  ControllerSettingsStruct& ControllerSettings) const
{
  if (!validControllerIndex(index)) { return false; }

  auto it = controllerSetings_cache.find(index);

  if (it == controllerSetings_cache.end()) {
    return false;
  }

  ControllerSettings = it->second;
  return true;
}

void Caches::setControllerSettings(controllerIndex_t index, const ControllerSettingsStruct& ControllerSettings)
{
  auto it = controllerSetings_cache.find(index);

  if (it != controllerSetings_cache.end()) {
    if (it->second.computeChecksum() == ControllerSettings.computeChecksum()) {
      return;
    }
    controllerSetings_cache.erase(it);
  }
  controllerSetings_cache.emplace(index, ControllerSettings);
}

void Caches::clearControllerSettings(controllerIndex_t index)
{
  auto it = controllerSetings_cache.find(index);

  if (it != controllerSetings_cache.end()) {
    controllerSetings_cache.erase(it);
  }
}

  #endif // ifdef ESP32

#include "../DataStructs/Scheduler_PluginDeviceTimerID.h"

#include "../Globals/Plugins.h"
#include "../Helpers/_Plugin_init.h"

PluginDeviceTimerID::PluginDeviceTimerID(pluginID_t pluginID, int Par1) :
  SchedulerTimerID(SchedulerTimerType_e::PLUGIN_DEVICETIMER_IN_e)
{
  // plugin number and par1 form a unique key that can be used to restart a timer
  // Use deviceIndex instead of pluginID, since the deviceIndex uses less bits.
  const deviceIndex_t deviceIndex = getDeviceIndex(pluginID);

  if (validDeviceIndex(deviceIndex)) {
    // FIXME TD-er: Must add a constexpr function with nr of included plugins.
    const unsigned nrBits = getNrBitsDeviceIndex();
    const unsigned mask   = MASK_BITS(nrBits);
    setId((deviceIndex.value & mask) | (Par1 << nrBits));
  }
}

deviceIndex_t PluginDeviceTimerID::get_deviceIndex() const {
  const unsigned nrBits = getNrBitsDeviceIndex();
  const unsigned mask   = MASK_BITS(nrBits);

  return deviceIndex_t::toDeviceIndex(getId() & mask);
}

#ifndef BUILD_NO_DEBUG
String PluginDeviceTimerID::decode() const
{
  const deviceIndex_t deviceIndex = get_deviceIndex();

  if (validDeviceIndex(deviceIndex)) {
    return getPluginNameFromDeviceIndex(deviceIndex);
  }
  return String(getId());
}

#endif // ifndef BUILD_NO_DEBUG

#include "../DataStructs/ESPEasyControllerCache.h"

#if FEATURE_RTC_CACHE_STORAGE

#include "../Helpers/Memory.h"

ControllerCache_struct::~ControllerCache_struct() {
  if (_RTC_cache_handler != nullptr) {
    delete _RTC_cache_handler;
    _RTC_cache_handler = nullptr;
  }
}

// Write a single sample set to the buffer
bool ControllerCache_struct::write(const uint8_t *data, unsigned int size) {
  if (_RTC_cache_handler == nullptr) {
    return false;
  }
  return _RTC_cache_handler->write(data, size);
}

// Read a single sample set, either from file or buffer.
// May delete a file if it is all read and not written to.
bool ControllerCache_struct::read(uint8_t *data, unsigned int size) {
  return true;
}

// Dump whatever is in the buffer to the filesystem
bool ControllerCache_struct::flush() {
  if (_RTC_cache_handler == nullptr) {
    return false;
  }
  return _RTC_cache_handler->flush();
}

void ControllerCache_struct::init() {
  if (_RTC_cache_handler == nullptr) {
    constexpr unsigned size = sizeof(RTC_cache_handler_struct);
    void *ptr               = special_calloc(1, size);

    if (ptr != nullptr) {
      _RTC_cache_handler = new (ptr) RTC_cache_handler_struct;
    }
    if (_RTC_cache_handler != nullptr) {
      _RTC_cache_handler->init();
    }
  }
}

bool ControllerCache_struct::isInitialized() const {
  return _RTC_cache_handler != nullptr;
}

// Clear all caches
void ControllerCache_struct::clearCache() {}

bool ControllerCache_struct::deleteOldestCacheBlock() {
  if (_RTC_cache_handler != nullptr) {
    return _RTC_cache_handler->deleteOldestCacheBlock();
  }
  return false;
}

void ControllerCache_struct::closeOpenFiles() {
  if (_RTC_cache_handler != nullptr) {
    _RTC_cache_handler->closeOpenFiles();
  }
}

bool ControllerCache_struct::deleteAllCacheBlocks() {
  if (_RTC_cache_handler != nullptr) {
    return _RTC_cache_handler->deleteAllCacheBlocks();
  }
  return false;
}

bool ControllerCache_struct::deleteCacheBlock(int fileNr) {
  if (_RTC_cache_handler != nullptr) {
    return _RTC_cache_handler->deleteCacheBlock(fileNr);
  }
  return false;
}

void ControllerCache_struct::resetpeek() {
  if (_RTC_cache_handler != nullptr) {
    _RTC_cache_handler->resetpeek();
  }
}

bool ControllerCache_struct::peekDataAvailable() const {
  if (_RTC_cache_handler == nullptr) {
    return false;
  }
  return _RTC_cache_handler->peekDataAvailable();
}

int  ControllerCache_struct::getPeekFilePos(int& peekFileNr) const {
  if (_RTC_cache_handler != nullptr) {
    return _RTC_cache_handler->getPeekFilePos(peekFileNr);
  }
  return -1;
}

int  ControllerCache_struct::getPeekFileSize(int peekFileNr) const {
  if (_RTC_cache_handler != nullptr) {
    return _RTC_cache_handler->getPeekFileSize(peekFileNr);
  }
  return -1;
}

void ControllerCache_struct::setPeekFilePos(int peekFileNr, int peekReadPos) {
  if (_RTC_cache_handler != nullptr) {
    _RTC_cache_handler->setPeekFilePos(peekFileNr, peekReadPos);
  }
}

// Read data without marking it as being read.
bool ControllerCache_struct::peek(uint8_t *data, unsigned int size) const {
  if (_RTC_cache_handler == nullptr) {
    return false;
  }
  return _RTC_cache_handler->peek(data, size);
}

String ControllerCache_struct::getNextCacheFileName(int& fileNr, bool& islast) {
  if (_RTC_cache_handler == nullptr) {
    fileNr = -1;
    islast = true;
    return EMPTY_STRING;
  }
  return _RTC_cache_handler->getNextCacheFileName(fileNr, islast);
}

#endif
#include "../DataStructs/RTC_cache_handler_struct.h"

#if FEATURE_RTC_CACHE_STORAGE

#include "../../ESPEasy_common.h"
#include "../DataStructs/RTCStruct.h"
#include "../Helpers/CRC_functions.h"
#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/StringConverter.h"

#include "../ESPEasyCore/ESPEasy_backgroundtasks.h"
#include "../ESPEasyCore/ESPEasy_Log.h"

#ifdef ESP8266
# include <user_interface.h>
#endif // ifdef ESP8266

#ifdef ESP32
  # include <soc/rtc.h>

// For ESP32 the RTC mapped structure may not be a member of an object,
// but must be declared 'static'
// This also means we can only have a single instance of this
// RTC_cache_handler_struct.
RTC_NOINIT_ATTR RTC_cache_struct RTC_cache;
RTC_NOINIT_ATTR uint8_t RTC_cache_data[RTC_CACHE_DATA_SIZE];
#endif // ifdef ESP32


/********************************************************************************************\
   RTC located cache
 \*********************************************************************************************/
RTC_cache_handler_struct::RTC_cache_handler_struct() {}

bool RTC_cache_handler_struct::init() {
  bool success = loadMetaData() && loadData();

  if (!success) {
      #ifdef RTC_STRUCT_DEBUG
    addLog(LOG_LEVEL_INFO, F("RTC  : Error reading cache data"));
      #endif // ifdef RTC_STRUCT_DEBUG
    RTC_cache.init();
    flush();
  } else {
      #ifdef RTC_STRUCT_DEBUG
    rtc_debug_log(F("Read from RTC cache"), RTC_cache.writePos);
      #endif // ifdef RTC_STRUCT_DEBUG
  }
  updateRTC_filenameCounters();
  return success;
}

unsigned int RTC_cache_handler_struct::getFreeSpace() {
  if (RTC_cache.writePos >= RTC_CACHE_DATA_SIZE) {
    return 0;
  }
  return RTC_CACHE_DATA_SIZE - RTC_cache.writePos;
}

void RTC_cache_handler_struct::resetpeek() {
  if (fp) {
    fp.close();
  }
  _peekfilenr  = 0;
  _peekreadpos = 0;
}

bool RTC_cache_handler_struct::peekDataAvailable() const {
  if (fp) {
    if ((_peekreadpos + 1) < fp.size()) { return true; }
  }
  if (_peekfilenr < RTC_cache.writeFileNr) {
    return true;
  }

  if (_peekfilenr == RTC_cache.writeFileNr) {
    if (fw) {
      constexpr size_t errorcode = (size_t)-1;
      size_t pos = fw.position();
      if (pos == errorcode) {
        pos = 0;
      }
      return ((_peekreadpos + 1) < pos);
    }
//    return true;
  }
  return false;
}

int RTC_cache_handler_struct::getPeekFilePos(int& peekFileNr) {
  peekFileNr = _peekfilenr;
  constexpr size_t errorcode = (size_t)-1;
  if (fp) {
    size_t pos = fp.position();
    if (pos == errorcode) {
      _peekreadpos = 0;
      return -1;
    }      
    _peekreadpos = pos;
  }

  if (_peekreadpos == errorcode)
    return -1;
  return _peekreadpos;
}

int RTC_cache_handler_struct::getPeekFileSize(int peekFileNr) const {
  if (fp) {
    return fp.size();
  }
  return -1;
}

void RTC_cache_handler_struct::setPeekFilePos(int newPeekFileNr, int newPeekReadPos) {
  validateFilePos(newPeekFileNr, newPeekReadPos);

  if (fp) {
    constexpr size_t errorcode = (size_t)-1;
    size_t pos = fp.position();
    if (pos == errorcode) {
      pos = 0;
    }

    if (newPeekReadPos < static_cast<int>(pos)) {
      _peekfilenr = newPeekFileNr;
      _peekreadpos = newPeekReadPos;
      fp.close();
    } else
    if (newPeekReadPos >= static_cast<int>(fp.size()) && static_cast<int>(_peekfilenr) == newPeekFileNr) {
      newPeekFileNr++;
      newPeekReadPos = 0;
      validateFilePos(newPeekFileNr, newPeekReadPos);
    }
    if (static_cast<int>(_peekfilenr) != newPeekFileNr) {
      // Not the same file
      fp.close();
      _peekfilenr = newPeekFileNr;
    }
  }


  if (!fp) {
    String fname = createCacheFilename(newPeekFileNr);

    if (fname.isEmpty()) { return; }

    fp = tryOpenFile(fname, "r");
  }

  if (fp) {
    _peekfilenr = newPeekFileNr;

    if (newPeekReadPos > 0) {
      const int fileSize = fp.size();

      if (fileSize <= newPeekReadPos) {
        fp.seek(0, fs::SeekEnd);

        constexpr size_t errorcode = (size_t)-1;
        size_t pos = fp.position();
        if (pos == errorcode) {
          pos = 0;
        }

        _peekreadpos = pos;
        return;
      }

      if (fp.seek(newPeekReadPos)) {
        _peekreadpos = newPeekReadPos;
      }
    } else {
      _peekreadpos = 0;
    }
    return;
  }

  _peekreadpos = 0;
  _peekfilenr  = 0;
}

bool RTC_cache_handler_struct::peek(uint8_t *data, unsigned int size) {
  if (!fp) {
    if (_peekfilenr == 0) {
      setPeekFilePos(0, 0);
    } else {
      if (!peekDataAvailable()) {
        return false;
      }
      setPeekFilePos(_peekfilenr, _peekreadpos);
    }
  }

  if (!peekDataAvailable()) { return false; }

  if (!fp) { return false; }

  const size_t bytesRead = fp.read(data, size);

  constexpr size_t errorcode = (size_t)-1;
  size_t pos = fp.position();
  if (pos == errorcode) {
    pos = 0;
  }

  _peekreadpos = pos;

  if (_peekreadpos >= fp.size()) {
    if (_peekfilenr < RTC_cache.writeFileNr) {
      fp.close();
      _peekreadpos = 0;
      ++_peekfilenr;
    }
  }

  return bytesRead == size;
}

// Write a single sample set to the buffer
bool RTC_cache_handler_struct::write(const uint8_t *data, unsigned int size) {
    #ifdef RTC_STRUCT_DEBUG
  rtc_debug_log(F("write RTC cache data"), size);
    #endif // ifdef RTC_STRUCT_DEBUG

  if (getFreeSpace() < size) {
    if (!flush()) {
      return false;
    }
  }

  // First store it in the buffer
  for (unsigned int i = 0; i < size; ++i) {
    RTC_cache_data[RTC_cache.writePos] = data[i];
    ++RTC_cache.writePos;
  }

  // Now store the updated part of the buffer to the RTC memory.
  // Pad some extra bytes around it to allow sample sizes not multiple of 4 bytes.
  int startOffset = RTC_cache.writePos - size;
  startOffset -= startOffset % 4;

  if (startOffset < 0) {
    startOffset = 0;
  }
  int nrBytes = RTC_cache.writePos - startOffset;

  if (nrBytes % 4 != 0) {
    nrBytes -= nrBytes % 4;
    nrBytes += 4;
  }

  if ((nrBytes + startOffset) >  RTC_CACHE_DATA_SIZE) {
    // Can this happen?
    nrBytes = RTC_CACHE_DATA_SIZE - startOffset;
  }
  return saveRTCcache(startOffset, nrBytes);
}

// Mark all content as being processed and empty buffer.
bool RTC_cache_handler_struct::flush() {
  if (RTC_cache.writePos > 0) {
    if (prepareFileForWrite()) {
      #ifdef RTC_STRUCT_DEBUG
      size_t filesize = fw.size();
      #endif // ifdef RTC_STRUCT_DEBUG

      // Make sure the read and peek file handles cannot be used on possibly deleted files.
      if (fr) {
        fr.close();
      }

      if (fp) {
        fp.close();
      }

      int bytesWritten = fw.write(&RTC_cache_data[0], RTC_cache.writePos);

      delay(0);
      fw.flush();
        #ifdef RTC_STRUCT_DEBUG
      addLog(LOG_LEVEL_INFO, F("RTC  : flush RTC cache"));
        #endif // ifdef RTC_STRUCT_DEBUG


      if ((bytesWritten < RTC_cache.writePos) /*|| (fw.size() == filesize)*/) {
          #ifdef RTC_STRUCT_DEBUG

        if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
          String log = F("RTC  : error writing file. Size before: ");
          log += filesize;
          log += F(" after: ");
          log += fw.size();
          log += F(" written: ");
          log += bytesWritten;
          addLogMove(LOG_LEVEL_ERROR, log);
        }
          #endif // ifdef RTC_STRUCT_DEBUG
        fw.close();

        if (!GarbageCollection()) {
          // Garbage collection was not able to remove anything
          writeError = true;
        }
        return false;
      }
      initRTCcache_data();
      clearRTCcacheData();
      saveRTCcache();
      return true;
    }
  }
  return false;
}

// Return usable filename for reading.
// Will be empty if there is no file to process.
String RTC_cache_handler_struct::getReadCacheFileName(int& readPos) {
  initRTCcache_data();

  for (int i = 0; i < 2; ++i) {
    String fname = createCacheFilename(RTC_cache.readFileNr);

    if (fileExists(fname)) {
      if (i != 0) {
        // First attempt failed, so stored read position is not valid
        RTC_cache.readPos = 0;
      }
      readPos = RTC_cache.readPos;
      return fname;
    }

    if (i == 0) {
      updateRTC_filenameCounters();
    }
  }

  // No file found
  RTC_cache.readPos = 0;
  readPos           = RTC_cache.readPos;
  return EMPTY_STRING;
}

String RTC_cache_handler_struct::getNextCacheFileName(int& fileNr, bool& islast)  {
  int filepos = 0;
  validateFilePos(fileNr, filepos);
  islast = fileNr >= RTC_cache.writeFileNr;
  const String fname = createCacheFilename(fileNr);

  if (fileExists(fname)) {
    return fname;
  }
  islast = true;
  return EMPTY_STRING;
}

bool RTC_cache_handler_struct::deleteOldestCacheBlock() {
  if (updateRTC_filenameCounters()) {
    return deleteCacheBlock(RTC_cache.readFileNr);
  }
#ifdef RTC_STRUCT_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, F("RTC  : No Cache files found"));
  }
#endif // ifdef RTC_STRUCT_DEBUG
  return false;
}

void RTC_cache_handler_struct::closeOpenFiles()
{
  if (fr) {
    fr.close();
  }

  if (fp) {
    fp.close();
  }
  if (fw) {
    fw.close();
  }
}

bool RTC_cache_handler_struct::deleteAllCacheBlocks()
{
  if (updateRTC_filenameCounters()) {

    if (RTC_cache.readFileNr < RTC_cache.writeFileNr) {
      bool fileDeleted = false;
      int  count       = 0;
      closeOpenFiles();

      for (int fileNr = RTC_cache.readFileNr; count < 25 && fileNr < RTC_cache.writeFileNr; ++fileNr)
      {
        String fname = createCacheFilename(fileNr);

        if (tryDeleteFile(fname)) {
          ++count;
          fileDeleted = true;
          #ifdef RTC_STRUCT_DEBUG

          if (loglevelActiveFor(LOG_LEVEL_INFO)) {
            addLogMove(LOG_LEVEL_INFO, concat(F("RTC  : Removed file from FS: "), fname));
          }
          #endif // ifdef RTC_STRUCT_DEBUG
          backgroundtasks();
        }
      }

      if (fileDeleted) {
        writeError = false;
        updateRTC_filenameCounters();
        return true;
      }
    }
  }
  return false;
}

bool RTC_cache_handler_struct::deleteCacheBlock(int fileNr)
{
  bool fileDeleted = false;
  if (updateRTC_filenameCounters()) {
    while ((RTC_cache.readFileNr < RTC_cache.writeFileNr) && (RTC_cache.readFileNr <= fileNr)) {
      // read and write file nr are not the same file, remove the read file nr.
      String fname = createCacheFilename(RTC_cache.readFileNr);

      writeError = false;

      // Make sure the read and peek file handles cannot be used on possibly deleted files.
      if (fr) {
        fr.close();
      }

      if (fp) {
        fp.close();
      }

      if (tryDeleteFile(fname)) {
        fileDeleted = true;
        #ifdef RTC_STRUCT_DEBUG
        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          addLogMove(LOG_LEVEL_INFO, concat(F("RTC  : Removed file from FS: "), fname));
        }
        #endif // ifdef RTC_STRUCT_DEBUG

        updateRTC_filenameCounters();
        backgroundtasks();
      }
    }
  }
  return fileDeleted;
}

bool RTC_cache_handler_struct::loadMetaData()
{
  // No need to load on ESP32, as the data is already allocated to the RTC memory by the compiler

  #ifdef ESP8266

  if (!system_rtc_mem_read(RTC_BASE_CACHE, reinterpret_cast<uint8_t *>(&RTC_cache), sizeof(RTC_cache))) {
    return false;
  }
  #endif // ifdef ESP8266

  return RTC_cache.checksumMetadata == calc_CRC32(reinterpret_cast<const uint8_t *>(&RTC_cache), sizeof(RTC_cache) - sizeof(uint32_t));
}

bool RTC_cache_handler_struct::loadData()
{
  initRTCcache_data();

  // No need to load on ESP32, as the data is already allocated to the RTC memory by the compiler
  #ifdef ESP8266

  if (!system_rtc_mem_read(RTC_BASE_CACHE + (sizeof(RTC_cache) / 4), reinterpret_cast<uint8_t *>(&RTC_cache_data[0]), RTC_CACHE_DATA_SIZE)) {
    return false;
  }
  #endif // ifdef ESP8266

  if (RTC_cache.checksumData != getDataChecksum()) {
        #ifdef RTC_STRUCT_DEBUG
    addLog(LOG_LEVEL_ERROR, F("RTC  : Checksum error reading RTC cache data"));
        #endif // ifdef RTC_STRUCT_DEBUG
    return false;
  }
  return RTC_cache.checksumData == getDataChecksum();
}

bool RTC_cache_handler_struct::saveRTCcache() {
  return saveRTCcache(0, RTC_CACHE_DATA_SIZE);
}

bool RTC_cache_handler_struct::saveRTCcache(unsigned int startOffset, size_t nrBytes)
{
  RTC_cache.checksumData     = getDataChecksum();
  RTC_cache.checksumMetadata = calc_CRC32(reinterpret_cast<const uint8_t *>(&RTC_cache), sizeof(RTC_cache) - sizeof(uint32_t));
  #ifdef ESP32
  return true;
  #endif // ifdef ESP32

  #ifdef ESP8266

  if (!system_rtc_mem_write(RTC_BASE_CACHE, reinterpret_cast<const uint8_t *>(&RTC_cache), sizeof(RTC_cache)) || !loadMetaData())
  {
        # ifdef RTC_STRUCT_DEBUG
    addLog(LOG_LEVEL_ERROR, F("RTC  : Error while writing cache metadata to RTC"));
        # endif // ifdef RTC_STRUCT_DEBUG
    return false;
  }
  delay(0);

  if (nrBytes > 0) { // Check needed?
    const size_t address = RTC_BASE_CACHE + ((sizeof(RTC_cache) + startOffset) / 4);

    if (!system_rtc_mem_write(address, reinterpret_cast<const uint8_t *>(&RTC_cache_data[startOffset]), nrBytes))
    {
          # ifdef RTC_STRUCT_DEBUG
      addLog(LOG_LEVEL_ERROR, F("RTC  : Error while writing cache data to RTC"));
          # endif // ifdef RTC_STRUCT_DEBUG
      return false;
    }
        # ifdef RTC_STRUCT_DEBUG
    rtc_debug_log(F("Write cache data to RTC"), nrBytes);
        # endif // ifdef RTC_STRUCT_DEBUG
  }
  return true;
  #endif        // ifdef ESP8266
}

uint32_t RTC_cache_handler_struct::getDataChecksum() {
  initRTCcache_data();

  /*
     size_t dataLength = RTC_cache.writePos;

     if (dataLength > RTC_CACHE_DATA_SIZE) {
     // Is this allowed to happen?
     dataLength = RTC_CACHE_DATA_SIZE;
     }
   */

  // Only compute the checksum over the number of samples stored.
  return calc_CRC32(reinterpret_cast<const uint8_t *>(&RTC_cache_data[0]), /*dataLength*/ RTC_CACHE_DATA_SIZE);
}

void RTC_cache_handler_struct::initRTCcache_data() {
  #ifdef ESP8266

  if (RTC_cache_data.size() != RTC_CACHE_DATA_SIZE) {
    RTC_cache_data.resize(RTC_CACHE_DATA_SIZE);
  }
  #endif // ifdef ESP8266

  if (RTC_cache.writeFileNr == 0) {
    // RTC value not reliable
    updateRTC_filenameCounters();
  }
}

void RTC_cache_handler_struct::clearRTCcacheData() {
  for (size_t i = 0; i < RTC_CACHE_DATA_SIZE; ++i) {
    RTC_cache_data[i] = 0;
  }
  RTC_cache.writePos = 0;
}

// Return true if any cache file found
bool RTC_cache_handler_struct::updateRTC_filenameCounters() {
  size_t filesizeHighest;

  if (getCacheFileCounters(RTC_cache.readFileNr, RTC_cache.writeFileNr, filesizeHighest)) {
    if (filesizeHighest >= CACHE_FILE_MAX_SIZE) {
      // Start new file
      ++RTC_cache.writeFileNr;
    }
    return true;
  } else {
    // Do not use 0, since that will be the cleared content of the struct, indicating invalid RTC data.
    RTC_cache.writeFileNr = 1;
  }
  return false;
}

bool RTC_cache_handler_struct::prepareFileForWrite() {
  //    if (storageLocation != CACHE_STORAGE_SPIFFS) {
  //      return false;
  //    }
  if (SpiffsFull()) {
    writeError = true;
      #ifdef RTC_STRUCT_DEBUG
    addLog(LOG_LEVEL_ERROR, F("RTC  : FS full"));
      #endif // ifdef RTC_STRUCT_DEBUG
  }

  // Make sure the read and peek file handles cannot be used on possibly deleted files.
  if (fr) {
    fr.close();
  }

  if (fp) {
    fp.close();
  }

  unsigned int retries = 3;

  while (retries > 0) {
    --retries;

    if (fw && (fw.size() >= CACHE_FILE_MAX_SIZE)) {
      fw.close();
      GarbageCollection();
    }

    if (!fw) {
      // Open file to write
      initRTCcache_data();

      if (updateRTC_filenameCounters()) {
        if (writeError || (SpiffsFreeSpace() < ((2 * CACHE_FILE_MAX_SIZE) + SpiffsBlocksize()))) {
          // Not enough room for another file, remove the oldest one.
          deleteOldestCacheBlock();
        }
      }

      String fname = createCacheFilename(RTC_cache.writeFileNr);
      fw = tryOpenFile(fname, "a+");

      if (!fw) {
          #ifdef RTC_STRUCT_DEBUG
        addLog(LOG_LEVEL_ERROR, F("RTC  : error opening file"));
          #endif // ifdef RTC_STRUCT_DEBUG
      } else {
          #ifdef RTC_STRUCT_DEBUG

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          String log = F("Write to ");
          log += fname;
          log += F(" size");
          rtc_debug_log(log, fw.size());
        }
          #endif // ifdef RTC_STRUCT_DEBUG
      }
    }
    delay(0);

    if (fw && (fw.size() < CACHE_FILE_MAX_SIZE)) {
      return true;
    }
  }
    #ifdef RTC_STRUCT_DEBUG
  addLog(LOG_LEVEL_ERROR, F("RTC  : prepareFileForWrite failed"));
    #endif // ifdef RTC_STRUCT_DEBUG
  return false;
}

void RTC_cache_handler_struct::validateFilePos(int& fileNr, int& readPos) {
  {
    // Check to see if we try to set it to a no longer existing file
    if (fileNr < RTC_cache.readFileNr) {
      fileNr  = RTC_cache.readFileNr;
      readPos = 0;
    }
  }

  if (fileNr > RTC_cache.writeFileNr) {
    // We're trying to set it to a not yet existing file
    fileNr = RTC_cache.writeFileNr;
  }
}

#ifdef RTC_STRUCT_DEBUG
void RTC_cache_handler_struct::rtc_debug_log(const String& description, size_t nrBytes) {
  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    String log;

    if (log.reserve(18 + description.length())) {
      log  = F("RTC  : ");
      log += description;
      log += ' ';
      log += nrBytes;
      log += F(" bytes");
      addLogMove(LOG_LEVEL_INFO, log);
    }
  }
}

#endif // ifdef RTC_STRUCT_DEBUG


#endif
#include "../DataStructs/FactoryDefault_CDN_customurl_NVS.h"

#ifdef ESP32

# if FEATURE_ALTERNATIVE_CDN_URL
#  include "../Helpers/ESPEasy_Storage.h"

#  define FACTORY_DEFAULT_NVS_CDN_CUSTOMURL_KEY "CDN_customurl"

bool FactoryDefault_CDN_customurl_NVS::applyToSettings_from_NVS(ESPEasy_NVS_Helper& preferences)
{
  String _url;

  if (preferences.getPreference(F(FACTORY_DEFAULT_NVS_CDN_CUSTOMURL_KEY), _url)) {
    set_CDN_url_custom(_url);
    return true;
  }
  return false;
}

void FactoryDefault_CDN_customurl_NVS::fromSettings_to_NVS(ESPEasy_NVS_Helper& preferences)
{
  preferences.setPreference(F(FACTORY_DEFAULT_NVS_CDN_CUSTOMURL_KEY), get_CDN_url_custom());
}

void FactoryDefault_CDN_customurl_NVS::clear_from_NVS(ESPEasy_NVS_Helper& preferences)
{
  preferences.remove(F(FACTORY_DEFAULT_NVS_CDN_CUSTOMURL_KEY));
}

# endif // if FEATURE_ALTERNATIVE_CDN_URL
#endif  // ifdef ESP32

#include "../DataStructs/FactoryDefault_LogConsoleSettings_NVS.h"


#ifdef ESP32

# include "../Globals/Settings.h"
# include "../Helpers/StringConverter.h"

// Max. 15 char keys for ESPEasy Factory Default marked keys
# define FACTORY_DEFAULT_NVS_LOG_SETTINGS_KEY  "Log"
# define FACTORY_DEFAULT_NVS_CONSOLE_SETTINGS_KEY  "Console"


bool FactoryDefault_LogConsoleSettings_NVS::applyToSettings_from_NVS(ESPEasy_NVS_Helper& preferences)
{
  bool updated = false;

  if (preferences.getPreference(F(FACTORY_DEFAULT_NVS_LOG_SETTINGS_KEY), LogSettings)) {
    updated                 = true;
    Settings.SyslogLevel    = LogSettings_bits.SyslogLevel;
    Settings.SerialLogLevel = LogSettings_bits.SerialLogLevel;
    Settings.WebLogLevel    = LogSettings_bits.WebLogLevel;
    Settings.SDLogLevel     = LogSettings_bits.SDLogLevel;
    Settings.SyslogFacility = LogSettings_bits.SyslogFacility;
    Settings.SyslogPort     = LogSettings_bits.SyslogPort;

    for (size_t i = 0; i < 4; ++i) {
      Settings.Syslog_IP[i] = LogSettings_bits.Syslog_IP[i];
    }
  }

  if (preferences.getPreference(F(FACTORY_DEFAULT_NVS_CONSOLE_SETTINGS_KEY), ConsoleSettings)) {
    updated                           = true;
    Settings.console_serial_port      = ConsoleSettings_bits.console_serial_port;
    Settings.UseSerial                = ConsoleSettings_bits.UseSerial;
    Settings.console_serial_rxpin     = ConsoleSettings_bits.console_serial_rxpin;
    Settings.console_serial_txpin     = ConsoleSettings_bits.console_serial_txpin;
    Settings.console_serial0_fallback = ConsoleSettings_bits.console_serial0_fallback;
    Settings.BaudRate                 = ConsoleSettings_bits.BaudRate;
  }
  return updated;
}

void FactoryDefault_LogConsoleSettings_NVS::fromSettings_to_NVS(ESPEasy_NVS_Helper& preferences)
{
  {
    LogSettings_bits.SyslogLevel    = Settings.SyslogLevel;
    LogSettings_bits.SerialLogLevel = Settings.SerialLogLevel;
    LogSettings_bits.WebLogLevel    = Settings.WebLogLevel;
    LogSettings_bits.SDLogLevel     = Settings.SDLogLevel;
    LogSettings_bits.SyslogFacility = Settings.SyslogFacility;
    LogSettings_bits.SyslogPort     = Settings.SyslogPort;

    for (size_t i = 0; i < 4; ++i) {
      LogSettings_bits.Syslog_IP[i] = Settings.Syslog_IP[i];
    }
    preferences.setPreference(F(FACTORY_DEFAULT_NVS_LOG_SETTINGS_KEY), LogSettings);
  }
  {
    ConsoleSettings_bits.console_serial_port      = Settings.console_serial_port;
    ConsoleSettings_bits.UseSerial                = Settings.UseSerial;
    ConsoleSettings_bits.console_serial_rxpin     = Settings.console_serial_rxpin;
    ConsoleSettings_bits.console_serial_txpin     = Settings.console_serial_txpin;
    ConsoleSettings_bits.console_serial0_fallback = Settings.console_serial0_fallback;
    ConsoleSettings_bits.BaudRate                 = Settings.BaudRate;

    preferences.setPreference(F(FACTORY_DEFAULT_NVS_CONSOLE_SETTINGS_KEY), ConsoleSettings);
  }
}

void FactoryDefault_LogConsoleSettings_NVS::clear_from_NVS(ESPEasy_NVS_Helper& preferences)
{
  preferences.remove(F(FACTORY_DEFAULT_NVS_LOG_SETTINGS_KEY));
  preferences.remove(F(FACTORY_DEFAULT_NVS_CONSOLE_SETTINGS_KEY));
}

#endif // ifdef ESP32

#include "../DataStructs/NotificationSettingsStruct.h"

#if FEATURE_NOTIFIER

NotificationSettingsStruct::NotificationSettingsStruct() {
  memset(this, 0, sizeof(NotificationSettingsStruct));
  Pin1 = -1;
  Pin2 = -1;
}

  void NotificationSettingsStruct::validate() {
    ZERO_TERMINATE(Server);
    ZERO_TERMINATE(Domain);
    ZERO_TERMINATE(Sender);
    ZERO_TERMINATE(Receiver);
    ZERO_TERMINATE(Subject);
    ZERO_TERMINATE(Body);
    ZERO_TERMINATE(User);
    ZERO_TERMINATE(Pass);
  }

#endif
#include "../DataStructs/DeviceStruct.h"

//constexpr size_t size = sizeof(DeviceStruct);

DeviceStruct::DeviceStruct() :
  _all(0u),
// PullUpOption       (false),
// InverseLogicOption (false),
// FormulaOption      (false),
// Custom             (false),
// SendDataOption     (false),
// GlobalSyncOption   (false),
// TimerOption        (false),
// TimerOptional      (false),
// DecimalsOnly       (false),
// DuplicateDetection (false),
// ExitTaskBeforeSave (true),
// ErrorStateValues   (false),
// PluginStats        (false),
// PluginLogsPeaks    (false),
// PowerManager       (false),
// TaskLogsOwnPeaks   (false),
// I2CNoDeviceCheck   (false),
// I2CMax100kHz       (false),
// HasFormatUserVar   (false),
// I2CNoBusSelection  (false),
// CustomVTypeVar     (false),

  Number(0), Type(0), VType(Sensor_VType::SENSOR_TYPE_NONE), Ports(0), ValueCount(0),
  OutputDataType(Output_Data_type_t::Default),
  Pin1Direction(static_cast<uint8_t>(gpio_direction::gpio_direction_MAX)),
  Pin2Direction(static_cast<uint8_t>(gpio_direction::gpio_direction_MAX)), 
  Pin3Direction(static_cast<uint8_t>(gpio_direction::gpio_direction_MAX))
{
  // Only initialize union members in the constructor body
  // TODO: Rename this so it can be default initialized to 'false'
  ExitTaskBeforeSave = true;
}

void DeviceStruct::clear() {
  _all = 0;

  ExitTaskBeforeSave = true;

  Number= 0;
  Type = 0;
  VType = Sensor_VType::SENSOR_TYPE_NONE;
  Ports = 0;
  ValueCount = 0;
  OutputDataType = Output_Data_type_t::Default;
  Pin1Direction = static_cast<uint8_t>(gpio_direction::gpio_direction_MAX);
  Pin2Direction = static_cast<uint8_t>(gpio_direction::gpio_direction_MAX);
  Pin3Direction = static_cast<uint8_t>(gpio_direction::gpio_direction_MAX);

}

bool DeviceStruct::connectedToGPIOpins() const {
  switch(Type) {
    case DEVICE_TYPE_SINGLE:  // Single GPIO
    case DEVICE_TYPE_SPI:
    case DEVICE_TYPE_CUSTOM1:

    case DEVICE_TYPE_DUAL:    // Dual GPIOs
    case DEVICE_TYPE_SERIAL:
    case DEVICE_TYPE_SPI2:
    case DEVICE_TYPE_CUSTOM2:

    case DEVICE_TYPE_TRIPLE:  // Triple GPIOs
    case DEVICE_TYPE_SERIAL_PLUS1:
    case DEVICE_TYPE_SPI3:
    case DEVICE_TYPE_CUSTOM3:    
      return true;
    default:
      return false;
  }
}

bool DeviceStruct::usesTaskDevicePin(int pin) const {
  if (pin == 1)
      return connectedToGPIOpins();
  if (pin == 2)
      return connectedToGPIOpins() && 
            !(Type == DEVICE_TYPE_SINGLE  ||
              Type == DEVICE_TYPE_SPI ||
              Type == DEVICE_TYPE_CUSTOM1);
  if (pin == 3)
      return Type == DEVICE_TYPE_TRIPLE || 
             Type == DEVICE_TYPE_SERIAL_PLUS1 || 
             Type == DEVICE_TYPE_SPI3 ||
             Type == DEVICE_TYPE_CUSTOM3;
  return false;
}

bool DeviceStruct::isSerial() const {
  return (Type == DEVICE_TYPE_SERIAL) || 
         (Type == DEVICE_TYPE_SERIAL_PLUS1);
}

bool DeviceStruct::isSPI() const {
  return (Type == DEVICE_TYPE_SPI) || 
         (Type == DEVICE_TYPE_SPI2) || 
         (Type == DEVICE_TYPE_SPI3);
}

bool DeviceStruct::isCustom() const {
  return (Type == DEVICE_TYPE_CUSTOM0) || 
         (Type == DEVICE_TYPE_CUSTOM1) || 
         (Type == DEVICE_TYPE_CUSTOM2) || 
         (Type == DEVICE_TYPE_CUSTOM3);
}

void DeviceStruct::setPinDirection(int pin, gpio_direction direction)
{
  const uint8_t val = static_cast<uint8_t>(direction) & ((1 << GPIO_DIRECTION_NR_BITS) - 1);
  switch (pin) {
    case 1: Pin1Direction = val; break;
    case 2: Pin2Direction = val; break;
    case 3: Pin3Direction = val; break;
  }
}

gpio_direction DeviceStruct::getPinDirection(int pin) const {
  switch (pin) {
    case 1:
      return static_cast<gpio_direction>(Pin1Direction);
    case 2:
      return static_cast<gpio_direction>(Pin2Direction);
    case 3:
      return static_cast<gpio_direction>(Pin3Direction);
  }
  return gpio_direction::gpio_direction_MAX;
}

PinSelectPurpose DeviceStruct::pinDirectionToPurpose(gpio_direction direction) const {
  switch (direction) {
  case gpio_direction::gpio_input:
    return PinSelectPurpose::Generic_input;
  case gpio_direction::gpio_output:
    return PinSelectPurpose::Generic_output;
  case gpio_direction::gpio_bidirectional:
    return PinSelectPurpose::Generic_bidir;
  case gpio_direction::gpio_direction_MAX:
    break;
  }
  return PinSelectPurpose::Generic;
}


PinSelectPurpose DeviceStruct::getPinSelectPurpose(int pin) const {
  return pinDirectionToPurpose(getPinDirection(pin));
}

#include "../DataStructs/PluginStats.h"

#if FEATURE_PLUGIN_STATS
# include "../../_Plugin_Helper.h"

# include "../Globals/TimeZone.h"

# include "../Helpers/ESPEasy_math.h"
# include "../Helpers/Memory.h"

# include "../WebServer/Chart_JS.h"


PluginStats::PluginStats(uint8_t nrDecimals, float errorValue) :
  _errorValue(errorValue),
  _nrDecimals(nrDecimals),
  _plugin_stats_timestamps(nullptr)

{
  // Try to allocate in PSRAM if possible
  void *ptr = special_calloc(1, sizeof(PluginStatsBuffer_t));

  if (ptr == nullptr) { _samples = nullptr; }
  else {
    _samples = new (ptr) PluginStatsBuffer_t();
  }
  _errorValueIsNaN   = isnan(_errorValue);
  _minValue          = std::numeric_limits<float>::max();
  _maxValue          = std::numeric_limits<float>::lowest();
  _minValueTimestamp = 0;
  _maxValueTimestamp = 0;
}

PluginStats::~PluginStats()
{
  if (_samples != nullptr) {
    free(_samples);

    //    delete _samples;
  }
  _samples                 = nullptr;
  _plugin_stats_timestamps = nullptr;
}

void PluginStats::processTimeSet(const double& time_offset)
{
  // Check to see if there was a unix time set before the system time was set
  // For example when receiving data from a p2p node
  const int64_t cur_micros    = getMicros64();
  const int64_t offset_micros = time_offset * 1000000ull;

  if ((_maxValueTimestamp > cur_micros) && (_maxValueTimestamp > offset_micros)) {
    _maxValueTimestamp -= offset_micros;
  }

  if ((_minValueTimestamp > cur_micros) && (_minValueTimestamp > offset_micros)) {
    _minValueTimestamp -= offset_micros;
  }
}

bool PluginStats::push(float value)
{
  if (_samples == nullptr) { return false; }
  return _samples->push(value);
}

bool PluginStats::matchesLastTwoEntries(float value) const
{
  const size_t nrSamples = getNrSamples();

  if (nrSamples < 2) { return false; }

  const float last       = (*_samples)[nrSamples - 1];
  const float beforeLast = (*_samples)[nrSamples - 2];

  const String value_str = toString(value, _nrDecimals);

  return
    toString(last,       _nrDecimals).equals(value_str) &&
    toString(beforeLast, _nrDecimals).equals(value_str);


  /*
     const bool  value_valid = isValidFloat(value);
     const bool  last_valid  = isValidFloat(last);

     if (value_valid != last_valid) {
      return false;
     }
     const bool beforeLast_valid = isValidFloat(beforeLast);

     if (value_valid != beforeLast_valid) {
      return false;
     }

     if (value_valid) {
      return
        approximatelyEqual(value, last) &&
        approximatelyEqual(value, beforeLast);
     }
     return true;
   */
}

void PluginStats::trackPeak(float value, int64_t timestamp)
{
  if ((value > _maxValue) || (value < _minValue)) {
    if (timestamp == 0) {
      // Make sure both extremes are flagged with the same timestamp.
      timestamp = getMicros64();
    }

    if (value > _maxValue) {
      _maxValueTimestamp = timestamp;
      _maxValue          = value;
    }

    if (value < _minValue) {
      _minValueTimestamp = timestamp;
      _minValue          = value;
    }
  }
}

void PluginStats::resetPeaks()
{
  _minValue          = std::numeric_limits<float>::max();
  _maxValue          = std::numeric_limits<float>::lowest();
  _minValueTimestamp = 0;
  _maxValueTimestamp = 0;
}

void PluginStats::clearSamples() {
  if (_samples != nullptr) {
    _samples->clear();
  }
}

size_t PluginStats::getNrSamples() const {
  if (_samples == nullptr) { return 0u; }
  return _samples->size();
}

float PluginStats::getSampleAvg() const {
  return getSampleAvg(getNrSamples());
}

float PluginStats::getSampleAvg(PluginStatsBuffer_t::index_t lastNrSamples) const
{
  const size_t nrSamples = getNrSamples();

  if (nrSamples == 0) { return _errorValue; }
  float sum = 0.0f;

  PluginStatsBuffer_t::index_t i = 0;

  if (lastNrSamples < nrSamples) {
    i = nrSamples - lastNrSamples;
  }
  PluginStatsBuffer_t::index_t samplesUsed = 0;

  for (; i < nrSamples; ++i) {
    const float sample((*_samples)[i]);

    if (usableValue(sample)) {
      ++samplesUsed;
      sum += sample;
    }
  }

  if (samplesUsed == 0) { return _errorValue; }
  return sum / samplesUsed;
}

float PluginStats::getSampleAvg_time(PluginStatsBuffer_t::index_t lastNrSamples, uint64_t& totalDuration_usec) const
{
  const size_t nrSamples = getNrSamples();

  totalDuration_usec = 0u;

  if ((nrSamples == 0) || (_plugin_stats_timestamps == nullptr)) {
    return _errorValue;
  }

  PluginStatsBuffer_t::index_t i = 0;

  if (lastNrSamples < nrSamples) {
    i = nrSamples - lastNrSamples;
  }

  int64_t lastTimestamp   = 0;
  float   lastValue       = 0.0f;
  bool    lastValueUsable = false;
  float   sum             = 0.0f;

  for (; i < nrSamples; ++i) {
    const float   sample((*_samples)[i]);
    const int64_t curTimestamp   = (*_plugin_stats_timestamps)[i];
    const bool    curValueUsable = usableValue(sample);

    if ((lastTimestamp != 0) && lastValueUsable) {
      const int64_t duration_usec = abs(timeDiff64(lastTimestamp, curTimestamp));

      if (curValueUsable) {
        // Old and new value usable, take average of this period.
        sum += ((lastValue + sample) / 2.0f) * duration_usec;
      } else {
        // New value is not usable, so just add the last value for the duration.
        sum += lastValue * duration_usec;
      }
      totalDuration_usec += duration_usec;
    }

    lastValueUsable = curValueUsable;
    lastTimestamp   = curTimestamp;
    lastValue       = sample;
  }

  if (totalDuration_usec == 0) { return _errorValue; }
  return sum / totalDuration_usec;
}

float PluginStats::getSampleStdDev(PluginStatsBuffer_t::index_t lastNrSamples) const
{
  const size_t nrSamples = getNrSamples();
  float variance         = 0.0f;
  const float average    = getSampleAvg(lastNrSamples);

  if (!usableValue(average)) { return 0.0f; }

  PluginStatsBuffer_t::index_t i = 0;

  if (lastNrSamples < nrSamples) {
    i = nrSamples - lastNrSamples;
  }
  PluginStatsBuffer_t::index_t samplesUsed = 0;

  for (; i < nrSamples; ++i) {
    const float sample((*_samples)[i]);

    if (usableValue(sample)) {
      ++samplesUsed;
      const float diff = sample - average;
      variance += diff * diff;
    }
  }

  if (samplesUsed < 2) { return 0.0f; }

  variance /= samplesUsed;
  return sqrtf(variance);
}

float PluginStats::getSampleExtreme(PluginStatsBuffer_t::index_t lastNrSamples, bool getMax) const
{
  const size_t nrSamples = getNrSamples();

  if (nrSamples == 0) { return _errorValue; }

  PluginStatsBuffer_t::index_t i = 0;

  if (lastNrSamples < nrSamples) {
    i = nrSamples - lastNrSamples;
  }

  bool changed = false;

  float res = getMax ? INT_MIN : INT_MAX;

  for (; i < nrSamples; ++i) {
    const float sample((*_samples)[i]);

    if (usableValue(sample)) {
      if ((getMax && (sample > res)) ||
          (!getMax && (sample < res))) {
        changed = true;
        res     = sample;
      }
    }
  }

  if (!changed) { return _errorValue; }

  return res;
}

float PluginStats::getSample(int lastNrSamples) const
{
  const size_t nrSamples = getNrSamples();

  if ((nrSamples == 0) || (nrSamples < abs(lastNrSamples))) { return _errorValue; }

  PluginStatsBuffer_t::index_t i = 0;

  if (lastNrSamples > 0) {
    i = nrSamples - lastNrSamples;
  } else if (lastNrSamples < 0) {
    i = abs(lastNrSamples) - 1;
  }

  if (i < nrSamples) {
    return (*_samples)[i];
  }
  return _errorValue;
}

float PluginStats::operator[](PluginStatsBuffer_t::index_t index) const
{
  const size_t nrSamples = getNrSamples();

  if (index < nrSamples) { return (*_samples)[index]; }
  return _errorValue;
}

bool PluginStats::matchedCommand(const String& command, const __FlashStringHelper *cmd_match, int& nrSamples)
{
  const String cmd_match_str(cmd_match);

  if (command.equals(cmd_match_str)) {
    nrSamples = INT_MIN;
    return true;
  }

  if (command.startsWith(cmd_match_str)) {
    nrSamples = 0;

    // FIXME TD-er: ESP_IDF 5.x needs strict matching thus int32_t != int
    int32_t tmp{};

    if (validIntFromString(command.substring(cmd_match_str.length()), tmp)) {
      nrSamples = tmp;
      return true;
    }
  }
  return false;
}

bool PluginStats::plugin_get_config_value_base(struct EventStruct *event, String& string) const
{
  // Full value name is something like "taskvaluename.avg"
  const String fullValueName = parseString(string, 1);
  const String command       = parseString(fullValueName, 2, '.');

  if (command.isEmpty()) {
    return false;
  }

  float value{};
  int   nrSamples = 0;
  bool  success   = false;

  switch (command[0])
  {
    case 'a':

      if (matchedCommand(command, F("avg"), nrSamples)) {
        success = nrSamples != 0;

        if (nrSamples < 0) { // [taskname#valuename.avg] Average value of the last N kept samples
          value = getSampleAvg();
        } else {
          // Check for "avgN", where N is the number of most recent samples to use.
          if (nrSamples > 0) {
            // [taskname#valuename.avgN] Average over N most recent samples
            value = getSampleAvg(nrSamples);
          }
        }
      }
      break;
    case 'm':

      if (equals(command, F("minp"))) { // [taskname#valuename.minp] Lowest value seen since value reset
        value   = getPeakLow();
        success = true;
      } else if (matchedCommand(command, F("min"), nrSamples)) {
        success = nrSamples != 0;

        if (nrSamples < 0) { // [taskname#valuename.min] Lowest value of all recent samples
          value = getSampleExtreme(getNrSamples(), false);
        } else {             // Check for "minN", where N is the number of most recent samples to use.
          if (nrSamples > 0) {
            value = getSampleExtreme(nrSamples, false);
          }
        }
      } else if (equals(command, F("maxp"))) { // [taskname#valuename.maxp] Highest value seen since value reset
        value   = getPeakHigh();
        success = true;
      } else if (matchedCommand(command, F("max"), nrSamples)) {
        success = nrSamples != 0;

        if (nrSamples < 0) { // [taskname#valuename.max] Highest value of all recent samples
          value = getSampleExtreme(getNrSamples(), true);
        } else {             // Check for "maxN", where N is the number of most recent samples to use.
          if (nrSamples > 0) {
            value = getSampleExtreme(nrSamples, true);
          }
        }
      }
      break;
    case 's':

      if (matchedCommand(command, F("stddev"), nrSamples)) {
        success = nrSamples != 0;

        if (nrSamples < 0) { // [taskname#valuename.stddev] Std deviation of the last N kept samples
          value = getSampleStdDev();
        } else {
          // Check for "stddevN", where N is the number of most recent samples to use.
          if (nrSamples > 0) {
            // [taskname#valuename.stddevN] Std. deviation over N most recent samples
            value = getSampleStdDev(nrSamples);
          }
        }
      } else if (matchedCommand(command, F("size"), nrSamples)) {
        // [taskname#valuename.size] Number of samples in memory
        value   = getNrSamples();
        success = true;
      } else if (matchedCommand(command, F("sample"), nrSamples)) {
        success = nrSamples != 0;

        if (nrSamples == INT_MIN) {
          // [taskname#valuename.sample] Number of samples in memory.
          value   = getNrSamples();
          success = true;
        } else {
          if (nrSamples != 0) {
            // [taskname#valuename.sampleN]
            // With sample N:
            //   N > 0: Return N'th most recent sample
            //   N < 0: Return abs(N)'th sample in memory, starting at the oldest one.
            //   abs(N) > [number of samples]: return error value
            value = getSample(nrSamples);
          }
        }
      }
      break;
    default:
      return false;
  }


  if (success) {
    string = toString(value, _nrDecimals);
  }
  return success;
}

bool PluginStats::webformLoad_show_stats(struct EventStruct *event) const
{
  bool somethingAdded = false;

  if (webformLoad_show_avg(event)) { somethingAdded = true; }

  if (webformLoad_show_stdev(event)) { somethingAdded = true; }

  if (webformLoad_show_peaks(event)) { somethingAdded = true; }

  if (somethingAdded) {
    addFormSeparator(4);
  }

  return somethingAdded;
}

bool PluginStats::webformLoad_show_avg(struct EventStruct *event) const
{
  if (getNrSamples() > 0) {
    addRowLabel(concat(getLabel(),  F(" Average / sample")));
    addHtmlFloat(getSampleAvg(), (_nrDecimals == 0) ? 1 : _nrDecimals);
    addHtml(strformat(F(" (%u samples)"), getNrSamples()));

    if (_plugin_stats_timestamps != nullptr) {
      uint64_t totalDuration_usec = 0u;
      const float avg_per_sec     = getSampleAvg_time(totalDuration_usec);

      if (totalDuration_usec > 0) {
        addRowLabel(concat(getLabel(),  F(" Average / sec")));
        addHtmlFloat(avg_per_sec, (_nrDecimals == 0) ? 1 : _nrDecimals);
        addHtml(strformat(F(" (%s duration)"), secondsToDayHourMinuteSecond_ms(totalDuration_usec).c_str()));
      }
    }
    return true;
  }
  return false;
}

bool PluginStats::webformLoad_show_stdev(struct EventStruct *event) const
{
  const float stdDev = getSampleStdDev();

  if (usableValue(stdDev) && (getNrSamples() > 1)) {
    addRowLabel(concat(getLabel(),  F(" std. dev")));
    addHtmlFloat(stdDev, (_nrDecimals == 0) ? 1 : _nrDecimals);
    addHtml(strformat(F(" (%u samples)"), getNrSamples()));
    return true;
  }
  return false;
}

bool PluginStats::webformLoad_show_peaks(struct EventStruct *event, bool include_peak_to_peak) const
{
  if (hasPeaks() && (getNrSamples() > 1)) {
    return webformLoad_show_peaks(
      event,
      getLabel(),
      toString(getPeakLow(),  _nrDecimals),
      toString(getPeakHigh(), _nrDecimals),
      include_peak_to_peak);
  }
  return false;
}

bool PluginStats::webformLoad_show_peaks(struct EventStruct *event,
                                         const String      & label,
                                         const String      & lowValue,
                                         const String      & highValue,
                                         bool                include_peak_to_peak) const
{
  if (hasPeaks() && (getNrSamples() > 1)) {
    uint32_t peakLow_frac{};
    uint32_t peakHigh_frac{};
    const uint32_t peakLow     = node_time.systemMicros_to_Unixtime(getPeakLowTimestamp(), peakLow_frac);
    const uint32_t peakHigh    = node_time.systemMicros_to_Unixtime(getPeakHighTimestamp(), peakHigh_frac);
    const uint32_t current     = node_time.getUnixTime();
    const bool     useTimeOnly = (current - peakLow) < 86400 && (current - peakHigh) < 86400;
    struct tm ts;
    breakTime(time_zone.toLocal(peakLow), ts);


    addRowLabel(concat(label,  F(" Peak Low")));
    addHtml(strformat(
              F("%s @ %s.%03u"),
              lowValue.c_str(),
              useTimeOnly
      ? formatTimeString(ts).c_str()
      : formatDateTimeString(ts).c_str(),
              unix_time_frac_to_millis(peakLow_frac)));


    breakTime(time_zone.toLocal(peakHigh), ts);

    addRowLabel(concat(label,  F(" Peak High")));
    addHtml(strformat(
              F("%s @ %s.%03u"),
              highValue.c_str(),
              useTimeOnly
      ? formatTimeString(ts).c_str()
      : formatDateTimeString(ts).c_str(),
              unix_time_frac_to_millis(peakHigh_frac)));

    if (include_peak_to_peak) {
      addRowLabel(concat(getLabel(),  F(" Peak-to-peak")));
      addHtmlFloat(getPeakHigh() - getPeakLow(), _nrDecimals);
    }
    return true;
  }
  return false;
}

void PluginStats::webformLoad_show_val(
  struct EventStruct      *event,
  const String           & label,
  ESPEASY_RULES_FLOAT_TYPE value,
  const String           & unit) const
{
  addRowLabel(concat(getLabel(), label));
  addHtmlFloat(value, _nrDecimals);

  if (!unit.isEmpty()) {
    addUnit(unit);
  }
}

# if FEATURE_CHART_JS
void PluginStats::plot_ChartJS_dataset() const
{
  add_ChartJS_dataset_header(_ChartJS_dataset_config);

  PluginStatsBuffer_t::index_t i = 0;
  const size_t nrSamples         = getNrSamples();

  for (; i < nrSamples; ++i) {
    if (i != 0) {
      addHtml(',');
    }

    addHtmlFloat_NaN_toNull((*_samples)[i], _nrDecimals);
  }
  add_ChartJS_dataset_footer();
}

# endif // if FEATURE_CHART_JS

bool PluginStats::usableValue(float value) const
{
  if (!isnan(value)) {
    if (_errorValueIsNaN || !essentiallyEqual(_errorValue, value)) {
      return true;
    }
  }
  return false;
}

#endif // if FEATURE_PLUGIN_STATS

#include "../DataStructs/EthernetEventData.h"

#if FEATURE_ETHERNET

#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../Globals/Settings.h"
#include "../Helpers/Networking.h"

#include <ETH.h>

// Bit numbers for Eth status
#define ESPEASY_ETH_CONNECTED               0
#define ESPEASY_ETH_GOT_IP                  1
#define ESPEASY_ETH_SERVICES_INITIALIZED    2


#if FEATURE_USE_IPV6
#include <esp_netif.h>

// -----------------------------------------------------------------------------------------------------------------------
// ---------------------------------------------------- Private functions ------------------------------------------------
// -----------------------------------------------------------------------------------------------------------------------

esp_netif_t* get_esp_interface_netif(esp_interface_t interface);
#endif



bool EthernetEventData_t::EthConnectAllowed() const {
  if (!ethConnectAttemptNeeded) return false;
  if (last_eth_connect_attempt_moment.isSet()) {
    // TODO TD-er: Make this time more dynamic.
    if (!last_eth_connect_attempt_moment.timeoutReached(10000)) {
      return false;
    }
  }
  return true;
}

bool EthernetEventData_t::unprocessedEthEvents() const {
  if (processedConnect && processedDisconnect && processedGotIP && processedDHCPTimeout
#if FEATURE_USE_IPV6
      && processedGotIP6
#endif
  )
  {
    return false;
  }
  return true;
}

void EthernetEventData_t::clearAll() {
  lastDisconnectMoment.clear();
  lastConnectMoment.clear();
  lastGetIPmoment.clear();
  last_eth_connect_attempt_moment.clear();

  lastEthResetMoment.setNow();
  eth_considered_stable = false;

  // Mark all flags to default to prevent handling old events.
  processedConnect          = true;
  processedDisconnect       = true;
  processedGotIP            = true;
  #if FEATURE_USE_IPV6
  processedGotIP6           = true;
  #endif
  processedDHCPTimeout      = true;
  ethConnectAttemptNeeded  = true;
  dns0_cache = IPAddress();
  dns1_cache = IPAddress();
}

void EthernetEventData_t::markEthBegin() {
  lastDisconnectMoment.clear();
  lastConnectMoment.clear();
  lastGetIPmoment.clear();
  last_eth_connect_attempt_moment.setNow();
  eth_considered_stable = false;
  ethConnectInProgress  = true;
  ++eth_connect_attempt;
}

bool EthernetEventData_t::EthDisconnected() const {
  return ethStatus == ESPEASY_ETH_DISCONNECTED;
}

bool EthernetEventData_t::EthGotIP() const {

  return bitRead(ethStatus, ESPEASY_ETH_GOT_IP);
}

bool EthernetEventData_t::EthConnected() const {
  return bitRead(ethStatus, ESPEASY_ETH_CONNECTED);
}

bool EthernetEventData_t::EthServicesInitialized() const {
  return bitRead(ethStatus, ESPEASY_ETH_SERVICES_INITIALIZED);
}

void EthernetEventData_t::setEthDisconnected() {
  processedConnect          = true;
  processedDisconnect       = true;
  processedGotIP            = true;
  #if FEATURE_USE_IPV6
  processedGotIP6           = true;
  #endif
  processedDHCPTimeout      = true;

  ethStatus = ESPEASY_ETH_DISCONNECTED;
}

void EthernetEventData_t::setEthGotIP() {
  bitSet(ethStatus, ESPEASY_ETH_GOT_IP);
  setEthServicesInitialized();
}

void EthernetEventData_t::setEthConnected() {
  bitSet(ethStatus, ESPEASY_ETH_CONNECTED);
  setEthServicesInitialized();
}

bool EthernetEventData_t::setEthServicesInitialized() {
  if (!unprocessedEthEvents() && !EthServicesInitialized()) {
    if (EthGotIP() && EthConnected()) {
      if (valid_DNS_address(ETH.dnsIP(0))) {
        dns0_cache = ETH.dnsIP(0);
      }
      if (valid_DNS_address(ETH.dnsIP(1))) {
        dns1_cache = ETH.dnsIP(1);
      }

      #ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_DEBUG, F("Eth : Eth services initialized"));
      #endif
      bitSet(ethStatus, ESPEASY_ETH_SERVICES_INITIALIZED);
      ethConnectInProgress = false;
      return true;
    }
  }
  return false;
}

void EthernetEventData_t::markGotIP() {
  lastGetIPmoment.setNow();

  // Create the 'got IP event' so mark the ethStatus to not have the got IP flag set
  // This also implies the services are not fully initialized.
  bitClear(ethStatus, ESPEASY_ETH_GOT_IP);
  bitClear(ethStatus, ESPEASY_ETH_SERVICES_INITIALIZED);
  processedGotIP = false;
}

#if FEATURE_USE_IPV6
void EthernetEventData_t::markGotIPv6(const IPAddress& ip6) {
  processedGotIP6 = false;
  unprocessed_IP6 = ip6;
}
#endif


void EthernetEventData_t::markLostIP() {
  bitClear(ethStatus, ESPEASY_ETH_GOT_IP);
  bitClear(ethStatus, ESPEASY_ETH_SERVICES_INITIALIZED);
  lastGetIPmoment.clear();
  processedGotIP = false;
}

void EthernetEventData_t::markDisconnect() {
  lastDisconnectMoment.setNow();

  if (last_eth_connect_attempt_moment.isSet() && !lastConnectMoment.isSet()) {
    // There was an unsuccessful connection attempt
    lastConnectedDuration_us = last_eth_connect_attempt_moment.timeDiff(lastDisconnectMoment);
  } else {
    lastConnectedDuration_us = lastConnectMoment.timeDiff(lastDisconnectMoment);
  }
  lastConnectMoment.clear();
  processedDisconnect  = false;
#if ESP_IDF_VERSION_MAJOR >= 5
  WiFi.STA.setDefault();
#endif
}

void EthernetEventData_t::markConnected() {
  lastConnectMoment.setNow();
  processedConnect    = false;
#if ESP_IDF_VERSION_MAJOR >= 5
  ETH.setDefault();
#endif

#if FEATURE_USE_IPV6
  if (Settings.EnableIPv6()) {
    ETH.enableIPv6(true);
  }
#endif
}

String EthernetEventData_t::ESPEasyEthStatusToString() const {
  String log;
  if (EthDisconnected()) {
    log = F("DISCONNECTED");
  } else {
    if (EthConnected()) {
      log += F("Conn. ");
    }
    if (EthGotIP()) {
      log += F("IP ");
    }
    if (EthServicesInitialized()) {
      log += F("Init");
    }
  }
  return log;
}

#endif

#include "../DataStructs/FactoryDefault_UnitName_NVS.h"

#ifdef ESP32

# include "../Globals/Settings.h"
# include "../Helpers/StringConverter.h"

// Max. 15 char keys for ESPEasy Factory Default marked keys
# define FACTORY_DEFAULT_NVS_UNIT_NAME_KEY  "UnitName"


void FactoryDefault_UnitName_NVS::fromSettings() {
  bitWrite(data[1], 0, Settings.appendUnitToHostname());
  data[0] = Settings.Unit;
  memcpy((char *)(data + 2), Settings.Name, sizeof(Settings.Name));
  data[2 + sizeof(Settings.Name)] = Settings.UDPPort >> 8;
  data[3 + sizeof(Settings.Name)] = Settings.UDPPort & 0xFF;
}

void FactoryDefault_UnitName_NVS::applyToSettings() const {
  Settings.appendUnitToHostname(bitRead(data[1], 0));
  Settings.Unit = data[0];
  memcpy(Settings.Name, (char *)(data + 2), sizeof(Settings.Name));

  Settings.UDPPort = data[2 + sizeof(Settings.Name)] << 8 | data[3 + sizeof(Settings.Name)];
}

bool FactoryDefault_UnitName_NVS::applyToSettings_from_NVS(ESPEasy_NVS_Helper& preferences) {
  if (preferences.getPreference(F(FACTORY_DEFAULT_NVS_UNIT_NAME_KEY), data, sizeof(data))) {
    applyToSettings();
    return true;
  }
  return false;
}

void FactoryDefault_UnitName_NVS::fromSettings_to_NVS(ESPEasy_NVS_Helper& preferences) {
  fromSettings();
  preferences.setPreference(F(FACTORY_DEFAULT_NVS_UNIT_NAME_KEY), data, sizeof(data));
}

void FactoryDefault_UnitName_NVS::clear_from_NVS(ESPEasy_NVS_Helper& preferences) {
  preferences.remove(F(FACTORY_DEFAULT_NVS_UNIT_NAME_KEY));
}

#endif // ifdef ESP32

#include "../DataStructs/Web_StreamingBuffer.h"

#include "../DataStructs/tcp_cleanup.h"
#include "../DataTypes/ESPEasyTimeSource.h"
#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../ESPEasyCore/ESPEasyNetwork.h"

// FIXME TD-er: Should keep a pointer to the webserver as a member, not use the global defined one.
#include "../Globals/Services.h"

#include "../Helpers/ESPEasy_time_calc.h"
#include "../Helpers/Convert.h"
#include "../Helpers/StringConverter.h"

#include "../../ESPEasy_common.h"

#ifdef ESP8266
#define CHUNKED_BUFFER_SIZE         512
#else 
#define CHUNKED_BUFFER_SIZE         1200
#endif

Web_StreamingBuffer::Web_StreamingBuffer(void) : lowMemorySkip(false),
  initialRam(0), beforeTXRam(0), duringTXRam(0), finalRam(0), maxCoreUsage(0),
  maxServerUsage(0), sentBytes(0), flashStringCalls(0), flashStringData(0)
{
  // Make sure this is allocated on the DRAM since access to primary heap is faster
  # ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  # endif // ifdef USE_SECOND_HEAP

  buf.reserve(CHUNKED_BUFFER_SIZE + 50);
  buf.clear();
}

Web_StreamingBuffer& Web_StreamingBuffer::operator+=(char a)                   {
  if (this->buf.length() >= CHUNKED_BUFFER_SIZE) {
    flush();
  }
  this->buf += a;
  return *this;
}

Web_StreamingBuffer& Web_StreamingBuffer::operator+=(uint64_t a) {
  return addString(ull2String(a));
}

Web_StreamingBuffer& Web_StreamingBuffer::operator+=(int64_t a) {
  return addString(ll2String(a));
}

Web_StreamingBuffer& Web_StreamingBuffer::operator+=(const float& a)           {
  return addString(toString(a, 2));
}

#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
Web_StreamingBuffer& Web_StreamingBuffer::operator+=(const double& a)          {
  return addString(doubleToString(a));
}
#endif

Web_StreamingBuffer& Web_StreamingBuffer::operator+=(const String& a)          {
  return addString(a);
}

Web_StreamingBuffer& Web_StreamingBuffer::operator+=(PGM_P str) {
  return addFlashString(str);
}

Web_StreamingBuffer& Web_StreamingBuffer::operator+=(const __FlashStringHelper* str) {
  return addFlashString((PGM_P)str);
}

Web_StreamingBuffer& Web_StreamingBuffer::addFlashString(PGM_P str, int length) {
  #ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  #endif


  if (!str) { 
    return *this; // return if the pointer is void
  }

  #ifdef USE_SECOND_HEAP
  if (mmu_is_iram(str)) {
    // Have to copy the string using mmu_get functions
    // This is not a flash string.
    bool done = false;
    const char* cur_char = str;
    while (!done) {
      const uint8_t ch = mmu_get_uint8(cur_char++);
      if (ch == 0) return *this;
      if (this->buf.length() >= CHUNKED_BUFFER_SIZE) {
        flush();
      }
      this->buf += (char)ch;
    }
  }
  #endif

  ++flashStringCalls;

  if (lowMemorySkip) { return *this; }

  checkFull();

  int flush_step = CHUNKED_BUFFER_SIZE - this->buf.length();
  if (flush_step < 1) { flush_step = 0; }

  {
    // Copy to internal buffer and send in chunks
    PGM_P pos = str;
    while (length != 0) {
      if (flush_step == 0) {
        flush();
        flush_step = CHUNKED_BUFFER_SIZE;
      }
      const char c = (char)pgm_read_byte(pos);
      if (c == '\0' && length < 0) {
        // Only check for \0 when length was given (e.g. binary data)
        return *this;
      }
      this->buf += c;
      ++flashStringData;
      ++pos;
      --length;
      --flush_step;
    }
  }
  return *this;
}

Web_StreamingBuffer& Web_StreamingBuffer::addString(const String& a) {
  # ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  # endif // ifdef USE_SECOND_HEAP

  if (lowMemorySkip) { return *this; }
  const unsigned int length = a.length();
  if (length == 0) { return *this; }

  checkFull();
  int flush_step = CHUNKED_BUFFER_SIZE - this->buf.length();

  if (flush_step < 1) { flush_step = 0; }

  if (length < static_cast<unsigned int>(flush_step)) {
    // Just use the faster String operator to copy flash strings.
    this->buf += a;
    return *this;
  }

  unsigned int pos = 0;
  while (pos < length) {
    if (flush_step <= 0) {
      flush();
      flush_step = CHUNKED_BUFFER_SIZE;
    } else {
      const int remaining = length - pos;
      const int fetchLength = flush_step >= remaining ? remaining : flush_step;
      const char* ch = a.begin() + pos;
      this->buf.concat(ch, static_cast<unsigned int>(fetchLength));
      pos += fetchLength;
      flush_step -= fetchLength;
    }
  }
  return *this;
}

void Web_StreamingBuffer::flush() {
  if (lowMemorySkip) {
    this->buf.clear();
  } else {
    if (this->buf.length() > 0) {
      sendContentBlocking(this->buf);
    }
  }
}

void Web_StreamingBuffer::checkFull() {
  if (lowMemorySkip) { this->buf.clear(); }

  if (this->buf.length() >= CHUNKED_BUFFER_SIZE) {
    trackTotalMem();
    flush();
  }
}

void Web_StreamingBuffer::startStream(int httpCode) {
  startStream(false, F("text/html"), F(""), httpCode);
}

void Web_StreamingBuffer::startStream(const __FlashStringHelper * origin, int httpCode) {
  startStream(false, F("text/html"), origin, httpCode);
}

void Web_StreamingBuffer::startStream(const __FlashStringHelper * content_type,
                                      const __FlashStringHelper * origin, 
                                      int httpCode, 
                                      bool cacheable) {
  startStream(false, content_type, origin, httpCode, cacheable);
}


void Web_StreamingBuffer::startJsonStream() {
  startStream(true, F("application/json"), F("*"));
}

void Web_StreamingBuffer::startStream(bool allowOriginAll, 
                                      const __FlashStringHelper * content_type, 
                                      const __FlashStringHelper * origin,
                                      int httpCode,
                                      bool cacheable) {
  #ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  #endif

  maxCoreUsage = maxServerUsage = 0;
  initialRam   = ESP.getFreeHeap();
  beforeTXRam  = initialRam;
  sentBytes    = 0;
  buf.clear();
  buf.reserve(CHUNKED_BUFFER_SIZE);
  web_server.client().setNoDelay(true);
#ifdef ESP32
  web_server.client().setSSE(false);
#endif
  
  if (beforeTXRam < 3000) {
    lowMemorySkip = true;
    web_server.send_P(200, (PGM_P)F("text/plain"), (PGM_P)F("Low memory. Cannot display webpage :-("));
      #if defined(ESP8266)
    tcpCleanup();
      #endif // if defined(ESP8266)
    return;
  } else {
    sendHeaderBlocking(allowOriginAll, content_type, origin, httpCode, cacheable);
  }
}

void Web_StreamingBuffer::trackTotalMem() {
  #ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  #endif

  beforeTXRam = ESP.getFreeHeap();

  if ((initialRam - beforeTXRam) > maxServerUsage) {
    maxServerUsage = initialRam - beforeTXRam;
  }
}

void Web_StreamingBuffer::trackCoreMem() {
  #ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  #endif

  duringTXRam = ESP.getFreeHeap();

  if ((initialRam - duringTXRam) > maxCoreUsage) {
    maxCoreUsage = (initialRam - duringTXRam);
  }
}

void Web_StreamingBuffer::endStream() {
  #ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  #endif

  if (!lowMemorySkip) {
    if (buf.length() > 0) { sendContentBlocking(buf); }
    buf.clear();
    sendContentBlocking(buf);

    web_server.client().PR_9453_FLUSH_TO_CLEAR();

    finalRam = ESP.getFreeHeap();

/*
#ifndef BUILD_NO_DEBUG
        if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
        String log = String("Ram usage: Webserver only: ") + maxServerUsage +
                    " including Core: " + maxCoreUsage +
                    " flashStringCalls: " + flashStringCalls +
                    " flashStringData: " + flashStringData;
        addLog(LOG_LEVEL_DEBUG, log);
        }
#endif // ifndef BUILD_NO_DEBUG
*/

  } else {
    if (loglevelActiveFor(LOG_LEVEL_ERROR))
      addLog(LOG_LEVEL_ERROR, concat("Webpage skipped: low memory: ", finalRam));
    lowMemorySkip = false;
  }

  delay(5);
  web_server.client().stop();
  #ifdef ESP8266
  tcpCleanup();
  #endif
}


void Web_StreamingBuffer::sendContentBlocking(String& data) {
  #ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  #endif

  delay(0); // Try to prevent WDT reboots

  const uint32_t length   = data.length();
#ifndef BUILD_NO_DEBUG
  if (loglevelActiveFor(LOG_LEVEL_DEBUG_DEV)) {
    addLogMove(LOG_LEVEL_DEBUG_DEV, strformat(
      F("sendcontent free: %u  chunk size: %u"), 
      ESP.getFreeHeap(), 
      length));
  }
#endif // ifndef BUILD_NO_DEBUG
  const uint32_t freeBeforeSend = ESP.getFreeHeap();
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("sendContentBlocking"));
  #endif

  if (beforeTXRam > freeBeforeSend) {
    beforeTXRam = freeBeforeSend;
  }
  duringTXRam = freeBeforeSend;
  
#if defined(ESP8266) && defined(ARDUINO_ESP8266_RELEASE_2_3_0)
  String size = formatToHex(length) + "\r\n";

  // do chunked transfer encoding ourselves (WebServer doesn't support it)
  web_server.sendContent(size);

  if (length > 0) { web_server.sendContent(data); }
  web_server.sendContent("\r\n");
#else // ESP8266 2.4.0rc2 and higher and the ESP32 webserver supports chunked http transfer
  #if defined(ESP8266) && defined(USE_SECOND_HEAP)
  {
    HeapSelectIram ephemeral;
    web_server.sendContent(data);
  }
  #else
  web_server.sendContent(data);
  #endif

  if (data.length() > (CHUNKED_BUFFER_SIZE + 1)) {
    free_string(data); // Clear also allocated memory
  } else {
    data.clear();
  }

  const uint32_t timeout = millis() + 100;
  while ((!data.reserve(CHUNKED_BUFFER_SIZE) || (ESP.getFreeHeap() < 4000 /*freeBeforeSend*/ )) &&
         !timeOutReached(timeout)) {
    if (ESP.getFreeHeap() < duringTXRam) {
      duringTXRam = ESP.getFreeHeap();
    }
    trackCoreMem();
    #ifndef BUILD_NO_RAM_TRACKER
    checkRAM(F("duringDataTX"));
    #endif

    delay(1);
  }
#endif // if defined(ESP8266) && defined(ARDUINO_ESP8266_RELEASE_2_3_0)

  sentBytes += length;
  delay(1);
}

void Web_StreamingBuffer::sendHeaderBlocking(bool allowOriginAll, 
                                             const String& content_type, 
                                             const String& origin,
                                             int httpCode,
                                             bool cacheable) {
  #ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  #endif

  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("sendHeaderBlocking"));
  #endif
  
  web_server.client().PR_9453_FLUSH_TO_CLEAR();

#if defined(ESP8266) && defined(ARDUINO_ESP8266_RELEASE_2_3_0)
  web_server.setContentLength(CONTENT_LENGTH_UNKNOWN);
  sendHeader(F("Accept-Ranges"),     F("none"));
  sendHeader(F("Cache-Control"),     F("no-cache"));
  sendHeader(F("Transfer-Encoding"), F("chunked"));

  if (allowOriginAll) {
    sendHeader(F("Access-Control-Allow-Origin"), "*");
  }
  web_server.send(httpCode, content_type, EMPTY_STRING);
#else // if defined(ESP8266) && defined(ARDUINO_ESP8266_RELEASE_2_3_0)
  unsigned int timeout          = 100;
  const uint32_t freeBeforeSend = ESP.getFreeHeap();

  const uint32_t beginWait = millis();

  web_server.setContentLength(CONTENT_LENGTH_UNKNOWN);
  if (!cacheable)
    web_server.sendHeader(F("Cache-Control"), F("no-cache"));

#if ESP_IDF_VERSION_MAJOR>4
  if (origin.equals("*")) {
    web_server.enableCORS(true);
  } else
#endif
  if (origin.length() > 0) {
#if ESP_IDF_VERSION_MAJOR>4
    web_server.enableCORS(false);
#endif
    web_server.sendHeader(F("Access-Control-Allow-Origin"), origin);
  }
  web_server.send(httpCode, content_type, EMPTY_STRING);
#ifdef ESP8266
  // dont wait on 2.3.0. Memory returns just too slow.
  while ((ESP.getFreeHeap() < freeBeforeSend) &&
         !timeOutReached(beginWait + timeout)) {
    #ifndef BUILD_NO_RAM_TRACKER
    checkRAM(F("duringHeaderTX"));
    #endif
    delay(1);
  }
#endif
#endif // if defined(ESP8266) && defined(ARDUINO_ESP8266_RELEASE_2_3_0)
  delay(0);
}

#include "../DataStructs/PluginStats_timestamp.h"

#if FEATURE_PLUGIN_STATS

# include "../Globals/ESPEasy_time.h"
# include "../Helpers/ESPEasy_time_calc.h"

PluginStats_timestamp::PluginStats_timestamp(bool useHighRes)
  : _internal_to_micros_ratio(useHighRes
? 20000ull   // 1/50 sec resolution
: 100000ull) // 1/10 sec resolution
{}

PluginStats_timestamp::~PluginStats_timestamp()
{}

bool PluginStats_timestamp::push(const int64_t& timestamp_sysmicros)
{
  return _timestamps.push(systemMicros_to_internalTimestamp(timestamp_sysmicros));
}

bool PluginStats_timestamp::updateLast(const int64_t& timestamp_sysmicros)
{
  const size_t nrElements = _timestamps.size();

  if (nrElements == 0) { return false; }
  return _timestamps.set(nrElements - 1, systemMicros_to_internalTimestamp(timestamp_sysmicros));
}

void PluginStats_timestamp::clear()
{
  _timestamps.clear();
}

void PluginStats_timestamp::processTimeSet(const double& time_offset)
{
  // Check to see if there was a unix time set before the system time was set
  // For example when receiving data from a p2p node

  /*
     const uint64_t cur_micros    = getMicros64();
     const uint64_t offset_micros = time_offset * 1000000ull;
     const size_t   nrSamples     = _timestamps.size();

     // GMT  Wed Jan 01 2020 00:00:00 GMT+0000
     const int64_t unixTime_20200101 = 1577836800ll * _internal_to_micros_ratio;

     for (PluginStatsTimestamps_t::index_t i = 0; i < nrSamples; ++i) {
       if (_timestamps[i] < unixTime_20200101) {
         _timestamps.set(i, _timestamps[i] + time_offset);
       }
     }
   */
}

int64_t PluginStats_timestamp::getTimestamp(int lastNrSamples) const
{
  if ((_timestamps.size() == 0) || (_timestamps.size() < abs(lastNrSamples))) { return 0u; }

  PluginStatsTimestamps_t::index_t i = 0;

  if (lastNrSamples > 0) {
    i = _timestamps.size() - lastNrSamples;
  } else if (lastNrSamples < 0) {
    i = abs(lastNrSamples) - 1;
  }

  if (i < _timestamps.size()) {
    return internalTimestamp_to_systemMicros(_timestamps[i]);
  }
  return 0u;
}

uint32_t PluginStats_timestamp::getFullPeriodInSec(uint32_t& time_frac) const
{
  const size_t nrSamples = _timestamps.size();

  time_frac = 0u;

  if (nrSamples <= 1) {
    return 0u;
  }

  const int64_t start = internalTimestamp_to_systemMicros(_timestamps[0]);
  const int64_t end   = internalTimestamp_to_systemMicros(_timestamps[nrSamples - 1]);

  const int64_t period_usec = (end < start) ? (start - end) : (end - start);

  return micros_to_sec_time_frac(period_usec, time_frac);
}

int64_t PluginStats_timestamp::operator[](PluginStatsTimestamps_t::index_t index) const
{
  if (index < _timestamps.size()) {
    return internalTimestamp_to_systemMicros(_timestamps[index]);
  }
  return 0u;
}

uint32_t PluginStats_timestamp::systemMicros_to_internalTimestamp(const int64_t& timestamp_sysmicros) const
{
  return static_cast<uint32_t>(timestamp_sysmicros / _internal_to_micros_ratio);
}

int64_t PluginStats_timestamp::internalTimestamp_to_systemMicros(const uint32_t& internalTimestamp) const
{
  const uint64_t cur_micros    = getMicros64();
  const uint64_t overflow_step = 4294967296ull * _internal_to_micros_ratio;

  uint64_t sysMicros = static_cast<uint64_t>(internalTimestamp) * _internal_to_micros_ratio;

  // Try to get in the range of the current system micros
  // This only does play a role in high res mode, when uptime is over 994 days.
  while ((sysMicros + overflow_step) < cur_micros) {
    sysMicros += overflow_step;
  }
  return sysMicros;
}

#endif // if FEATURE_PLUGIN_STATS

#include "../DataStructs/C013_p2p_SensorDataStruct.h"

#ifdef USES_C013

# include "../DataStructs/NodeStruct.h"
# include "../Globals/Nodes.h"
# include "../Globals/ESPEasy_time.h"
# include "../Globals/Plugins.h"

# include "../CustomBuild/CompiletimeDefines.h"

bool C013_SensorDataStruct::prepareForSend()
{
  sourceNodeBuild = get_build_nr();
  checksum.clear();

  if (sourceNodeBuild >= 20871) {
    if (node_time.systemTimePresent()) {
      uint32_t unix_time_frac{};
      timestamp_sec  = node_time.getUnixTime(unix_time_frac);
      timestamp_frac = unix_time_frac >> 16;
    }


    // Make sure to add checksum as last step
    constexpr unsigned len_upto_checksum = offsetof(C013_SensorDataStruct, checksum);

    const ShortChecksumType tmpChecksum(
      reinterpret_cast<const uint8_t *>(this),
      sizeof(C013_SensorDataStruct),
      len_upto_checksum);

    checksum = tmpChecksum;
  }

  return validTaskIndex(sourceTaskIndex) &&
         validTaskIndex(destTaskIndex);
}

bool C013_SensorDataStruct::setData(const uint8_t *data, size_t size)
{
  // First clear entire struct
  memset(this, 0, sizeof(C013_SensorDataStruct));

  if (size < 6) {
    return false;
  }

  if ((data[0] != 255) || // header
      (data[1] != 5)) {   // ID
    return false;
  }

  constexpr unsigned len_upto_checksum = offsetof(C013_SensorDataStruct, checksum);
  const ShortChecksumType tmpChecksum(
    data,
    size,
    len_upto_checksum);


  // Need to keep track of different possible versions of data which still need to be supported.
  // Really old versions of ESPEasy might send upto 80 bytes of uninitialized data
  // meaning for sizes > 24 bytes we may need to check the version of ESPEasy running on the node.
  if (size > sizeof(C013_SensorDataStruct)) {
    size = sizeof(C013_SensorDataStruct);
  }
  NodeStruct *sourceNode = Nodes.getNode(data[2]); // sourceUnit

  if (sourceNode != nullptr) {
    if (sourceNode->build < 20871) {
      if (size > 24) {
        size = 24;
      }
    }
  }

  if (size <= 24) {
    deviceNumber = INVALID_PLUGIN_ID;
    sensorType   = Sensor_VType::SENSOR_TYPE_NONE;

    if (sourceNode != nullptr) {
      sourceNodeBuild = sourceNode->build;
    }
  }

  memcpy(this, data, size);

  if (checksum.isSet()) {
    if (!(tmpChecksum == checksum)) {
      return false;
    }
  }

  return validTaskIndex(sourceTaskIndex) &&
         validTaskIndex(destTaskIndex);
}

bool C013_SensorDataStruct::matchesPluginID(pluginID_t pluginID) const
{
  if ((deviceNumber.value == 255) || !validPluginID(deviceNumber) || !validPluginID(pluginID)) {
    // Was never set, so probably received data from older node.
    return true;
  }
  return pluginID == deviceNumber;
}

bool C013_SensorDataStruct::matchesSensorType(Sensor_VType sensor_type) const
{
  if ((deviceNumber.value == 255) || (sensorType == Sensor_VType::SENSOR_TYPE_NONE)) {
    // Was never set, so probably received data from older node.
    return true;
  }
  return sensorType == sensor_type;
}

#endif // ifdef USES_C013

#include "../DataStructs/TimingStats.h"

#if FEATURE_TIMING_STATS

# include "../DataTypes/ESPEasy_plugin_functions.h"
# include "../Globals/CPlugins.h"
# include "../Helpers/_CPlugin_Helper.h"
# include "../Helpers/StringConverter.h"

std::map<int, TimingStats> pluginStats;
std::map<int, TimingStats> controllerStats;
std::map<TimingStatsElements, TimingStats> miscStats;
unsigned long timingstats_last_reset(0);


void TimingStats::add(int32_t duration_usec) {
  // Max duration in usec is roughly 35 minutes.
  // For timing stats more than enough
  if (duration_usec < 0) return;
  _timeTotal += static_cast<uint64_t>(duration_usec);
  ++_count;

  if (static_cast<uint32_t>(duration_usec) > _maxVal) { _maxVal = duration_usec; }

  if (static_cast<uint32_t>(duration_usec) < _minVal) { _minVal = duration_usec; }
}

void TimingStats::reset() {
  _timeTotal = 0u;
  _count     = 0u;
  _maxVal    = 0u;
  _minVal    = 4294967295u;
}

bool TimingStats::isEmpty() const {
  return _count == 0u;
}

float TimingStats::getAvg() const {
  if (_count == 0) { return 0.0f; }
  return static_cast<float>(_timeTotal) / static_cast<float>(_count);
}

uint32_t TimingStats::getMinMax(uint32_t& minVal, uint32_t& maxVal) const {
  minVal = _minVal;
  maxVal = _maxVal;
  return _count;
}

bool TimingStats::thresholdExceeded(const uint32_t& threshold) const {
  if (_count == 0) {
    return false;
  }
  return _maxVal > threshold;
}

/********************************************************************************************\
   Functions used for displaying timing stats
 \*********************************************************************************************/
const __FlashStringHelper* getPluginFunctionName(int function) {
  switch (function) {
    case PLUGIN_INIT_ALL:              return F("INIT_ALL");
    case PLUGIN_INIT:                  return F("INIT");
    case PLUGIN_READ:                  return F("READ");
    case PLUGIN_ONCE_A_SECOND:         return F("ONCE_A_SECOND");
    case PLUGIN_TEN_PER_SECOND:        return F("TEN_PER_SECOND");
    case PLUGIN_DEVICE_ADD:            return F("DEVICE_ADD");
    case PLUGIN_EVENTLIST_ADD:         return F("EVENTLIST_ADD");
    case PLUGIN_WEBFORM_SAVE:          return F("WEBFORM_SAVE");
    case PLUGIN_WEBFORM_LOAD:          return F("WEBFORM_LOAD");
    case PLUGIN_WEBFORM_SHOW_VALUES:   return F("WEBFORM_SHOW_VALUES");
    case PLUGIN_FORMAT_USERVAR:        return F("FORMAT_USERVAR");
    case PLUGIN_GET_DEVICENAME:        return F("GET_DEVICENAME");
    case PLUGIN_GET_DEVICEVALUENAMES:  return F("GET_DEVICEVALUENAMES");
    case PLUGIN_GET_DEVICEVALUECOUNT:  return F("GET_DEVICEVALUECOUNT");
    case PLUGIN_GET_DEVICEVTYPE:       return F("GET_DEVICEVTYPE");
    case PLUGIN_WRITE:                 return F("WRITE");
    case PLUGIN_WEBFORM_SHOW_CONFIG:   return F("WEBFORM_SHOW_CONFIG");
    #if FEATURE_PLUGIN_STATS
    case PLUGIN_WEBFORM_LOAD_SHOW_STATS: return F("WEBFORM_LOAD_SHOW_STATS");
    #endif
    case PLUGIN_SERIAL_IN:             return F("SERIAL_IN");
    case PLUGIN_UDP_IN:                return F("UDP_IN");
    case PLUGIN_CLOCK_IN:              return F("CLOCK_IN");
    case PLUGIN_TASKTIMER_IN:          return F("TASKTIMER_IN");
    case PLUGIN_FIFTY_PER_SECOND:      return F("FIFTY_PER_SECOND");
    case PLUGIN_SET_CONFIG:            return F("SET_CONFIG");
    case PLUGIN_GET_DEVICEGPIONAMES:   return F("GET_DEVICEGPIONAMES");
    case PLUGIN_EXIT:                  return F("EXIT");
    case PLUGIN_GET_CONFIG_VALUE:      return F("GET_CONFIG");
//    case PLUGIN_UNCONDITIONAL_POLL:    return F("UNCONDITIONAL_POLL");
    case PLUGIN_REQUEST:               return F("REQUEST");
    case PLUGIN_PROCESS_CONTROLLER_DATA: return F("PROCESS_CONTROLLER_DATA");
    case PLUGIN_I2C_GET_ADDRESS:       return F("I2C_CHECK_DEVICE");
    case PLUGIN_READ_ERROR_OCCURED:    return F("PLUGIN_READ_ERROR_OCCURED");
  }
  return F("Unknown");
}

bool mustLogFunction(int function) {
  if (!Settings.EnableTimingStats()) { return false; }

  switch (function) {
//    case PLUGIN_INIT_ALL:              return false;
//    case PLUGIN_INIT:                  return false;
    case PLUGIN_READ:                  return true;
    case PLUGIN_ONCE_A_SECOND:         return true;
    case PLUGIN_TEN_PER_SECOND:        return true;
//    case PLUGIN_DEVICE_ADD:            return false;
//    case PLUGIN_EVENTLIST_ADD:         return false;
//    case PLUGIN_WEBFORM_SAVE:          return false;
//    case PLUGIN_WEBFORM_LOAD:          return false;
//    case PLUGIN_WEBFORM_SHOW_VALUES:   return false;
    case PLUGIN_FORMAT_USERVAR:        return true;
    case PLUGIN_GET_DEVICENAME:        return true;
//    case PLUGIN_GET_DEVICEVALUENAMES:  return false;
//    case PLUGIN_GET_DEVICEVALUECOUNT:  return true;
//    case PLUGIN_GET_DEVICEVTYPE:       return true;
    case PLUGIN_WRITE:                 return true;
//    case PLUGIN_WEBFORM_SHOW_CONFIG:   return false;
    case PLUGIN_SERIAL_IN:             return true;
//    case PLUGIN_UDP_IN:                return false;
//    case PLUGIN_CLOCK_IN:              return false;
    case PLUGIN_TASKTIMER_IN:          return true;
    case PLUGIN_FIFTY_PER_SECOND:      return true;
//    case PLUGIN_SET_CONFIG:            return false;
//    case PLUGIN_GET_DEVICEGPIONAMES:   return false;
//    case PLUGIN_EXIT:                  return false;
//    case PLUGIN_GET_CONFIG_VALUE:      return false;
//    case PLUGIN_UNCONDITIONAL_POLL:    return false;
    case PLUGIN_REQUEST:               return true;
    case PLUGIN_I2C_GET_ADDRESS:       return true;
    case PLUGIN_PROCESS_CONTROLLER_DATA: return true;
    case PLUGIN_READ_ERROR_OCCURED:    return true;
  }
  return false;
}

const __FlashStringHelper* getCPluginCFunctionName(CPlugin::Function function) {
  switch (function) {
    case CPlugin::Function::CPLUGIN_PROTOCOL_ADD:              return F("CPLUGIN_PROTOCOL_ADD");
    case CPlugin::Function::CPLUGIN_CONNECT_SUCCESS:           return F("CPLUGIN_CONNECT_SUCCESS");
    case CPlugin::Function::CPLUGIN_CONNECT_FAIL:              return F("CPLUGIN_CONNECT_FAIL");
    case CPlugin::Function::CPLUGIN_PROTOCOL_TEMPLATE:         return F("CPLUGIN_PROTOCOL_TEMPLATE");
    case CPlugin::Function::CPLUGIN_PROTOCOL_SEND:             return F("CPLUGIN_PROTOCOL_SEND");
    case CPlugin::Function::CPLUGIN_PROTOCOL_RECV:             return F("CPLUGIN_PROTOCOL_RECV");
    case CPlugin::Function::CPLUGIN_GET_DEVICENAME:            return F("CPLUGIN_GET_DEVICENAME");
    case CPlugin::Function::CPLUGIN_WEBFORM_SAVE:              return F("CPLUGIN_WEBFORM_SAVE");
    case CPlugin::Function::CPLUGIN_WEBFORM_LOAD:              return F("CPLUGIN_WEBFORM_LOAD");
    case CPlugin::Function::CPLUGIN_GET_PROTOCOL_DISPLAY_NAME: return F("CPLUGIN_GET_PROTOCOL_DISPLAY_NAME");
    case CPlugin::Function::CPLUGIN_TASK_CHANGE_NOTIFICATION:  return F("CPLUGIN_TASK_CHANGE_NOTIFICATION");
    case CPlugin::Function::CPLUGIN_INIT:                      return F("CPLUGIN_INIT");
    case CPlugin::Function::CPLUGIN_UDP_IN:                    return F("CPLUGIN_UDP_IN");
    case CPlugin::Function::CPLUGIN_FLUSH:                     return F("CPLUGIN_FLUSH");
    case CPlugin::Function::CPLUGIN_TEN_PER_SECOND:            return F("CPLUGIN_TEN_PER_SECOND");
    case CPlugin::Function::CPLUGIN_FIFTY_PER_SECOND:          return F("CPLUGIN_FIFTY_PER_SECOND");
    case CPlugin::Function::CPLUGIN_INIT_ALL:                  return F("CPLUGIN_INIT_ALL");
    case CPlugin::Function::CPLUGIN_EXIT:                      return F("CPLUGIN_EXIT");
    case CPlugin::Function::CPLUGIN_WRITE:                     return F("CPLUGIN_WRITE");

    case CPlugin::Function::CPLUGIN_GOT_CONNECTED:
    case CPlugin::Function::CPLUGIN_GOT_INVALID:
    case CPlugin::Function::CPLUGIN_INTERVAL:
    case CPlugin::Function::CPLUGIN_ACKNOWLEDGE:
    case CPlugin::Function::CPLUGIN_WEBFORM_SHOW_HOST_CONFIG:
      break;
  }
  return F("Unknown");
}

bool mustLogCFunction(CPlugin::Function function) {
  if (!Settings.EnableTimingStats()) { return false; }

  switch (function) {
    case CPlugin::Function::CPLUGIN_PROTOCOL_ADD:              return false;
    case CPlugin::Function::CPLUGIN_CONNECT_SUCCESS:           return true;
    case CPlugin::Function::CPLUGIN_CONNECT_FAIL:              return true;
    case CPlugin::Function::CPLUGIN_PROTOCOL_TEMPLATE:         return false;
    case CPlugin::Function::CPLUGIN_PROTOCOL_SEND:             return true;
    case CPlugin::Function::CPLUGIN_PROTOCOL_RECV:             return true;
    case CPlugin::Function::CPLUGIN_GET_DEVICENAME:            return false;
    case CPlugin::Function::CPLUGIN_WEBFORM_SAVE:              return false;
    case CPlugin::Function::CPLUGIN_WEBFORM_LOAD:              return false;
    case CPlugin::Function::CPLUGIN_GET_PROTOCOL_DISPLAY_NAME: return false;
    case CPlugin::Function::CPLUGIN_TASK_CHANGE_NOTIFICATION:  return false;
    case CPlugin::Function::CPLUGIN_INIT:                      return false;
    case CPlugin::Function::CPLUGIN_UDP_IN:                    return true;
    case CPlugin::Function::CPLUGIN_FLUSH:                     return false;
    case CPlugin::Function::CPLUGIN_TEN_PER_SECOND:            return true;
    case CPlugin::Function::CPLUGIN_FIFTY_PER_SECOND:          return true;
    case CPlugin::Function::CPLUGIN_INIT_ALL:                  return false;
    case CPlugin::Function::CPLUGIN_EXIT:                      return false;
    case CPlugin::Function::CPLUGIN_WRITE:                     return true;

    case CPlugin::Function::CPLUGIN_GOT_CONNECTED:
    case CPlugin::Function::CPLUGIN_GOT_INVALID:
    case CPlugin::Function::CPLUGIN_INTERVAL:
    case CPlugin::Function::CPLUGIN_ACKNOWLEDGE:
    case CPlugin::Function::CPLUGIN_WEBFORM_SHOW_HOST_CONFIG:
      break;
  }
  return false;
}

// Return flash string type to reduce bin size
const __FlashStringHelper* getMiscStatsName_F(TimingStatsElements stat) {
  switch (stat) {
    case TimingStatsElements::LOADFILE_STATS:             return F("Load File");
    case TimingStatsElements::SAVEFILE_STATS:             return F("Save File");
    case TimingStatsElements::LOOP_STATS:                 return F("Loop");
    case TimingStatsElements::PLUGIN_CALL_50PS:           return F("Plugin call 50 p/s");
    case TimingStatsElements::PLUGIN_CALL_10PS:           return F("Plugin call 10 p/s");
    case TimingStatsElements::PLUGIN_CALL_10PSU:          return F("Plugin call 10 p/s U");
    case TimingStatsElements::PLUGIN_CALL_1PS:            return F("Plugin call  1 p/s");
    case TimingStatsElements::CPLUGIN_CALL_50PS:          return F("CPlugin call 50 p/s");
    case TimingStatsElements::CPLUGIN_CALL_10PS:          return F("CPlugin call 10 p/s");
    case TimingStatsElements::SENSOR_SEND_TASK:           return F("SensorSendTask()");
    case TimingStatsElements::COMMAND_EXEC_INTERNAL:      return F("Exec Internal Command");
    case TimingStatsElements::COMMAND_DECODE_INTERNAL:    return F("Decode Internal Command");
    case TimingStatsElements::CONSOLE_LOOP:               return F("Console loop()");
    case TimingStatsElements::CONSOLE_WRITE_SERIAL:       return F("Console out");
    case TimingStatsElements::SEND_DATA_STATS:            return F("sendData()");
    case TimingStatsElements::COMPUTE_FORMULA_STATS:      return F("Compute formula");
    case TimingStatsElements::COMPUTE_STATS:              return F("Compute()");
    case TimingStatsElements::PLUGIN_CALL_DEVICETIMER_IN: return F("PLUGIN_DEVICETIMER_IN");
    case TimingStatsElements::SET_NEW_TIMER:              return F("setNewTimerAt()");
    case TimingStatsElements::MQTT_DELAY_QUEUE:           return F("Delay queue MQTT");
    case TimingStatsElements::TRY_CONNECT_HOST_TCP:       return F("try_connect_host() (TCP)");
    case TimingStatsElements::TRY_CONNECT_HOST_UDP:       return F("try_connect_host() (UDP)");
    case TimingStatsElements::HOST_BY_NAME_STATS:         return F("hostByName()");
    case TimingStatsElements::CONNECT_CLIENT_STATS:       return F("connectClient()");
    case TimingStatsElements::LOAD_CUSTOM_TASK_STATS:     return F("LoadCustomTaskSettings()");
    case TimingStatsElements::WIFI_ISCONNECTED_STATS:     return F("WiFi.isConnected()");
    case TimingStatsElements::WIFI_NOTCONNECTED_STATS:    return F("WiFi.isConnected() (fail)");
    case TimingStatsElements::LOAD_TASK_SETTINGS:         return F("LoadTaskSettings()");
    case TimingStatsElements::SAVE_TASK_SETTINGS:         return F("SaveTaskSettings()");
    case TimingStatsElements::LOAD_CONTROLLER_SETTINGS:   return F("LoadControllerSettings()");
    #ifdef ESP32
    case TimingStatsElements::LOAD_CONTROLLER_SETTINGS_C: return F("LoadControllerSettings() (cached)");
    #endif
    case TimingStatsElements::SAVE_CONTROLLER_SETTINGS:   return F("SaveControllerSettings()");
    case TimingStatsElements::TRY_OPEN_FILE:              return F("TryOpenFile()");
    case TimingStatsElements::FS_GC_SUCCESS:              return F("ESPEASY_FS GC success");
    case TimingStatsElements::FS_GC_FAIL:                 return F("ESPEASY_FS GC fail");
    case TimingStatsElements::RULES_PROCESSING:           return F("rulesProcessing()");
    case TimingStatsElements::RULES_PARSE_LINE:           return F("parseCompleteNonCommentLine()");
    case TimingStatsElements::RULES_PROCESS_MATCHED:      return F("processMatchedRule()");
    case TimingStatsElements::RULES_MATCH:                return F("rulesMatch()");
    case TimingStatsElements::GRAT_ARP_STATS:             return F("sendGratuitousARP()");
    case TimingStatsElements::SAVE_TO_RTC:                return F("saveToRTC()");
    case TimingStatsElements::BACKGROUND_TASKS:           return F("backgroundtasks()");
    case TimingStatsElements::UPDATE_RTTTL:               return F("update_rtttl()");
    case TimingStatsElements::CHECK_UDP:                  return F("checkUDP()");
    case TimingStatsElements::C013_SEND_UDP:              return F("C013_sendUDP() SUCCESS");
    case TimingStatsElements::C013_SEND_UDP_FAIL:         return F("C013_sendUDP() FAIL");
    case TimingStatsElements::C013_RECEIVE_SENSOR_DATA:   return F("C013 Receive sensor data");
    case TimingStatsElements::WEBSERVER_HANDLE_CLIENT:    return F("web_server.handleClient()");
    case TimingStatsElements::PROCESS_SYSTEM_EVENT_QUEUE: return F("process_system_event_queue()");
    case TimingStatsElements::FORMAT_USER_VAR:            return F("doFormatUserVar()");
    case TimingStatsElements::IS_NUMERICAL:               return F("isNumerical()");
    case TimingStatsElements::HANDLE_SCHEDULER_IDLE:      return F("handle_schedule() idle");
    case TimingStatsElements::HANDLE_SCHEDULER_TASK:      return F("handle_schedule() task");
    case TimingStatsElements::PARSE_TEMPLATE_PADDED:      return F("parseTemplate_padded()");
    case TimingStatsElements::PARSE_SYSVAR:               return F("parseSystemVariables()");
    case TimingStatsElements::PARSE_SYSVAR_NOCHANGE:      return F("parseSystemVariables() No change");
    case TimingStatsElements::HANDLE_SERVING_WEBPAGE:     return F("handle webpage");
    case TimingStatsElements::HANDLE_SERVING_WEBPAGE_JSON: return F("handle webpage JSON");
    case TimingStatsElements::WIFI_SCAN_ASYNC:            return F("WiFi Scan Async");
    case TimingStatsElements::WIFI_SCAN_SYNC:             return F("WiFi Scan Sync (blocking)");
    case TimingStatsElements::NTP_SUCCESS:                return F("NTP Success");
    case TimingStatsElements::NTP_FAIL:                   return F("NTP Fail");
    case TimingStatsElements::SYSTIME_UPDATED:            return F("Systime Set");
    case TimingStatsElements::C018_AIR_TIME:              return F("C018 LoRa TTN - Air Time");
#ifdef LIMIT_BUILD_SIZE
    default: break;
#else
    // Include all elements of the enum, to allow the compiler to check if we missed some
    case TimingStatsElements::C001_DELAY_QUEUE:
    case TimingStatsElements::C002_DELAY_QUEUE:
    case TimingStatsElements::C003_DELAY_QUEUE:
    case TimingStatsElements::C004_DELAY_QUEUE:
    case TimingStatsElements::C005_DELAY_QUEUE:
    case TimingStatsElements::C006_DELAY_QUEUE:
    case TimingStatsElements::C007_DELAY_QUEUE:
    case TimingStatsElements::C008_DELAY_QUEUE:
    case TimingStatsElements::C009_DELAY_QUEUE:
    case TimingStatsElements::C010_DELAY_QUEUE:
    case TimingStatsElements::C011_DELAY_QUEUE:
    case TimingStatsElements::C012_DELAY_QUEUE:
    case TimingStatsElements::C013_DELAY_QUEUE:
    case TimingStatsElements::C014_DELAY_QUEUE:
    case TimingStatsElements::C015_DELAY_QUEUE:
    case TimingStatsElements::C016_DELAY_QUEUE:
    case TimingStatsElements::C017_DELAY_QUEUE:
    case TimingStatsElements::C018_DELAY_QUEUE:
    case TimingStatsElements::C019_DELAY_QUEUE:
    case TimingStatsElements::C020_DELAY_QUEUE:
    case TimingStatsElements::C021_DELAY_QUEUE:
    case TimingStatsElements::C022_DELAY_QUEUE:
    case TimingStatsElements::C023_DELAY_QUEUE:
    case TimingStatsElements::C024_DELAY_QUEUE:
    case TimingStatsElements::C025_DELAY_QUEUE:
      break;

#endif
  }
  return F("Unknown");
}

String getMiscStatsName(TimingStatsElements stat) {
  if ((stat >= TimingStatsElements::C001_DELAY_QUEUE) && 
      (stat <= TimingStatsElements::C025_DELAY_QUEUE)) {
    return concat(
      F("Delay queue "),
      get_formatted_Controller_number(static_cast<cpluginID_t>(static_cast<int>(stat) - static_cast<int>(TimingStatsElements::C001_DELAY_QUEUE) + 1)));
  }
  return getMiscStatsName_F(static_cast<TimingStatsElements>(stat));
}

void stopTimerTask(deviceIndex_t T, int F, uint32_t statisticsTimerStart)
{
  if (mustLogFunction(F)) { pluginStats[static_cast<int>(T.value) * 256 + (F)].add(usecPassedSince_fast(statisticsTimerStart)); }
}

void stopTimerController(protocolIndex_t T, CPlugin::Function F, uint32_t statisticsTimerStart)
{
  if (mustLogCFunction(F)) { controllerStats[static_cast<int>(T) * 256 + static_cast<int>(F)].add(usecPassedSince_fast(statisticsTimerStart)); }
}

void stopTimer(TimingStatsElements L, uint32_t statisticsTimerStart)
{
  if (Settings.EnableTimingStats()) { miscStats[L].add(usecPassedSince_fast(statisticsTimerStart)); }
}

void addMiscTimerStat(TimingStatsElements L, int32_t T)
{
  if (Settings.EnableTimingStats()) { miscStats[L].add(T); }
}

#endif // if FEATURE_TIMING_STATS

#include "../DataStructs/LogStruct.h"

#include "../Helpers/ESPEasy_time_calc.h"
#include "../Helpers/StringConverter.h"

void LogStruct::add_end() {
  if (isFull()) {
    read_idx = nextIndex(read_idx);
  }
  write_idx = nextIndex(write_idx);
  is_full   = (write_idx == read_idx);
}

void LogStruct::add(const uint8_t loglevel, const String& line) {
  if (Message[write_idx].add(loglevel, line)) {
    add_end();
  }
}

void LogStruct::add(const uint8_t loglevel, String&& line) {
  if (Message[write_idx].add(loglevel, std::move(line))) {
    add_end();
  }
}

bool LogStruct::getNext(bool& logLinesAvailable, unsigned long& timestamp, String& message, uint8_t& loglevel) {
  lastReadTimeStamp = millis();
  logLinesAvailable = false;

  if (isEmpty()) {
    return false;
  }
  timestamp = Message[read_idx]._timestamp;
  message   = std::move(Message[read_idx]._message);
  loglevel  = Message[read_idx]._loglevel;
  clearOldest();

  if (!isEmpty()) {
    logLinesAvailable = true;
  }
  return true;
}


bool LogStruct::logActiveRead() {
  clearExpiredEntries();
  return timePassedSince(lastReadTimeStamp) < LOG_BUFFER_ACTIVE_READ_TIMEOUT;
}

void LogStruct::clearExpiredEntries() {
  unsigned int maxLoops = LOG_STRUCT_MESSAGE_LINES;

  while (maxLoops > 0) {
    --maxLoops;

    if (isEmpty() || // Nothing left
        !Message[read_idx].isExpired())
    {
      return;
    }
    clearOldest();
  }
}

void LogStruct::clearOldest() {
  if (!isEmpty()) {
    is_full = false;
    Message[read_idx].clear();
    read_idx = nextIndex(read_idx);
  }
}

#include "../DataStructs/Scheduler_ConstIntervalTimerID.h"

/*

ConstIntervalTimerID::ConstIntervalTimerID(SchedulerIntervalTimer_e timer) :
  SchedulerTimerID(SchedulerTimerType_e::ConstIntervalTimer)
{
  id = static_cast<uint32_t>(timer);
}

SchedulerIntervalTimer_e ConstIntervalTimerID::getIntervalTimer() const
{
  return static_cast<SchedulerIntervalTimer_e>(id);
}

#ifndef BUILD_NO_DEBUG
String ConstIntervalTimerID::decode() const
{
  return toString(getIntervalTimer());
}

#endif // ifndef BUILD_NO_DEBUG
*/
#include "../DataStructs/Scheduler_RulesTimerID.h"

#include "../Helpers/StringConverter.h"

RulesTimerID::RulesTimerID(unsigned int timerIndex) :
  SchedulerTimerID(SchedulerTimerType_e::RulesTimer)
{
  setId(timerIndex);
}

#ifndef BUILD_NO_DEBUG
String RulesTimerID::decode() const
{
  return concat(F("Rules#Timer="), getId());
}

#endif // ifndef BUILD_NO_DEBUG

#include "../DataStructs/NTP_candidate.h"

#if FEATURE_ESPEASY_P2P

# include "../CustomBuild/CompiletimeDefines.h"
# include "../DataTypes/ESPEasyTimeSource.h"
# include "../Globals/Settings.h"
# include "../Helpers/ESPEasy_time_calc.h"
# include "../Helpers/StringConverter.h"


bool NTP_candidate_struct::set(const NodeStruct& node)
{
  if (node.unit == Settings.Unit) { return false; }

  if (node.unix_time_sec < get_build_unixtime()) { return false; }
  const timeSource_t timeSource = static_cast<timeSource_t>(node.timeSource);

  if (timeSource == timeSource_t::No_time_source) { return false; }

  // Only allow time from p2p nodes who only got it via p2p themselves as "last resource"
  const unsigned long p2p_source_penalty =
    isExternalTimeSource(timeSource)  ? 0 : 10000;
  const unsigned long time_wander_other =
    p2p_source_penalty + 
    computeExpectedWander(timeSource, node.lastUpdated); // node.lastUpdated is already set to "time passed since" when sent


  if (timePassedSince(_received_moment) > EXT_TIME_SOURCE_MIN_UPDATE_INTERVAL_MSEC
      && timeSource < timeSource_t::ESP_now_peer) 
  {
    // Every now and then refresh the collected time source, but only on reliable sources
    clear(); 
  }

  if ((_time_wander < 0) || (time_wander_other < static_cast<unsigned long>(_time_wander))) {
    _time_wander     = time_wander_other;
    _unix_time_sec   = node.unix_time_sec;
    _unix_time_frac  = node.unix_time_frac;
    _received_moment = millis();
    _unit            = node.unit;
    _timeSource      = timeSource;

    if (_first_received_moment == 0) {
      _first_received_moment = _received_moment;
    }
    # ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      String log = F("NTP  : Time candidate: ");
      log += node.getSummary();

      log += concat(F(" time: "), _unix_time_sec);
      log += ' ';
      log += toString(timeSource);
      log += concat(F(" est. wander: "), _time_wander);
      addLogMove(LOG_LEVEL_DEBUG, log);
    }
    # endif // ifndef BUILD_NO_DEBUG
    return true;
  }
  return false;
}

void NTP_candidate_struct::clear()
{
  _unix_time_sec         = 0;
  _unix_time_frac        = 0;
  _time_wander           = -1;
  _received_moment       = 0;
  _first_received_moment = 0;
  _timeSource            = timeSource_t::No_time_source;
}

timeSource_t NTP_candidate_struct::getUnixTime(
  double & unix_time_d,
  int32_t& wander,
  uint8_t& unit) const
{
  if ((_unix_time_sec == 0) ||
      (_time_wander < 0) ||
      (_received_moment == 0) ||
      (_timeSource == timeSource_t::No_time_source)) {
    return timeSource_t::No_time_source;
  }

  if (timePassedSince(_first_received_moment) < 30000) {
    // Make sure to allow for enough time to collect the "best" option.
    return timeSource_t::No_time_source;
  }

  unit = _unit;

  const int32_t timePassed = timePassedSince(_received_moment);

  const int64_t unix_time_usec =
    sec_time_frac_to_Micros(_unix_time_sec, _unix_time_frac) +
    (static_cast<int64_t>(timePassed) * 1000ll); // Add time since it was received

  unix_time_d = static_cast<double>(unix_time_usec) / 1000000.0;

  wander = updateExpectedWander(_time_wander, timePassed);

  // FIXME TD-er: Must somehow know whether the p2p node was seen via UDP or ESPEasy-NOW
  return timeSource_t::ESPEASY_p2p_UDP;
}

#endif // if FEATURE_ESPEASY_P2P

#include "../DataStructs/Scheduler_IntendedRebootTimerID.h"


IntendedRebootTimerID::IntendedRebootTimerID(IntendedRebootReason_e reason) :
  SchedulerTimerID(SchedulerTimerType_e::IntendedReboot)
{
  setId(static_cast<uint32_t>(reason));
}


#include "../DataStructs/ShortChecksumType.h"

#include "../Helpers/StringConverter.h"

#include <MD5Builder.h>

void ShortChecksumType::md5sumToShortChecksum(const uint8_t md5[16], uint8_t shortChecksum[4])
{
  memset(shortChecksum, 0, 4);

  for (uint8_t i = 0; i < 16; ++i) {
    // shortChecksum is XOR per 32 bit
    shortChecksum[i % 4] ^= md5[i];
  }
}

ShortChecksumType::ShortChecksumType(const ShortChecksumType& rhs)
{
  memcpy(_checksum, rhs._checksum, 4);
}

ShortChecksumType::ShortChecksumType(uint8_t checksum[4])
{
  memcpy(_checksum, checksum, 4);
}

ShortChecksumType::ShortChecksumType(const uint8_t *data,
                                     size_t         data_length)
{
  computeChecksum(_checksum, data, data_length, data_length, true);
}

ShortChecksumType::ShortChecksumType(const uint8_t *data,
                                     size_t         data_length,
                                     size_t         len_upto_checksum)
{
  computeChecksum(_checksum, data, data_length, len_upto_checksum, true);
}

ShortChecksumType::ShortChecksumType(const String strings[], size_t nrStrings)
{
  MD5Builder md5;

  md5.begin();

  for (size_t i = 0; i < nrStrings; ++i) {
    md5.add(strings[i].c_str());
  }
  md5.calculate();
  uint8_t tmp_md5[16] = { 0 };

  md5.getBytes(tmp_md5);
  md5sumToShortChecksum(tmp_md5, _checksum);
}

bool ShortChecksumType::computeChecksum(
  uint8_t        checksum[4],
  const uint8_t *data,
  size_t         data_length,
  size_t         len_upto_checksum,
  bool           updateChecksum)
{
  if (len_upto_checksum > data_length) { len_upto_checksum = data_length; }
  MD5Builder md5;

  md5.begin();

  if (len_upto_checksum > 0) {
    // MD5Builder::add has non-const argument
    md5.add(const_cast<uint8_t *>(data), len_upto_checksum);
  }

  if ((len_upto_checksum + 4) < data_length) {
    data += len_upto_checksum + 4;
    const int len_after_checksum = data_length - 4 - len_upto_checksum;

    if (len_after_checksum > 0) {
      // MD5Builder::add has non-const argument
      md5.add(const_cast<uint8_t *>(data), len_after_checksum);
    }
  }
  md5.calculate();
  uint8_t tmp_checksum[4] = { 0 };

  {
    uint8_t tmp_md5[16] = { 0 };
    md5.getBytes(tmp_md5);
    md5sumToShortChecksum(tmp_md5, tmp_checksum);
  }

  if (memcmp(tmp_checksum, checksum, 4) != 0) {
    // Data has changed, copy computed checksum
    if (updateChecksum) {
      memcpy(checksum, tmp_checksum, 4);
    }
    return false;
  }
  return true;
}

void ShortChecksumType::getChecksum(uint8_t checksum[4]) const {
  memcpy(checksum, _checksum, 4);
}

void ShortChecksumType::setChecksum(const uint8_t checksum[4]) {
  memcpy(_checksum, checksum, 4);
}

bool ShortChecksumType::matchChecksum(const uint8_t checksum[4]) const {
  return memcmp(_checksum, checksum, 4) == 0;
}

bool ShortChecksumType::operator==(const ShortChecksumType& rhs) const {
  return memcmp(_checksum, rhs._checksum, 4) == 0;
}

ShortChecksumType& ShortChecksumType::operator=(const ShortChecksumType& rhs) {
  memcpy(_checksum, rhs._checksum, 4);
  return *this;
}

String ShortChecksumType::toString() const {
  return formatToHex_array(_checksum, 4);
}

bool ShortChecksumType::isSet() const {
  return
    _checksum[0] != 0 ||
    _checksum[1] != 0 ||
    _checksum[2] != 0 ||
    _checksum[3] != 0;
}

void ShortChecksumType::clear()
{
  memset(_checksum, 0, 4);
}

#include "../DataStructs/WiFi_AP_Candidates_NVS.h"

#ifdef ESP32
# include "../Helpers/ESPEasy_NVS_Helper.h"

# define WIFI_AP_CANDIDATE_NVS_KEY "WIFICANDIDATE"

struct WiFi_AP_Candidates_NVS_data_t {
  union {
    struct {
      uint8_t BSSID[6];
      uint8_t lastWiFiChannel;
      uint8_t lastWiFiSettingsIndex;
    } APdata;

    uint64_t rawdata{};
  };
};

bool WiFi_AP_Candidates_NVS::loadCandidate_from_NVS(WiFi_AP_Candidate& candidate)
{
  WiFi_AP_Candidates_NVS_data_t fromNVS;

  {
    ESPEasy_NVS_Helper preferences;

    if (!preferences.begin(F(WIFI_CONNECTION_NVS_NAMESPACE))) {
      return false;
    }

    if (!preferences.getPreference(F(WIFI_AP_CANDIDATE_NVS_KEY), fromNVS.rawdata)) {
      return false;
    }
  }
  candidate.bssid.set(fromNVS.APdata.BSSID);
  candidate.channel = fromNVS.APdata.lastWiFiChannel;
  candidate.index   = fromNVS.APdata.lastWiFiSettingsIndex;
  return true;
}

void WiFi_AP_Candidates_NVS::currentConnection_to_NVS(const WiFi_AP_Candidate& candidate)
{
  ESPEasy_NVS_Helper preferences;

  preferences.begin(F(WIFI_CONNECTION_NVS_NAMESPACE));
  WiFi_AP_Candidates_NVS_data_t toNVS;

  candidate.bssid.get(toNVS.APdata.BSSID);
  toNVS.APdata.lastWiFiChannel       = candidate.channel;
  toNVS.APdata.lastWiFiSettingsIndex = candidate.index;

  preferences.setPreference(F(WIFI_AP_CANDIDATE_NVS_KEY), toNVS.rawdata);
}

void WiFi_AP_Candidates_NVS::clear_from_NVS()
{
  ESPEasy_NVS_Helper preferences;

  if (preferences.begin(F(WIFI_CONNECTION_NVS_NAMESPACE))) {
    preferences.remove(F(WIFI_AP_CANDIDATE_NVS_KEY));
  }
}

#endif // ifdef ESP32

#include "../DataStructs/NodesHandler.h"

#include "../../ESPEasy_common.h"

#if FEATURE_ESPEASY_P2P
#include "../../ESPEasy-Globals.h"

#ifdef USES_ESPEASY_NOW
#include "../Globals/ESPEasy_now_peermanager.h"
#include "../Globals/ESPEasy_now_state.h"
#endif

#include "../Globals/EventQueue.h"

#include "../DataTypes/NodeTypeID.h"

#if FEATURE_MQTT
#include "../ESPEasyCore/Controller.h"
#endif

#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../ESPEasyCore/ESPEasyNetwork.h"
#include "../ESPEasyCore/ESPEasyWifi.h"
#include "../Globals/ESPEasy_time.h"
#include "../Globals/ESPEasyWiFiEvent.h"
#include "../Globals/MQTT.h"
#include "../Globals/NetworkState.h"
#include "../Globals/RTC.h"
#include "../Globals/Settings.h"
#include "../Helpers/ESPEasy_time_calc.h"
#include "../Helpers/Misc.h"
#include "../Helpers/PeriodicalActions.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringGenerator_System.h"

#define ESPEASY_NOW_ALLOWED_AGE_NO_TRACEROUTE  35000

bool NodesHandler::addNode(const NodeStruct& node)
{
  int8_t rssi = 0;
  MAC_address match_sta;
  MAC_address match_ap;
  MAC_address ESPEasy_NOW_MAC;

  bool isNewNode = true;

  // Erase any existing node with matching MAC address
  for (auto it = _nodes.begin(); it != _nodes.end(); )
  {
    const MAC_address sta = it->second.sta_mac;
    const MAC_address ap  = it->second.ap_mac;
    if ((!sta.all_zero() && node.match(sta)) || (!ap.all_zero() && node.match(ap))) {
      rssi = it->second.getRSSI();
      if (!sta.all_zero())
        match_sta = sta;
      if (!ap.all_zero())
        match_ap = ap;
      ESPEasy_NOW_MAC = it->second.ESPEasy_Now_MAC();

      isNewNode = false;
      {
        _nodes_mutex.lock();
        it = _nodes.erase(it);
        _nodes_mutex.unlock();
      }
    } else {
      ++it;
    }
  }
  bool ntp_candidate_updated = false;
  {
    _nodes_mutex.lock();
    {
      #ifdef USE_SECOND_HEAP
      // FIXME TD-er: Must check whether this is working well as the NodesMap is a std::map
      HeapSelectIram ephemeral;
      #endif
      _nodes[node.unit] = node;
    }
    // Make sure to set first as NTP candidate, as it was set to time since
    // last time sync by the sender node before sending.
    ntp_candidate_updated = _ntp_candidate.set(node);
    // Now set lastUpdated so we can keep track of its age.
    _nodes[node.unit].lastUpdated = millis();
    if (node.getRSSI() >= 0 && rssi < 0) {
      _nodes[node.unit].setRSSI(rssi);
    }
    const MAC_address node_ap(node.ap_mac);
    if (node_ap.all_zero()) {
      _nodes[node.unit].setAP_MAC(node_ap);
    }
    if (node.ESPEasy_Now_MAC().all_zero()) {
      _nodes[node.unit].setESPEasyNow_mac(ESPEasy_NOW_MAC);
    }
    _nodes_mutex.unlock();
  }

  // Check whether the current time source is considered "worse" than received from p2p node.
  if (!node_time.systemTimePresent() ||
      (node_time.getTimeSource() > timeSource_t::ESPEASY_p2p_UDP) ||
      (timePassedSince(node_time.lastSyncTime_ms) > EXT_TIME_SOURCE_MIN_UPDATE_INTERVAL_MSEC)) {
    double  unixTime{};
    uint8_t unit                  = 0;
    int32_t wander                = -1;
    const timeSource_t timeSource = _ntp_candidate.getUnixTime(unixTime, wander, unit);

    if (timeSource != timeSource_t::No_time_source) {
      bool shouldUpdate = false;

      if (!node_time.systemTimePresent()) {
        if (timeSource < timeSource_t::ESP_now_peer || getUptimeMinutes() > 0) {
          // After 1 minute we should have had at least 2 loops of p2p nodes announcing their time source.
          // Though no need to wait longer if we already got a good source
          shouldUpdate = true;
        }
      } else if (ntp_candidate_updated) {
        // Got a better time source
        shouldUpdate = true;
      }

      if (shouldUpdate) {
        node_time.setExternalTimeSource_withTimeWander(unixTime, timeSource, wander, unit);
      }
    }
  }

  if (isNewNode) {
    if (Settings.UseRules && (node.unit != 0))
    {
      // Generate event announcing new p2p node
      // TODO TD-er: Maybe also add other info like ESP type, IP-address, etc?
      eventQueue.addMove(strformat(
                           F("p2pNode#Connected=%d,'%s','%s'"),
                           node.unit,
                           node.getNodeName().c_str(),
                           formatSystemBuildNr(node.build).c_str()
                           ));
    }
  }

  return isNewNode;
}

#ifdef USES_ESPEASY_NOW
bool NodesHandler::addNode(const NodeStruct& node, const ESPEasy_now_traceroute_struct& traceRoute)
{
  const bool isNewNode = addNode(node);
  {
    _nodeStats_mutex.lock();
    _nodeStats[node.unit].setDiscoveryRoute(node.unit, traceRoute);
    _nodeStats_mutex.unlock();
  }

  ESPEasy_now_peermanager.addPeer(node.ESPEasy_Now_MAC(), node.channel);  

  if (!node.isThisNode()) {
    if (traceRoute.getDistance() != 255) {
      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        String log;
        if (reserve_special(log, 80)) {
          log  = F(ESPEASY_NOW_NAME);
          log += F(": Node: ");
          log += String(node.unit);
          log += F(" DiscoveryRoute received: ");
          log += traceRoute.toString();
          addLog(LOG_LEVEL_INFO, log);
        }
      }
    } else {}
  }
  return isNewNode;
}
#endif

bool NodesHandler::hasNode(uint8_t unit_nr) const
{
  return _nodes.find(unit_nr) != _nodes.end();
}

bool NodesHandler::hasNode(const uint8_t *mac) const
{
  return getNodeByMac(mac) != nullptr;
}

NodeStruct * NodesHandler::getNode(uint8_t unit_nr)
{
  auto it = _nodes.find(unit_nr);

  if (it == _nodes.end()) {
    return nullptr;
  }
  return &(it->second);
}

const NodeStruct * NodesHandler::getNode(uint8_t unit_nr) const
{
  auto it = _nodes.find(unit_nr);

  if (it == _nodes.end()) {
    return nullptr;
  }
  return &(it->second);
}

NodeStruct * NodesHandler::getNodeByMac(const MAC_address& mac)
{
  if (mac.all_zero()) {
    return nullptr;
  }
  delay(0);

  for (auto it = _nodes.begin(); it != _nodes.end(); ++it)
  {
    if (mac == it->second.sta_mac) {
      return &(it->second);
    }

    if (mac == it->second.ap_mac) {
      return &(it->second);
    }
  }
  return nullptr;
}

const NodeStruct * NodesHandler::getNodeByMac(const MAC_address& mac) const
{
  bool match_STA;

  return getNodeByMac(mac, match_STA);
}

const NodeStruct * NodesHandler::getNodeByMac(const MAC_address& mac, bool& match_STA) const
{
  if (mac.all_zero()) {
    return nullptr;
  }
  delay(0);

  for (auto it = _nodes.begin(); it != _nodes.end(); ++it)
  {
    if (mac == it->second.sta_mac) {
      match_STA = true;
      return &(it->second);
    }

    if (mac == it->second.ap_mac) {
      match_STA = false;
      return &(it->second);
    }
  }
  return nullptr;
}

const NodeStruct * NodesHandler::getPreferredNode() const {
  MAC_address dummy;

  return getPreferredNode_notMatching(dummy);
}

const NodeStruct* NodesHandler::getPreferredNode_notMatching(uint8_t unit_nr) const {
  MAC_address not_matching;
  if (unit_nr != 0 && unit_nr != 255) {
    const NodeStruct* node = getNode(unit_nr);
    if (node != nullptr) {
      not_matching = node->ESPEasy_Now_MAC();
    }
  }
  return getPreferredNode_notMatching(not_matching);
}

const NodeStruct * NodesHandler::getPreferredNode_notMatching(const MAC_address& not_matching) const {
  MAC_address this_mac = NetworkMacAddress();
  const NodeStruct *thisNode = getNodeByMac(this_mac);
  const NodeStruct *reject   = getNodeByMac(not_matching);

  const NodeStruct *res = nullptr;

  for (auto it = _nodes.begin(); it != _nodes.end(); ++it)
  {
    if ((&(it->second) != reject) && (&(it->second) != thisNode)) {
      bool mustSet = false;
      if (res == nullptr) {
        mustSet = true;
      } else {
        #ifdef USES_ESPEASY_NOW

        uint8_t distance_new, distance_res = 255;

        const int successRate_new = getRouteSuccessRate(it->second.unit, distance_new);
        const int successRate_res = getRouteSuccessRate(res->unit, distance_res);

        if (successRate_new == 0 || successRate_res == 0) {
          // One of the nodes does not (yet) have a route.
          if (successRate_new == 0 && successRate_res == 0) {
            distance_new = it->second.distance;
            distance_res = res->distance;
          } else if (successRate_res == 0) {
            // The new one has a route, so must set the new one.
            distance_res = res->distance;
            if (distance_new < 255) {
              mustSet = true;
            }
          }
        }

        if (distance_new == distance_res) {
          if (successRate_new > successRate_res && distance_new < 255) {
            mustSet = true;
          }
        } else if (distance_new < distance_res) {
          if (it->second.getAge() < ESPEASY_NOW_ALLOWED_AGE_NO_TRACEROUTE) {
            // Only allow this new one if it was seen recently 
            // as it does not (yet) have a traceroute.
            mustSet = true;
          }
        }
        #else
        if (it->second < *res) {
            mustSet = true;
        }
        #endif
      }
      if (mustSet) {
        #ifdef USES_ESPEASY_NOW
        if (it->second.ESPEasyNowPeer && it->second.distance < 255) {
          res = &(it->second);
        }
        #else
        res = &(it->second);
        #endif
      }
    }
  }

/*
  #ifdef USES_ESPEASY_NOW
  if (res != nullptr)
  {
    uint8_t distance_res = 255;
    const int successRate_res = getRouteSuccessRate(res->unit, distance_res);
    if (distance_res == 255) {
      return nullptr;
    }
  }
  #endif
*/

  return res;
}

#ifdef USES_ESPEASY_NOW
const ESPEasy_now_traceroute_struct* NodesHandler::getTraceRoute(uint8_t unit) const
{
  auto trace_it = _nodeStats.find(unit);
  if (trace_it == _nodeStats.end()) {
    return nullptr;
  }
  return trace_it->second.bestRoute();
}

const ESPEasy_now_traceroute_struct* NodesHandler::getDiscoveryRoute(uint8_t unit) const
{
  auto trace_it = _nodeStats.find(unit);
  if (trace_it == _nodeStats.end()) {
    return nullptr;
  }
  return &(trace_it->second.discoveryRoute());
}

void NodesHandler::setTraceRoute(const MAC_address& mac, const ESPEasy_now_traceroute_struct& traceRoute)
{
  if (traceRoute.computeSuccessRate() == 0) {
    // No need to store traceroute with low success rate.
    return;
  }
  NodeStruct* node = getNodeByMac(mac);
  if (node != nullptr) {
    auto trace_it = _nodeStats.find(node->unit);
    if (trace_it != _nodeStats.end()) {
      _lastTimeValidDistance = millis();
      trace_it->second.addRoute(node->unit, traceRoute);
    }
  }
}

#endif


void NodesHandler::updateThisNode() {
  NodeStruct thisNode;

  // Set local data
  {
    MAC_address mac = NetworkMacAddress();
    mac.get(thisNode.sta_mac);
  }
  WiFi.softAPmacAddress(thisNode.ap_mac);
  {
    const bool addIP = NetworkConnected();
    #ifdef USES_ESPEASY_NOW
    if (use_EspEasy_now) {
      thisNode.useAP_ESPEasyNow = 1;
    }
    #endif
    if (addIP) {
      const IPAddress localIP = NetworkLocalIP();

      for (uint8_t i = 0; i < 4; ++i) {
        thisNode.ip[i] = localIP[i];
      }
    }
  }
  #ifdef USES_ESPEASY_NOW
  thisNode.channel = getESPEasyNOW_channel();
  #else
  thisNode.channel = WiFiEventData.usedChannel;
  #endif
  if (thisNode.channel == 0) {
    thisNode.channel = WiFi.channel();
  }

  thisNode.unit  = Settings.Unit;
  thisNode.build = Settings.Build;
  memcpy(thisNode.nodeName, Settings.getName().c_str(), 25);
  thisNode.nodeType = NODE_TYPE_ID;

  thisNode.webgui_portnumber = Settings.WebserverPort;
  const int load_int = getCPUload() * 2.55;

  if (load_int > 255) {
    thisNode.load = 255;
  } else {
    thisNode.load = load_int;
  }
  thisNode.timeSource = static_cast<uint8_t>(node_time.getTimeSource());

  switch (node_time.getTimeSource()) {
    case timeSource_t::No_time_source:
      thisNode.lastUpdated = (1 << 30);
      break;
    default:
    {
      thisNode.lastUpdated = timePassedSince(node_time.lastSyncTime_ms);
      break;
    }
  }
  if (node_time.systemTimePresent()) {
    // NodeStruct is a packed struct, so we cannot directly use its members as a reference.
    uint32_t unix_time_frac = 0;
    thisNode.unix_time_sec = node_time.getUnixTime(unix_time_frac);
    thisNode.unix_time_frac = unix_time_frac;
  }
  #ifdef USES_ESPEASY_NOW
  if (Settings.UseESPEasyNow()) {
    thisNode.ESPEasyNowPeer = 1;
  }
  #endif

  const uint8_t lastDistance = _distance;
  #ifdef USES_ESPEASY_NOW
  ESPEasy_now_traceroute_struct thisTraceRoute;
  #endif
  if (isEndpoint()) {
    _distance = 0;
    _lastTimeValidDistance = millis();
    if (lastDistance != _distance) {
      _recentlyBecameDistanceZero = true;
    }
    #ifdef USES_ESPEASY_NOW
    thisNode.distance = _distance;
    thisNode.setRSSI(WiFi.RSSI());
    thisTraceRoute.addUnit(thisNode.unit);
    #endif
  } else {
    _distance = 255;
    #ifdef USES_ESPEASY_NOW
    const NodeStruct *preferred = getPreferredNode_notMatching(thisNode.sta_mac);

    if (preferred != nullptr) {
      if (!preferred->isExpired()) {
        // Only take the distance of another node if it is running a build which does not send out traceroute
        // If it is a build sending traceroute, only consider having a distance if you know how to reach the gateway node
        // This does impose an issue when a gateway node is running an older version, as the next hops never will have a traceroute too.
        // Therefore the reported build for those units will be faked to be an older version.
        if (preferred->build < 20113) {
          if (preferred->distance != 255) {
            _distance = preferred->distance + 1;
            thisNode.build = 20112;
          }
        } else {
          const ESPEasy_now_traceroute_struct* tracert_ptr = getTraceRoute(preferred->unit);
          if (tracert_ptr != nullptr && tracert_ptr->getDistance() < 255) {
            // Make a copy of the traceroute
            thisTraceRoute = *tracert_ptr;
            thisTraceRoute.addUnit(thisNode.unit);
            if (preferred->distance != 255) {
              // Traceroute is only updated when a node is connected.
              // Thus the traceroute may be outdated, while the node info will already indicate if a node has lost its route to the gateway node.
              // So we only must set the distance of this node if the preferred node has a distance.
              _distance = thisTraceRoute.getDistance();  // This node is already included in the traceroute.
            }
          }
        }
      }
    }
    #endif
  }
  thisNode.distance = _distance;

  #if FEATURE_USE_IPV6
  thisNode.hasIPv4 = thisNode.IP() != INADDR_NONE;
  thisNode.hasIPv6_mac_based_link_local = is_IPv6_link_local_from_MAC(thisNode.sta_mac);
  thisNode.hasIPv6_mac_based_link_global = is_IPv6_global_from_MAC(thisNode.sta_mac);
  #endif

  #ifdef USES_ESPEASY_NOW
  addNode(thisNode, thisTraceRoute);
  if (thisNode.distance == 0) {
    // Since we're the end node, claim highest success rate
    updateSuccessRate(thisNode.unit, 255);
  }
  #else
  addNode(thisNode);
  #endif
}

const NodeStruct * NodesHandler::getThisNode() {
//  node_time.now();
  updateThisNode();
  MAC_address this_mac = NetworkMacAddress();
  return getNodeByMac(this_mac.mac);
}

uint8_t NodesHandler::getDistance() const {
  // Perform extra check since _distance is only updated once every 30 seconds.
  // And we don't want to tell other nodes we have distance 0 when we haven't.
  if (isEndpoint()) return 0;
  if (_distance == 0) {
    // Outdated info, so return "we don't know"
    return 255;
  }
  return _distance;
}


NodesMap::const_iterator NodesHandler::begin() const {
  return _nodes.begin();
}

NodesMap::const_iterator NodesHandler::end() const {
  return _nodes.end();
}

NodesMap::const_iterator NodesHandler::find(uint8_t unit_nr) const
{
  return _nodes.find(unit_nr);
}

bool NodesHandler::refreshNodeList(unsigned long max_age_allowed, unsigned long& max_age)
{
  max_age = 0;
  bool nodeRemoved = false;

  for (auto it = _nodes.begin(); it != _nodes.end();) {
    unsigned long age = it->second.getAge();
    if (age > max_age_allowed) {
      bool mustErase = true;
      #ifdef USES_ESPEASY_NOW
      auto route_it = _nodeStats.find(it->second.unit);
      if (route_it != _nodeStats.end()) {
        if (route_it->second.getAge() > max_age_allowed) {
          _nodeStats_mutex.lock();
          _nodeStats.erase(route_it);
          _nodeStats_mutex.unlock();
        } else {
          mustErase = false;
        }
      }
      #endif
      if (mustErase) {
        if (Settings.UseRules && it->second.unit != 0)
        {
          // Add event about removing node from nodeslist.
          eventQueue.addMove(strformat(F("p2pNode#Disconnected=%d"), it->second.unit));
        }
        {
          _nodes_mutex.lock();
          it          = _nodes.erase(it);
          _nodes_mutex.unlock();
        }
        nodeRemoved = true;
      }
    } else {
      ++it;

      if (age > max_age) {
        max_age = age;
      }
    }
  }
  return nodeRemoved;
}

// FIXME TD-er: should be a check per controller to see if it will accept messages
bool NodesHandler::isEndpoint() const
{
  // FIXME TD-er: Must check controller to see if it needs wifi (e.g. LoRa or cache controller do not need it)
  #if FEATURE_MQTT
  controllerIndex_t enabledMqttController = firstEnabledMQTT_ControllerIndex();
  if (validControllerIndex(enabledMqttController)) {
    // FIXME TD-er: Must call updateMQTTclient_connected() and see what effect
    // the MQTTclient_connected state has when using ESPEasy-NOW.
    return MQTTclient_connected;
  }
  #endif

  if (!NetworkConnected()) return false;

  return false;
}

#ifdef USES_ESPEASY_NOW
uint8_t NodesHandler::getESPEasyNOW_channel() const
{
  if (active_network_medium == NetworkMedium_t::WIFI && NetworkConnected()) {
    return WiFi.channel();
  }
  if (Settings.ForceESPEasyNOWchannel > 0) {
    return Settings.ForceESPEasyNOWchannel;
  }
  if (isEndpoint()) {
    if (active_network_medium == NetworkMedium_t::WIFI) {
      return WiFi.channel();
    }
  }
  const NodeStruct *preferred = getPreferredNode();
  if (preferred != nullptr) {
    if (preferred->distance < 255) {
      return preferred->channel;
    }
  }
  return WiFiEventData.usedChannel;
}
#endif

bool NodesHandler::recentlyBecameDistanceZero() {
  if (!_recentlyBecameDistanceZero) {
    return false;
  }
  _recentlyBecameDistanceZero = false;
  return true;
}

void NodesHandler::setRSSI(const MAC_address& mac, int rssi)
{
  setRSSI(getNodeByMac(mac), rssi);
}

void NodesHandler::setRSSI(uint8_t unit, int rssi)
{
  setRSSI(getNode(unit), rssi);
}

void NodesHandler::setRSSI(NodeStruct * node, int rssi)
{
  if (node != nullptr) {
    node->setRSSI(rssi);
  }
}

bool NodesHandler::lastTimeValidDistanceExpired() const
{
//  if (_lastTimeValidDistance == 0) return false;
  return timePassedSince(_lastTimeValidDistance) > 120000; // 2 minutes
}

#ifdef USES_ESPEASY_NOW
void NodesHandler::updateSuccessRate(uint8_t unit, bool success)
{
  auto it = _nodeStats.find(unit);
  if (it != _nodeStats.end()) {
    it->second.updateSuccessRate(unit, success);
  }
}

void NodesHandler::updateSuccessRate(const MAC_address& mac, bool success)
{
  const NodeStruct * node = getNodeByMac(mac);
  if (node == nullptr) {
    return;
  }
  updateSuccessRate(node->unit, success);
}

int NodesHandler::getRouteSuccessRate(uint8_t unit, uint8_t& distance) const
{
  distance = 255;
  auto it = _nodeStats.find(unit);
  if (it != _nodeStats.end()) {
    const ESPEasy_now_traceroute_struct* route = it->second.bestRoute();
    if (route != nullptr) {
      distance = route->getDistance();
      return route->computeSuccessRate();
    }
  }
  return 0;
}

uint8_t NodesHandler::getSuccessRate(uint8_t unit) const
{
  auto it = _nodeStats.find(unit);
  if (it != _nodeStats.end()) {
    return it->second.getNodeSuccessRate();
  }
  return 127;
}

ESPEasy_Now_MQTT_QueueCheckState::Enum NodesHandler::getMQTTQueueState(uint8_t unit) const
{
  auto it = _nodeStats.find(unit);
  if (it != _nodeStats.end()) {
    return it->second.getMQTTQueueState();
  }
  return ESPEasy_Now_MQTT_QueueCheckState::Enum::Unset;

}

void NodesHandler::setMQTTQueueState(uint8_t unit, ESPEasy_Now_MQTT_QueueCheckState::Enum state)
{
  auto it = _nodeStats.find(unit);
  if (it != _nodeStats.end()) {
    it->second.setMQTTQueueState(state);
  }
}

void NodesHandler::setMQTTQueueState(const MAC_address& mac, ESPEasy_Now_MQTT_QueueCheckState::Enum state)
{
  const NodeStruct * node = getNodeByMac(mac);
  if (node != nullptr) {
    setMQTTQueueState(node->unit, state);
  }
}

#endif

#endif
#include "../DataStructs/WiFi_AP_Candidate.h"

#include "../Globals/ESPEasyWiFiEvent.h"
#include "../Globals/SecuritySettings.h"
#include "../Globals/Statistics.h"
#include "../Helpers/ESPEasy_time_calc.h"
#include "../Helpers/Misc.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringGenerator_WiFi.h"
#include "../../ESPEasy_common.h"

#if defined(ESP8266)
  # include <ESP8266WiFi.h>
#endif // if defined(ESP8266)
#if defined(ESP32)
  # include <WiFi.h>
#endif // if defined(ESP32)

#define WIFI_AP_CANDIDATE_MAX_AGE   300000  // 5 minutes in msec


WiFi_AP_Candidate::WiFi_AP_Candidate() :
#ifdef ESP32
# if ESP_IDF_VERSION_MAJOR >= 5
country({
    .cc = "01",
    .schan = 1,
    .nchan = 14,
    .policy = WIFI_COUNTRY_POLICY_AUTO,
}),
#endif
#endif
  last_seen(0), rssi(0), channel(0), index(0), enc_type(0)
{
  _allBits = 0u;
}

WiFi_AP_Candidate::WiFi_AP_Candidate(const WiFi_AP_Candidate& other)
: ssid(other.ssid),
  last_seen(other.last_seen), 
  bssid(other.bssid),
  rssi(other.rssi), 
  channel(other.channel), 
  index(other.index), 
  enc_type(other.enc_type)
{
  _allBits = other._allBits;
  #ifdef ESP32
  # if ESP_IDF_VERSION_MAJOR >= 5
  memcpy(&this->country, &other.country, sizeof(wifi_country_t));
  #endif
  #endif
}

WiFi_AP_Candidate::WiFi_AP_Candidate(uint8_t index_c, const String& ssid_c) :
#ifdef ESP32
# if ESP_IDF_VERSION_MAJOR >= 5
country({
    .cc = "01",
    .schan = 1,
    .nchan = 14,
    .policy = WIFI_COUNTRY_POLICY_AUTO,
}),
#endif
#endif
  last_seen(0), rssi(0), channel(0), index(index_c), enc_type(0)
{
  _allBits = 0u;

  const size_t ssid_length = ssid_c.length();

  if ((ssid_length == 0) || equals(ssid_c, F("ssid"))) {
    return;
  }

  if (ssid_length > 32) { return; }

  ssid = ssid_c;
}

WiFi_AP_Candidate::WiFi_AP_Candidate(uint8_t networkItem) : index(0) {
  // Need to make sure the phy isn't known as we can't get this information from the AP
  // See: https://github.com/letscontrolit/ESPEasy/issues/4996
  // Not sure why this makes any difference as the flags should already have been set to 0.
  _allBits = 0u;

  ssid     = WiFi.SSID(networkItem);
  rssi     = WiFi.RSSI(networkItem);
  channel  = WiFi.channel(networkItem);
  bssid    = WiFi.BSSID(networkItem);
  enc_type = WiFi.encryptionType(networkItem);
  #ifdef ESP8266
  bits.isHidden = WiFi.isHidden(networkItem);
  # ifdef CORE_POST_3_0_0
  const bss_info *it = reinterpret_cast<const bss_info *>(WiFi.getScanInfoByIndex(networkItem));

  if (it) {
    bits.phy_11b = it->phy_11b;
    bits.phy_11g = it->phy_11g;
    bits.phy_11n = it->phy_11n;
    bits.wps     = it->wps;
  }
  # endif // ifdef CORE_POST_3_0_0
  #endif // ifdef ESP8266
  #ifdef ESP32
  bits.isHidden = ssid.isEmpty();
  wifi_ap_record_t *it = reinterpret_cast<wifi_ap_record_t *>(WiFi.getScanInfoByIndex(networkItem));

  if (it) {
    bits.phy_11b = it->phy_11b;
    bits.phy_11g = it->phy_11g;
    bits.phy_11n = it->phy_11n;
    bits.phy_lr  = it->phy_lr;
# if ESP_IDF_VERSION_MAJOR >= 5
    bits.phy_11ax      = it->phy_11ax;
    bits.ftm_initiator = it->ftm_initiator;
    bits.ftm_responder = it->ftm_responder;
# endif // if ESP_IDF_VERSION_MAJOR >= 5
    bits.wps = it->wps;

    // FIXME TD-er: Maybe also add other info like 2nd channel, ftm and phy_lr support?
# if ESP_IDF_VERSION_MAJOR >= 5
    memcpy(&country, &(it->country), sizeof(wifi_country_t));
#endif
  }
  #endif // ifdef ESP32
  last_seen = millis();
}

#ifdef ESP8266
# if FEATURE_ESP8266_DIRECT_WIFI_SCAN
WiFi_AP_Candidate::WiFi_AP_Candidate(const bss_info& ap) :
  rssi(ap.rssi), channel(ap.channel), bssid(ap.bssid),
  index(0), enc_type(0), isHidden(ap.is_hidden),
  phy_11b(ap.phy_11b), phy_11g(ap.phy_11g), phy_11n(ap.phy_11n),
  wps(ap.wps)
{
  _allBits = 0u;
  last_seen = millis();

  switch (ap.authmode) {
    case AUTH_OPEN: enc_type         = ENC_TYPE_NONE; break;
    case AUTH_WEP:  enc_type         = ENC_TYPE_WEP; break;
    case AUTH_WPA_PSK: enc_type      =  ENC_TYPE_TKIP; break;
    case AUTH_WPA2_PSK: enc_type     =  ENC_TYPE_CCMP; break;
    case AUTH_WPA_WPA2_PSK: enc_type =  ENC_TYPE_AUTO; break;
    case AUTH_MAX: break;
  }

  char tmp[33]; // ssid can be up to 32chars, => plus null term
  const size_t ssid_len = std::min(static_cast<size_t>(ap.ssid_len), sizeof(ap.ssid));

  memcpy(tmp, ap.ssid, ssid_len);
  tmp[ssid_len] = 0; // nullterm marking end of string

  ssid = String(reinterpret_cast<const char *>(tmp));
}

# endif // if FEATURE_ESP8266_DIRECT_WIFI_SCAN
#endif // ifdef ESP8266


bool WiFi_AP_Candidate::operator<(const WiFi_AP_Candidate& other) const {
  if (bits.isEmergencyFallback != other.bits.isEmergencyFallback) {
    return bits.isEmergencyFallback;
  }

  if (bits.lowPriority != other.bits.lowPriority) {
    return !bits.lowPriority;
  }

  // Prefer non hidden over hidden.
  if (bits.isHidden != other.bits.isHidden) {
    return !bits.isHidden;
  }

  // RSSI values >= 0 are invalid
  if (rssi >= 0) { return false; }

  if (other.rssi >= 0) { return true; }

  // RSSI values are negative, so the larger value is the better one.
  return rssi > other.rssi;
}

WiFi_AP_Candidate& WiFi_AP_Candidate::operator=(const WiFi_AP_Candidate& other)
{
  ssid = other.ssid;
  last_seen = other.last_seen;
  bssid = other.bssid;
  rssi = other.rssi;
  channel = other.channel;
  index = other.index;
  enc_type = other.enc_type;
  _allBits = other._allBits;
  #ifdef ESP32
  # if ESP_IDF_VERSION_MAJOR >= 5
  memcpy(&this->country, &other.country, sizeof(wifi_country_t));
  #endif
  #endif

  return *this;
}

bool WiFi_AP_Candidate::usable() const {
  // Allow for empty pass
  // if (key.isEmpty()) return false;
  if (bits.isEmergencyFallback) {
    int allowedUptimeMinutes = 10;
    #ifdef CUSTOM_EMERGENCY_FALLBACK_ALLOW_MINUTES_UPTIME
    allowedUptimeMinutes = CUSTOM_EMERGENCY_FALLBACK_ALLOW_MINUTES_UPTIME;
    #endif // ifdef CUSTOM_EMERGENCY_FALLBACK_ALLOW_MINUTES_UPTIME

    if ((getUptimeMinutes() > allowedUptimeMinutes) ||
        !SecuritySettings.hasWiFiCredentials() ||
        WiFiEventData.performedClearWiFiCredentials ||
        (lastBootCause != BOOT_CAUSE_COLD_BOOT)) {
      return false;
    }
  }

  if (!bits.isHidden && (ssid.isEmpty())) { return false; }
  return !expired();
}

bool WiFi_AP_Candidate::expired() const {
  if (last_seen == 0) {
    // Not set, so cannot expire
    return false;
  }
  return timePassedSince(last_seen) > WIFI_AP_CANDIDATE_MAX_AGE;
}

String WiFi_AP_Candidate::toString(const String& separator) const {
  String result = ssid;

  htmlEscape(result);

  if (bits.isHidden) {
    result += F("#Hidden#");
  }
  result += strformat(
    F("%s%s%sCh:%u"),
    separator.c_str(),
    bssid.toString().c_str(),
    separator.c_str(),
    channel);

  if (rssi == -1) {
    result += F(" (RTC) ");
  } else {
    result += strformat(F(" (%ddBm) "), rssi);
  }

  result += encryption_type();

#ifdef ESP32
# if ESP_IDF_VERSION_MAJOR >= 5
  // Country code string
  if (country.cc[0] != '\0' && country.cc[1] != '\0') {
    result += strformat(F(" '%c%c'"), country.cc[0], country.cc[1]);
    switch (country.cc[2]) {
      case 'O': // Outdoor
      case 'I': // Indoor
      case 'X': // "non-country"
        result += strformat(F("(%c)"), country.cc[2]);
        break;
    }
  }
  if (country.nchan > 0) {
    result += strformat(F(" ch: %d..%d"), country.schan, country.schan + country.nchan - 1);
  }
#endif
#endif

  if (phy_known()) {
    String phy_str;

    if (bits.phy_11b) { phy_str += 'b'; }

    if (bits.phy_11g) { phy_str += 'g'; }

    if (bits.phy_11n) { phy_str += 'n'; }
#ifdef ESP32

    if (bits.phy_11ax) { phy_str += F("/ax"); }

    if (bits.phy_lr) { phy_str += F("/lr"); }

    if (bits.ftm_initiator) { phy_str += F("/FTM_i"); }

    if (bits.ftm_responder) { phy_str += F("/FTM_r"); }
#endif // ifdef ESP32

    if (phy_str.length()) {
      result += strformat(F(" (%s)"), phy_str.c_str());
    }
  }
  return result;
}

String WiFi_AP_Candidate::encryption_type() const {
  return WiFi_encryptionType(enc_type);
}

#include "../DataStructs/ESPEasy_packed_raw_data.h"

#if FEATURE_PACKED_RAW_DATA

uint8_t getPackedDataTypeSize(PackedData_enum dtype, float& factor, float& offset) {
  offset = 0;
  factor = 1;
  if (dtype > 0x1000 && dtype < 0x12FF) {
    const uint32_t exponent = dtype & 0xF;
    switch(exponent) {
      case 0: factor = 1; break;
      case 1: factor = 1e1; break;
      case 2: factor = 1e2; break;
      case 3: factor = 1e3; break;
      case 4: factor = 1e4; break;
      case 5: factor = 1e5; break;
      case 6: factor = 1e6; break;
    }
    const uint8_t size = (dtype >> 4) & 0xF;
    return size;
  }
  switch (dtype) {
    case PackedData_pluginid:    factor = 1;         return 1;
    case PackedData_latLng:      factor = 46600;     return 3; // 2^23 / 180
    case PackedData_hdop:        factor = 10;        return 1;
    case PackedData_altitude:    factor = 4;     offset = 1000; return 2; // -1000 .. 15383.75 meter
    case PackedData_vcc:         factor = 41.83; offset = 1;    return 1; // -1 .. 5.12V
    case PackedData_pct_8:       factor = 2.56;                 return 1; // 0 .. 100%
    default:
      break;
  }

  // Unknown type
  factor = 1;
  return 0;
}

void LoRa_uintToBytes(uint64_t value, uint8_t byteSize, uint8_t *data, uint8_t& cursor) {
  // Clip values to upper limit
  const uint64_t upperlimit = (1ull << (8*byteSize)) - 1;
  if (value > upperlimit) { value = upperlimit; }
  for (uint8_t x = 0; x < byteSize; x++) {
    uint8_t next = 0;
    if (sizeof(value) > x) {
      next = static_cast<uint8_t>((value >> (x * 8)) & 0xFF);
    }
    data[cursor] = next;
    ++cursor;
  }
}

void LoRa_intToBytes(int64_t value, uint8_t byteSize, uint8_t *data, uint8_t& cursor) {
  // Clip values to lower limit
  const int64_t lowerlimit = (1ull << ((8*byteSize) - 1)) * -1;
  if (value < lowerlimit) { value = lowerlimit; }
  if (value < 0) {
    value += (1ull << (8*byteSize));
  }
  LoRa_uintToBytes(value, byteSize, data, cursor);
}

String LoRa_base16Encode(uint8_t *data, size_t size) {
  String output;
  output.reserve(size * 2);
  char buffer[3];
  for (unsigned i=0; i<size; i++)
  {
    sprintf_P(buffer, PSTR("%02X"), data[i]);
    output += buffer[0];
    output += buffer[1];
  }
  return output;
}

String LoRa_addInt(uint64_t value, PackedData_enum datatype) {
  float factor, offset;
  uint8_t byteSize = getPackedDataTypeSize(datatype, factor, offset);
  uint8_t data[4] = {0};
  uint8_t cursor = 0;
  LoRa_uintToBytes((value + offset) * factor, byteSize, &data[0], cursor);
  return LoRa_base16Encode(data, cursor);
}


String LoRa_addFloat(float value, PackedData_enum datatype) {
  float factor, offset;
  uint8_t byteSize = getPackedDataTypeSize(datatype, factor, offset);
  uint8_t data[4] = {0};
  uint8_t cursor = 0;
  LoRa_intToBytes((value + offset) * factor, byteSize, &data[0], cursor);
  return LoRa_base16Encode(data, cursor);
}

#endif
#include "../DataStructs/TimeChangeRule.h"



TimeChangeRule::TimeChangeRule() :  week(0), dow(1), month(1), hour(0), offset(0) {}

TimeChangeRule::TimeChangeRule(uint8_t weeknr, uint8_t downr, uint8_t m, uint8_t h, int16_t minutesoffset) :
  week(weeknr), dow(downr), month(m), hour(h), offset(minutesoffset) {}

// Construct time change rule from stored values optimized for minimum space.
TimeChangeRule::TimeChangeRule(uint16_t flash_stored_value, int16_t minutesoffset) : offset(minutesoffset) {
  hour  = flash_stored_value & 0x001f;
  month = (flash_stored_value >> 5) & 0x000f;
  dow   = (flash_stored_value >> 9) & 0x0007;
  week  = (flash_stored_value >> 12) & 0x0007;
}

uint16_t TimeChangeRule::toFlashStoredValue() const {
  uint16_t value = hour;

  value = value | (month << 5);
  value = value | (dow << 9);
  value = value | (week << 12);
  return value;
}

bool TimeChangeRule::isValid() const {
  return (week <= 4) && (dow != 0) && (dow <= 7) &&
          (month != 0) && (month <= 12) && (hour <= 23) &&
          (offset > -720) && (offset < 900); // UTC-12h ... UTC+14h + 1h DSToffset
}

#include "../DataStructs/EventQueue.h"

#include "../../ESPEasy_common.h"

#include "../Globals/Settings.h"
#include "../Helpers/Misc.h"
#include "../Helpers/StringConverter.h"



void EventQueueStruct::add(const String& event, bool deduplicate)
{
  if (!deduplicate || !isDuplicate(event)) {
#if defined(USE_SECOND_HEAP) || defined(ESP32)
    String tmp;
    reserve_special(tmp, event.length());
    tmp = event;

#ifdef USE_SECOND_HEAP
    // Do not add to the list while on 2nd heap
    HeapSelectDram ephemeral;
#endif


    _eventQueue.emplace_back(std::move(tmp));
#else
    _eventQueue.push_back(event);
#endif
  }
}

void EventQueueStruct::add(const __FlashStringHelper *event, bool deduplicate)
{
  String str;
  move_special(str, String(event));
  add(str, deduplicate);
}

void EventQueueStruct::addMove(String&& event, bool deduplicate)
{
  if (!event.length()) { return; }

  if (!deduplicate || !isDuplicate(event)) {
    #if defined(USE_SECOND_HEAP) || defined(ESP32)
    String tmp;
    move_special(tmp, std::move(event));

    #ifdef USE_SECOND_HEAP
    // Do not add to the list while on 2nd heap
    HeapSelectDram ephemeral;
    #endif
    _eventQueue.emplace_back(std::move(tmp));
    #else
    _eventQueue.emplace_back(std::move(event));
    #endif // ifdef USE_SECOND_HEAP
  }
}

void EventQueueStruct::add(taskIndex_t TaskIndex, const String& varName, const String& eventValue)
{
  if (Settings.UseRules) {
    if (eventValue.isEmpty()) {
      addMove(strformat(
        F("%s#%s"), 
        getTaskDeviceName(TaskIndex).c_str(), 
        varName.c_str()));
    } else {
      addMove(strformat(
        F("%s#%s=%s"), 
        getTaskDeviceName(TaskIndex).c_str(), 
        varName.c_str(), 
        eventValue.c_str()));
    }
  }
}

void EventQueueStruct::add(taskIndex_t TaskIndex, const String& varName, int eventValue)
{
  if (Settings.UseRules) {
    add(TaskIndex, varName, String(eventValue));
  }
}

void EventQueueStruct::add(taskIndex_t TaskIndex, const __FlashStringHelper *varName, const String& eventValue)
{
  if (Settings.UseRules) {
    add(TaskIndex, String(varName), eventValue);
  }
}

void EventQueueStruct::add(taskIndex_t TaskIndex, const __FlashStringHelper *varName, int eventValue)
{
  if (Settings.UseRules) {
    add(TaskIndex, String(varName), String(eventValue));
  }
}

bool EventQueueStruct::getNext(String& event)
{
  if (_eventQueue.empty()) {
    return false;
  }
  event = std::move(_eventQueue.front());
  _eventQueue.pop_front();
  return true;
}

void EventQueueStruct::clear()
{
  _eventQueue.clear();
}

bool EventQueueStruct::isEmpty() const
{
  return _eventQueue.empty();
}

bool EventQueueStruct::isDuplicate(const String& event) {
  return std::find(_eventQueue.begin(), _eventQueue.end(), event) != _eventQueue.end();
}

#include "../DataStructs/SecurityStruct.h"

#include "../../ESPEasy_common.h"
#include "../CustomBuild/ESPEasyLimits.h"
#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../Globals/CPlugins.h"

SecurityStruct::SecurityStruct() {
  ZERO_FILL(WifiSSID);
  ZERO_FILL(WifiKey);
  ZERO_FILL(WifiSSID2);
  ZERO_FILL(WifiKey2);
  ZERO_FILL(WifiAPKey);

  for (controllerIndex_t i = 0; i < CONTROLLER_MAX; ++i) {
    ZERO_FILL(ControllerUser[i]);
    ZERO_FILL(ControllerPassword[i]);
  }
  ZERO_FILL(Password);
}

ChecksumType SecurityStruct::computeChecksum() const {
  constexpr size_t len_upto_md5 = offsetof(SecurityStruct, md5);
  return ChecksumType(
    reinterpret_cast<const uint8_t *>(this), 
    sizeof(SecurityStruct),
    len_upto_md5);
}

bool SecurityStruct::checksumMatch() const {
  return computeChecksum().matchChecksum(md5);
}

bool SecurityStruct::updateChecksum() {
  const ChecksumType checksum = computeChecksum();
  if (checksum.matchChecksum(md5)) {
    return false;
  }
  checksum.getChecksum(md5);
  return true;
}

void SecurityStruct::validate() {
  ZERO_TERMINATE(WifiSSID);
  ZERO_TERMINATE(WifiKey);
  ZERO_TERMINATE(WifiSSID2);
  ZERO_TERMINATE(WifiKey2);
  ZERO_TERMINATE(WifiAPKey);

  for (controllerIndex_t i = 0; i < CONTROLLER_MAX; ++i) {
    ZERO_TERMINATE(ControllerUser[i]);
    ZERO_TERMINATE(ControllerPassword[i]);
  }
  ZERO_TERMINATE(Password);
}

void SecurityStruct::forceSave() {
  memset(md5, 0, 16);
}

void SecurityStruct::clearWiFiCredentials() {
  ZERO_FILL(WifiSSID);
  ZERO_FILL(WifiKey);
  ZERO_FILL(WifiSSID2);
  ZERO_FILL(WifiKey2);
  #ifndef BUILD_MINIMAL_OTA
  addLog(LOG_LEVEL_INFO, F("WiFi : Clear WiFi credentials from settings"));
  #endif
}

void SecurityStruct::clearWiFiCredentials(SecurityStruct::WiFiCredentialsSlot slot) {
  if (slot == SecurityStruct::WiFiCredentialsSlot::first) {
    ZERO_FILL(WifiSSID);
    ZERO_FILL(WifiKey);
  } else if (slot == SecurityStruct::WiFiCredentialsSlot::second) {
    ZERO_FILL(WifiSSID2);
    ZERO_FILL(WifiKey2);
  }
}

bool SecurityStruct::hasWiFiCredentials() const {
  return hasWiFiCredentials(SecurityStruct::WiFiCredentialsSlot::first) ||
         hasWiFiCredentials(SecurityStruct::WiFiCredentialsSlot::second);
}

bool SecurityStruct::hasWiFiCredentials(SecurityStruct::WiFiCredentialsSlot slot) const {
  if (slot == SecurityStruct::WiFiCredentialsSlot::first)
      return (WifiSSID[0] != 0 && !String(WifiSSID).equalsIgnoreCase(F("ssid")));
  if (slot == SecurityStruct::WiFiCredentialsSlot::second)
      return (WifiSSID2[0] != 0 && !String(WifiSSID2).equalsIgnoreCase(F("ssid")));

  return false;
}

String SecurityStruct::getPassword() const {
  String res;
  const size_t passLength = strnlen(Password, sizeof(Password));
  res.reserve(passLength);
  for (size_t i = 0; i < passLength; ++i) {
    res += Password[i];
  }
  return res;
}
#include "../DataStructs/C013_p2p_SensorInfoStruct.h"

#ifdef USES_C013

# include "../DataStructs/NodeStruct.h"
# include "../Globals/ExtraTaskSettings.h"
# include "../Globals/Nodes.h"
# include "../Globals/Plugins.h"
# include "../Globals/Settings.h"

# include "../CustomBuild/CompiletimeDefines.h"

# include "../Helpers/ESPEasy_Storage.h"
# include "../Helpers/StringConverter.h"

bool C013_SensorInfoStruct::prepareForSend(size_t& sizeToSend)
{
  if (!(validTaskIndex(sourceTaskIndex) &&
        validTaskIndex(destTaskIndex) &&
        validPluginID(deviceNumber))) {
    return false;
  }

  sizeToSend = sizeof(C013_SensorInfoStruct);

  sourceNodeBuild = get_build_nr();
  checksum.clear();

  ZERO_FILL(taskName);
  safe_strncpy(taskName, getTaskDeviceName(sourceTaskIndex), sizeof(taskName));

  for (uint8_t x = 0; x < VARS_PER_TASK; x++) {
    ZERO_FILL(ValueNames[x]);
    safe_strncpy(ValueNames[x], getTaskValueName(sourceTaskIndex, x), sizeof(ValueNames[x]));
  }


  if (sourceNodeBuild >= 20871) {
    LoadTaskSettings(sourceTaskIndex);

    ExtraTaskSettings_version = ExtraTaskSettings.version;

    for (uint8_t x = 0; x < VARS_PER_TASK; x++) {
      TaskDeviceValueDecimals[x] = ExtraTaskSettings.TaskDeviceValueDecimals[x];
      TaskDeviceMinValue[x]      = ExtraTaskSettings.TaskDeviceMinValue[x];
      TaskDeviceMaxValue[x]      = ExtraTaskSettings.TaskDeviceMaxValue[x];
      TaskDeviceErrorValue[x]    = ExtraTaskSettings.TaskDeviceErrorValue[x];
      VariousBits[x]             = ExtraTaskSettings.VariousBits[x];

/*
      ZERO_FILL(TaskDeviceFormula[x]);

      if (ExtraTaskSettings.TaskDeviceFormula[x][0] != 0) {
        safe_strncpy(TaskDeviceFormula[x], ExtraTaskSettings.TaskDeviceFormula[x], sizeof(TaskDeviceFormula[x]));
      }
*/
    }

    for (uint8_t x = 0; x < PLUGIN_CONFIGVAR_MAX; ++x) {
      TaskDevicePluginConfig[x] = Settings.TaskDevicePluginConfig[sourceTaskIndex][x];
    }
  }

  // Check to see if last bytes are all zero, so we can simply not send them
  bool doneShrinking                          = false;
  constexpr unsigned len_upto_sourceNodeBuild = offsetof(C013_SensorInfoStruct, sourceNodeBuild);

  const uint8_t *data = reinterpret_cast<const uint8_t *>(this);

  while (!doneShrinking) {
    if (sizeToSend < len_upto_sourceNodeBuild) {
      doneShrinking = true;
    }
    else {
      if (data[sizeToSend - 1] == 0) {
        --sizeToSend;
      } else {
        doneShrinking = true;
      }
    }
  }

  if (sourceNodeBuild >= 20871) {
    // Make sure to add checksum as last step
    constexpr unsigned len_upto_checksum = offsetof(C013_SensorInfoStruct, checksum);
    const ShortChecksumType tmpChecksum(
      reinterpret_cast<const uint8_t *>(this),
      sizeToSend,
      len_upto_checksum);

    checksum = tmpChecksum;
  }

  return true;
}

bool C013_SensorInfoStruct::setData(const uint8_t *data, size_t size)
{
  // First clear entire struct
  memset(this, 0, sizeof(C013_SensorInfoStruct));

  if (size < 6) {
    return false;
  }

  if ((data[0] != 255) || // header
      (data[1] != 3)) {   // ID
    return false;
  }

  // Before copying the data, compute the checksum of the entire packet
  constexpr unsigned len_upto_checksum = offsetof(C013_SensorInfoStruct, checksum);
  const ShortChecksumType tmpChecksum(
    data,
    size,
    len_upto_checksum);

  // Need to keep track of different possible versions of data which still need to be supported.
  if (size > sizeof(C013_SensorInfoStruct)) {
    size = sizeof(C013_SensorInfoStruct);
  }

  if (size <= 138) {
    deviceNumber = INVALID_PLUGIN_ID;
    sensorType   = Sensor_VType::SENSOR_TYPE_NONE;

    NodeStruct *sourceNode = Nodes.getNode(data[2]); // sourceUnit

    if (sourceNode != nullptr) {
      sourceNodeBuild = sourceNode->build;
    }
  }

  memcpy(this, data, size);

  if (checksum.isSet()) {
    if (!(tmpChecksum == checksum)) {
      return false;
    }
  }

  return validTaskIndex(sourceTaskIndex) &&
         validTaskIndex(destTaskIndex) &&
         validPluginID(deviceNumber);
}

#endif // ifdef USES_C013

#include "../DataStructs/ExtendedControllerCredentialsStruct.h"

#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/StringConverter.h"

#define EXT_CONTR_CRED_USER_OFFSET 0
#define EXT_CONTR_CRED_PASS_OFFSET 1


ChecksumType last_ExtendedControllerCredentialsStruct_md5;


ExtendedControllerCredentialsStruct::ExtendedControllerCredentialsStruct() {}

bool ExtendedControllerCredentialsStruct::validateChecksum() const
{
  const ChecksumType tmp_checksum(_strings, CONTROLLER_MAX * 2);
  if (tmp_checksum == last_ExtendedControllerCredentialsStruct_md5) {
    return true;
  }
  // Data has changed, copy computed checksum
  last_ExtendedControllerCredentialsStruct_md5 = tmp_checksum;
  return false;
}

void ExtendedControllerCredentialsStruct::clear() {
  for (size_t i = 0; i < CONTROLLER_MAX * 2; ++i) {
    free_string(_strings[i]);
  }
}

String ExtendedControllerCredentialsStruct::load()
{
  const String res =
    LoadStringArray(SettingsType::Enum::ExtdControllerCredentials_Type,
                    0,
                    _strings, CONTROLLER_MAX * 2, 0);

  for (int i = 0; i < CONTROLLER_MAX * 2; ++i) {
    _strings[i].trim();
  }

  // Update the checksum after loading.
  validateChecksum();

  return res;
}

String ExtendedControllerCredentialsStruct::save() const
{
  if (validateChecksum()) {
    return EMPTY_STRING;
  }
  return SaveStringArray(SettingsType::Enum::ExtdControllerCredentials_Type,
                         0,
                         _strings, CONTROLLER_MAX * 2, 0);
}

String ExtendedControllerCredentialsStruct::getControllerUser(controllerIndex_t controller_idx) const
{
  if (validControllerIndex(controller_idx)) {
    return _strings[controller_idx * 2 + EXT_CONTR_CRED_USER_OFFSET];
  }
  return EMPTY_STRING;
}

String ExtendedControllerCredentialsStruct::getControllerPass(controllerIndex_t controller_idx) const
{
  if (validControllerIndex(controller_idx)) {
    return _strings[controller_idx * 2 + EXT_CONTR_CRED_PASS_OFFSET];
  }
  return EMPTY_STRING;
}

void ExtendedControllerCredentialsStruct::setControllerUser(controllerIndex_t controller_idx, const String& user)
{
  if (validControllerIndex(controller_idx)) {
    _strings[controller_idx * 2 + EXT_CONTR_CRED_USER_OFFSET] = user;
  }
}

void ExtendedControllerCredentialsStruct::setControllerPass(controllerIndex_t controller_idx, const String& pass)
{
  if (validControllerIndex(controller_idx)) {
    _strings[controller_idx * 2 + EXT_CONTR_CRED_PASS_OFFSET] = pass;
  }
}

#include "../DataStructs/ChartJS_dataset_config.h"

#if FEATURE_CHART_JS

ChartJS_dataset_config::ChartJS_dataset_config(
  const __FlashStringHelper * set_label,
  const __FlashStringHelper * set_color)
  : label(set_label), color(set_color) {}

ChartJS_dataset_config::ChartJS_dataset_config(
  const String& set_label,
  const String& set_color)
  : label(set_label), color(set_color) {}

#endif
#include "../DataStructs/UnitMessageCount.h"

bool UnitLastMessageCount_map::isNew(const UnitMessageCount_t *count) const {
  if (count == nullptr) { return true; }
  auto it = _map.find(count->unit);

  if (it != _map.end()) {
    return it->second != count->count;
  }
  return true;
}

void UnitLastMessageCount_map::add(const UnitMessageCount_t *count) {
  if (count == nullptr) { return; }

  if ((count->unit != 0) && (count->unit != 255)) {
    _map[count->unit] = count->count;
  }
}

#include "../DataStructs/MAC_address.h"

#include "../../ESPEasy_common.h"


MAC_address::MAC_address(const uint8_t new_mac[6])
{
  memcpy(mac, new_mac, 6);
}

MAC_address::MAC_address(const MAC_address& other)
{
  for (int i = 0; i < 6; ++i) {
    mac[i] = other.mac[i];
  }
}

MAC_address& MAC_address::operator=(const MAC_address& other)
{
  for (int i = 0; i < 6; ++i) {
    mac[i] = other.mac[i];
  }
  return *this;
}

bool MAC_address::set(const char *string)
{
  unsigned u[6];
  int c = sscanf(string, "%x:%x:%x:%x:%x:%x", u, u + 1, u + 2, u + 3, u + 4, u + 5);

  if (c != 6) {
    return false;
  }

  for (int i = 0; i < 6; ++i) {
    mac[i] = static_cast<uint8_t>(u[i]);
  }
  return true;
}

void MAC_address::set(const uint8_t other[6])
{
  memcpy(mac, other, 6);
}

void MAC_address::get(uint8_t mac_out[6]) const
{
  memcpy(mac_out, mac, 6);
}


bool MAC_address::all_zero() const
{
  for (int i = 0; i < 6; ++i) {
    if (mac[i] != 0) {
      return false;
    }
  }
  return true;
}

bool MAC_address::all_one() const
{
  for (int i = 0; i < 6; ++i) {
    if (mac[i] != 0xFF) {
      return false;
    }
  }
  return true;
}

String MAC_address::toString() const
{
  char str[18] = { 0 };
  sprintf_P(str, PSTR("%02X:%02X:%02X:%02X:%02X:%02X"), mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
  return String(str);
}

bool MAC_address::mac_addr_cmp(const uint8_t other[6]) const
{
  for (int i = 0; i < 6; ++i) {
    if (mac[i] != other[i]) {
      return false;
    }
  }
  return true;
}
#include "../DataStructs/timer_id_couple.h"

#include "../Helpers/ESPEasy_time_calc.h"

bool timer_id_couple::operator<(const timer_id_couple& other) const {
  const unsigned long now(millis());

  // timediff > 0, means timer has already passed
  return timeDiff(_timer, now) > timeDiff(other._timer, now);
}

#include "../DataStructs/NodeStruct.h"

#if FEATURE_ESPEASY_P2P
#include "../../ESPEasy-Globals.h"
#include "../DataTypes/NodeTypeID.h"
#include "../ESPEasyCore/ESPEasyNetwork.h"
#include "../Globals/SecuritySettings.h"
#include "../Globals/Settings.h"
#include "../Helpers/ESPEasy_time_calc.h"
#include "../Helpers/StringConverter.h"


#define NODE_STRUCT_AGE_TIMEOUT 300000  // 5 minutes

NodeStruct::NodeStruct() : 
  ESPEasyNowPeer(0), 
  useAP_ESPEasyNow(0), 
  scaled_rssi(0)
#if FEATURE_USE_IPV6
   ,hasIPv4(0)
   ,hasIPv6_mac_based_link_local(0)
   ,hasIPv6_mac_based_link_global(0)
   ,unused(0)
#endif
{}

bool NodeStruct::valid() const {
  // FIXME TD-er: Must make some sanity checks to see if it is a valid message
  return true;
}

bool NodeStruct::validate(const IPAddress& remoteIP) {
  if (build < 20107) {
    // webserverPort introduced in 20107
    webgui_portnumber = 80;
    for (uint8_t i = 0; i < 6; ++i) {
      ap_mac[i] = 0;
    }
    load              = 0;
    distance          = 255;
    timeSource        = static_cast<uint8_t>(timeSource_t::No_time_source);
    channel           = 0;
    ESPEasyNowPeer    = 0;
    useAP_ESPEasyNow  = 0;
    setRSSI(0);
    lastUpdated = 0;
  }
  if (build < 20253) {
    version = 0;
#if FEATURE_USE_IPV6
    hasIPv4                       = 0;
    hasIPv6_mac_based_link_local  = 0;
    hasIPv6_mac_based_link_global = 0;

    unused = 0;
#else
    unused = 0;
#endif

    unix_time_frac = 0;
    unix_time_sec = 0;
  }

#if FEATURE_USE_IPV6
  // Check if we're in the same global subnet
  if (Settings.EnableIPv6() &&
      hasIPv6_mac_based_link_global && 
      remoteIP.type() == IPv6) {
    const IPAddress this_global = NetworkGlobalIP6();
    // Check first 64 bit to see if we're in the same global scope
    for (int i = 0; i < 8 && hasIPv6_mac_based_link_global; ++i) {
      if (this_global[i] != remoteIP[i])
        hasIPv6_mac_based_link_global = false;
    }
  }
#endif

  // FIXME TD-er: Must make some sanity checks to see if it is a valid message
  return valid();
}

bool NodeStruct::operator<(const NodeStruct &other) const {
  const bool thisExpired = isExpired();
  if (thisExpired != other.isExpired()) {
    return !thisExpired;
  }

  const bool markedAsPriority = markedAsPriorityPeer();
  if (markedAsPriority != other.markedAsPriorityPeer()) {
    return markedAsPriority;
  }

  if (ESPEasyNowPeer != other.ESPEasyNowPeer) {
    // One is confirmed, so prefer that one.
    return ESPEasyNowPeer;
  }

  const int8_t thisRssi = getRSSI();
  const int8_t otherRssi = other.getRSSI();

  int score_this = getLoad();
  int score_other = other.getLoad();

  if (distance != other.distance) {
    if (!isExpired() && !other.isExpired()) {
      // Distance is not the same, so take distance into account.
      return distance < other.distance;
/*
      int distance_penalty = distance - other.distance;
      distance_penalty = distance_penalty * distance_penalty * 10;
      if (distance > other.distance) {
        score_this += distance_penalty;
      } else {
        score_other += distance_penalty;
      }
*/
    }
  }

  if (thisRssi >= 0 || otherRssi >= 0) {
    // One or both have no RSSI, so cannot use RSSI in computing score
  } else {
    // RSSI value is negative, so subtract the value
    // RSSI range from -38 ... 99
    // Shift RSSI and add a weighing factor to make sure
    // A load of 100% with RSSI of -40 is preferred over a load of 20% with an RSSI of -80.
    score_this -= (thisRssi + 38) * 2;
    score_other -= (otherRssi + 38) * 2;
  }
  return score_this < score_other;
}


const __FlashStringHelper * NodeStruct::getNodeTypeDisplayString() const {
  return toNodeTypeDisplayString(nodeType);
}

String NodeStruct::getNodeName() const {
  String res;
  size_t length = strnlen(reinterpret_cast<const char *>(nodeName), sizeof(nodeName));

  if (reserve_special(res, length)) {
    for (size_t i = 0; i < length; ++i) {
      res += static_cast<char>(nodeName[i]);
    }
  }
  return res;
}

IPAddress NodeStruct::IP() const {
  return IPAddress(ip[0], ip[1], ip[2], ip[3]);
}

#if FEATURE_USE_IPV6
IPAddress NodeStruct::IPv6_link_local(bool stripZone) const
{
  if (Settings.EnableIPv6() && hasIPv6_mac_based_link_local) {
    // Base IPv6 on MAC address
    IPAddress ipv6;
    if (IPv6_link_local_from_MAC(sta_mac, ipv6)) {
      if (stripZone) {
        return IPAddress(IPv6, &ipv6[0], 0);
      }
      return ipv6;
    }
  }
  return IN6ADDR_ANY;
}

IPAddress NodeStruct::IPv6_global() const
{
  if (Settings.EnableIPv6() && hasIPv6_mac_based_link_global) {
    // Base IPv6 on MAC address
    IPAddress ipv6;
    if (IPv6_global_from_MAC(sta_mac, ipv6)) {
      return ipv6;
    }
  }
  return IN6ADDR_ANY;
}

bool NodeStruct::hasIPv6() const {
  if (!Settings.EnableIPv6()) return false;
  return hasIPv6_mac_based_link_local ||
         hasIPv6_mac_based_link_global;
}
#endif


MAC_address NodeStruct::STA_MAC() const {
  return MAC_address(sta_mac);
}

MAC_address NodeStruct::ESPEasy_Now_MAC() const {
  if (ESPEasyNowPeer == 0) return MAC_address();
  if (useAP_ESPEasyNow) {
    return MAC_address(ap_mac);
  }
  return MAC_address(sta_mac);
}

unsigned long NodeStruct::getAge() const {
  return timePassedSince(lastUpdated);
}

bool  NodeStruct::isExpired() const {
  return getAge() > NODE_STRUCT_AGE_TIMEOUT;
}

float NodeStruct::getLoad() const {
  return load / 2.55;
}

String NodeStruct::getSummary() const {
  String res;
  if (reserve_special(res, 48)) {
    res  = F("Unit: ");
    res += unit;
    res += F(" \"");
    res += getNodeName();
    res += '"';
    res += F(" load: ");
    res += String(getLoad(), 1);
    res += F(" RSSI: ");
    res += getRSSI();
    res += F(" ch: ");
    res += channel;
    res += F(" dst: ");
    res += distance;
  }
  return res;
}

bool NodeStruct::setESPEasyNow_mac(const MAC_address& received_mac)
{
  if (received_mac.all_zero()) return false;
  if (received_mac == sta_mac) {
    ESPEasyNowPeer   = 1;
    useAP_ESPEasyNow = 0;
    return true;
  }

  if (received_mac == ap_mac) {
    ESPEasyNowPeer   = 1;
    useAP_ESPEasyNow = 1;
    return true;
  }
  return false;
}

int8_t NodeStruct::getRSSI() const
{
  if (scaled_rssi == 0) {
    return 0; // Not set
  }

  if (scaled_rssi == 0x3F) {
    return 31; // Error state
  }

  // scaled_rssi = 1 ... 62
  // output = -38 ... -99
  int8_t rssi = scaled_rssi + 37;
  return rssi * -1;
}

void NodeStruct::setRSSI(int8_t rssi)
{
  if (rssi == 0) {
    // Not set
    scaled_rssi = 0;
    return;
  }

  if (rssi > 0) {
    // Error state
    scaled_rssi = 0x3F;
    return;
  }
  rssi *= -1;
  rssi -= 37;

  if (rssi < 1) {
    scaled_rssi = 1;
    return;
  }

  if (rssi >= 0x3F) {
    scaled_rssi = 0x3F - 1;
    return;
  }
  scaled_rssi = rssi;
}

bool NodeStruct::markedAsPriorityPeer() const
{
#ifdef USES_ESPEASY_NOW
  for (int i = 0; i < ESPEASY_NOW_PEER_MAX; ++i) {
    if (SecuritySettings.peerMacSet(i)) {
      if (match(SecuritySettings.EspEasyNowPeerMAC[i])) {
        return true;
      }
    }
  }
#endif
  return false;
}

bool NodeStruct::match(const MAC_address& mac) const
{
  return (mac == sta_mac || mac == ap_mac);
}

bool NodeStruct::isThisNode() const
{
    // Check to see if we process a node we've sent ourselves.
    if (WifiSoftAPmacAddress() == ap_mac) return true;
    if (WifiSTAmacAddress() == sta_mac) return true;

    return false;
}

void NodeStruct::setAP_MAC(const MAC_address& mac)
{
  mac.get(ap_mac);
}

#endif

#include "../DataStructs/LogEntry.h"

#include "../Helpers/ESPEasy_time_calc.h"
#include "../Helpers/StringConverter.h"

#define LOG_STRUCT_MESSAGE_SIZE 128

#ifdef ESP32
  #define LOG_BUFFER_EXPIRE         30000  // Time after which a buffered log item is considered expired.
#else
  #define LOG_BUFFER_EXPIRE         5000  // Time after which a buffered log item is considered expired.
#endif


bool LogEntry_t::add(const uint8_t loglevel, const String& line)
{
  if (line.length() == 0) {
    return false;
  }

  {
    if (line.length() > LOG_STRUCT_MESSAGE_SIZE - 1) {
      #ifdef USE_SECOND_HEAP

      // Need to make a substring or a copy, which is a new allocation, on the 2nd heap
      HeapSelectIram ephemeral;
      #endif // ifdef USE_SECOND_HEAP
      move_special(_message, line.substring(0, LOG_STRUCT_MESSAGE_SIZE - 1));
    } else {
      reserve_special(_message, line.length());
      _message = line;
    }
  }
  _loglevel  = loglevel;
  _timestamp = millis();
  return true;
}

bool LogEntry_t::add(const uint8_t loglevel, String&& line)
{
  if (line.length() == 0) {
    return false;
  }

  if (line.length() > LOG_STRUCT_MESSAGE_SIZE - 1) {
      #ifdef USE_SECOND_HEAP

    // Need to make a substring, which is a new allocation, on the 2nd heap
    HeapSelectIram ephemeral;
      #endif // ifdef USE_SECOND_HEAP
    move_special(_message, line.substring(0, LOG_STRUCT_MESSAGE_SIZE - 1));
  } else {
    move_special(_message, std::move(line));
  }
  _loglevel  = loglevel;
  _timestamp = millis();
  return true;
}

void LogEntry_t::clear()
{
  free_string(_message);
  _timestamp = 0;
  _loglevel  = 0;
}

bool LogEntry_t::isExpired() const
{
    return timePassedSince(_timestamp) >= LOG_BUFFER_EXPIRE;

}
#include "../DataStructs/mBusPacket.h"

#include "../Helpers/CRC_functions.h"
#include "../Helpers/StringConverter.h"

#define FRAME_FORMAT_A_FIRST_BLOCK_LENGTH 10
#define FRAME_FORMAT_A_OTHER_BLOCK_LENGTH 16


mBusPacket_header_t::mBusPacket_header_t()
{
  _manufacturer = mBus_packet_wildcard_manufacturer;
  _meterType    = mBus_packet_wildcard_metertype;
  _serialNr     = mBus_packet_wildcard_serial;
  _length       = 0u;
}

String mBusPacket_header_t::decodeManufacturerID(int id)
{
  String res;
  int    shift = 15;

  for (int i = 0; i < 3; ++i) {
    shift -= 5;
    res   += static_cast<char>(((id >> shift) & 0x1f) + 64);
  }
  return res;
}

int mBusPacket_header_t::encodeManufacturerID(const String& id_str)
{
  int res     = 0;
  int nrChars = id_str.length();

  if (nrChars > 3) { nrChars = 3; }

  int i = 0;

  while (i < nrChars) {
    res <<= 5;
    const int c = static_cast<int>(toUpperCase(id_str[i])) - 64;

    if (c >= 0) {
      res += c & 0x1f;
    }
    ++i;
  }
  return res;
}

String mBusPacket_header_t::getManufacturerId() const
{
  return decodeManufacturerID(_manufacturer);
}

String mBusPacket_header_t::toString() const
{
  String res = decodeManufacturerID(_manufacturer);

  res += '.';
  res += formatToHex_no_prefix(_meterType, 2);
  res += '.';
  res += formatToHex_no_prefix(_serialNr, 8);
  return res;
}

uint64_t mBusPacket_header_t::encode_toUInt64() const
{
  if (!isValid()) { return 0ull; }
  mBusPacket_header_t tmp(*this);

  tmp._length = 0;
  return tmp._encodedValue;
}

void mBusPacket_header_t::decode_fromUint64(uint64_t encodedValue)
{
  _encodedValue = encodedValue;
  _length       = 1; // To pass isValid() check
}

bool mBusPacket_header_t::isValid() const
{
  return
    _manufacturer != mBus_packet_wildcard_manufacturer &&
    _meterType != mBus_packet_wildcard_metertype &&
    _serialNr != mBus_packet_wildcard_serial &&
    _length > 0;
}

void mBusPacket_header_t::clear()
{
  _manufacturer = mBus_packet_wildcard_manufacturer;
  _meterType    = mBus_packet_wildcard_metertype;
  _serialNr     = mBus_packet_wildcard_serial;
  _length       = 0;
}

bool mBusPacket_header_t::matchSerial(uint32_t serialNr) const
{
  return isValid() && (_serialNr == serialNr);
}

const mBusPacket_header_t * mBusPacket_t::getDeviceHeader() const
{
  // FIXME TD-er: Which deviceID is the device and which the wrapper?
  if (_deviceId1.isValid()) { return &_deviceId1; }

  if (_deviceId2.isValid()) { return &_deviceId2; }

  return nullptr;
}

uint32_t mBusPacket_t::getDeviceSerial() const
{
  const mBusPacket_header_t *header = getDeviceHeader();

  if (header == nullptr) { return 0u; }
  return header->_serialNr;
}

uint32_t mBusPacket_t::deviceID_to_map_key() const
{
  return deviceID_to_map_key(_deviceId1._encodedValue, _deviceId2._encodedValue);
}

uint32_t mBusPacket_t::deviceID_to_map_key_no_length() const {
  return deviceID_to_map_key(_deviceId1.encode_toUInt64(), _deviceId2.encode_toUInt64());
}

uint32_t mBusPacket_t::deviceID_to_map_key(uint64_t id1, uint64_t id2)
{
  uint32_t res = 0;

  if (id1 != 0ull) {
    res ^= calc_CRC32((const uint8_t *)(&id1), sizeof(uint64_t));
  }

  if (id2 != 0ull) {
    // There is a forwarding device.
    // To prevent issues when the forwarding device is the same as the forwarded device, alter the already existing checksum.
    res ^= calc_CRC32((const uint8_t *)(&res), sizeof(res));
    res ^= calc_CRC32((const uint8_t *)(&id2), sizeof(uint64_t));
  }

  return res;
}

bool mBusPacket_t::parse(const String& payload)
{
  if (payload[0] != 'b') { return false; }

  _checksum = 0;
  mBusPacket_data payloadWithoutChecksums;

  if (payload[1] == 'Y') {
    // Start with "bY"
    payloadWithoutChecksums = removeChecksumsFrameB(payload, _checksum);
  } else {
    payloadWithoutChecksums = removeChecksumsFrameA(payload, _checksum);
  }

  if (payloadWithoutChecksums.size() < 10) { return false; }

  int pos_semicolon = payload.indexOf(';');

  if (pos_semicolon == -1) { pos_semicolon = payload.length(); }

  _lqi_rssi = hexToUL(payload, pos_semicolon - 4, 4);
  return parseHeaders(payloadWithoutChecksums);
}

int16_t mBusPacket_t::decode_LQI_RSSI(uint16_t lqi_rssi, uint8_t& LQI)
{
  LQI = (lqi_rssi >> 8) & 0x7f; // Bit 7 = CRC OK Bit

  int rssi = lqi_rssi & 0xFF;

  if (rssi >= 128) {
    rssi -= 256; // 2-complement
  }
  return (rssi / 2) - 74;
}

bool mBusPacket_t::matchSerial(uint32_t serialNr) const
{
  return _deviceId1.matchSerial(serialNr) || _deviceId2.matchSerial(serialNr);
}

bool mBusPacket_t::parseHeaders(const mBusPacket_data& payloadWithoutChecksums)
{
  const int payloadSize = payloadWithoutChecksums.size();

  _deviceId1.clear();
  _deviceId2.clear();

  if (payloadSize < 10) { return false; }
  int offset = 0;

  // 1st block is a static DataLinkLayer of 10 bytes
  {
    _deviceId1._manufacturer = makeWord(payloadWithoutChecksums[offset + 3], payloadWithoutChecksums[offset + 2]);

    // Type (offset + 9; convert to hex)
    _deviceId1._meterType = payloadWithoutChecksums[offset + 9];

    // Serial (offset + 4; 4 Bytes; least significant first; converted to hex)
    _deviceId1._serialNr = 0;

    for (int i = 0; i < 4; ++i) {
      const uint32_t val = payloadWithoutChecksums[offset + 4 + i];
      _deviceId1._serialNr += val << (i * 8);
    }
    offset            += 10;
    _deviceId1._length = payloadWithoutChecksums[0];
  }

  // next blocks can be anything. we skip known blocks of no interest, parse known blocks if interest and stop on onknown blocks
  while (offset < payloadSize) {
    switch (static_cast<int>(payloadWithoutChecksums[offset])) {
      case 0x8C:                                            // ELL short
        offset            += 3;                             // fixed length
        _deviceId1._length = payloadSize - offset;
        break;
      case 0x90:                                            // AFL
        offset++;
        offset += (payloadWithoutChecksums[offset] & 0xff); // dynamic length with length in 1st byte
        offset++;                                           // length byte
        _deviceId1._length = payloadSize - offset;
        break;
      case 0x72:                                            // TPL_RESPONSE_MBUS_LONG_HEADER
        _deviceId2 = _deviceId1;

        // note that serial/manufacturer are swapped !!

        _deviceId1._manufacturer = makeWord(payloadWithoutChecksums[offset + 6], payloadWithoutChecksums[offset + 5]);

        // Type (offset + 9; convert to hex)
        _deviceId1._meterType = payloadWithoutChecksums[offset + 8];

        // Serial (offset + 4; 4 Bytes; least significant first; converted to hex)
        _deviceId1._serialNr = 0;


        for (int i = 0; i < 4; ++i) {
          const uint32_t val = payloadWithoutChecksums[offset + 1 + i];
          _deviceId1._serialNr += val << (i * 8);
        }

        // We're done
        offset = payloadSize;
        break;
      default:
        // We're done
        //        addLog(LOG_LEVEL_ERROR, concat(F("CUL : offset "), offset) + F(" Data: ") + formatToHex(payloadWithoutChecksums[offset]));
        offset = payloadSize;
        break;
    }
  }

  if (_deviceId1.isValid() && _deviceId2.isValid() && _deviceId1.toString().startsWith(F("ITW.30."))) {
    // ITW does not follow the spec and puts the redio converter behind the actual meter. Need to swap both
    std::swap(_deviceId1, _deviceId2);
  }

  return _deviceId1.isValid();
}

String mBusPacket_t::toString() const
{
  static size_t expectedSize = 96;
  String res;

  if (res.reserve(expectedSize)) {
    if (_deviceId1.isValid()) {
      res += F(" deviceId1: ");
      res += _deviceId1.toString();
      res += '(';
      res += static_cast<uint8_t>(_deviceId1._length);
      res += ')';
    }

    if (_deviceId2.isValid()) {
      res += F(" deviceId2: ");
      res += _deviceId2.toString();
      res += '(';
      res += static_cast<uint8_t>(_deviceId2._length);
      res += ')';
    }
    res += F(" chksum: ");
    res += formatToHex(_checksum, 8);

    uint8_t LQI        = 0;
    const int16_t rssi = decode_LQI_RSSI(_lqi_rssi, LQI);
    res += F(" LQI: ");
    res += LQI;
    res += F(" RSSI: ");
    res += rssi;
  }

  if (res.length() > expectedSize) { expectedSize = res.length(); }

  return res;
}

uint8_t mBusPacket_t::hexToByte(const String& str, size_t index)
{
  // Need to have at least 2 HEX nibbles
  if ((index + 1) >= str.length()) { return 0; }
  return hexToUL(str, index, 2);
}

/**
 * Format:
 * [10 bytes message] + [2 bytes CRC]
 * [16 bytes message] + [2 bytes CRC]
 * [16 bytes message] + [2 bytes CRC]
 * ...
 * (last block can be < 16 bytes)
 */
mBusPacket_data mBusPacket_t::removeChecksumsFrameA(const String& payload, uint32_t& checksum)
{
  mBusPacket_data result;
  const int payloadLength = payload.length();

  if (payloadLength < 4) { return result; }

  int sourceIndex = 1; // Starts with "b"
  int targetIndex = 0;

  // 1st byte contains length of data (excuding 1st byte and excluding CRC)
  const int expectedMessageSize = hexToByte(payload, sourceIndex) + 1;

  if (payloadLength < (2 * expectedMessageSize)) {
    // Not an exact check, but close enough to fail early on packets which are seriously too short.
    return result;
  }

  result.reserve(expectedMessageSize);

  while (targetIndex < expectedMessageSize) {
    // end index is start index + block size + 2 byte checksums
    int blockSize = (sourceIndex == 1) ? FRAME_FORMAT_A_FIRST_BLOCK_LENGTH : FRAME_FORMAT_A_OTHER_BLOCK_LENGTH;

    if ((targetIndex + blockSize) > expectedMessageSize) { // last block
      blockSize = expectedMessageSize - targetIndex;
    }

    // FIXME: handle truncated source messages
    for (int i = 0; i < blockSize; ++i) {
      result.push_back(hexToByte(payload, sourceIndex));
      sourceIndex += 2; // 2 hex chars
    }

    // [2 bytes CRC]
    checksum   <<= 8;
    checksum    ^= hexToUL(payload, sourceIndex, 4);
    sourceIndex += 4; // Skip 2 bytes CRC => 4 hex chars
    targetIndex += blockSize;
  }
  return result;
}

/**
 * Format:
 * [126 bytes message] + [2 bytes CRC]
 * [125 bytes message] + [2 bytes CRC]
 * (if message length <=126 bytes, only the 1st block exists)
 * (last block can be < 125 bytes)
 */
mBusPacket_data mBusPacket_t::removeChecksumsFrameB(const String& payload, uint32_t& checksum)
{
  mBusPacket_data result;
  const int payloadLength = payload.length();

  if (payloadLength < 4) { return result; }

  int sourceIndex = 2; // Starts with "bY"

  // 1st byte contains length of data (excuding 1st byte BUT INCLUDING CRC)
  int expectedMessageSize = hexToByte(payload, sourceIndex) + 1;

  if (payloadLength < (2 * expectedMessageSize)) {
    return result;
  }

  expectedMessageSize -= 2;   // CRC of 1st block

  if (expectedMessageSize > 128) {
    expectedMessageSize -= 2; // CRC of 2nd block
  }

  result.reserve(expectedMessageSize);

  // FIXME: handle truncated source messages

  const int block1Size = expectedMessageSize < 126 ? expectedMessageSize : 126;

  for (int i = 0; i < block1Size; ++i) {
    result.push_back(hexToByte(payload, sourceIndex));
    sourceIndex += 2; // 2 hex chars
  }

  // [2 bytes CRC]
  checksum   <<= 8;
  checksum    ^= hexToUL(payload, sourceIndex, 4);
  sourceIndex += 4; // Skip 2 bytes CRC => 4 hex chars

  if (expectedMessageSize > 126) {
    int block2Size = expectedMessageSize - 127;

    if (block2Size > 124) { block2Size = 124; }

    for (int i = 0; i < block2Size; ++i) {
      result.push_back(hexToByte(payload, sourceIndex));
      sourceIndex += 2; // 2 hex chars
    }

    // [2 bytes CRC]
    checksum <<= 8;
    checksum  ^= hexToUL(payload, sourceIndex, 4);
  }

  // remove the checksums and the 1st byte from the actual message length, so that the meaning of this byte is the same as in Frame A
  result[0] = static_cast<uint8_t>((expectedMessageSize - 1) & 0xff);

  return result;
}

#include "../DataStructs/Scheduler_TaskDeviceTimerID.h"

#include "../Helpers/Misc.h"
#include "../Helpers/StringConverter.h"

TaskDeviceTimerID::TaskDeviceTimerID(taskIndex_t taskIndex) :
  SchedulerTimerID(SchedulerTimerType_e::TaskDeviceTimer)
{
  setId(static_cast<uint32_t>(taskIndex));
}

#ifndef BUILD_NO_DEBUG
String TaskDeviceTimerID::decode() const
{
  const taskIndex_t taskIndex = getTaskIndex();

  return concat(F("Task "), validTaskIndex(taskIndex)
        ? getTaskDeviceName(taskIndex)
        : String(getId()));
}

#endif // ifndef BUILD_NO_DEBUG

#include "../DataStructs/ChecksumType.h"

#include "../Helpers/StringConverter.h"

#include <MD5Builder.h>

ChecksumType::ChecksumType(const ChecksumType& rhs)
{
  memcpy(_checksum, rhs._checksum, 16);
}

ChecksumType::ChecksumType(uint8_t checksum[16])
{
  memcpy(_checksum, checksum, 16);
}

ChecksumType::ChecksumType(const uint8_t *data,
                           size_t   data_length)
{
  computeChecksum(_checksum, data, data_length, data_length, true);
}

ChecksumType::ChecksumType(const uint8_t *data,
                           size_t   data_length,
                           size_t   len_upto_md5)
{
  computeChecksum(_checksum, data, data_length, len_upto_md5, true);
}

ChecksumType::ChecksumType(const String strings[], size_t nrStrings)
{
  MD5Builder md5;

  md5.begin();

  for (size_t i = 0; i < nrStrings; ++i) {
    md5.add(strings[i].c_str());
  }
  md5.calculate();
  md5.getBytes(_checksum);
}

bool ChecksumType::computeChecksum(
  uint8_t  checksum[16],
  const uint8_t *data,
  size_t   data_length,
  size_t   len_upto_md5,
  bool     updateChecksum)
{
  if (len_upto_md5 > data_length) { len_upto_md5 = data_length; }
  MD5Builder md5;

  md5.begin();

  if (len_upto_md5 > 0) {
    // MD5Builder::add has non-const argument
    md5.add(const_cast<uint8_t *>(data), len_upto_md5);
  }

  if ((len_upto_md5 + 16) < data_length) {
    data += len_upto_md5 + 16;
    const int len_after_md5 = data_length - 16 - len_upto_md5;

    if (len_after_md5 > 0) {
      // MD5Builder::add has non-const argument
      md5.add(const_cast<uint8_t *>(data), len_after_md5);
    }
  }
  md5.calculate();
  uint8_t tmp_md5[16] = { 0 };

  md5.getBytes(tmp_md5);

  if (memcmp(tmp_md5, checksum, 16) != 0) {
    // Data has changed, copy computed checksum
    if (updateChecksum) {
      memcpy(checksum, tmp_md5, 16);
    }
    return false;
  }
  return true;
}

void ChecksumType::getChecksum(uint8_t checksum[16]) const {
  memcpy(checksum, _checksum, 16);
}

void ChecksumType::setChecksum(const uint8_t checksum[16]) {
  memcpy(_checksum, checksum, 16);
}

bool ChecksumType::matchChecksum(const uint8_t checksum[16]) const {
  return memcmp(_checksum, checksum, 16) == 0;
}

bool ChecksumType::operator==(const ChecksumType& rhs) const {
  return memcmp(_checksum, rhs._checksum, 16) == 0;
}

ChecksumType& ChecksumType::operator=(const ChecksumType& rhs) {
  memcpy(_checksum, rhs._checksum, 16);
  return *this;
}

String ChecksumType::toString() const {
  return formatToHex_array(_checksum, 16);
}
#include "../DataStructs/GpioFactorySettingsStruct.h"

#include "../CustomBuild/ESPEasyDefaults.h"

GpioFactorySettingsStruct::GpioFactorySettingsStruct(DeviceModel model)
  :
  status_led(DEFAULT_PIN_STATUS_LED),
  i2c_sda(DEFAULT_PIN_I2C_SDA),
  i2c_scl(DEFAULT_PIN_I2C_SCL),
  eth_phyaddr(DEFAULT_ETH_PHY_ADDR),
  eth_phytype(DEFAULT_ETH_PHY_TYPE),
  eth_mdc(DEFAULT_ETH_PIN_MDC),
  eth_mdio(DEFAULT_ETH_PIN_MDIO),
  eth_power(DEFAULT_ETH_PIN_POWER),
  eth_clock_mode(DEFAULT_ETH_CLOCK_MODE),
  network_medium(DEFAULT_NETWORK_MEDIUM)

{
  for (int i = 0; i < 4; ++i) {
    button[i] = -1;
    relais[i] = -1;
  }

#ifndef LIMIT_BUILD_SIZE

  switch (model) {
#if defined(ESP8266)
    case DeviceModel::DeviceModel_Sonoff_Basic:
    case DeviceModel::DeviceModel_Sonoff_TH1x:
    case DeviceModel::DeviceModel_Sonoff_S2x:
    case DeviceModel::DeviceModel_Sonoff_TouchT1:
    case DeviceModel::DeviceModel_Sonoff_POWr2:
      button[0]  = 0;  // Single Button
      relais[0]  = 12; // Red Led and Relay (0 = Off, 1 = On)
      status_led = 13; // Green/Blue Led (0 = On, 1 = Off)
      i2c_sda    = -1;
      i2c_scl    = -1;
      break;
    case DeviceModel::DeviceModel_Sonoff_POW:
      button[0]  = 0;  // Single Button
      relais[0]  = 12; // Red Led and Relay (0 = Off, 1 = On)
      status_led = 15; // Blue Led (0 = On, 1 = Off)
      i2c_sda    = -1;
      i2c_scl    = -1; // GPIO5 conflicts with HLW8012 Sel output
      break;
    case DeviceModel::DeviceModel_Sonoff_TouchT2:
      button[0]  = 0;  // Button 1
      button[1]  = 9;  // Button 2
      relais[0]  = 12; // Led and Relay1 (0 = Off, 1 = On)
      relais[1]  = 4;  // Led and Relay2 (0 = Off, 1 = On)
      status_led = 13; // Blue Led (0 = On, 1 = Off)
      i2c_sda    = -1; // GPIO4 conflicts with GPIO_REL3
      i2c_scl    = -1; // GPIO5 conflicts with GPIO_REL2
      break;
    case DeviceModel::DeviceModel_Sonoff_TouchT3:
      button[0]  = 0;  // Button 1
      button[1]  = 10; // Button 2
      button[2]  = 9;  // Button 3
      relais[0]  = 12; // Led and Relay1 (0 = Off, 1 = On)
      relais[1]  = 5;  // Led and Relay2 (0 = Off, 1 = On)
      relais[2]  = 4;  // Led and Relay3 (0 = Off, 1 = On)
      status_led = 13; // Blue Led (0 = On, 1 = Off)
      i2c_sda    = -1; // GPIO4 conflicts with GPIO_REL3
      i2c_scl    = -1; // GPIO5 conflicts with GPIO_REL2
      break;

    case DeviceModel::DeviceModel_Sonoff_4ch:
      button[0]  = 0;             // Button 1
      button[1]  = 9;             // Button 2
      button[2]  = 10;            // Button 3
      button[3]  = 14;            // Button 4
      relais[0]  = 12;            // Red Led and Relay1 (0 = Off, 1 = On)
      relais[1]  = 5;             // Red Led and Relay2 (0 = Off, 1 = On)
      relais[2]  = 4;             // Red Led and Relay3 (0 = Off, 1 = On)
      relais[3]  = 15;            // Red Led and Relay4 (0 = Off, 1 = On)
      status_led = 13;            // Blue Led (0 = On, 1 = Off)
      i2c_sda    = -1;            // GPIO4 conflicts with GPIO_REL3
      i2c_scl    = -1;            // GPIO5 conflicts with GPIO_REL2
      break;
    case DeviceModel::DeviceModel_Shelly1:
      button[0]  = 5;             // Single Button
      relais[0]  = 4;             // Red Led and Relay (0 = Off, 1 = On)
      status_led = 15;            // Blue Led (0 = On, 1 = Off)
      i2c_sda    = -1;            // GPIO4 conflicts with relay control.
      i2c_scl    = -1;            // GPIO5 conflicts with SW input
      break;
    case DeviceModel::DeviceModel_ShellyPLUG_S:
      button[0]  = 13;            // Single Button
      relais[0]  = 15;            // Red Led and Relay (0 = Off, 1 = On)
      status_led = 2;             // Blue Led (0 = On, 1 = Off)
      i2c_sda    = -1;            // GPIO4 conflicts with relay control.
      i2c_scl    = -1;            // GPIO5 conflicts with SW input
      break;
#else
    case DeviceModel::DeviceModel_Sonoff_Basic:
    case DeviceModel::DeviceModel_Sonoff_TH1x:
    case DeviceModel::DeviceModel_Sonoff_S2x:
    case DeviceModel::DeviceModel_Sonoff_TouchT1:
    case DeviceModel::DeviceModel_Sonoff_POWr2:
    case DeviceModel::DeviceModel_Sonoff_POW:
    case DeviceModel::DeviceModel_Sonoff_TouchT2:
    case DeviceModel::DeviceModel_Sonoff_TouchT3:
    case DeviceModel::DeviceModel_Sonoff_4ch:
    case DeviceModel::DeviceModel_Shelly1:
    case DeviceModel::DeviceModel_ShellyPLUG_S:
      break;
#endif


# if CONFIG_ETH_USE_ESP32_EMAC
    case DeviceModel::DeviceModel_Olimex_ESP32_PoE:
      button[0]             = 34; // BUT1 Button
      relais[0]             = -1; // No LED's or relays on board
      status_led            = -1;
      i2c_sda               = 13;
      i2c_scl               = 16;
      eth_phyaddr           = 0;
      eth_phytype           = EthPhyType_t::LAN8720;
      eth_mdc               = 23;
      eth_mdio              = 18;
      eth_power             = 12;
      eth_clock_mode        = EthClockMode_t::Int_50MHz_GPIO_17_inv;
      network_medium = NetworkMedium_t::Ethernet;
      break;
    case DeviceModel::DeviceModel_Olimex_ESP32_EVB:
      button[0] = 34; // BUT1 Button
      relais[0] = 32; // LED1 + Relay1 (0 = Off, 1 = On)
      relais[1] = 33; // LED2 + Relay2 (0 = Off, 1 = On)

      status_led            = -1;
      i2c_sda               = 13;
      i2c_scl               = 16;
      eth_phyaddr           = 0;
      eth_phytype           = EthPhyType_t::LAN8720;
      eth_mdc               = 23;
      eth_mdio              = 18;
      eth_power             = -1; // No Ethernet power pin
      eth_clock_mode        = EthClockMode_t::Ext_crystal_osc;
      network_medium = NetworkMedium_t::Ethernet;
      break;

    case DeviceModel::DeviceModel_Olimex_ESP32_GATEWAY:
      button[0]             = 34; // BUT1 Button
      relais[0]             = -1; // No LED's or relays on board
      status_led            = 33;
      i2c_sda               = -1;
      i2c_scl               = -1;
      eth_phyaddr           = 0;
      eth_phytype           = EthPhyType_t::LAN8720;
      eth_mdc               = 23;
      eth_mdio              = 18;
      eth_power             = 5;
      eth_clock_mode        = EthClockMode_t::Int_50MHz_GPIO_17_inv;
      network_medium = NetworkMedium_t::Ethernet;
      // Rev A to E:
      // GPIO 5, 17 can be used only if Ethernet functionality is not used
      // GPIO 6, 7, 8, 9, 10, 11 used for internal flash and SD card
      // GPIO 33 - Status LED
      // GPIO 34 - User button
      // GPIO 16, 32, 35, 36, 39 free to use

      // Rev F and up:
      // GPIO 5, 17 can be used only if Ethernet functionality is not used
      // GPIO 2, 14, 15 are used for SD card, they are free to use if SD is not used.
      // GPIO 33 - Status LED
      // GPIO 34 - User button
      // GPIO 4, 12, 13, 32, 35, 36, 39 free to use

      // ESPEasy default setting:
      // No GPIO pins selected in profile for I2C.
      // Since there are none free to use in all revisions capable of input/output.
      // N.B. GPIO 35 and up are input only.

      break;

    case DeviceModel::DeviceModel_wESP32:
      status_led            = -1;
      i2c_sda               = 15;
      i2c_scl               = 4;
      eth_phyaddr           = 0;
      eth_phytype           = EthPhyType_t::LAN8720;
      eth_mdc               = 16;
      eth_mdio              = 17;
      eth_power             = -1;
      eth_clock_mode        = EthClockMode_t::Ext_crystal_osc;
      network_medium = NetworkMedium_t::Ethernet;
      break;

    case DeviceModel::DeviceModel_WT32_ETH01:
      status_led            = -1;
      i2c_sda               = 21;
      i2c_scl               = 22;
      eth_phyaddr           = 1;
      eth_phytype           = EthPhyType_t::LAN8720;
      eth_mdc               = 23;
      eth_mdio              = 18;
      eth_power             = 12;  // TODO TD-er: Better to use GPIO-16? as shown here: https://letscontrolit.com/forum/viewtopic.php?p=50133#p50133
      eth_clock_mode        = EthClockMode_t::Ext_crystal_osc;
      network_medium = NetworkMedium_t::Ethernet;
      break;

  #endif

    case DeviceModel::DeviceModel_default:
    case DeviceModel::DeviceModel_MAX:
      break;

      // Do not use default: as this allows the compiler to detect any missing cases.
      // default: break;
  }
  #endif
}

#include "../DataStructs/NTP_packet.h"

#include "../Helpers/ESPEasy_time_calc.h"

#include "../Helpers/StringConverter.h"

NTP_packet::NTP_packet()
{
  // li, vn, and mode:
  // - li.   2 bits. Leap indicator.
  //    0 = no warning
  //    1 = last minute of the day has 61 seconds
  //    2 = last minute of the day has 59 seconds
  //    3 = unknown (clock unsynchronized)
  // - vn.   3 bits. Version number of the protocol. (0b100 = v4)
  // - mode. 3 bits. Client will pick mode 3 for client.
  //    0 = reserved
  //    1 = symmetric active
  //    2 = symmetric passive
  //    3 = client
  //    4 = server
  //    5 = broadcast
  //    6 = NTP control message
  //    7 = reserved for private use
  data[0] = 0b11100011; // Unsynchronized, V4, client mode

  // Stratum level of the local clock.
  //    0      = unspecified or invalid
  //    1      = primary server (e.g., equipped with a GPS receiver)
  //    2-15   = secondary server (via NTP)
  //    16     = unsynchronized
  //    17-255 = reserved
  data[1] = 0u;

  // Poll: 8-bit signed integer representing the maximum interval between
  //       successive messages, in log2 seconds.  Suggested default limits for
  //       minimum and maximum poll intervals are 6 and 10, respectively.
  data[2] = 6u;

  // Precision: 8-bit signed integer representing the precision of the
  //    system clock, in log2 seconds.  For instance, a value of -18
  //    corresponds to a precision of about one microsecond.  The precision
  //    can be determined when the service first starts up as the minimum
  //    time of several iterations to read the system clock.
  data[3] = 0xEC; // -20 -> 2^-20 sec -> microsec precision.

  //constexpr int8_t precision = 0xEC;

  // Reference clock identifier. ASCII: "1N14"
  data[12] = 0x31;
  data[13] = 0x4E;
  data[14] = 0x31;
  data[15] = 0x34;
}

uint32_t NTP_packet::readWord(uint8_t startIndex) const
{
  uint32_t res{};

  res  = (uint32_t)data[startIndex] << 24;
  res |= (uint32_t)data[startIndex + 1] << 16;
  res |= (uint32_t)data[startIndex + 2] << 8;
  res |= (uint32_t)data[startIndex + 3];
  return res;
}

uint64_t NTP_packet::ntp_timestamp_to_Unix_time(uint8_t startIndex) const {
  // Apply offset from 1900/01/01 to 1970/01/01
  constexpr uint64_t offset_since_1900 = 2208988800ULL * 1000000ull;

  const uint32_t Tm_s      = readWord(startIndex);
  const uint32_t Tm_f      = readWord(startIndex + 4);
  uint64_t usec_since_1900 = sec_time_frac_to_Micros(Tm_s, Tm_f);

  if (usec_since_1900 < offset_since_1900) {
    // Fix overflow which will occur in 2036
    usec_since_1900 += (4294967296ull * 1000000ull);
  }
  return usec_since_1900 - offset_since_1900;
}

void NTP_packet::writeWord(uint32_t value, uint8_t startIndex)
{
  data[startIndex]     = (value >> 24) & 0xFF;
  data[startIndex + 1] = (value >> 16) & 0xFF;
  data[startIndex + 2] = (value >> 8) & 0xFF;
  data[startIndex + 3] = (value) & 0xFF;
}

bool NTP_packet::isUnsynchronized() const
{
  return (data[0] /*li_vn_mode*/ & 0b11000000) == 0b11000000;
}

uint64_t NTP_packet::getReferenceTimestamp_usec() const
{
  return ntp_timestamp_to_Unix_time(16);
}

uint64_t NTP_packet::getOriginTimestamp_usec() const
{
  return ntp_timestamp_to_Unix_time(24);
}

uint64_t NTP_packet::getReceiveTimestamp_usec() const
{
  return ntp_timestamp_to_Unix_time(32);
}

uint64_t NTP_packet::getTransmitTimestamp_usec() const
{
  return ntp_timestamp_to_Unix_time(40);
}

void NTP_packet::setTxTimestamp(uint64_t micros)
{
  constexpr uint64_t offset_since_1900 = 2208988800ULL * 1000000ull;

  micros += offset_since_1900;
  uint32_t tmp_origTm_f{};

  writeWord(micros_to_sec_time_frac(micros, tmp_origTm_f), 40);
  writeWord(tmp_origTm_f,                                  44);
}

bool NTP_packet::compute_usec(
  uint64_t localTXTimestamp_usec,
  uint64_t localRxTimestamp_usec,
  int64_t& offset_usec,
  int64_t& roundtripDelay_usec) const
{
  int64_t t1 = getOriginTimestamp_usec();

  if (t1 == 0) {
    t1 = localTXTimestamp_usec;
  }

  const int64_t t2 = getReceiveTimestamp_usec();
  const int64_t t3 = getTransmitTimestamp_usec();

  if ((t3 == 0) || (t3 < t2)) {
    // No time stamp received
    return false;
  }
  const int64_t t4 = localRxTimestamp_usec;

  offset_usec  = (t2 - t1) + (t3 - t4);
  offset_usec /= 2;

  roundtripDelay_usec = (t4 - t1) - (t3 - t2);
  return true;
}

String NTP_packet::getRefID_str(bool& isError) const
{
  String refID;

  const uint8_t stratum = data[1];

  isError = false;

  if ((stratum == 0) || (stratum == 1)) {
    refID = strformat(F("%c%c%c%c"),
                      static_cast<char>(data[12] & 0x7F),
                      static_cast<char>(data[13] & 0x7F),
                      static_cast<char>(data[14] & 0x7F),
                      static_cast<char>(data[15] & 0x7F));

    if (stratum == 0) {
      if (refID.equals(F("DENY")) ||
          refID.equals(F("RSTR"))) {
        // For kiss codes DENY and RSTR, the client MUST
        // demobilize any associations to that server and
        // stop sending packets to that server;
        // DENY = Access denied by remote server.
        // RSTR = Access denied due to local policy.
        isError = true;
      } else if (refID.equals(F("RATE"))) {
        // For kiss code RATE, the client MUST immediately reduce its
        // polling interval to that server and continue to reduce it each
        // time it receives a RATE kiss code.
      }
    }
  } else {
    const IPAddress addrv4(readWord(12));
    refID = addrv4.toString();
  }
  return refID;
}

#ifndef BUILD_NO_DEBUG
String NTP_packet::toDebugString() const
{
  const uint8_t li   = (data[0] >> 6) & 0x3; // Leap Indicator
  const uint8_t ver  = (data[0] >> 3) & 0x7; // Version
  const uint8_t mode = data[0] & 0x7;        // Mode

  bool isError{};

  return strformat(
    F("    li: %u ver: %u mode: %u\n"
      "    strat: %u poll: %d prec: %d\n"
      "    del:   %u disp: %u refID: '%s'\n"
      "    refTm_s : %u refTm_f : %u\n"
      "    origTm_s: %u origTm_f: %u\n"
      "    rxTm_s  : %u rxTm_f  : %u\n"
      "    txTm_s  : %u txTm_f  : %u\n"),
    li, ver, mode,                                        // li_vn_mode
    data[1],                                              // stratum,
    (int8_t)(data[2]),                                    // poll in log2 seconds
    (int8_t)(data[3]),                                    // precision in log2 seconds
    readWord(4), readWord(8),                             // rootDelay, rootDispersion,
    getRefID_str(isError).c_str(),
    readWord(16), unix_time_frac_to_micros(readWord(20)), // refTm_s, unix_time_frac_to_micros(refTm_f),
    readWord(24), unix_time_frac_to_micros(readWord(28)), // origTm_s, unix_time_frac_to_micros(origTm_f),
    readWord(32), unix_time_frac_to_micros(readWord(36)), // rxTm_s, unix_time_frac_to_micros(rxTm_f),
    readWord(40), unix_time_frac_to_micros(readWord(44))  // txTm_s, unix_time_frac_to_micros(txTm_f)

    );
}

#endif // ifndef BUILD_NO_DEBUG

#include "../DataStructs/I2CTypes.h"

const __FlashStringHelper* toString(I2C_bus_state state) {
  switch (state) {
    case I2C_bus_state::BusCleared:            return F("Bus Cleared");
    case I2C_bus_state::OK:                    return F("OK");
    case I2C_bus_state::NotConfigured:         return F("Not Configured");
    case I2C_bus_state::ClearingProcessActive: return F("Clearing Process Active");
    case I2C_bus_state::SCL_Low:               return F("SCL Low");
    case I2C_bus_state::SDA_Low_over_2_sec:    return F("SCL Low by I2C device clock stretch > 2 sec");
    case I2C_bus_state::SDA_Low_20_clocks:     return F("SDA Low");
  }
  return F("");
}

#include "../DataStructs/CRCStruct.h"

#include <string.h>

bool CRCStruct::checkPassed() const
{
  return memcmp(compileTimeMD5, runTimeMD5, 16) == 0;
}

#include "../DataTypes/TaskIndex.h"

#include "../../ESPEasy_common.h"

taskIndex_t    const INVALID_TASK_INDEX    = TASKS_MAX;
userVarIndex_t const INVALID_USERVAR_INDEX = USERVAR_MAX_INDEX;
taskVarIndex_t const INVALID_TASKVAR_INDEX = VARS_PER_TASK;
#include "../DataTypes/SchedulerIntervalTimer.h"

#include "../Helpers/StringConverter.h"

#ifndef BUILD_NO_DEBUG
const __FlashStringHelper * toString_f(SchedulerIntervalTimer_e timer) {
  switch (timer) {
    case SchedulerIntervalTimer_e::TIMER_20MSEC:           return F("TIMER_20MSEC");
    case SchedulerIntervalTimer_e::TIMER_100MSEC:          return F("TIMER_100MSEC");
    case SchedulerIntervalTimer_e::TIMER_1SEC:             return F("TIMER_1SEC");
    case SchedulerIntervalTimer_e::TIMER_30SEC:            return F("TIMER_30SEC");
    case SchedulerIntervalTimer_e::TIMER_MQTT:             return F("TIMER_MQTT");
    case SchedulerIntervalTimer_e::TIMER_STATISTICS:       return F("TIMER_STATISTICS");
    case SchedulerIntervalTimer_e::TIMER_GRATUITOUS_ARP:   return F("TIMER_GRATUITOUS_ARP");
    case SchedulerIntervalTimer_e::TIMER_MQTT_DELAY_QUEUE: return F("TIMER_MQTT_DELAY_QUEUE");
    default: 
      break;
  }
  return F("unknown");
}
#endif

String toString(SchedulerIntervalTimer_e timer) {
#ifdef BUILD_NO_DEBUG
  return String(static_cast<int>(timer));
#else // ifdef BUILD_NO_DEBUG

  if (timer >= SchedulerIntervalTimer_e::TIMER_C001_DELAY_QUEUE && 
      timer <= SchedulerIntervalTimer_e::TIMER_C025_DELAY_QUEUE) 
  {
    const int id = static_cast<int>(timer) - static_cast<int>(SchedulerIntervalTimer_e::TIMER_C001_DELAY_QUEUE) + 1;
    String res;
    res.reserve(24);
    res = F("TIMER_");
    res += get_formatted_Controller_number(id);
    res += F("_DELAY_QUEUE");
    return res;
  }
  return toString_f(timer);
#endif // ifdef BUILD_NO_DEBUG
}
#include "../DataTypes/PluginID.h"

#include "../Helpers/StringConverter.h"


String pluginID_t::toDisplayString() const {
  if (value == 0) { return F("P---"); }
  return strformat(F("P%03d"), value);
}

const pluginID_t INVALID_PLUGIN_ID;

#include "../DataTypes/TLS_types.h"

#if FEATURE_TLS
const __FlashStringHelper* toString(TLS_types tls_type)
{
  switch (tls_type) {
    case TLS_types::NoTLS:        break;
//    case TLS_types::TLS_PSK:      return F("TLS PreSharedKey");
    case TLS_types::TLS_CA_CERT:  return F("TLS CA Cert");
    case TLS_types::TLS_insecure: return F("TLS No Checks (insecure)");
    case TLS_types::TLS_FINGERPRINT: return F("TLS Certficate Fingerprint");
  }
  return F("No TLS");
}
#endif
#include "../DataTypes/TaskValues_Data.h"

#include "../DataStructs/TimingStats.h"
#include "../Helpers/Numerical.h"
#include "../Helpers/StringConverter_Numerical.h"

TaskValues_Data_t::TaskValues_Data_t() {
  ZERO_FILL(binary);
}

TaskValues_Data_t::TaskValues_Data_t(const TaskValues_Data_t& other)
{
  memcpy(binary, other.binary, sizeof(binary));
}

TaskValues_Data_t& TaskValues_Data_t::operator=(const TaskValues_Data_t& other)
{
  memcpy(binary, other.binary, sizeof(binary));
  return *this;
}

void TaskValues_Data_t::clear() {
  ZERO_FILL(binary);
}

void TaskValues_Data_t::copyValue(const TaskValues_Data_t& other, uint8_t varNr, Sensor_VType sensorType)
{
  if (sensorType != Sensor_VType::SENSOR_TYPE_STRING) {
    if (is32bitOutputDataType(sensorType)) {
      if (varNr < VARS_PER_TASK) {
        constexpr unsigned int size_32bit = sizeof(float);
        memcpy(binary + (varNr * size_32bit), other.binary + (varNr * size_32bit), size_32bit);
      }
    }
#if FEATURE_EXTENDED_TASK_VALUE_TYPES
    else {
      if ((varNr < (VARS_PER_TASK / 2))) {
        constexpr unsigned int size_64bit = sizeof(uint64_t);
        memcpy(binary + (varNr * size_64bit), other.binary + (varNr * size_64bit), size_64bit);
      }
    }
#endif
  }
}

unsigned long TaskValues_Data_t::getSensorTypeLong() const
{
  const uint16_t low   = getFloat(0);
  const uint16_t high  = getFloat(1);
  unsigned long  value = high;

  value <<= 16;
  value  |= low;
  return value;
}

void TaskValues_Data_t::setSensorTypeLong(unsigned long value)
{
  setFloat(0, value & 0xFFFF);
  setFloat(1, (value >> 16) & 0xFFFF);
}

#if FEATURE_EXTENDED_TASK_VALUE_TYPES

int32_t TaskValues_Data_t::getInt32(uint8_t varNr) const
{
  if (varNr < VARS_PER_TASK) {
    int32_t res{};
    constexpr unsigned int size_32bit = sizeof(float);
    memcpy(&res, binary + (varNr * size_32bit), size_32bit);
    return res;
  }
  return 0;
}

void TaskValues_Data_t::setInt32(uint8_t varNr, int32_t value)
{
  if (varNr < VARS_PER_TASK) {
    constexpr unsigned int size_32bit = sizeof(float);
    memcpy(binary + (varNr * size_32bit), &value, size_32bit);
  }
}
#endif

uint32_t TaskValues_Data_t::getUint32(uint8_t varNr) const
{
  if (varNr < VARS_PER_TASK) {
    uint32_t res{};
    constexpr unsigned int size_32bit = sizeof(float);
    memcpy(&res, binary + (varNr * size_32bit), size_32bit);
    return res;
  }
  return 0u;
}

void TaskValues_Data_t::setUint32(uint8_t varNr, uint32_t value)
{
  if (varNr < VARS_PER_TASK) {
    constexpr unsigned int size_32bit = sizeof(float);
    memcpy(binary + (varNr * size_32bit), &value, size_32bit);
  }
}

#if FEATURE_EXTENDED_TASK_VALUE_TYPES

int64_t TaskValues_Data_t::getInt64(uint8_t varNr) const
{
  if ((varNr < (VARS_PER_TASK / 2))) {
    int64_t res{};
    constexpr unsigned int size_64bit = sizeof(uint64_t);
    memcpy(&res, binary + (varNr * size_64bit), size_64bit);
    return res;
  }
  return 0;
}

void TaskValues_Data_t::setInt64(uint8_t varNr, int64_t value)
{
  if ((varNr < (VARS_PER_TASK / 2))) {
    constexpr unsigned int size_64bit = sizeof(uint64_t);
    memcpy(binary + (varNr * size_64bit), &value, size_64bit);
  }
}

uint64_t TaskValues_Data_t::getUint64(uint8_t varNr) const
{
  if ((varNr < (VARS_PER_TASK / 2))) {
    uint64_t res{};
    constexpr unsigned int size_64bit = sizeof(uint64_t);
    memcpy(&res, binary + (varNr * size_64bit), size_64bit);
    return res;
  }
  return 0u;
}

void TaskValues_Data_t::setUint64(uint8_t varNr, uint64_t value)
{
  if ((varNr < (VARS_PER_TASK / 2))) {
    constexpr unsigned int size_64bit = sizeof(uint64_t);
    memcpy(binary + (varNr * size_64bit), &value, size_64bit);
  }
}
#endif

float TaskValues_Data_t::getFloat(uint8_t varNr) const
{
  if (varNr < VARS_PER_TASK) {
    float res{};
    constexpr unsigned int size_32bit = sizeof(float);
    memcpy(&res, binary + (varNr * size_32bit), size_32bit);
    return res;
  }
  return 0.0f;
}

void TaskValues_Data_t::setFloat(uint8_t varNr, float  value)
{
  if (varNr < VARS_PER_TASK) {
    constexpr unsigned int size_32bit = sizeof(float);
    memcpy(binary + (varNr * size_32bit), &value, size_32bit);
  }
}

#if FEATURE_EXTENDED_TASK_VALUE_TYPES
#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
double TaskValues_Data_t::getDouble(uint8_t varNr) const
{
  if ((varNr < (VARS_PER_TASK / 2))) {
    double res{};
    constexpr unsigned int size_64bit = sizeof(uint64_t);
    memcpy(&res, binary + (varNr * size_64bit), size_64bit);
    return res;
  }
  return 0.0;
}

void TaskValues_Data_t::setDouble(uint8_t varNr, double  value)
{
  if ((varNr < (VARS_PER_TASK / 2))) {
    constexpr unsigned int size_64bit = sizeof(uint64_t);
    memcpy(binary + (varNr * size_64bit), &value, size_64bit);
  }
}
#endif
#endif

ESPEASY_RULES_FLOAT_TYPE TaskValues_Data_t::getAsDouble(uint8_t varNr, Sensor_VType sensorType) const
{
  if (sensorType == Sensor_VType::SENSOR_TYPE_ULONG) {
    return getSensorTypeLong();
  } else if (isFloatOutputDataType(sensorType)) {
    return getFloat(varNr);
#if FEATURE_EXTENDED_TASK_VALUE_TYPES
  } else if (isUInt32OutputDataType(sensorType)) {
    return getUint32(varNr);
  } else if (isInt32OutputDataType(sensorType)) {
    return getInt32(varNr);
  } else if (isUInt64OutputDataType(sensorType)) {
    return getUint64(varNr);
  } else if (isInt64OutputDataType(sensorType)) {
    return getInt64(varNr);
#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
  } else if (isDoubleOutputDataType(sensorType)) {
    return getDouble(varNr);
#endif    
#endif
  }
  return 0.0;
}

void TaskValues_Data_t::set(uint8_t varNr, const ESPEASY_RULES_FLOAT_TYPE& value, Sensor_VType sensorType)
{
  if (sensorType == Sensor_VType::SENSOR_TYPE_ULONG) {
    // Legacy formatting the old "SENSOR_TYPE_ULONG" type
    setSensorTypeLong(value);
  } else if (isFloatOutputDataType(sensorType)) {
    setFloat(varNr, value);
#if FEATURE_EXTENDED_TASK_VALUE_TYPES
  } else if (isUInt32OutputDataType(sensorType)) {
    setUint32(varNr, value);
  } else if (isInt32OutputDataType(sensorType)) {
    setInt32(varNr, value);
  } else if (isUInt64OutputDataType(sensorType)) {
    setUint64(varNr, value);
  } else if (isInt64OutputDataType(sensorType)) {
    setInt64(varNr, value);
#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
  } else if (isDoubleOutputDataType(sensorType)) {
    setDouble(varNr, value);
#endif
#endif
  }
}

bool TaskValues_Data_t::isValid(uint8_t varNr, Sensor_VType  sensorType) const
{
  if (sensorType == Sensor_VType::SENSOR_TYPE_NONE) {
    return false;
  } else if (isFloatOutputDataType(sensorType)) {
    return isValidFloat(getFloat(varNr));
#if FEATURE_EXTENDED_TASK_VALUE_TYPES
#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
  } else if (isDoubleOutputDataType(sensorType)) {
    return isValidDouble(getDouble(varNr));
#endif
#endif
  }
  return true;
}

String TaskValues_Data_t::getAsString(uint8_t varNr, Sensor_VType  sensorType, uint8_t nrDecimals) const
{
  String result;

  if (isFloatOutputDataType(sensorType)) {
    const float value = getFloat(varNr);
    if (nrDecimals == 254) {  // FIXME TD-er: Must use defines for these special situations
      nrDecimals = maxNrDecimals_fpType(value);
    }
    result = toString(value, nrDecimals);
#if FEATURE_EXTENDED_TASK_VALUE_TYPES
#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
  } else if (isDoubleOutputDataType(sensorType)) {
    const double value = getDouble(varNr);
    if (nrDecimals == 254) {  // FIXME TD-er: Must use defines for these special situations
      nrDecimals = maxNrDecimals_fpType(value);
    }
    result = doubleToString(value, nrDecimals);
#endif
#endif
  } else if (sensorType == Sensor_VType::SENSOR_TYPE_ULONG) {
    return String(getSensorTypeLong());
#if FEATURE_EXTENDED_TASK_VALUE_TYPES
  } else if (isUInt32OutputDataType(sensorType)) {
    return String(getUint32(varNr));
  } else if (isInt32OutputDataType(sensorType)) {
    return String(getInt32(varNr));
  } else if (isUInt64OutputDataType(sensorType)) {
    return ull2String(getUint64(varNr));
  } else if (isInt64OutputDataType(sensorType)) {
    return ll2String(getInt64(varNr));
#endif
  }
  result.trim();
  return result;
}

#include "../DataTypes/FormSelectorOptions.h"

#include "../WebServer/Markup.h"
#include "../WebServer/Markup_Forms.h"
#include "../WebServer/HTML_wrappers.h"

FormSelectorOptions::FormSelectorOptions()
  : classname(F("wide")), _onlySelectorHead(true)
{}


FormSelectorOptions::FormSelectorOptions(int optionCount)
  :   classname(F("wide")), _optionCount(optionCount)
{}

FormSelectorOptions::FormSelectorOptions(
  int          optionCount,
  const int    indices[],
  const String attr[]) :
  classname(F("wide")),
  _optionCount(optionCount),
  _indices(indices),
  _attr_str(attr)
{}

FormSelectorOptions::FormSelectorOptions(
  int          optionCount,
  const String options[],
  const int    indices[],
  const String attr[]) :
  classname(F("wide")),
  _optionCount(optionCount),
  _names_str(options),
  _indices(indices),
  _attr_str(attr)
{}

FormSelectorOptions::FormSelectorOptions(
  int                        optionCount,
  const __FlashStringHelper *options[],
  const int                  indices[],
  const String               attr[]) :
  classname(F("wide")),
  _optionCount(optionCount),
  _names_f(options),
  _indices(indices),
  _attr_str(attr)
{}

FormSelectorOptions::~FormSelectorOptions() {}

String FormSelectorOptions::getOptionString(int index) const
{
  if (index >= _optionCount) {
    return EMPTY_STRING;
  }

  if (_names_f != nullptr) {
    return String(_names_f[index]);
  }

  if (_names_str != nullptr) {
    return String(_names_str[index]);
  }

  if (_indices != nullptr) {
    return String(_indices[index]);
  }
  return String(index);
}

int FormSelectorOptions::getIndexValue(int index) const
{
  if (index >= _optionCount) {
    return -1;
  }

  if (_indices != nullptr) {
    return _indices[index];
  }
  return index;
}

bool FormSelectorOptions::isDone(int index) const
{
  return index >= _optionCount;
}

void FormSelectorOptions::clearClassName()
{
  classname = F("");
}

void FormSelectorOptions::addFormSelector(
  const __FlashStringHelper *label,
  const __FlashStringHelper *id,
  int                        selectedIndex) const
{
  addFormSelector(String(label), String(id), selectedIndex);
}

void FormSelectorOptions::addFormSelector(
  const String             & label,
  const __FlashStringHelper *id,
  int                        selectedIndex) const
{
  addFormSelector(label, String(id), selectedIndex);
}

void FormSelectorOptions::addFormSelector(
  const __FlashStringHelper *label,
  const String             & id,
  int                        selectedIndex) const
{
  addFormSelector(String(label), id, selectedIndex);
}

void FormSelectorOptions::addFormSelector(
  const String& label,
  const String& id,
  int           selectedIndex) const
{
  addRowLabel_tr_id(label, id);
  addSelector(id, selectedIndex);
}

void FormSelectorOptions::addSelector(const __FlashStringHelper *id,
                                      int                        selectedIndex) const
{
  addSelector(String(id), selectedIndex);
}

void FormSelectorOptions::addSelector(const String& id,
                                      int           selectedIndex) const
{
  // FIXME TD-er Change bool 'enabled' to disabled
  if (reloadonchange)
  {
    addSelector_Head_reloadOnChange(id, classname, !enabled
                                    #if FEATURE_TOOLTIPS
                                    , tooltip
                                    #endif // if FEATURE_TOOLTIPS
                                    );
  } else {
    do_addSelector_Head(id, classname, onChangeCall, !enabled
                        #if FEATURE_TOOLTIPS
                        , tooltip
                        #endif // if FEATURE_TOOLTIPS
                        );
  }

  if (_onlySelectorHead) { return; }

  for (int i = 0; !isDone(i); ++i)
  {
    const int index = getIndexValue(i);
    String optionString = getOptionString(i);
    if (index == default_index) {
      optionString += F(" (default)");
    }
    addSelector_Item(
      optionString,
      index,
      selectedIndex == index,
      false,
      _attr_str ? _attr_str[i] : EMPTY_STRING);

    if ((i & 0x07) == 0) { delay(0); }
  }

  addSelectorFoot();
}

void FormSelectorOptions::addSelectorFoot() const
{
  addSelector_Foot(reloadonchange);
}

#include "../DataTypes/NPluginID.h"

#include "../Helpers/StringConverter.h"

String npluginID_t::toDisplayString() const {
  if (value == 0) { return F("N---"); }
  return strformat(F("N%03d"), value);
}

const npluginID_t INVALID_N_PLUGIN_ID;

#include "../DataTypes/NodeTypeID.h"


const __FlashStringHelper* toNodeTypeDisplayString(uint8_t nodeType) {
  switch (nodeType)
  {
    case NODE_TYPE_ID_ESP_EASY_STD:      return F("ESP Easy");
    case NODE_TYPE_ID_ESP_EASYM_STD:     return F("ESP Easy Mega");
    case NODE_TYPE_ID_ESP_EASY32_STD:    return F("ESP Easy 32");
    case NODE_TYPE_ID_ESP_EASY32S2_STD:  return F("ESP Easy 32-S2");
    case NODE_TYPE_ID_ESP_EASY32S3_STD:  return F("ESP Easy 32-S3");
    case NODE_TYPE_ID_ESP_EASY32C2_STD:  return F("ESP Easy 32-C2");
    case NODE_TYPE_ID_ESP_EASY32C3_STD:  return F("ESP Easy 32-C3");
    case NODE_TYPE_ID_ESP_EASY32C5_STD:  return F("ESP Easy 32-C5");
    case NODE_TYPE_ID_ESP_EASY32C6_STD:  return F("ESP Easy 32-C6");
    case NODE_TYPE_ID_ESP_EASY32C61_STD: return F("ESP Easy 32-C61");
    case NODE_TYPE_ID_ESP_EASY32H2_STD:  return F("ESP Easy 32-H2");
    case NODE_TYPE_ID_ESP_EASY32P4_STD:  return F("ESP Easy 32-P4");
    case NODE_TYPE_ID_RPI_EASY_STD:      return F("RPI Easy");
    case NODE_TYPE_ID_ARDUINO_EASY_STD:  return F("Arduino Easy");
    case NODE_TYPE_ID_NANO_EASY_STD:     return F("Nano Easy");
  }
  return F("");
}
#include "../DataTypes/ControllerIndex.h"

#include "../CustomBuild/ESPEasyLimits.h"

controllerIndex_t INVALID_CONTROLLER_INDEX = CONTROLLER_MAX;
#include "../DataTypes/IntendedRebootReason.h"

const __FlashStringHelper * toString_f(IntendedRebootReason_e reason) {
  switch (reason) {
    case IntendedRebootReason_e::DeepSleep:              return F("DeepSleep");
    case IntendedRebootReason_e::DelayedReboot:          return F("DelayedReboot");
    case IntendedRebootReason_e::ResetFactory:           return F("ResetFactory");
    case IntendedRebootReason_e::ResetFactoryPinActive:  return F("ResetFactoryPinActive");
    case IntendedRebootReason_e::ResetFactoryCommand:    return F("ResetFactoryCommand");
    case IntendedRebootReason_e::CommandReboot:          return F("CommandReboot");
    case IntendedRebootReason_e::RestoreSettings:        return F("RestoreSettings");
    case IntendedRebootReason_e::OTA_error:              return F("OTA_error");
    case IntendedRebootReason_e::ConnectionFailuresThreshold: return F("ConnectionFailuresThreshold");
  }
  return F("");
}

String toString(IntendedRebootReason_e reason) {
  String res = toString_f(reason);
  if (res.isEmpty()) return String(static_cast<int>(reason));
  return res;
}

#include "../DataTypes/SchedulerPluginPtrType.h"

const __FlashStringHelper* toString(SchedulerPluginPtrType_e pluginType) {
  switch (pluginType) {
    case SchedulerPluginPtrType_e::TaskPlugin:         return F("Plugin");
    case SchedulerPluginPtrType_e::ControllerPlugin:   return F("Controller");
#if FEATURE_NOTIFIER
    case SchedulerPluginPtrType_e::NotificationPlugin: return F("Notification");
#endif // if FEATURE_NOTIFIER
  }
  return F("Unknown");
}

#include "../DataTypes/DeviceIndex.h"


deviceIndex_t INVALID_DEVICE_INDEX = deviceIndex_t::toDeviceIndex(DEVICE_INDEX_MAX);

#include "../DataTypes/ESPEasyFileType.h"

#include "../../ESPEasy_common.h"

#include "../CustomBuild/StorageLayout.h"

#include "../Globals/ResetFactoryDefaultPref.h"

#include "../Helpers/StringConverter.h"

bool matchFileType(const String& filename, FileType::Enum filetype)
{
  if (filename.startsWith(F("/"))) {
    return matchFileType(filename.substring(1), filetype);
  }
  return filename.equalsIgnoreCase(getFileName(filetype));
}

bool isProtectedFileType(const String& filename)
{
  #if FEATURE_EXTENDED_CUSTOM_SETTINGS
  bool isTaskSpecificConfig = false;
  const String fname        = filename.substring(filename.startsWith(F("/")) ? 1 : 0);
  const String mask         = F(DAT_TASKS_CUSTOM_EXTENSION_FILEMASK);
  const int8_t mPerc        = mask.indexOf('%');

  if ((mPerc > -1) && fname.startsWith(mask.substring(0, mPerc))) {
    for (uint8_t n = 0; n < TASKS_MAX && !isTaskSpecificConfig; ++n) {
      isTaskSpecificConfig |= (fname.equalsIgnoreCase(strformat(mask, n + 1)));
    }
  }
  #endif // if FEATURE_EXTENDED_CUSTOM_SETTINGS

  return
    #if FEATURE_EXTENDED_CUSTOM_SETTINGS
    isTaskSpecificConfig || // Support for extcfgNN.dat
    #endif // if FEATURE_EXTENDED_CUSTOM_SETTINGS
    matchFileType(filename, FileType::CONFIG_DAT) ||
    matchFileType(filename, FileType::SECURITY_DAT) ||
    matchFileType(filename, FileType::NOTIFICATION_DAT) ||
    matchFileType(filename, FileType::PROVISIONING_DAT);
}

const __FlashStringHelper* getFileName(FileType::Enum filetype) {
  switch (filetype)
  {
    case FileType::CONFIG_DAT:       return F("config.dat");
    case FileType::NOTIFICATION_DAT: return F("notification.dat");
    case FileType::SECURITY_DAT:     return F("security.dat");
    case FileType::PROVISIONING_DAT: return F("provisioning.dat");
    case FileType::RULES_TXT:
      // Use getRulesFileName
      break;

    case FileType::MAX_FILETYPE:
      break;
  }
  return F("");
}

String getFileName(FileType::Enum filetype, unsigned int filenr) {
  if (filetype == FileType::RULES_TXT) {
    return getRulesFileName(filenr);
  }
  return getFileName(filetype);
}

// filenr = 0...3 for files rules1.txt ... rules4.txt
String getRulesFileName(unsigned int filenr) {
  String result;

  if (filenr < RULESETS_MAX) {
    result += F("rules");
    result += filenr + 1;
    result += F(".txt");
  }
  return result;
}

bool getDownloadFiletypeChecked(FileType::Enum filetype, unsigned int filenr) {
  switch (filetype) {
    case FileType::CONFIG_DAT:       return ResetFactoryDefaultPreference.fetchConfigDat();
    case FileType::SECURITY_DAT:     return ResetFactoryDefaultPreference.fetchSecurityDat();
    case FileType::NOTIFICATION_DAT: return ResetFactoryDefaultPreference.fetchNotificationDat();
    case FileType::RULES_TXT:        return ResetFactoryDefaultPreference.fetchRulesTXT(filenr);
    case FileType::PROVISIONING_DAT: return ResetFactoryDefaultPreference.fetchProvisioningDat();
      break;

    case FileType::MAX_FILETYPE:
      break;
  }
  return false;
}

#include "../DataTypes/WiFiConnectionProtocol.h"

const __FlashStringHelper * toString(WiFiConnectionProtocol proto) {
  switch (proto) {
    case WiFiConnectionProtocol::WiFi_Protocol_11b:
      return F("802.11b");
    case WiFiConnectionProtocol::WiFi_Protocol_11g:
      return F("802.11g");
#ifdef ESP8266
    case WiFiConnectionProtocol::WiFi_Protocol_11n:
      return F("802.11n");
#endif
#ifdef ESP32
    case WiFiConnectionProtocol::WiFi_Protocol_HT20:
      return F("802.11n (HT20)");
    case WiFiConnectionProtocol::WiFi_Protocol_HT40:
      return F("802.11n (HT40)");
    case WiFiConnectionProtocol::WiFi_Protocol_HE20:
      return F("802.11ax (HE20)");
#if ESP_IDF_VERSION > ESP_IDF_VERSION_VAL(5, 2, 0)
    case WiFiConnectionProtocol::WiFi_Protocol_11a:
      return F("802.11a");
    case WiFiConnectionProtocol::WiFi_Protocol_VHT20:
      return F("802.11ac (VHT20)");
#endif
    case WiFiConnectionProtocol::WiFi_Protocol_LR:
      return F("802.11lr");

#endif
    case WiFiConnectionProtocol::Unknown:
      break;;
  }
  return F("-");
}
#include "../DataTypes/SPI_options.h"


#ifdef ESP32

// ESP32 VSPI:
//  SCK  = 18
//  MISO = 19
//  MOSI = 23
// ESP32 HSPI:
//  SCK  = 14
//  MISO = 12
//  MOSI = 13


// ESP32-S2 FSPI:
//  SCK  = 36
//  MISO = 37
//  MOSI = 35

// ESP32-S3 FSPI:
//  SCK  = 36
//  MISO = 37
//  MOSI = 35

// ESP32-C2 SPI:
//  SCK  = 4
//  MISO = 5
//  MOSI = 6

// ESP32-C3 SPI:
//  SCK  = 4
//  MISO = 5
//  MOSI = 6


#  if CONFIG_IDF_TARGET_ESP32S3   // ESP32-S3
#define VSPI_FSPI_SHORT_STRING "FSPI"
#  elif CONFIG_IDF_TARGET_ESP32S2   // ESP32-S2
#define VSPI_FSPI_SHORT_STRING "FSPI"
#  elif CONFIG_IDF_TARGET_ESP32C2 // ESP32-C2
#define VSPI_FSPI_SHORT_STRING "FSPI"
#  elif CONFIG_IDF_TARGET_ESP32C3 // ESP32-C3
#define VSPI_FSPI_SHORT_STRING "SPI"
#  elif CONFIG_IDF_TARGET_ESP32C6 // ESP32-C6
#define VSPI_FSPI_SHORT_STRING "FSPI"
#  elif CONFIG_IDF_TARGET_ESP32   // ESP32/PICO-D4
#define VSPI_FSPI_SHORT_STRING "VSPI"

#  else // if CONFIG_IDF_TARGET_ESP32S2
#   error Target CONFIG_IDF_TARGET is not supported
#  endif // if CONFIG_IDF_TARGET_ESP32S2


const __FlashStringHelper* getSPI_optionToString(SPI_Options_e option) {
  switch (option) {
    case SPI_Options_e::None:
      return F("Disabled");
    case SPI_Options_e::Vspi_Fspi:
      return F(
        VSPI_FSPI_SHORT_STRING 
        ": CLK=GPIO-" STRINGIFY(VSPI_FSPI_SCK) 
        ", MISO=GPIO-" STRINGIFY(VSPI_FSPI_MISO) 
        ", MOSI=GPIO-" STRINGIFY(VSPI_FSPI_MOSI) );
#ifdef ESP32_CLASSIC
    case SPI_Options_e::Hspi:
      return F(
        "HSPI"
        ": CLK=GPIO-" STRINGIFY(HSPI_SCLK) 
        ", MISO=GPIO-" STRINGIFY(HSPI_MISO) 
        ", MOSI=GPIO-" STRINGIFY(HSPI_MOSI) );

      
#endif
    case SPI_Options_e::UserDefined:
      return F("User-defined: CLK, MISO, MOSI GPIO-pins");
  }
  return F("Unknown");
}

const __FlashStringHelper* getSPI_optionToShortString(SPI_Options_e option) {
  switch (option) {
    case SPI_Options_e::None:
      return F("Disabled");
    case SPI_Options_e::Vspi_Fspi:
      return F(VSPI_FSPI_SHORT_STRING);
#ifdef ESP32_CLASSIC
    case SPI_Options_e::Hspi:
      return F("HSPI");
#endif
    case SPI_Options_e::UserDefined:
      return F("User-defined SPI");
  }
  return F("Unknown");
}

#endif // ifdef ESP32

#include "../DataTypes/NotifierIndex.h"

#include "../CustomBuild/ESPEasyLimits.h"

notifierIndex_t INVALID_NOTIFIER_INDEX = NOTIFICATION_MAX;

#include "../DataTypes/ProtocolIndex.h"

#include "../CustomBuild/ESPEasyLimits.h"

protocolIndex_t   INVALID_PROTOCOL_INDEX   = CPLUGIN_MAX;
#include "../DataTypes/CPluginID.h"

const cpluginID_t INVALID_C_PLUGIN_ID = 0;


#include "../DataTypes/NetworkMedium.h"

bool isValid(NetworkMedium_t medium) {
  switch (medium) {
    case NetworkMedium_t::WIFI:
    case NetworkMedium_t::Ethernet:
#ifdef USES_ESPEASY_NOW
    case NetworkMedium_t::ESPEasyNOW_only:
#endif
      return true;

    case NetworkMedium_t::NotSet:
      return false;

      // Do not use default: as this allows the compiler to detect any missing cases.
  }
  return false;
}

const __FlashStringHelper * toString(NetworkMedium_t medium) {
  switch (medium) {
    case NetworkMedium_t::WIFI:     return F("WiFi");
    case NetworkMedium_t::Ethernet: return F("Ethernet");
#ifdef USES_ESPEASY_NOW
    case NetworkMedium_t::ESPEasyNOW_only:  return F(ESPEASY_NOW_NAME " only");
#endif
    case NetworkMedium_t::NotSet:   return F("Not Set");

      // Do not use default: as this allows the compiler to detect any missing cases.
  }
  return F("Unknown");
}
#include "../DataTypes/TimeSource.h"

const __FlashStringHelper* toString(ExtTimeSource_e timeSource)
{
  switch (timeSource) {
    case ExtTimeSource_e::None: break;
    case ExtTimeSource_e::DS1307:  return F("DS1307");
    case ExtTimeSource_e::DS3231:  return F("DS3231");
    case ExtTimeSource_e::PCF8523: return F("PCF8523");
    case ExtTimeSource_e::PCF8563: return F("PCF8563");
  }
  return F("-");
}

#include "../DataTypes/SensorVType.h"
#include "../Helpers/StringConverter.h"


/*********************************************************************************************\
   Get value count from sensor type

   Only use this function to determine nr of output values when changing output type of a task
   To get the actual output values for a task, use getValueCountForTask
\*********************************************************************************************/
uint8_t getValueCountFromSensorType(Sensor_VType sensorType) {
  return getValueCountFromSensorType(sensorType, true);
}
uint8_t getValueCountFromSensorType(Sensor_VType sensorType, bool log)
{
  switch (sensorType)
  {
    case Sensor_VType::SENSOR_TYPE_NONE:
      return 0;
    case Sensor_VType::SENSOR_TYPE_SINGLE:        // single value sensor, used for Dallas, BH1750, etc
#if FEATURE_EXTENDED_TASK_VALUE_TYPES
    case Sensor_VType::SENSOR_TYPE_INT32_SINGLE:  // 1x int32_t
    case Sensor_VType::SENSOR_TYPE_UINT64_SINGLE: // 1x uint64_t
    case Sensor_VType::SENSOR_TYPE_INT64_SINGLE:  // 1x int64_t
    case Sensor_VType::SENSOR_TYPE_DOUBLE_SINGLE: // 1x ESPEASY_RULES_FLOAT_TYPE
#endif // if FEATURE_EXTENDED_TASK_VALUE_TYPES
    case Sensor_VType::SENSOR_TYPE_SWITCH:
    case Sensor_VType::SENSOR_TYPE_DIMMER:
    case Sensor_VType::SENSOR_TYPE_ULONG:  // single unsigned LONG value, stored in two floats (rfid tags)
    case Sensor_VType::SENSOR_TYPE_STRING: // String type data stored in the event->String2
      return 1;
    case Sensor_VType::SENSOR_TYPE_TEMP_HUM:
    case Sensor_VType::SENSOR_TYPE_TEMP_BARO:
    case Sensor_VType::SENSOR_TYPE_DUAL:        // 2x float
#if FEATURE_EXTENDED_TASK_VALUE_TYPES
    case Sensor_VType::SENSOR_TYPE_UINT32_DUAL: // 2x uint32_t
    case Sensor_VType::SENSOR_TYPE_INT32_DUAL:  // 2x int32_t
    case Sensor_VType::SENSOR_TYPE_UINT64_DUAL: // 2x uint64_t
    case Sensor_VType::SENSOR_TYPE_INT64_DUAL:  // 2x int64_t
    case Sensor_VType::SENSOR_TYPE_DOUBLE_DUAL: // 2x ESPEASY_RULES_FLOAT_TYPE
#endif // if FEATURE_EXTENDED_TASK_VALUE_TYPES
      return 2;
    case Sensor_VType::SENSOR_TYPE_TEMP_HUM_BARO:
    case Sensor_VType::SENSOR_TYPE_TEMP_EMPTY_BARO: // Values 1 and 3 will contain data.
    case Sensor_VType::SENSOR_TYPE_TRIPLE:          // 3x float
#if FEATURE_EXTENDED_TASK_VALUE_TYPES
    case Sensor_VType::SENSOR_TYPE_UINT32_TRIPLE:   // 3x uint32_t
    case Sensor_VType::SENSOR_TYPE_INT32_TRIPLE:    // 3x int32_t
#endif // if FEATURE_EXTENDED_TASK_VALUE_TYPES
    case Sensor_VType::SENSOR_TYPE_WIND:
      return 3;
    case Sensor_VType::SENSOR_TYPE_QUAD:        // 4x float
#if FEATURE_EXTENDED_TASK_VALUE_TYPES
    case Sensor_VType::SENSOR_TYPE_UINT32_QUAD: // 4x uint32_t
    case Sensor_VType::SENSOR_TYPE_INT32_QUAD:  // 4x int32_t
#endif // if FEATURE_EXTENDED_TASK_VALUE_TYPES
      return 4;
    case Sensor_VType::SENSOR_TYPE_NOT_SET:  break;

    // FIXME To be ignored?
    case Sensor_VType::SENSOR_TYPE_ANALOG_ONLY:
    case Sensor_VType::SENSOR_TYPE_TEMP_ONLY:
    case Sensor_VType::SENSOR_TYPE_HUM_ONLY:
    case Sensor_VType::SENSOR_TYPE_LUX_ONLY:
    case Sensor_VType::SENSOR_TYPE_DISTANCE_ONLY:
    case Sensor_VType::SENSOR_TYPE_DIRECTION_ONLY:
    case Sensor_VType::SENSOR_TYPE_DUSTPM2_5_ONLY:
    case Sensor_VType::SENSOR_TYPE_DUSTPM1_0_ONLY:
    case Sensor_VType::SENSOR_TYPE_DUSTPM10_ONLY:
    case Sensor_VType::SENSOR_TYPE_MOISTURE_ONLY:
    case Sensor_VType::SENSOR_TYPE_CO2_ONLY:
    case Sensor_VType::SENSOR_TYPE_GPS_ONLY:
    case Sensor_VType::SENSOR_TYPE_UV_ONLY:
    case Sensor_VType::SENSOR_TYPE_UV_INDEX_ONLY:
    case Sensor_VType::SENSOR_TYPE_IR_ONLY:
    case Sensor_VType::SENSOR_TYPE_WEIGHT_ONLY:
    case Sensor_VType::SENSOR_TYPE_VOLTAGE_ONLY:
    case Sensor_VType::SENSOR_TYPE_CURRENT_ONLY:
    case Sensor_VType::SENSOR_TYPE_POWER_USG_ONLY:
    case Sensor_VType::SENSOR_TYPE_POWER_FACT_ONLY:
    case Sensor_VType::SENSOR_TYPE_APPRNT_POWER_USG_ONLY:
    case Sensor_VType::SENSOR_TYPE_TVOC_ONLY:
    case Sensor_VType::SENSOR_TYPE_BARO_ONLY:
    case Sensor_VType::SENSOR_TYPE_COLOR_RED_ONLY:
    case Sensor_VType::SENSOR_TYPE_COLOR_GREEN_ONLY:
    case Sensor_VType::SENSOR_TYPE_COLOR_BLUE_ONLY:
    case Sensor_VType::SENSOR_TYPE_COLOR_TEMP_ONLY:
    case Sensor_VType::SENSOR_TYPE_REACTIVE_POWER_ONLY:
    case Sensor_VType::SENSOR_TYPE_AQI_ONLY:
    case Sensor_VType::SENSOR_TYPE_NOX_ONLY:
    case Sensor_VType::SENSOR_TYPE_SWITCH_INVERTED:
    case Sensor_VType::SENSOR_TYPE_WIND_SPEED:
    case Sensor_VType::SENSOR_TYPE_DURATION:
    case Sensor_VType::SENSOR_TYPE_DATE:
    case Sensor_VType::SENSOR_TYPE_TIMESTAMP:
    case Sensor_VType::SENSOR_TYPE_DATA_RATE:
    case Sensor_VType::SENSOR_TYPE_DATA_SIZE:
    case Sensor_VType::SENSOR_TYPE_SOUND_PRESSURE:
    case Sensor_VType::SENSOR_TYPE_SIGNAL_STRENGTH:
    case Sensor_VType::SENSOR_TYPE_REACTIVE_ENERGY:
    case Sensor_VType::SENSOR_TYPE_FREQUENCY:
    case Sensor_VType::SENSOR_TYPE_ENERGY:
    case Sensor_VType::SENSOR_TYPE_ENERGY_STORAGE:
    case Sensor_VType::SENSOR_TYPE_ABS_HUMIDITY:
    case Sensor_VType::SENSOR_TYPE_ATMOS_PRESSURE:
    case Sensor_VType::SENSOR_TYPE_BLOOD_GLUCOSE_C:
    case Sensor_VType::SENSOR_TYPE_CO_ONLY:
    case Sensor_VType::SENSOR_TYPE_ENERGY_DISTANCE:
    case Sensor_VType::SENSOR_TYPE_GAS_ONLY:
    case Sensor_VType::SENSOR_TYPE_NITROUS_OXIDE:
    case Sensor_VType::SENSOR_TYPE_OZONE_ONLY:
    case Sensor_VType::SENSOR_TYPE_PRECIPITATION:
    case Sensor_VType::SENSOR_TYPE_PRECIPITATION_INTEN:
    case Sensor_VType::SENSOR_TYPE_SULPHUR_DIOXIDE:
    case Sensor_VType::SENSOR_TYPE_VOC_PARTS:
    case Sensor_VType::SENSOR_TYPE_VOLUME:
    case Sensor_VType::SENSOR_TYPE_VOLUME_FLOW_RATE:
    case Sensor_VType::SENSOR_TYPE_VOLUME_STORAGE:
    case Sensor_VType::SENSOR_TYPE_WATER:
      return 1;
  }
  #ifndef BUILD_NO_DEBUG
  if (log) {
    addLog(LOG_LEVEL_ERROR, F("getValueCountFromSensorType: Unknown sensortype"));
  }
  #endif // ifndef BUILD_NO_DEBUG
  return 0;
}

const __FlashStringHelper* getSensorTypeLabel(Sensor_VType sensorType) {
  switch (sensorType) {
    case Sensor_VType::SENSOR_TYPE_SWITCH:           return F("Switch");
    case Sensor_VType::SENSOR_TYPE_DIMMER:           return F("Dimmer");
    case Sensor_VType::SENSOR_TYPE_TEMP_HUM:         return F("Temp / Hum");
    case Sensor_VType::SENSOR_TYPE_TEMP_BARO:        return F("Temp / Baro");
    case Sensor_VType::SENSOR_TYPE_TEMP_EMPTY_BARO:  return F("Temp / - / Baro");
    case Sensor_VType::SENSOR_TYPE_TEMP_HUM_BARO:    return F("Temp / Hum / Baro");
    case Sensor_VType::SENSOR_TYPE_SINGLE:           return F("Single");
    case Sensor_VType::SENSOR_TYPE_DUAL:             return F("Dual");
    case Sensor_VType::SENSOR_TYPE_TRIPLE:           return F("Triple");
    case Sensor_VType::SENSOR_TYPE_QUAD:             return F("Quad");
#if FEATURE_EXTENDED_TASK_VALUE_TYPES
    case Sensor_VType::SENSOR_TYPE_INT32_SINGLE:     return F("Int32 (1x)");
    case Sensor_VType::SENSOR_TYPE_INT32_DUAL:       return F("Int32 (2x)");
    case Sensor_VType::SENSOR_TYPE_INT32_TRIPLE:     return F("Int32 (3x)");
    case Sensor_VType::SENSOR_TYPE_INT32_QUAD:       return F("Int32 (4x)");
#endif // if FEATURE_EXTENDED_TASK_VALUE_TYPES
    case Sensor_VType::SENSOR_TYPE_ULONG:            return F("UInt32 (1x)");
#if FEATURE_EXTENDED_TASK_VALUE_TYPES
    case Sensor_VType::SENSOR_TYPE_UINT32_DUAL:      return F("UInt32 (2x)");
    case Sensor_VType::SENSOR_TYPE_UINT32_TRIPLE:    return F("UInt32 (3x)");
    case Sensor_VType::SENSOR_TYPE_UINT32_QUAD:      return F("UInt32 (4x)");
    case Sensor_VType::SENSOR_TYPE_UINT64_SINGLE:    return F("UInt64 (1x)");
    case Sensor_VType::SENSOR_TYPE_UINT64_DUAL:      return F("UInt64 (2x)");
    case Sensor_VType::SENSOR_TYPE_INT64_SINGLE:     return F("Int64 (1x)");
    case Sensor_VType::SENSOR_TYPE_INT64_DUAL:       return F("Int64 (2x)");
    case Sensor_VType::SENSOR_TYPE_DOUBLE_SINGLE:    return F("Double (1x)");
    case Sensor_VType::SENSOR_TYPE_DOUBLE_DUAL:      return F("Double (2x)");
#endif // if FEATURE_EXTENDED_TASK_VALUE_TYPES
    case Sensor_VType::SENSOR_TYPE_WIND:             return F("Wind");
    case Sensor_VType::SENSOR_TYPE_STRING:           return F("String");
    case Sensor_VType::SENSOR_TYPE_NONE:             return F("None");
    case Sensor_VType::SENSOR_TYPE_NOT_SET:  break;

      // FIXME To be ignored?
    #if FEATURE_MQTT_DISCOVER || FEATURE_CUSTOM_TASKVAR_VTYPE
    case Sensor_VType::SENSOR_TYPE_ANALOG_ONLY:      return F("Analog");
    case Sensor_VType::SENSOR_TYPE_TEMP_ONLY:        return F("Temp");
    case Sensor_VType::SENSOR_TYPE_HUM_ONLY:         return F("Hum");
    case Sensor_VType::SENSOR_TYPE_LUX_ONLY:         return F("Lux");
    case Sensor_VType::SENSOR_TYPE_DISTANCE_ONLY:    return F("Distance");
    case Sensor_VType::SENSOR_TYPE_DIRECTION_ONLY:   return F("Direction");
    case Sensor_VType::SENSOR_TYPE_DUSTPM2_5_ONLY:   return F("Dust PM2.5");
    case Sensor_VType::SENSOR_TYPE_DUSTPM1_0_ONLY:   return F("Dust PM1.0");
    case Sensor_VType::SENSOR_TYPE_DUSTPM10_ONLY:    return F("Dust PM10");
    case Sensor_VType::SENSOR_TYPE_MOISTURE_ONLY:    return F("Moisture");
    case Sensor_VType::SENSOR_TYPE_CO2_ONLY:         return F("(e)CO2");
    case Sensor_VType::SENSOR_TYPE_GPS_ONLY:         return F("GPS");
    case Sensor_VType::SENSOR_TYPE_UV_ONLY:          return F("UV");
    case Sensor_VType::SENSOR_TYPE_UV_INDEX_ONLY:    return F("UV Index");
    case Sensor_VType::SENSOR_TYPE_IR_ONLY:          return F("IR");
    case Sensor_VType::SENSOR_TYPE_WEIGHT_ONLY:      return F("Weight");
    case Sensor_VType::SENSOR_TYPE_VOLTAGE_ONLY:     return F("Voltage");
    case Sensor_VType::SENSOR_TYPE_CURRENT_ONLY:     return F("Current");
    case Sensor_VType::SENSOR_TYPE_POWER_USG_ONLY:   return F("Power Usage");
    case Sensor_VType::SENSOR_TYPE_POWER_FACT_ONLY:  return F("Power Factor");
    case Sensor_VType::SENSOR_TYPE_APPRNT_POWER_USG_ONLY: return F("Apparent Power Usage");
    case Sensor_VType::SENSOR_TYPE_TVOC_ONLY:        return F("TVOC");
    case Sensor_VType::SENSOR_TYPE_BARO_ONLY:        return F("Baro");
    case Sensor_VType::SENSOR_TYPE_COLOR_RED_ONLY:   return F("Red");
    case Sensor_VType::SENSOR_TYPE_COLOR_GREEN_ONLY: return F("Green");
    case Sensor_VType::SENSOR_TYPE_COLOR_BLUE_ONLY:  return F("Blue");
    case Sensor_VType::SENSOR_TYPE_COLOR_TEMP_ONLY:  return F("Color temperature");
    case Sensor_VType::SENSOR_TYPE_REACTIVE_POWER_ONLY: return F("Reactive Power");
    case Sensor_VType::SENSOR_TYPE_AQI_ONLY:         return F("AQI");
    case Sensor_VType::SENSOR_TYPE_NOX_ONLY:         return F("NOx");
    case Sensor_VType::SENSOR_TYPE_SWITCH_INVERTED:  return F("Switch (inv.)");
    case Sensor_VType::SENSOR_TYPE_WIND_SPEED:       return F("Wind speed");
    case Sensor_VType::SENSOR_TYPE_DURATION:         return F("Duration");
    case Sensor_VType::SENSOR_TYPE_DATE:             return F("Date");
    case Sensor_VType::SENSOR_TYPE_TIMESTAMP:        return F("Timestamp");
    case Sensor_VType::SENSOR_TYPE_DATA_RATE:        return F("Data rate");
    case Sensor_VType::SENSOR_TYPE_DATA_SIZE:        return F("Data size");
    case Sensor_VType::SENSOR_TYPE_SOUND_PRESSURE:   return F("Sound pressure");
    case Sensor_VType::SENSOR_TYPE_SIGNAL_STRENGTH:  return F("Signal strength");
    case Sensor_VType::SENSOR_TYPE_REACTIVE_ENERGY:  return F("Reactive Energy");
    case Sensor_VType::SENSOR_TYPE_FREQUENCY:        return F("Frequency");
    case Sensor_VType::SENSOR_TYPE_ENERGY:           return F("Energy");
    case Sensor_VType::SENSOR_TYPE_ENERGY_STORAGE:   return F("Energy storage");
    case Sensor_VType::SENSOR_TYPE_ABS_HUMIDITY:     return F("Absolute humidity");
    case Sensor_VType::SENSOR_TYPE_ATMOS_PRESSURE:   return F("Atmospheric pressure");
    case Sensor_VType::SENSOR_TYPE_BLOOD_GLUCOSE_C:  return F("Blood glucose conc.");
    case Sensor_VType::SENSOR_TYPE_CO_ONLY:          return F("CO");
    case Sensor_VType::SENSOR_TYPE_ENERGY_DISTANCE:  return F("Energy distance");
    case Sensor_VType::SENSOR_TYPE_GAS_ONLY:         return F("Gas");
    case Sensor_VType::SENSOR_TYPE_NITROUS_OXIDE:    return F("N2O");
    case Sensor_VType::SENSOR_TYPE_OZONE_ONLY:       return F("Ozone");
    case Sensor_VType::SENSOR_TYPE_PRECIPITATION:    return F("Precipitation");
    case Sensor_VType::SENSOR_TYPE_PRECIPITATION_INTEN: return F("Precipitation intensity");
    case Sensor_VType::SENSOR_TYPE_SULPHUR_DIOXIDE:  return F("SO2");
    case Sensor_VType::SENSOR_TYPE_VOC_PARTS:        return F("VOC parts");
    case Sensor_VType::SENSOR_TYPE_VOLUME:           return F("Volume");
    case Sensor_VType::SENSOR_TYPE_VOLUME_FLOW_RATE: return F("Volume flow rate");
    case Sensor_VType::SENSOR_TYPE_VOLUME_STORAGE:   return F("Volume storage");
    case Sensor_VType::SENSOR_TYPE_WATER:            return F("Water cons.");
    #else // if FEATURE_MQTT_DISCOVER || FEATURE_CUSTOM_TASKVAR_VTYPE
    case Sensor_VType::SENSOR_TYPE_ANALOG_ONLY:
    case Sensor_VType::SENSOR_TYPE_TEMP_ONLY:
    case Sensor_VType::SENSOR_TYPE_HUM_ONLY:
    case Sensor_VType::SENSOR_TYPE_LUX_ONLY:
    case Sensor_VType::SENSOR_TYPE_DISTANCE_ONLY:
    case Sensor_VType::SENSOR_TYPE_DIRECTION_ONLY:
    case Sensor_VType::SENSOR_TYPE_DUSTPM2_5_ONLY:
    case Sensor_VType::SENSOR_TYPE_DUSTPM1_0_ONLY:
    case Sensor_VType::SENSOR_TYPE_DUSTPM10_ONLY:
    case Sensor_VType::SENSOR_TYPE_MOISTURE_ONLY:
    case Sensor_VType::SENSOR_TYPE_CO2_ONLY:
    case Sensor_VType::SENSOR_TYPE_GPS_ONLY:
    case Sensor_VType::SENSOR_TYPE_UV_ONLY:
    case Sensor_VType::SENSOR_TYPE_UV_INDEX_ONLY:
    case Sensor_VType::SENSOR_TYPE_IR_ONLY:
    case Sensor_VType::SENSOR_TYPE_WEIGHT_ONLY:
    case Sensor_VType::SENSOR_TYPE_VOLTAGE_ONLY:
    case Sensor_VType::SENSOR_TYPE_CURRENT_ONLY:
    case Sensor_VType::SENSOR_TYPE_POWER_USG_ONLY:
    case Sensor_VType::SENSOR_TYPE_POWER_FACT_ONLY:
    case Sensor_VType::SENSOR_TYPE_APPRNT_POWER_USG_ONLY:
    case Sensor_VType::SENSOR_TYPE_TVOC_ONLY:
    case Sensor_VType::SENSOR_TYPE_BARO_ONLY:
    case Sensor_VType::SENSOR_TYPE_COLOR_RED_ONLY:
    case Sensor_VType::SENSOR_TYPE_COLOR_GREEN_ONLY:
    case Sensor_VType::SENSOR_TYPE_COLOR_BLUE_ONLY:
    case Sensor_VType::SENSOR_TYPE_COLOR_TEMP_ONLY:
    case Sensor_VType::SENSOR_TYPE_REACTIVE_POWER_ONLY:
    case Sensor_VType::SENSOR_TYPE_AQI_ONLY:
    case Sensor_VType::SENSOR_TYPE_NOX_ONLY:
    case Sensor_VType::SENSOR_TYPE_SWITCH_INVERTED:
    case Sensor_VType::SENSOR_TYPE_WIND_SPEED:
    case Sensor_VType::SENSOR_TYPE_DURATION:
    case Sensor_VType::SENSOR_TYPE_DATE:
    case Sensor_VType::SENSOR_TYPE_TIMESTAMP:
    case Sensor_VType::SENSOR_TYPE_DATA_RATE:
    case Sensor_VType::SENSOR_TYPE_DATA_SIZE:
    case Sensor_VType::SENSOR_TYPE_SOUND_PRESSURE:
    case Sensor_VType::SENSOR_TYPE_SIGNAL_STRENGTH:
    case Sensor_VType::SENSOR_TYPE_REACTIVE_ENERGY:
    case Sensor_VType::SENSOR_TYPE_FREQUENCY:
    case Sensor_VType::SENSOR_TYPE_ENERGY:
    case Sensor_VType::SENSOR_TYPE_ENERGY_STORAGE:
    case Sensor_VType::SENSOR_TYPE_ABS_HUMIDITY:
    case Sensor_VType::SENSOR_TYPE_ATMOS_PRESSURE:
    case Sensor_VType::SENSOR_TYPE_BLOOD_GLUCOSE_C:
    case Sensor_VType::SENSOR_TYPE_CO_ONLY:
    case Sensor_VType::SENSOR_TYPE_ENERGY_DISTANCE:
    case Sensor_VType::SENSOR_TYPE_GAS_ONLY:
    case Sensor_VType::SENSOR_TYPE_NITROUS_OXIDE:
    case Sensor_VType::SENSOR_TYPE_OZONE_ONLY:
    case Sensor_VType::SENSOR_TYPE_PRECIPITATION:
    case Sensor_VType::SENSOR_TYPE_PRECIPITATION_INTEN:
    case Sensor_VType::SENSOR_TYPE_SULPHUR_DIOXIDE:
    case Sensor_VType::SENSOR_TYPE_VOC_PARTS:
    case Sensor_VType::SENSOR_TYPE_VOLUME:
    case Sensor_VType::SENSOR_TYPE_VOLUME_FLOW_RATE:
    case Sensor_VType::SENSOR_TYPE_VOLUME_STORAGE:
    case Sensor_VType::SENSOR_TYPE_WATER:
      break;
    #endif // if FEATURE_MQTT_DISCOVER || FEATURE_CUSTOM_TASKVAR_VTYPE
  }
  return F("");
}

#if FEATURE_CUSTOM_TASKVAR_VTYPE
bool isMQTTDiscoverySensorType(Sensor_VType sensorType)
{
  switch (sensorType)
  {
    case Sensor_VType::SENSOR_TYPE_NONE:
    case Sensor_VType::SENSOR_TYPE_SINGLE:        // single value sensor, used for Dallas, BH1750, etc
#if FEATURE_EXTENDED_TASK_VALUE_TYPES
    case Sensor_VType::SENSOR_TYPE_INT32_SINGLE:  // 1x int32_t
    case Sensor_VType::SENSOR_TYPE_UINT64_SINGLE: // 1x uint64_t
    case Sensor_VType::SENSOR_TYPE_INT64_SINGLE:  // 1x int64_t
    case Sensor_VType::SENSOR_TYPE_DOUBLE_SINGLE: // 1x ESPEASY_RULES_FLOAT_TYPE
#endif // if FEATURE_EXTENDED_TASK_VALUE_TYPES
    case Sensor_VType::SENSOR_TYPE_ULONG:  // single unsigned LONG value, stored in two floats (rfid tags)
    case Sensor_VType::SENSOR_TYPE_STRING: // String type data stored in the event->String2
    case Sensor_VType::SENSOR_TYPE_DUAL:        // 2x float
#if FEATURE_EXTENDED_TASK_VALUE_TYPES
    case Sensor_VType::SENSOR_TYPE_UINT32_DUAL: // 2x uint32_t
    case Sensor_VType::SENSOR_TYPE_INT32_DUAL:  // 2x int32_t
    case Sensor_VType::SENSOR_TYPE_UINT64_DUAL: // 2x uint64_t
    case Sensor_VType::SENSOR_TYPE_INT64_DUAL:  // 2x int64_t
    case Sensor_VType::SENSOR_TYPE_DOUBLE_DUAL: // 2x ESPEASY_RULES_FLOAT_TYPE
#endif // if FEATURE_EXTENDED_TASK_VALUE_TYPES
    case Sensor_VType::SENSOR_TYPE_TRIPLE:          // 3x float
#if FEATURE_EXTENDED_TASK_VALUE_TYPES
    case Sensor_VType::SENSOR_TYPE_UINT32_TRIPLE:   // 3x uint32_t
    case Sensor_VType::SENSOR_TYPE_INT32_TRIPLE:    // 3x int32_t
#endif // if FEATURE_EXTENDED_TASK_VALUE_TYPES
    case Sensor_VType::SENSOR_TYPE_QUAD:        // 4x float
#if FEATURE_EXTENDED_TASK_VALUE_TYPES
    case Sensor_VType::SENSOR_TYPE_UINT32_QUAD: // 4x uint32_t
    case Sensor_VType::SENSOR_TYPE_INT32_QUAD:  // 4x int32_t
#endif // if FEATURE_EXTENDED_TASK_VALUE_TYPES
    case Sensor_VType::SENSOR_TYPE_NOT_SET:
    case Sensor_VType::SENSOR_TYPE_ANALOG_ONLY: // TODO Implement discovery
    case Sensor_VType::SENSOR_TYPE_DIMMER:      // TODO Implement discovery
    case Sensor_VType::SENSOR_TYPE_GPS_ONLY:    // TODO Implement discovery
      break;

    // All value types that are used in MQTT AutoDiscovery
    case Sensor_VType::SENSOR_TYPE_SWITCH:
    case Sensor_VType::SENSOR_TYPE_TEMP_HUM:
    case Sensor_VType::SENSOR_TYPE_TEMP_BARO:
    case Sensor_VType::SENSOR_TYPE_TEMP_HUM_BARO:
    case Sensor_VType::SENSOR_TYPE_TEMP_EMPTY_BARO: // Values 1 and 3 will contain data.
    case Sensor_VType::SENSOR_TYPE_WIND:
    case Sensor_VType::SENSOR_TYPE_TEMP_ONLY:
    case Sensor_VType::SENSOR_TYPE_HUM_ONLY:
    case Sensor_VType::SENSOR_TYPE_LUX_ONLY:
    case Sensor_VType::SENSOR_TYPE_DISTANCE_ONLY:
    case Sensor_VType::SENSOR_TYPE_DIRECTION_ONLY:
    case Sensor_VType::SENSOR_TYPE_DUSTPM2_5_ONLY:
    case Sensor_VType::SENSOR_TYPE_DUSTPM1_0_ONLY:
    case Sensor_VType::SENSOR_TYPE_DUSTPM10_ONLY:
    case Sensor_VType::SENSOR_TYPE_MOISTURE_ONLY:
    case Sensor_VType::SENSOR_TYPE_CO2_ONLY:
    case Sensor_VType::SENSOR_TYPE_UV_ONLY:
    case Sensor_VType::SENSOR_TYPE_UV_INDEX_ONLY:
    case Sensor_VType::SENSOR_TYPE_IR_ONLY:
    case Sensor_VType::SENSOR_TYPE_WEIGHT_ONLY:
    case Sensor_VType::SENSOR_TYPE_VOLTAGE_ONLY:
    case Sensor_VType::SENSOR_TYPE_CURRENT_ONLY:
    case Sensor_VType::SENSOR_TYPE_POWER_USG_ONLY:
    case Sensor_VType::SENSOR_TYPE_POWER_FACT_ONLY:
    case Sensor_VType::SENSOR_TYPE_APPRNT_POWER_USG_ONLY:
    case Sensor_VType::SENSOR_TYPE_TVOC_ONLY:
    case Sensor_VType::SENSOR_TYPE_BARO_ONLY:
    case Sensor_VType::SENSOR_TYPE_COLOR_RED_ONLY:
    case Sensor_VType::SENSOR_TYPE_COLOR_GREEN_ONLY:
    case Sensor_VType::SENSOR_TYPE_COLOR_BLUE_ONLY:
    case Sensor_VType::SENSOR_TYPE_COLOR_TEMP_ONLY:
    case Sensor_VType::SENSOR_TYPE_REACTIVE_POWER_ONLY:
    case Sensor_VType::SENSOR_TYPE_AQI_ONLY:
    case Sensor_VType::SENSOR_TYPE_NOX_ONLY:
    case Sensor_VType::SENSOR_TYPE_SWITCH_INVERTED:
    case Sensor_VType::SENSOR_TYPE_WIND_SPEED:
    case Sensor_VType::SENSOR_TYPE_DURATION:
    case Sensor_VType::SENSOR_TYPE_DATE:
    case Sensor_VType::SENSOR_TYPE_TIMESTAMP:
    case Sensor_VType::SENSOR_TYPE_DATA_RATE:
    case Sensor_VType::SENSOR_TYPE_DATA_SIZE:
    case Sensor_VType::SENSOR_TYPE_SOUND_PRESSURE:
    case Sensor_VType::SENSOR_TYPE_SIGNAL_STRENGTH:
    case Sensor_VType::SENSOR_TYPE_REACTIVE_ENERGY:
    case Sensor_VType::SENSOR_TYPE_FREQUENCY:
    case Sensor_VType::SENSOR_TYPE_ENERGY:
    case Sensor_VType::SENSOR_TYPE_ENERGY_STORAGE:
    case Sensor_VType::SENSOR_TYPE_ABS_HUMIDITY:
    case Sensor_VType::SENSOR_TYPE_ATMOS_PRESSURE:
    case Sensor_VType::SENSOR_TYPE_BLOOD_GLUCOSE_C:
    case Sensor_VType::SENSOR_TYPE_CO_ONLY:
    case Sensor_VType::SENSOR_TYPE_ENERGY_DISTANCE:
    case Sensor_VType::SENSOR_TYPE_GAS_ONLY:
    case Sensor_VType::SENSOR_TYPE_NITROUS_OXIDE:
    case Sensor_VType::SENSOR_TYPE_OZONE_ONLY:
    case Sensor_VType::SENSOR_TYPE_PRECIPITATION:
    case Sensor_VType::SENSOR_TYPE_PRECIPITATION_INTEN:
    case Sensor_VType::SENSOR_TYPE_SULPHUR_DIOXIDE:
    case Sensor_VType::SENSOR_TYPE_VOC_PARTS:
    case Sensor_VType::SENSOR_TYPE_VOLUME:
    case Sensor_VType::SENSOR_TYPE_VOLUME_FLOW_RATE:
    case Sensor_VType::SENSOR_TYPE_VOLUME_STORAGE:
    case Sensor_VType::SENSOR_TYPE_WATER:
      return true;
  }
  return false;
}
#endif // FEATURE_CUSTOM_TASKVAR_VTYPE

bool isSimpleOutputDataType(Sensor_VType sensorType)
{
  return sensorType == Sensor_VType::SENSOR_TYPE_SINGLE ||
         sensorType == Sensor_VType::SENSOR_TYPE_DUAL   ||
         sensorType == Sensor_VType::SENSOR_TYPE_TRIPLE ||
         sensorType == Sensor_VType::SENSOR_TYPE_QUAD;
}

bool isUInt32OutputDataType(Sensor_VType sensorType)
{
#if FEATURE_EXTENDED_TASK_VALUE_TYPES
  return sensorType == Sensor_VType::SENSOR_TYPE_ULONG         ||
         sensorType == Sensor_VType::SENSOR_TYPE_UINT32_DUAL   ||
         sensorType == Sensor_VType::SENSOR_TYPE_UINT32_TRIPLE ||
         sensorType == Sensor_VType::SENSOR_TYPE_UINT32_QUAD;
#else // if FEATURE_EXTENDED_TASK_VALUE_TYPES
  return sensorType == Sensor_VType::SENSOR_TYPE_ULONG;
#endif // if FEATURE_EXTENDED_TASK_VALUE_TYPES
}

#if FEATURE_EXTENDED_TASK_VALUE_TYPES
bool isInt32OutputDataType(Sensor_VType sensorType)
{
  return sensorType == Sensor_VType::SENSOR_TYPE_INT32_SINGLE ||
         sensorType == Sensor_VType::SENSOR_TYPE_INT32_DUAL   ||
         sensorType == Sensor_VType::SENSOR_TYPE_INT32_TRIPLE ||
         sensorType == Sensor_VType::SENSOR_TYPE_INT32_QUAD;
}

bool isUInt64OutputDataType(Sensor_VType sensorType)
{
  return sensorType == Sensor_VType::SENSOR_TYPE_UINT64_SINGLE ||
         sensorType == Sensor_VType::SENSOR_TYPE_UINT64_DUAL;
}

bool isInt64OutputDataType(Sensor_VType sensorType)
{
  return sensorType == Sensor_VType::SENSOR_TYPE_INT64_SINGLE ||
         sensorType == Sensor_VType::SENSOR_TYPE_INT64_DUAL;
}

#endif // if FEATURE_EXTENDED_TASK_VALUE_TYPES

bool isFloatOutputDataType(Sensor_VType sensorType)
{
  return sensorType != Sensor_VType::SENSOR_TYPE_NONE &&
         sensorType != Sensor_VType::SENSOR_TYPE_ULONG &&
         sensorType < Sensor_VType::SENSOR_TYPE_STRING;
}

#if FEATURE_EXTENDED_TASK_VALUE_TYPES
bool isDoubleOutputDataType(Sensor_VType sensorType)
{
  return sensorType == Sensor_VType::SENSOR_TYPE_DOUBLE_SINGLE ||
         sensorType == Sensor_VType::SENSOR_TYPE_DOUBLE_DUAL;
}

#endif // if FEATURE_EXTENDED_TASK_VALUE_TYPES

bool isIntegerOutputDataType(Sensor_VType sensorType)
{
#if FEATURE_EXTENDED_TASK_VALUE_TYPES
  return isUInt32OutputDataType(sensorType)  ||
         isInt32OutputDataType(sensorType)   ||
         isUInt64OutputDataType(sensorType) ||
         isInt64OutputDataType(sensorType);
#else // if FEATURE_EXTENDED_TASK_VALUE_TYPES
  return isUInt32OutputDataType(sensorType);
#endif // if FEATURE_EXTENDED_TASK_VALUE_TYPES
}

bool is32bitOutputDataType(Sensor_VType sensorType)
{
#if FEATURE_EXTENDED_TASK_VALUE_TYPES

  if (isUInt64OutputDataType(sensorType) ||
      isInt64OutputDataType(sensorType) ||
      isDoubleOutputDataType(sensorType) ||
      (sensorType == Sensor_VType::SENSOR_TYPE_STRING)) {
    return false;
  }
#endif // if FEATURE_EXTENDED_TASK_VALUE_TYPES
  return true;
}

# if FEATURE_MQTT && FEATURE_MQTT_DISCOVER
const char mqtt_valueType_ha_deviceclass_names[] PROGMEM = // !! Offset, starting from Sensor_VType::SENSOR_TYPE_ANALOG_ONLY !!
  "|temperature|humidity|illuminance|distance|wind_direction|" // ANALOG_ONLY .. DIRECTION_ONLY
  "pm25|pm1|pm10|mdi:cup-water|carbon_dioxide||" // DUSTPM2_5_ONLY .. GPS_ONLY
  "irradiance|irradiance|irradiance|mdi:scale|" // UV_ONLY .. WEIGHT_ONLY
  "voltage|current|power|power_factor|power|" // VOLTAGE_ONLY .. APPRNT_POWER_USG_ONLY
  "volatile_organic_compounds|pressure|mdi:palette|mdi:palette|mdi:palette|" // TVOC_ONLY .. COLOR_BLUE_ONLY
  "mdi:temperature-kelvin|reactive_power|aqi|nitrogen_dioxide|" // COLOR_TEMP_ONLY .. NOX_ONLY
  "|wind_speed|duration|date|" // SWITCH_INVERTED .. DATE
  "timestamp|data_rate|data_size|sound_pressure|signal_strength|reactive_energy|" // TIMESTAMP .. REACTIVE_ENERGY
  "frequency|energy|energy_storage|" // FREQUENCY .. ENERGY_STORAGE
  "absolute_humidity|atmospheric_pressure|blood_glucose_concentration|" // ABS_HUMIDITY .. ATMOSPH_PRESSURE
  "carbon_monoxide|energy_distance|gas|nitrous_oxide|ozone|" // CO_ONLY .. OZONE_ONLY
  "precipitation|precipitation_intensity|sulphur_dioxide|volatile_organic_compounds_parts|" // PRECIPITATION .. VOC_PARTS
  "volume|volume_flow_rate|volume_storage|water|" // VOLUME .. WATER
  ;

/**
 * getValueType2HADeviceClass: Convert the ValueType to a HA Device Class
 */
String getValueType2HADeviceClass(Sensor_VType sensorType) {
  char tmp[33]{};                                                   // length: volatile_organic_compounds_parts + \0
  int devClassIndex = static_cast<int>(sensorType);
  if (sensorType >= Sensor_VType::SENSOR_TYPE_ANALOG_ONLY) {
    devClassIndex -= static_cast<int>(Sensor_VType::SENSOR_TYPE_ANALOG_ONLY); // Subtract offset
  // } else if (sensorType == Sensor_VType::SENSOR_TYPE_SWITCH) {
  //   return EMPTY_STRING;
  } else if (sensorType == Sensor_VType::SENSOR_TYPE_WIND) {
    return F("wind_speed");
  // } else if (sensorType == Sensor_VType::SENSOR_TYPE_DIMMER) {
  //   return EMPTY_STRING;
  } else {
    return EMPTY_STRING;
  }

  String result(GetTextIndexed(tmp, sizeof(tmp), devClassIndex, mqtt_valueType_ha_deviceclass_names));

  return result;
}

const char mqtt_valueType_default_ha_uom_names[] PROGMEM = // !! Offset, starting from Sensor_VType::SENSOR_TYPE_ANALOG_ONLY !!
  "|°C|%|lx|cm|°|" // ANALOG_ONLY .. DIRECTION_ONLY
  "µg/m³|µg/m³|µg/m³|%|ppm||" // DUSTPM2_5_ONLY .. GPS_ONLY
  "W/m²|UV Index|W/m²|kg|" // UV_ONLY .. WEIGHT_ONLY
  "V|A|W|%|VA|" // VOLTAGE_ONLY .. APPRNT_POWER_USG_ONLY
  "ppd|hPa|lx|lx|lx|" // TVOC_ONLY .. COLOR_BLUE_ONLY
  "K|var||µg/m³|" // COLOR_TEMP_ONLY .. NOX_ONLY
  "|m/s|min||" // SWITCH_INVERTED .. DATE
  "|bit/s|B|dB|dBm|kvar|" // TIMESTAMP .. REACTIVE_ENERGY
  "Hz|kWh|kWh|" // FREQUENCY .. ENERGY_STORAGE
  "g/m³|mbar|mmol/L|" // ABS_HUMIDITY .. BLOOD_GLUCOSE_C
  "ppm|kWh/100km|L|µg/m³|µg/m³|" // CO_ONLY .. OZONE_ONLY
  "mm|mm/h|µg/m³|ppm|" // PRICIPIATION .. VOC_PARTS
  "L|m³/h|L|L|" // VOLUME .. WATER
  ;

/**
 * getValueType2DefaultHAUoM: Convert the Value Type to a default UoM for HA AutoDiscovery
 */
String getValueType2DefaultHAUoM(Sensor_VType sensorType) {
  char tmp[9]{};                                                   // length: UV Index + \0
  int devClassIndex = static_cast<int>(sensorType);
  if (sensorType >= Sensor_VType::SENSOR_TYPE_ANALOG_ONLY) {
    devClassIndex -= static_cast<int>(Sensor_VType::SENSOR_TYPE_ANALOG_ONLY); // Subtract offset
  // } else if (sensorType == Sensor_VType::SENSOR_TYPE_SWITCH) {
  //   return EMPTY_STRING;
  } else if (sensorType == Sensor_VType::SENSOR_TYPE_WIND) {
    return F("m/s");
  // } else if (sensorType == Sensor_VType::SENSOR_TYPE_DIMMER) {
  //   return EMPTY_STRING;
  } else {
    return EMPTY_STRING;
  }

  String result(GetTextIndexed(tmp, sizeof(tmp), devClassIndex, mqtt_valueType_default_ha_uom_names));

  return result;
}
# endif // if FEATURE_MQTT && FEATURE_MQTT_DISCOVER

#include "../DataTypes/EthernetParameters.h"

bool isValid(EthClockMode_t clockMode) {
  switch (clockMode) {
    case  EthClockMode_t::Ext_crystal_osc:
    case  EthClockMode_t::Int_50MHz_GPIO_0:
    case  EthClockMode_t::Int_50MHz_GPIO_16:
    case  EthClockMode_t::Int_50MHz_GPIO_17_inv:
      return true;

      // Do not use default: as this allows the compiler to detect any missing cases.
  }
  return false;
}

bool isGpioUsedInETHClockMode(EthClockMode_t clockMode,
                              int8_t         gpio) {
  if (((clockMode == EthClockMode_t::Int_50MHz_GPIO_0)      && (gpio == 0)) ||
      ((clockMode == EthClockMode_t::Int_50MHz_GPIO_16)     && (gpio == 16)) ||
      ((clockMode == EthClockMode_t::Int_50MHz_GPIO_17_inv) && (gpio == 17))) {
    return true;
  }
  return false;
}

const __FlashStringHelper* toString(EthClockMode_t clockMode) {
  switch (clockMode) {
    case  EthClockMode_t::Ext_crystal_osc:       return F("External crystal oscillator");
    case  EthClockMode_t::Int_50MHz_GPIO_0:      return F("50MHz APLL Output on GPIO0");
    case  EthClockMode_t::Int_50MHz_GPIO_16:     return F("50MHz APLL Output on GPIO16");
    case  EthClockMode_t::Int_50MHz_GPIO_17_inv: return F("50MHz APLL Inverted Output on GPIO17");

      // Do not use default: as this allows the compiler to detect any missing cases.
  }
  return F("Unknown");
}

bool isValid(EthPhyType_t phyType) {
  switch (phyType) {
#if CONFIG_ETH_USE_ESP32_EMAC
    case EthPhyType_t::LAN8720:
    case EthPhyType_t::TLK110:
# if ESP_IDF_VERSION_MAJOR > 3
    case EthPhyType_t::RTL8201:
#if ETH_TYPE_JL1101_SUPPORTED
    case EthPhyType_t::JL1101:
#endif
    case EthPhyType_t::DP83848:
    case EthPhyType_t::KSZ8041:
    case EthPhyType_t::KSZ8081:
# endif // if ESP_IDF_VERSION_MAJOR > 3
      return true;
#endif // if CONFIG_ETH_USE_ESP32_EMAC

#if ESP_IDF_VERSION_MAJOR >= 5
# if CONFIG_ETH_SPI_ETHERNET_DM9051
    case EthPhyType_t::DM9051: return true;
# endif // if CONFIG_ETH_SPI_ETHERNET_DM9051
# if CONFIG_ETH_SPI_ETHERNET_W5500
    case EthPhyType_t::W5500:  return true;
# endif // if CONFIG_ETH_SPI_ETHERNET_W5500
# if CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL
    case EthPhyType_t::KSZ8851: return true;
# endif // if CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL
#endif // if ESP_IDF_VERSION_MAJOR >= 5
    case EthPhyType_t::notSet:
      break;
  }
  return false;
}

#if FEATURE_ETHERNET
bool isSPI_EthernetType(EthPhyType_t phyType) {
# if ESP_IDF_VERSION_MAJOR >= 5
  return
#  if CONFIG_ETH_SPI_ETHERNET_DM9051
    phyType ==  EthPhyType_t::DM9051 ||
#  endif // if CONFIG_ETH_SPI_ETHERNET_DM9051
#  if CONFIG_ETH_SPI_ETHERNET_W5500
    phyType ==  EthPhyType_t::W5500 ||
#  endif // if CONFIG_ETH_SPI_ETHERNET_W5500
#  if CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL
    phyType ==  EthPhyType_t::KSZ8851 ||
#  endif // if CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL
    false;
# else // if ESP_IDF_VERSION_MAJOR >= 5
  return false;
# endif // if ESP_IDF_VERSION_MAJOR >= 5
}

eth_phy_type_t to_ESP_phy_type(EthPhyType_t phyType)
{
  switch (phyType) {
# if CONFIG_ETH_USE_ESP32_EMAC
    case EthPhyType_t::LAN8720:  return ETH_PHY_LAN8720;
    case EthPhyType_t::TLK110:   return ETH_PHY_TLK110;
#  if ESP_IDF_VERSION_MAJOR > 3
    case EthPhyType_t::RTL8201:  return ETH_PHY_RTL8201;
#   if ETH_TYPE_JL1101_SUPPORTED
    case EthPhyType_t::JL1101:   return ETH_PHY_JL1101;
#   endif
    case EthPhyType_t::DP83848:  return ETH_PHY_DP83848;
    case EthPhyType_t::KSZ8041:  return ETH_PHY_KSZ8041;
    case EthPhyType_t::KSZ8081:  return ETH_PHY_KSZ8081;
#  endif // if ESP_IDF_VERSION_MAJOR > 3
# endif // if CONFIG_ETH_USE_ESP32_EMAC

# if ESP_IDF_VERSION_MAJOR >= 5
#  if CONFIG_ETH_SPI_ETHERNET_DM9051
    case EthPhyType_t::DM9051:   return ETH_PHY_DM9051;
#  endif // if CONFIG_ETH_SPI_ETHERNET_DM9051
#  if CONFIG_ETH_SPI_ETHERNET_W5500
    case EthPhyType_t::W5500:   return ETH_PHY_W5500;
#  endif // if CONFIG_ETH_SPI_ETHERNET_W5500
#  if CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL
    case EthPhyType_t::KSZ8851:   return ETH_PHY_KSZ8851;
#  endif // if CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL
# endif // if ESP_IDF_VERSION_MAJOR >= 5
    case EthPhyType_t::notSet:
      break;
  }
  return ETH_PHY_MAX;
}

#endif // if FEATURE_ETHERNET


const __FlashStringHelper* toString(EthPhyType_t phyType) {
  switch (phyType) {
#if CONFIG_ETH_USE_ESP32_EMAC
    case EthPhyType_t::LAN8720:  return F("LAN8710/LAN8720");
    case EthPhyType_t::TLK110:   return F("TLK110");
# if ESP_IDF_VERSION_MAJOR > 3
    case EthPhyType_t::RTL8201:  return F("RTL8201");
#if ETH_TYPE_JL1101_SUPPORTED
    case EthPhyType_t::JL1101:   return F("JL1101");
#endif
    case EthPhyType_t::DP83848:  return F("DP83848");
    case EthPhyType_t::KSZ8041:  return F("KSZ8041");
    case EthPhyType_t::KSZ8081:  return F("KSZ8081");
# endif // if ESP_IDF_VERSION_MAJOR > 3
#endif // if CONFIG_ETH_USE_ESP32_EMAC

#if ESP_IDF_VERSION_MAJOR >= 5
# if CONFIG_ETH_SPI_ETHERNET_DM9051
    case EthPhyType_t::DM9051:   return F("DM9051(SPI)");
# endif // if CONFIG_ETH_SPI_ETHERNET_DM9051
# if CONFIG_ETH_SPI_ETHERNET_W5500
    case EthPhyType_t::W5500:   return F("W5500(SPI)");
# endif // if CONFIG_ETH_SPI_ETHERNET_W5500
# if CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL
    case EthPhyType_t::KSZ8851:   return F("KSZ8851(SPI)");
# endif // if CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL
#endif // if ESP_IDF_VERSION_MAJOR >= 5
    case EthPhyType_t::notSet:
      break;

      // Do not use default: as this allows the compiler to detect any missing cases.
  }
  return F("- None -");
}

#include "../DataTypes/TaskEnabledState.h"

TaskEnabledState::TaskEnabledState() : value(0) {}

TaskEnabledState & TaskEnabledState::operator=(const bool& enabledState)
{
  enabled = enabledState;
  return *this;
}

void TaskEnabledState::clearTempDisableFlags()
{
  const bool enabledSet = enabled;

  value   = 0;
  enabled = enabledSet;
}

void TaskEnabledState::setRetryInit()
{
  // If intended state is enabled, set to retry.
  retryInit = enabled;
}

#include "../DataTypes/SettingsType.h"

#include "../CustomBuild/StorageLayout.h"
#include "../DataStructs/ControllerSettingsStruct.h"
#include "../DataStructs/ExtraTaskSettingsStruct.h"
#include "../DataStructs/NotificationSettingsStruct.h"
#include "../DataStructs/SecurityStruct.h"
#include "../DataTypes/ESPEasyFileType.h"
#include "../Globals/Settings.h"
#include "../Helpers/StringConverter.h"

const __FlashStringHelper * SettingsType::getSettingsTypeString(Enum settingsType) {
  switch (settingsType) {
    case Enum::BasicSettings_Type:             return F("Settings");
    case Enum::TaskSettings_Type:              return F("TaskSettings");
    case Enum::CustomTaskSettings_Type:        return F("CustomTaskSettings");
    case Enum::ControllerSettings_Type:        return F("ControllerSettings");
    case Enum::CustomControllerSettings_Type:  return F("CustomControllerSettings");
    case Enum::NotificationSettings_Type:      
    #if FEATURE_NOTIFIER
        return F("NotificationSettings");
    #else
        break;
    #endif
    case Enum::SecuritySettings_Type:          return F("SecuritySettings");
    case Enum::ExtdControllerCredentials_Type: return F("ExtendedControllerCredentials");
    #if FEATURE_ALTERNATIVE_CDN_URL
    case Enum::CdnSettings_Type:               return F("CDN_url");
    #endif

    case Enum::SettingsType_MAX: break;
  }
  return F("");
}

/********************************************************************************************\
   Offsets in settings files
 \*********************************************************************************************/
bool SettingsType::getSettingsParameters(Enum settingsType, int index, int& max_index, int& offset, int& max_size, int& struct_size) {
  // The defined offsets should be used with () just in case they are the result of a formula in the defines.
  struct_size = 0;
  max_index = -1;
  offset    = -1;

  switch (settingsType) {
    case Enum::BasicSettings_Type:
    {
      max_index   = 1;
      offset      = 0;
      max_size    = (DAT_BASIC_SETTINGS_SIZE);
      struct_size = sizeof(SettingsStruct);
      break;
    }
    case Enum::TaskSettings_Type:
    {
      max_index   = TASKS_MAX;
      offset      = (DAT_OFFSET_TASKS) + (index * (DAT_TASKS_DISTANCE));
      max_size    = DAT_TASKS_SIZE;
      struct_size = sizeof(ExtraTaskSettingsStruct);
      break;
    }
    case Enum::CustomTaskSettings_Type:
    {
      if (!getSettingsParameters(Enum::TaskSettings_Type, index, max_index, offset, max_size, struct_size))
        return false;
      offset  += (DAT_TASKS_CUSTOM_OFFSET);
      max_size = (DAT_TASKS_CUSTOM_SIZE + DAT_TASKS_CUSTOM_EXTENSION_SIZE);

      // struct_size may differ.
      struct_size = 0;
      break;
    }
    case Enum::ControllerSettings_Type:
    {
      max_index   = CONTROLLER_MAX;
      offset      = (DAT_OFFSET_CONTROLLER) + (index * (DAT_CONTROLLER_SIZE));
      max_size    = DAT_CONTROLLER_SIZE;
      struct_size = sizeof(ControllerSettingsStruct);
      break;
    }
    case Enum::CustomControllerSettings_Type:
    {
      max_index = CONTROLLER_MAX;
      offset    = (DAT_OFFSET_CUSTOM_CONTROLLER) + (index * (DAT_CUSTOM_CONTROLLER_SIZE));
      max_size  = DAT_CUSTOM_CONTROLLER_SIZE;

      // struct_size may differ.
      struct_size = 0;
      break;
    }
    case Enum::NotificationSettings_Type:
    {
#if FEATURE_NOTIFIER
      max_index   = NOTIFICATION_MAX;
      offset      = index * (DAT_NOTIFICATION_SIZE);
      max_size    = DAT_NOTIFICATION_SIZE;
      struct_size = sizeof(NotificationSettingsStruct);
      break;
#else
      return false;
#endif
    }
    case Enum::SecuritySettings_Type:
    {
      max_index   = 1;
      offset      = 0;
      max_size    = DAT_SECURITYSETTINGS_SIZE;
      struct_size = sizeof(SecurityStruct);
      break;
    }
    case Enum::ExtdControllerCredentials_Type:
    {
      max_index = 1;
      offset    = DAT_EXTDCONTR_CRED_OFFSET;
      max_size  = DAT_EXTDCONTR_CRED_SIZE;

      // struct_size may differ.
      struct_size = 0;
      break;
    }
#if FEATURE_ALTERNATIVE_CDN_URL
    case Enum::CdnSettings_Type:
    {
      max_index   = 1;
      offset      = DAT_OFFSET_CDN;
      max_size    = DAT_CDN_SIZE;

      // struct_size may differ.
      struct_size = 0;
    }
    break;
#endif

    case Enum::SettingsType_MAX:
    {
      max_index = -1;
      offset    = -1;
      return false;
    }
  }
  return index >= 0 && index < max_index;
}

bool SettingsType::getSettingsParameters(Enum settingsType, int index, int& offset, int& max_size) {
  int max_index = -1;
  int struct_size;

  if (!getSettingsParameters(settingsType, index, max_index, offset, max_size, struct_size)) {
    return false;
  }

  if ((index >= 0) && (index < max_index)) { return true; }
  offset = -1;
  return false;
}

int SettingsType::getMaxFilePos(Enum settingsType) {
  int max_index, offset, max_size{};
  int struct_size = 0;

  if (getSettingsParameters(settingsType, 0,             max_index, offset, max_size, struct_size) &&
      getSettingsParameters(settingsType, max_index - 1, offset,    max_size))
    return offset + max_size - 1;
  return -1;
}

int SettingsType::getFileSize(Enum settingsType) {
  SettingsType::SettingsFileEnum file_type = SettingsType::getSettingsFile(settingsType);
  int max_file_pos                         = 0;

  for (int st = 0; st < static_cast<int>(Enum::SettingsType_MAX); ++st) {
    if (SettingsType::getSettingsFile(static_cast<Enum>(st)) == file_type) {
      const int filePos = SettingsType::getMaxFilePos(static_cast<Enum>(st));

      if (filePos > max_file_pos) {
        max_file_pos = filePos;
      }
    }
  }
  return max_file_pos;
}

#ifndef BUILD_MINIMAL_OTA
unsigned int SettingsType::getSVGcolor(Enum settingsType) {
  switch (settingsType) {
    case Enum::BasicSettings_Type:
      return 0x5F0A87;
    case Enum::TaskSettings_Type:
      return 0xEE6352;
    case Enum::CustomTaskSettings_Type:
      return 0x59CD90;
    case Enum::ControllerSettings_Type:
      return 0x3FA7D6;
    case Enum::CustomControllerSettings_Type:
      return 0xFAC05E;
    case Enum::NotificationSettings_Type:
      return 0xF79D84;

    case Enum::SecuritySettings_Type:
      return 0xff00a2;
    case Enum::ExtdControllerCredentials_Type:
      return 0xc300ff;
#if FEATURE_ALTERNATIVE_CDN_URL
    case Enum::CdnSettings_Type:
      return 0xff6600;
#endif
    case Enum::SettingsType_MAX:
      break;
  }
  return 0;
}

#endif // ifndef BUILD_MINIMAL_OTA

SettingsType::SettingsFileEnum SettingsType::getSettingsFile(Enum settingsType)
{
  switch (settingsType) {
    case Enum::BasicSettings_Type:
    case Enum::TaskSettings_Type:
    case Enum::CustomTaskSettings_Type:
    case Enum::ControllerSettings_Type:
    case Enum::CustomControllerSettings_Type:
#if FEATURE_ALTERNATIVE_CDN_URL
    case Enum::CdnSettings_Type:
#endif
      return SettingsFileEnum::FILE_CONFIG_type;
    case Enum::NotificationSettings_Type:
      return SettingsFileEnum::FILE_NOTIFICATION_type;
    case Enum::SecuritySettings_Type:
    case Enum::ExtdControllerCredentials_Type:
      return SettingsFileEnum::FILE_SECURITY_type;

    case Enum::SettingsType_MAX:
      break;
  }
  return SettingsFileEnum::FILE_UNKNOWN_type;
}

String SettingsType::getSettingsFileName(Enum settingsType, int index) {
  #if FEATURE_EXTENDED_CUSTOM_SETTINGS
  if ((Enum::CustomTaskSettings_Type == settingsType) && validTaskIndex(index)) {
    return strformat(F(DAT_TASKS_CUSTOM_EXTENSION_FILEMASK), index + 1); // Add 0/1 offset to match displayed task ID
  }
  #endif // if FEATURE_EXTENDED_CUSTOM_SETTINGS
  return getSettingsFileName(getSettingsFile(settingsType));
}

const __FlashStringHelper * SettingsType::getSettingsFileName(SettingsType::SettingsFileEnum file_type) {
  switch (file_type) {
    case SettingsFileEnum::FILE_CONFIG_type:        return getFileName(FileType::CONFIG_DAT);
    case SettingsFileEnum::FILE_NOTIFICATION_type:  return getFileName(FileType::NOTIFICATION_DAT);
    case SettingsFileEnum::FILE_SECURITY_type:      return getFileName(FileType::SECURITY_DAT);
    case SettingsFileEnum::FILE_UNKNOWN_type:       break;
  }
  return F("");
}

size_t SettingsType::getInitFileSize(SettingsType::SettingsFileEnum file_type) {
  switch (file_type) {
    case SettingsFileEnum::FILE_CONFIG_type:        return CONFIG_FILE_SIZE;
    case SettingsFileEnum::FILE_NOTIFICATION_type:  return 4096;
    case SettingsFileEnum::FILE_SECURITY_type:      return 4096;
    case SettingsFileEnum::FILE_UNKNOWN_type:       break;
  }
  return 0;
}
#include "../DataTypes/SchedulerTimerType.h"



const __FlashStringHelper * toString(SchedulerTimerType_e timerType) {
  switch (timerType) {
    case SchedulerTimerType_e::SystemEventQueue:        return F("SystemEventQueue");
    case SchedulerTimerType_e::ConstIntervalTimer:      return F("Const Interval");
    case SchedulerTimerType_e::PLUGIN_TASKTIMER_IN_e:   return F("PLUGIN_TASKTIMER_IN");
    case SchedulerTimerType_e::TaskDeviceTimer:         return F("PLUGIN_READ");
    case SchedulerTimerType_e::GPIO_timer:              return F("GPIO_timer");
    case SchedulerTimerType_e::PLUGIN_DEVICETIMER_IN_e: return F("PLUGIN_DEVICETIMER_IN");
    case SchedulerTimerType_e::RulesTimer:              return F("Rules#Timer");
    case SchedulerTimerType_e::IntendedReboot:          return F("Intended Reboot");
  }
  return F("Unknown");
}

#include "../DataTypes/ESPEasyTimeSource.h"

#include "../../ESPEasy_common.h"

const __FlashStringHelper* toString(timeSource_t timeSource)
{
  switch (timeSource) {
    case timeSource_t::GPS_PPS_time_source:      return F("GPS PPS");
    case timeSource_t::GPS_time_source:          return F("GPS");
    case timeSource_t::GPS_time_source_no_fix:   return F("GPS no Fix");
    case timeSource_t::NTP_time_source:          return F("NTP");
    case timeSource_t::Manual_set:               return F("Manual");
    case timeSource_t::ESP_now_peer:             return F(ESPEASY_NOW_NAME " peer");
    case timeSource_t::ESPEASY_p2p_UDP:          return F("ESPEasy p2p");
    case timeSource_t::External_RTC_time_source: return F("Ext. RTC at boot");
    case timeSource_t::Restore_RTC_time_source:  return F("RTC at boot");
    case timeSource_t::No_time_source:           return F("No time set");
  }
  return F("Unknown");
}

bool isExternalTimeSource(timeSource_t timeSource)
{
  // timeSource_t::ESP_now_peer or timeSource_t::ESPEASY_p2p_UDP
  // should NOT be considered "external"
  // It may be an unreliable source if no other source is present in the network.

  switch (timeSource) {
    case timeSource_t::GPS_PPS_time_source:
    case timeSource_t::GPS_time_source:
    case timeSource_t::GPS_time_source_no_fix:
    case timeSource_t::NTP_time_source:
    case timeSource_t::External_RTC_time_source:
    case timeSource_t::Manual_set:
      return true;
    default:
      return false;
  }
}

// Typical time wander for ESP nodes should be less than 10 ppm
// Meaning per hour, the time should wander less than 36 msec.
#define TIME_WANDER_FACTOR  100000

uint32_t updateExpectedWander(
  int32_t  current_wander,
  uint32_t timePassedSinceLastTimeSync)
{
  if (current_wander < 0) {
    return current_wander;
  }
  return current_wander + (timePassedSinceLastTimeSync / TIME_WANDER_FACTOR);
}

uint32_t computeExpectedWander(timeSource_t timeSource,
                               uint32_t     timePassedSinceLastTimeSync)
{
  uint32_t expectedWander_ms = timePassedSinceLastTimeSync / TIME_WANDER_FACTOR;

  switch (timeSource) {
    case timeSource_t::GPS_PPS_time_source:
    {
      expectedWander_ms += 1;
      break;
    }
    case timeSource_t::GPS_time_source:
    {
      // Not sure about the wander here, as GPS does not have a drift.
      // But the moment a message is received from a second's start may differ.
      expectedWander_ms += 10;
      break;
    }
    case timeSource_t::GPS_time_source_no_fix:
    {
      // When the GPS has no fix, the reported time may differ quite a bit
      expectedWander_ms += 2000;
      break;
    }
    case timeSource_t::NTP_time_source:
    {
      // Typical time needed to perform a NTP request to an online NTP server
      expectedWander_ms += 30;
      break;
    }

    case timeSource_t::ESP_now_peer:
    case timeSource_t::ESPEASY_p2p_UDP:
    {
      // expected wander is 36 per hour.
      // Using a 'penalty' of 1000 makes it only preferrable over NTP after +/- 28 hour.
      expectedWander_ms += 1000;
      break;
    }

    case timeSource_t::External_RTC_time_source:
    {
      // Will be off by +/- 500 msec
      expectedWander_ms += 500;
      break;
    }
    case timeSource_t::Restore_RTC_time_source:
    {
      // May be off by the time needed to reboot + some time since the last update of the RTC
      // If a reboot was due to a watchdog reset, then it will be an additional 2 - 6 seconds.
      expectedWander_ms += 5000;
      break;
    }
    case timeSource_t::Manual_set:
    {
      expectedWander_ms += 10000;
      break;
    }
    case timeSource_t::No_time_source:
    {
      // Cannot sync from it.
      return 1 << 30;
    }
  }
  return expectedWander_ms;
}

#include "../ESPEasyCore/ESPEasy_loop.h"


#include "../../ESPEasy-Globals.h"
#include "../Commands/ExecuteCommand.h"
#include "../DataStructs/TimingStats.h"
#include "../ESPEasyCore/ESPEasyNetwork.h"
#include "../ESPEasyCore/ESPEasyWifi_ProcessEvent.h"
#include "../ESPEasyCore/ESPEasy_backgroundtasks.h"
#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../Globals/ESPEasy_Scheduler.h"
#include "../Globals/EventQueue.h"
#include "../Globals/RTC.h"
#include "../Globals/Settings.h"
#include "../Globals/Statistics.h"
#include "../Helpers/DeepSleep.h"
#include "../Helpers/ESPEasyRTC.h"
#include "../Helpers/ESPEasy_time_calc.h"
#include "../Helpers/I2C_access.h"
#include "../Helpers/Misc.h"
#include "../Helpers/Networking.h"
#include "../Helpers/PeriodicalActions.h"
#include "../Helpers/StringConverter.h"


#include "../Commands/InternalCommands_decoder.h"

void updateLoopStats() {
  ++loopCounter;
  ++loopCounter_full;

  if (lastLoopStart == 0) {
    lastLoopStart = micros();
    return;
  }
  const int32_t usecSince = usecPassedSince_fast(lastLoopStart);

  #if FEATURE_TIMING_STATS
  ADD_TIMER_STAT(LOOP_STATS, usecSince);
  #endif // if FEATURE_TIMING_STATS

  loop_usec_duration_total += usecSince;
  lastLoopStart             = micros();

  if ((usecSince <= 0) || (usecSince > 10000000)) {
    return; // No loop should take > 10 sec.
  }

  if (shortestLoop > static_cast<unsigned long>(usecSince)) {
    shortestLoop   = usecSince;
    loopCounterMax = 30 * 1000000 / usecSince;
  }

  if (longestLoop < static_cast<unsigned long>(usecSince)) {
    longestLoop = usecSince;
  }
}

/*********************************************************************************************\
* MAIN LOOP
\*********************************************************************************************/
void ESPEasy_loop()
{
  #ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  #endif
  /*
     //FIXME TD-er: No idea what this does.
     if(MainLoopCall_ptr)
      MainLoopCall_ptr();
   */

  updateLoopStats();

  handle_unprocessedNetworkEvents();

  bool firstLoopConnectionsEstablished = NetworkConnected() && firstLoop;

  if (firstLoopConnectionsEstablished) {
    #ifndef BUILD_MINIMAL_OTA
    addLog(LOG_LEVEL_INFO, F("firstLoopConnectionsEstablished"));
    #endif
    firstLoop               = false;
    timerAwakeFromDeepSleep = millis(); // Allow to run for "awake" number of seconds, now we have wifi.

    // schedule_all_task_device_timers(); // Disabled for now, since we are now using queues for controllers.
    if (Settings.UseRules && isDeepSleepEnabled())
    {
      String event = F("System#NoSleep=");
      event += Settings.deepSleep_wakeTime;
      eventQueue.addMove(std::move(event));
    }

#ifndef BUILD_NO_DEBUG
    checkAll_internalCommands();
#endif


    RTC.bootFailedCount = 0;
    saveToRTC();
    #if FEATURE_ESPEASY_P2P
    sendSysInfoUDP(1);
    #endif
  }
#if FEATURE_CLEAR_I2C_STUCK
  if (Settings.EnableClearHangingI2Cbus())
  {
    // Check I2C bus to see if it needs to be cleared.
    // See: http://www.forward.com.au/pfod/ArduinoProgramming/I2C_ClearBus/index.html
    const I2C_bus_state I2C_state_prev = I2C_state;  
    I2C_state = I2C_check_bus(Settings.Pin_i2c_scl, Settings.Pin_i2c_sda);
    switch (I2C_state) {
      case I2C_bus_state::BusCleared:
        // Log I2C bus cleared, update stats
        ++I2C_bus_cleared_count;
        addLog(LOG_LEVEL_ERROR, F("I2C  : Cleared I2C bus error state"));
        I2C_state = I2C_bus_state::OK;
        initI2C();
        break;
      case I2C_bus_state::SCL_Low:
        addLog(LOG_LEVEL_ERROR, F("I2C  : I2C bus error, SCL clock line held low"));
        break;
      case I2C_bus_state::SDA_Low_over_2_sec:
        addLog(LOG_LEVEL_ERROR, F("I2C  : I2C bus error, SCL clock line held low by slave clock stretch for >2 sec"));
        break;
      case I2C_bus_state::SDA_Low_20_clocks:
        addLog(LOG_LEVEL_ERROR, F("I2C  : I2C bus error, SDA data line held low"));
        break;
      case I2C_bus_state::ClearingProcessActive:
        if (I2C_state_prev != I2C_state) {
          addLog(LOG_LEVEL_ERROR, F("I2C  : I2C bus error, start clearing process"));
        }
        break;
      case I2C_bus_state::NotConfigured:
      case I2C_bus_state::OK:
        break;
    }
  }
#endif


  // Work around for nodes that do not have WiFi connection for a long time and may reboot after N unsuccessful connect attempts
  static bool bootFailedCountReset = false;
  if (getUptimeMinutes() > 2 && !bootFailedCountReset) {
    // Apparently the uptime is already a few minutes. Let's consider it a successful boot.
    RTC.bootFailedCount = 0;
    saveToRTC();
    bootFailedCountReset = true;
  }

  // Deep sleep mode, just run all tasks one (more) time and go back to sleep as fast as possible
  if ((firstLoopConnectionsEstablished || readyForSleep()) && isDeepSleepEnabled())
  {
#if FEATURE_MQTT
    runPeriodicalMQTT();
#endif // if FEATURE_MQTT
    // Now run all frequent tasks
    run50TimesPerSecond();
    run10TimesPerSecond();
    runEach30Seconds();
    runOncePerSecond();
  }

  // normal mode, run each task when its time
  else
  {
    if (!UseRTOSMultitasking) {
      // On ESP32, when using RTOS multitasking, the schedule is executed in a separate RTOS task
      Scheduler.handle_schedule();
    }
  }

  // Calls above may have received/generated commands for the command queue, thus need to process them.
  processExecuteCommandQueue();
  backgroundtasks();

  if (readyForSleep()) {
    prepare_deepSleep(Settings.Delay);

    // deepsleep will never return, its a special kind of reboot
  }
}

#include "../ESPEasyCore/ESPEasy_Console.h"


#include "../Commands/ExecuteCommand.h"

#include "../DataStructs/TimingStats.h"

#include "../DataTypes/ESPEasy_plugin_functions.h"

#include "../Globals/Cache.h"
#include "../Globals/Logging.h"
#include "../Globals/Plugins.h"
#include "../Globals/Settings.h"

#include "../Helpers/Memory.h"

#include <ESPEasySerialPort.h>


EspEasy_Console_t::EspEasy_Console_t()
{
#if FEATURE_DEFINE_SERIAL_CONSOLE_PORT
  const ESPEasySerialPort port = static_cast<ESPEasySerialPort>(_console_serial_port);

# if USES_USBCDC

  /*
     if (port == ESPEasySerialPort::usb_cdc_0 ||
        port == ESPEasySerialPort::usb_cdc_1)
     {
      USB.manufacturerName(F("ESPEasy"));
      USB.productName()
     }
   */
# endif // if USES_USBCDC

# ifdef ESP8266
  constexpr size_t buffSize = 256;
# endif // ifdef ESP8266
# ifdef ESP32

  // Ideal buffer size is a trade-off between bootspeed
  // and not missing data when the ESP is busy processing stuff.
  // Since we do have a separate buffer in the console,
  // it may just take less time in the background tasks to dump
  // any logs as larger chunks can be transferred at once.
  constexpr size_t buffSize = 512;
# endif // ifdef ESP32

  ESPEasySerialConfig config;
  config.port          = port;
  config.baud          = DEFAULT_SERIAL_BAUD;
  config.receivePin    = _console_serial_rxpin;
  config.transmitPin   = _console_serial_txpin;
  config.inverse_logic = false;
  config.rxBuffSize    = 256;
  config.txBuffSize    = buffSize;

  {
    # ifdef USE_SECOND_HEAP
    HeapSelectDram ephemeral;
    # endif // ifdef USE_SECOND_HEAP

    _mainSerial._serial = new (std::nothrow) ESPeasySerial(config);
  }

# if USES_ESPEASY_CONSOLE_FALLBACK_PORT

  if (Settings.console_serial0_fallback && (port != ESPEasySerialPort::serial0)) {
    config.port        = ESPEasySerialPort::serial0;
    config.receivePin  = SOC_RX0;
    config.transmitPin = SOC_TX0;

    _fallbackSerial._serial = new (std::nothrow) ESPeasySerial(config);
  }
# endif // if USES_ESPEASY_CONSOLE_FALLBACK_PORT


#endif  // if FEATURE_DEFINE_SERIAL_CONSOLE_PORT
}

void EspEasy_Console_t::reInit()
{
  updateActiveTaskUseSerial0();
  bool somethingChanged = false;
#if FEATURE_DEFINE_SERIAL_CONSOLE_PORT
  const ESPEasySerialPort port = static_cast<ESPEasySerialPort>(Settings.console_serial_port);

  const bool consoleUseSerial0 = (
# ifdef ESP8266
    (port == ESPEasySerialPort::serial0_swap) ||
# endif // ifdef ESP8266
    port == ESPEasySerialPort::serial0);

  const bool canUseSerial0 = !activeTaskUseSerial0() && !log_to_serial_disabled;


  bool mustHaveSerial = Settings.UseSerial && (!consoleUseSerial0 || canUseSerial0);


# if USES_ESPEASY_CONSOLE_FALLBACK_PORT

  bool mustHaveFallback = false;

  if (Settings.UseSerial) {
    if (consoleUseSerial0 && canUseSerial0 && (_fallbackSerial._serial != nullptr)) {
      // Should not destruct an already running fallback serial port
      mustHaveFallback = true;
      mustHaveSerial   = false;
    } else {
      if (Settings.console_serial0_fallback && !consoleUseSerial0 && canUseSerial0)
      {
        mustHaveFallback = true;
      }
    }
  }

  if (!mustHaveFallback) {
    if (_fallbackSerial._serial != nullptr) {
      _fallbackSerial._serial->flush();
      _fallbackSerial._serial->end();
      delete _fallbackSerial._serial;
      _fallbackSerial._serial = nullptr;
      somethingChanged = true;
    }
  }
# endif // if USES_ESPEASY_CONSOLE_FALLBACK_PORT


  if ((_console_serial_port != Settings.console_serial_port) ||
      (_console_serial_rxpin != Settings.console_serial_rxpin) ||
      (_console_serial_txpin != Settings.console_serial_txpin) ||
      !mustHaveSerial) {
    if (_mainSerial._serial != nullptr) {
      _mainSerial._serial->flush();
      _mainSerial._serial->end();
      delete _mainSerial._serial;
      _mainSerial._serial = nullptr;
      somethingChanged = true;
    }

    _console_serial_port  = Settings.console_serial_port;
    _console_serial_rxpin = Settings.console_serial_rxpin;
    _console_serial_txpin = Settings.console_serial_txpin;
  }

  if ((_mainSerial._serial == nullptr) && mustHaveSerial) {
    # ifdef USE_SECOND_HEAP
    HeapSelectDram ephemeral;
    # endif // ifdef USE_SECOND_HEAP

    unsigned int buffsize = 128;

    const ESPEasySerialPort mainSerialPort = static_cast<ESPEasySerialPort>(_console_serial_port);

#if USES_HWCDC
    if (mainSerialPort == ESPEasySerialPort::usb_hw_cdc) {
      buffsize = 2048;
    }
#endif // if USES_HWCDC

    _mainSerial._serial = new (std::nothrow) ESPeasySerial(
      mainSerialPort,
      _console_serial_rxpin,
      _console_serial_txpin, 
      false,
      buffsize);
    somethingChanged = true;
  }
# if USES_ESPEASY_CONSOLE_FALLBACK_PORT

  if ((_fallbackSerial._serial == nullptr) && mustHaveFallback) {
    _fallbackSerial._serial = new (std::nothrow) ESPeasySerial(
      ESPEasySerialPort::serial0,
      SOC_RX0,
      SOC_TX0);
    somethingChanged = true;
  }
# endif // if USES_ESPEASY_CONSOLE_FALLBACK_PORT


# if USES_ESPEASY_CONSOLE_FALLBACK_PORT

  if (_fallbackSerial._serial == nullptr) {
    _fallbackSerial._serialWriteBuffer.clear();
  }

# endif // if USES_ESPEASY_CONSOLE_FALLBACK_PORT

  if (_mainSerial._serial == nullptr) {
    _mainSerial._serialWriteBuffer.clear();
  }
#endif // if FEATURE_DEFINE_SERIAL_CONSOLE_PORT
  if (somethingChanged) {
    begin(Settings.BaudRate);
  }
}

void EspEasy_Console_t::begin(uint32_t baudrate)
{
  updateActiveTaskUseSerial0();
  _baudrate = baudrate;

  if (_mainSerial._serial != nullptr) {
#if FEATURE_DEFINE_SERIAL_CONSOLE_PORT
    _mainSerial._serial->begin(baudrate);
    addLog(LOG_LEVEL_INFO, F("ESPEasy console using ESPEasySerial"));
#else // if FEATURE_DEFINE_SERIAL_CONSOLE_PORT
# ifdef ESP8266
    _mainSerial._serial->begin(baudrate);
    #ifndef BUILD_MINIMAL_OTA
    addLog(LOG_LEVEL_INFO, F("ESPEasy console using HW Serial"));
    #endif
# endif // ifdef ESP8266
# ifdef ESP32

    // Allow to flush data from the serial buffers
    // When not opening the USB serial port, the ESP may hang at boot.
    delay(10);
    _mainSerial._serial->end();
    delay(10);
    _mainSerial._serial->begin(baudrate);
    _mainSerial._serial->flush();
# endif // ifdef ESP32
#endif  // if FEATURE_DEFINE_SERIAL_CONSOLE_PORT
  }
#if USES_ESPEASY_CONSOLE_FALLBACK_PORT

  if (_fallbackSerial._serial != nullptr) {
    _fallbackSerial._serial->begin(baudrate);
    addLog(LOG_LEVEL_INFO, F("ESPEasy console fallback enabled"));
  }
#endif // if USES_ESPEASY_CONSOLE_FALLBACK_PORT
}

void EspEasy_Console_t::init() {
#if FEATURE_IMPROV
  _mainSerial._improv.init();
# if FEATURE_DEFINE_SERIAL_CONSOLE_PORT
#  if USES_ESPEASY_CONSOLE_FALLBACK_PORT
  _fallbackSerial._improv.init();
#  endif // if USES_ESPEASY_CONSOLE_FALLBACK_PORT
# endif // if FEATURE_DEFINE_SERIAL_CONSOLE_PORT
#endif // if FEATURE_IMPROV
  updateActiveTaskUseSerial0();

  if (!Settings.UseSerial) {
    return;
  }

#if !FEATURE_DEFINE_SERIAL_CONSOLE_PORT

  if (activeTaskUseSerial0() || log_to_serial_disabled) {
    return;
  }
#endif // if !FEATURE_DEFINE_SERIAL_CONSOLE_PORT

  begin(Settings.BaudRate);
}

void EspEasy_Console_t::loop()
{
  if (!Settings.UseSerial) return;

  START_TIMER;

#if FEATURE_DEFINE_SERIAL_CONSOLE_PORT
  const bool consoleUsesSerial0 =
    (static_cast<ESPEasySerialPort>(_console_serial_port) == ESPEasySerialPort::serial0
# ifdef ESP8266
     || static_cast<ESPEasySerialPort>(_console_serial_port) == ESPEasySerialPort::serial0_swap
# endif // ifdef ESP8266
    );

  if (handledByPluginSerialIn())
  {
    // Any serial0 data is already dealt with
    if (!consoleUsesSerial0 && (_mainSerial._serial != nullptr)) {
      readInput(_mainSerial);
    }
    return;
  }
#else // if FEATURE_DEFINE_SERIAL_CONSOLE_PORT

  if (handledByPluginSerialIn()) {
    return;
  }
#endif // if FEATURE_DEFINE_SERIAL_CONSOLE_PORT

  readInput(_mainSerial);
#if USES_ESPEASY_CONSOLE_FALLBACK_PORT

  readInput(_fallbackSerial);
#endif // if USES_ESPEASY_CONSOLE_FALLBACK_PORT

  STOP_TIMER(CONSOLE_LOOP);
}

void EspEasy_Console_t::addToSerialBuffer(const __FlashStringHelper *line)
{
  addToSerialBuffer(String(line));
}

void EspEasy_Console_t::addToSerialBuffer(char c)
{
  _mainSerial.addToSerialBuffer(c);
#if USES_ESPEASY_CONSOLE_FALLBACK_PORT

  _fallbackSerial.addToSerialBuffer(c);
#endif // if USES_ESPEASY_CONSOLE_FALLBACK_PORT
  process_serialWriteBuffer();
}

void EspEasy_Console_t::addToSerialBuffer(const String& line) {
  process_serialWriteBuffer();

  _mainSerial.addToSerialBuffer(line);
#if USES_ESPEASY_CONSOLE_FALLBACK_PORT

  _fallbackSerial.addToSerialBuffer(line);
#endif // if USES_ESPEASY_CONSOLE_FALLBACK_PORT
  process_serialWriteBuffer();
}

void EspEasy_Console_t::addNewlineToSerialBuffer() {
  _mainSerial.addNewlineToSerialBuffer();
#if USES_ESPEASY_CONSOLE_FALLBACK_PORT
  _fallbackSerial.addNewlineToSerialBuffer();
#endif // if USES_ESPEASY_CONSOLE_FALLBACK_PORT
  process_serialWriteBuffer();
}

bool EspEasy_Console_t::process_serialWriteBuffer() {
  START_TIMER;
  bool res = false;

  if (_mainSerial.process_serialWriteBuffer()) {
    res = true;
  }

#if USES_ESPEASY_CONSOLE_FALLBACK_PORT

  if (_fallbackSerial.process_serialWriteBuffer()) {
    res = true;
  }
#endif // if USES_ESPEASY_CONSOLE_FALLBACK_PORT

  STOP_TIMER(CONSOLE_WRITE_SERIAL);
  return res;
}

void EspEasy_Console_t::setDebugOutput(bool enable)
{
  auto port = getPort();

  if (port != nullptr) {
    port->setDebugOutput(enable);
  }
}

String EspEasy_Console_t::getPortDescription() const
{
  return _mainSerial.getPortDescription();
}

#if USES_ESPEASY_CONSOLE_FALLBACK_PORT
String EspEasy_Console_t::getFallbackPortDescription() const
{
  return _fallbackSerial.getPortDescription();
}

#endif // if USES_ESPEASY_CONSOLE_FALLBACK_PORT

bool EspEasy_Console_t::handledByPluginSerialIn()
{
#if USES_ESPEASY_CONSOLE_FALLBACK_PORT

  if ((_fallbackSerial._serial != nullptr) &&
      _fallbackSerial._serial->available())
  {
    String dummy;

    return PluginCall(PLUGIN_SERIAL_IN, 0, dummy);
  }
#endif // if USES_ESPEASY_CONSOLE_FALLBACK_PORT
#if FEATURE_DEFINE_SERIAL_CONSOLE_PORT

  if ((_mainSerial._serial != nullptr) && _mainSerial._serial->available() &&
      (static_cast<ESPEasySerialPort>(_console_serial_port) == ESPEasySerialPort::serial0
# ifdef ESP8266
       || static_cast<ESPEasySerialPort>(_console_serial_port) == ESPEasySerialPort::serial0_swap
# endif // ifdef ESP8266
      ))
#endif  // if FEATURE_DEFINE_SERIAL_CONSOLE_PORT
  {
    String dummy;

    return PluginCall(PLUGIN_SERIAL_IN, 0, dummy);
  }
  return false;
}

void EspEasy_Console_t::readInput(EspEasy_Console_Port& port)
{
  size_t bytesToRead = port.available();

  while (bytesToRead > 0)
  {
    --bytesToRead;
    delay(0);
    const int SerialInByte = port.read();

    if (SerialInByte >= 0) {
      if (port.process_consoleInput(SerialInByte)) {
        // Processed a full line
        return;
      }
    }
  }
}

#if FEATURE_DEFINE_SERIAL_CONSOLE_PORT
ESPeasySerial * EspEasy_Console_t::getPort()
{
  if (_mainSerial._serial != nullptr) {
    return _mainSerial._serial;
  }
# if USES_ESPEASY_CONSOLE_FALLBACK_PORT

  if (_fallbackSerial._serial != nullptr) {
    return _fallbackSerial._serial;
  }
# endif // if USES_ESPEASY_CONSOLE_FALLBACK_PORT
  return nullptr;
}

#else // if FEATURE_DEFINE_SERIAL_CONSOLE_PORT
HardwareSerial * EspEasy_Console_t::getPort()
{
  if (_mainSerial._serial != nullptr) {
    return _mainSerial._serial;
  }
  return nullptr;
}

#endif // if FEATURE_DEFINE_SERIAL_CONSOLE_PORT


void EspEasy_Console_t::endPort()
{
  _mainSerial.endPort();
#if USES_ESPEASY_CONSOLE_FALLBACK_PORT
  _fallbackSerial.endPort();
#endif // if USES_ESPEASY_CONSOLE_FALLBACK_PORT
  delay(10);
}

int EspEasy_Console_t::availableForWrite()
{
  auto serial = getPort();

  if (serial != nullptr) {
    return serial->availableForWrite();
  }
  return 0;
}

#include "../ESPEasyCore/ESPEasy_Console_Port.h"

#include "../Commands/ExecuteCommand.h"

#include "../DataStructs/TimingStats.h"

#include "../DataTypes/ESPEasy_plugin_functions.h"

#include "../Globals/Cache.h"
#include "../Globals/Logging.h"
#include "../Globals/Plugins.h"
#include "../Globals/Settings.h"

#include "../Helpers/Memory.h"

#include <ESPEasySerialPort.h>


/*
 #if FEATURE_DEFINE_SERIAL_CONSOLE_PORT
 # include "../Helpers/_Plugin_Helper_serial.h"
 #endif // if FEATURE_DEFINE_SERIAL_CONSOLE_PORT
 */

EspEasy_Console_Port::~EspEasy_Console_Port()
{
#if FEATURE_DEFINE_SERIAL_CONSOLE_PORT
  if (_serial != nullptr) {
    delete _serial;
    _serial = nullptr;
  }
#endif
}

EspEasy_Console_Port::operator bool() const
{
  if (_serial != nullptr) {
    return true;

    //    return _serial->operator bool();
  }
  return false;
}

int EspEasy_Console_Port::read()
{
#if FEATURE_IMPROV
  uint8_t b;

  if (_improv.getFromBuffer(b)) {
    return b;
  }

#endif // if FEATURE_IMPROV
  int res = -1;

  if (_serial != nullptr)
  {
    res = _serial->read();
#if FEATURE_IMPROV

    if (res >= 0) {
      if (_improv.handle(res, _serial)) {
        // Looks like it might be an IMPROV command, so like we didn't have any data
        return -1;
      }

      if (_improv.getFromBuffer(b)) {
        return b;
      }
    }

#endif // if FEATURE_IMPROV
  }
  return res;
}

size_t EspEasy_Console_Port::available() const
{
  size_t res = 0u;

#if FEATURE_IMPROV
  res += _improv.available();
#endif // if FEATURE_IMPROV

  if (_serial != nullptr) {
    res += _serial->available();
  }
  return res;
}

void EspEasy_Console_Port::endPort()
{
  if (_serial != nullptr) {
    _serial->end();
  }
}

void EspEasy_Console_Port::addToSerialBuffer(char c)
{
  if (_serial != nullptr) {
    _serialWriteBuffer.add(c);
  }
}

void EspEasy_Console_Port::addToSerialBuffer(const String& line)
{
  if (_serial != nullptr) {
    _serialWriteBuffer.add(line);
  }
}

void EspEasy_Console_Port::addNewlineToSerialBuffer()
{
  if (_serial != nullptr) {
    _serialWriteBuffer.addNewline();
  }
}

bool EspEasy_Console_Port::process_serialWriteBuffer()
{
  if (_serial != nullptr) {
    const int snip = _serial->availableForWrite();
    
    if (snip > 0) {
      return _serialWriteBuffer.write(*_serial, snip) != 0;
    }
  }
  return false;
}

bool EspEasy_Console_Port::process_consoleInput(uint8_t SerialInByte)
{
  if (isprint(SerialInByte))
  {
    if (SerialInByteCounter < CONSOLE_INPUT_BUFFER_SIZE) { // add char to string if it still fits
      InputBuffer_Serial[SerialInByteCounter++] = SerialInByte;
    }
  }

  if ((SerialInByte == '\b') && (SerialInByteCounter > 0)) // Correct a typo using BackSpace
  {
    --SerialInByteCounter;
  } else
  if ((SerialInByte == '\r') || (SerialInByte == '\n'))
  {
    // Ignore empty command
    if (SerialInByteCounter != 0) {
      InputBuffer_Serial[SerialInByteCounter] = 0; // serial data completed
      addToSerialBuffer('>');
      String cmd(InputBuffer_Serial);
      addToSerialBuffer(cmd);
      addToSerialBuffer('\n');
      ExecuteCommand_all({EventValueSource::Enum::VALUE_SOURCE_SERIAL, std::move(cmd)}, true);
      SerialInByteCounter   = 0;
      InputBuffer_Serial[0] = 0; // serial data processed, clear buffer
      return true;
    }
  }
  return false;
}

String EspEasy_Console_Port::getPortDescription() const
{
  if (_serial != nullptr) {
  #if FEATURE_DEFINE_SERIAL_CONSOLE_PORT
    return _serial->getPortDescription();
  #else // if FEATURE_DEFINE_SERIAL_CONSOLE_PORT
    String res = F("HW Serial0 @ ");
    res += _serial->baudRate();
    return res;
  #endif // if FEATURE_DEFINE_SERIAL_CONSOLE_PORT
  }

  return F("-");
}

#include "../ESPEasyCore/ESPEasyNetwork.h"

#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../ESPEasyCore/ESPEasyEth.h"
#include "../ESPEasyCore/ESPEasyWifi.h"
#include "../Globals/ESPEasy_time.h"
#include "../Globals/ESPEasyWiFiEvent.h"
#include "../Globals/NetworkState.h"
#include "../Globals/Settings.h"

#include "../Helpers/Network.h"
#include "../Helpers/Networking.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/MDNS_Helper.h"

#if FEATURE_ETHERNET
#include "../Globals/ESPEasyEthEvent.h"
#include <ETH.h>
#endif


#if FEATURE_USE_IPV6
#include <esp_netif.h>

// -----------------------------------------------------------------------------------------------------------------------
// ---------------------------------------------------- Private functions ------------------------------------------------
// -----------------------------------------------------------------------------------------------------------------------

esp_netif_t* get_esp_interface_netif(esp_interface_t interface);
#endif


void setNetworkMedium(NetworkMedium_t new_medium) {
#if !(FEATURE_ETHERNET)
  if (new_medium == NetworkMedium_t::Ethernet) {
    new_medium = NetworkMedium_t::WIFI;
  }
#endif
  if (active_network_medium == new_medium) {
    return;
  }
  switch (active_network_medium) {
    case NetworkMedium_t::Ethernet:
      #if FEATURE_ETHERNET
      // FIXME TD-er: How to 'end' ETH?
//      ETH.end();
      if (new_medium == NetworkMedium_t::WIFI) {
        WiFiEventData.clearAll();
#if ESP_IDF_VERSION_MAJOR >= 5
        WiFi.STA.setDefault();
#endif
      }
      #endif
      break;
    case NetworkMedium_t::WIFI:
      WiFiEventData.timerAPoff.setMillisFromNow(WIFI_AP_OFF_TIMER_DURATION);
      WiFiEventData.timerAPstart.clear();
      if (new_medium == NetworkMedium_t::Ethernet) {
#if ESP_IDF_VERSION_MAJOR >= 5
#if FEATURE_ETHERNET
        ETH.setDefault();
#endif
#endif
        WifiDisconnect();
      }
      break;
    case NetworkMedium_t::NotSet:
      break;
  }
  statusLED(true);
  active_network_medium = new_medium;
  #ifndef BUILD_MINIMAL_OTA
  addLog(LOG_LEVEL_INFO, concat(F("Set Network mode: "), toString(active_network_medium)));
  #endif
}


/*********************************************************************************************\
   Ethernet or Wifi Support for ESP32 Build flag FEATURE_ETHERNET
\*********************************************************************************************/
void NetworkConnectRelaxed() {
  if (NetworkConnected()) return;
#if FEATURE_ETHERNET
  if(active_network_medium == NetworkMedium_t::Ethernet) {
    if (ETHConnectRelaxed()) {
      return;
    }
    // Failed to start the Ethernet network, probably not present of wrong parameters.
    // So set the runtime active medium to WiFi to try connecting to WiFi or at least start the AP.
    setNetworkMedium(NetworkMedium_t::WIFI);
  }
#endif
  // Failed to start the Ethernet network, probably not present of wrong parameters.
  // So set the runtime active medium to WiFi to try connecting to WiFi or at least start the AP.
  WiFiConnectRelaxed();
}

bool NetworkConnected() {
  #if FEATURE_ETHERNET
  if(active_network_medium == NetworkMedium_t::Ethernet) {
    return ETHConnected();
  }
  #endif
  return WiFiConnected();
}

IPAddress NetworkLocalIP() {
  #if FEATURE_ETHERNET
  if(active_network_medium == NetworkMedium_t::Ethernet) {
    if(EthEventData.ethInitSuccess) {
      return ETH.localIP();
    } else {
      addLog(LOG_LEVEL_ERROR, F("Call NetworkLocalIP() only on connected Ethernet!"));
      return IPAddress();
    }
  }
  #endif
  return WiFi.localIP();
}

IPAddress NetworkSubnetMask() {
  #if FEATURE_ETHERNET
  if(active_network_medium == NetworkMedium_t::Ethernet) {
    if(EthEventData.ethInitSuccess) {
      return ETH.subnetMask();
    } else {
      addLog(LOG_LEVEL_ERROR, F("Call NetworkSubnetMask() only on connected Ethernet!"));
      return IPAddress();
    }
  }
  #endif
  return WiFi.subnetMask();
}

IPAddress NetworkGatewayIP() {
  #if FEATURE_ETHERNET
  if(active_network_medium == NetworkMedium_t::Ethernet) {
    if(EthEventData.ethInitSuccess) {
      return ETH.gatewayIP();
    } else {
      addLog(LOG_LEVEL_ERROR, F("Call NetworkGatewayIP() only on connected Ethernet!"));
      return IPAddress();
    }
  }
  #endif
  return WiFi.gatewayIP();
}

IPAddress NetworkDnsIP(uint8_t dns_no) {
  scrubDNS();
  #if FEATURE_ETHERNET
  if(active_network_medium == NetworkMedium_t::Ethernet) {
    if(EthEventData.ethInitSuccess) {
      return ETH.dnsIP(dns_no);
    } else {
      addLog(LOG_LEVEL_ERROR, F("Call NetworkDnsIP(uint8_t dns_no) only on connected Ethernet!"));
      return IPAddress();
    }
  }
  #endif
  return WiFi.dnsIP(dns_no);
}

#if FEATURE_USE_IPV6
esp_netif_t * getActiveNetworkMediumInterface() {
  esp_interface_t iface = ESP_IF_MAX;
  #if FEATURE_ETHERNET
  if(active_network_medium == NetworkMedium_t::Ethernet) {
    if(EthEventData.ethInitSuccess) {
      esp_netif_t *res = ETH.netif();
      if (res == nullptr) {
        res = get_esp_interface_netif(ESP_IF_ETH);
      }
      if (res != nullptr)
        return res;
    }
  } else
  #endif
  {
    if (WifiIsSTA(WiFi.getMode())) {
      iface = ESP_IF_WIFI_STA;
    }
  }
  if (ESP_IF_MAX == iface) 
    return nullptr;
  return get_esp_interface_netif(iface);
}

IPAddress NetworkLocalIP6() {
  esp_netif_t * iface = getActiveNetworkMediumInterface();
  esp_ip6_addr_t addr;
  if (nullptr == iface ||
     esp_netif_get_ip6_linklocal(iface, &addr)) 
  {
    return IN6ADDR_ANY;
  }

  IPAddress res(IPv6, (const uint8_t*)addr.addr, addr.zone);
  return res;
}

IPAddress NetworkGlobalIP6() {
  esp_netif_t * iface = getActiveNetworkMediumInterface();
  esp_ip6_addr_t addr;
  if (nullptr == iface ||
     esp_netif_get_ip6_global(iface, &addr)) 
  {
    return IN6ADDR_ANY;
  }

  IPAddress res(IPv6, (const uint8_t*)addr.addr, addr.zone);
  return res;
}

IP6Addresses_t NetworkAllIPv6() {
  IP6Addresses_t addresses;
  esp_netif_t * iface = getActiveNetworkMediumInterface();
  if (nullptr != iface) {
    esp_ip6_addr_t esp_ip6_addr[LWIP_IPV6_NUM_ADDRESSES]{};

    int count = esp_netif_get_all_ip6(iface, esp_ip6_addr);
    for (int i = 0; i < count; ++i) {
      addresses.emplace_back(IPv6, (const uint8_t*)esp_ip6_addr[i].addr, esp_ip6_addr[i].zone);
    }
  }

  return addresses;
}

bool IPv6_from_MAC(const MAC_address& mac, IPAddress& ipv6)
{
  if (ipv6 == IN6ADDR_ANY) { return false; }
  int index_offset = 8;

  for (int i = 0; i < 6; ++i, ++index_offset) {
    ipv6[index_offset] = mac.mac[i];

    if (i == 0) {
      // invert bit 2
      bitToggle(ipv6[index_offset], 1);
    }

    if (i == 2) {
      ipv6[++index_offset] = 0xFF;
      ipv6[++index_offset] = 0xFE;
    }
  }
/*
  addLog(LOG_LEVEL_INFO, strformat(
     F("IPv6_from_MAC: Mac %s IP %s"),
     mac.toString().c_str(),
     ipv6.toString(true).c_str()
     ));
*/
  return true;
}

bool is_IPv6_based_on_MAC(const MAC_address& mac, const IPAddress& ipv6)
{
  IPAddress tmp = ipv6;

  if (IPv6_from_MAC(mac, tmp)) {
    return ipv6 == tmp;
  }
  return false;
}

bool IPv6_link_local_from_MAC(const MAC_address& mac, IPAddress& ipv6)
{
  ipv6 = NetworkLocalIP6();
  return IPv6_from_MAC(mac, ipv6);
}

bool is_IPv6_link_local_from_MAC(const MAC_address& mac)
{
  return is_IPv6_based_on_MAC(mac, NetworkLocalIP6());
}

// Assume we're in the same subnet, thus use our own IPv6 global address
bool IPv6_global_from_MAC(const MAC_address& mac, IPAddress& ipv6)
{
  ipv6 = NetworkGlobalIP6();
  return IPv6_from_MAC(mac, ipv6);
}

bool is_IPv6_global_from_MAC(const MAC_address& mac)
{
  return is_IPv6_based_on_MAC(mac, NetworkGlobalIP6());
}

#endif // if FEATURE_USE_IPV6



MAC_address NetworkMacAddress() {
  #if FEATURE_ETHERNET
  if(active_network_medium == NetworkMedium_t::Ethernet) {
    return ETHMacAddress();
  }
  #endif
  MAC_address mac;
  WiFi.macAddress(mac.mac);
  return mac;
}

String NetworkGetHostname() {
    #ifdef ESP32
      #if FEATURE_ETHERNET 
      if(Settings.NetworkMedium == NetworkMedium_t::Ethernet && EthEventData.ethInitSuccess) {
        return String(ETH.getHostname());
      }
      #endif
      return String(WiFi.getHostname());
    #else
      return String(WiFi.hostname());
    #endif
}

// ********************************************************************************
// Determine Wifi AP name to set. (also used for mDNS)
// ********************************************************************************
String NetworkGetHostNameFromSettings(bool force_add_unitnr)
{
  if (force_add_unitnr) return Settings.getHostname(true);
  return Settings.getHostname();
}

String NetworkCreateRFCCompliantHostname(bool force_add_unitnr) {
  // Create hostname with - instead of spaces
  return makeRFCCompliantName(NetworkGetHostNameFromSettings(force_add_unitnr));
}

String makeRFCCompliantName(const String& name, const char replaceChar, const char allowedChar, const size_t maxlength) {
  String hostname(name);
  // See RFC952. (when using the default arguments: '-', '-', 24)
  // Allowed chars:
  // * letters (a-z, A-Z)
  // * numerals (0-9)
  // * Hyphen (-)
  // * Max length 24
  replaceUnicodeByChar(hostname, replaceChar);
  for (size_t i = 0; i < hostname.length(); ++i) {
    const char c = hostname[i];
    if (!isAlphaNumeric(c) && (c != allowedChar)) {
      hostname[i] = replaceChar;
    }
  }

  // May not start or end with a hyphen
  const String dash(replaceChar);
  while (hostname.startsWith(dash)) {
    hostname = hostname.substring(1);
  }
  while (hostname.endsWith(dash)) {
    hostname = hostname.substring(0, hostname.length() - 1);
  }

  // May not contain only numerals
  bool onlyNumerals = true;
  for (size_t i = 0; onlyNumerals && i < hostname.length(); ++i) {
    const char c = hostname[i];
    if (!isdigit(c)) {
      onlyNumerals = false;
    }
  }
  if (onlyNumerals) {
    hostname = strformat(F("ESPEasy%c%s"), replaceChar, hostname.c_str());
  }

  if ((maxlength > 0) && (hostname.length() > maxlength)) {
    hostname = hostname.substring(0, maxlength);
  }

  return hostname;
}

MAC_address WifiSoftAPmacAddress() {
  MAC_address mac;
  WiFi.softAPmacAddress(mac.mac);
  return mac;
}

MAC_address WifiSTAmacAddress() {
  MAC_address mac;
  WiFi.macAddress(mac.mac);
  return mac;
}

void CheckRunningServices() {
  // First try to get the time, since that may be used in logs
  if (Settings.UseNTP() && node_time.getTimeSource() > timeSource_t::NTP_time_source) {
    node_time.lastNTPSyncTime_ms = 0;
    node_time.initTime();
  }
#if FEATURE_SET_WIFI_TX_PWR
  if (active_network_medium == NetworkMedium_t::WIFI) 
  {
    SetWiFiTXpower();
  }
#endif
  set_mDNS();
}

#if FEATURE_ETHERNET
bool EthFullDuplex()
{
  if (EthEventData.ethInitSuccess)
    return ETH.fullDuplex();
  return false;
}

bool EthLinkUp()
{
  if (EthEventData.ethInitSuccess) {
    #if ESP_IDF_VERSION_MAJOR < 5
    // FIXME TD-er: See: https://github.com/espressif/arduino-esp32/issues/6105
    return EthEventData.EthConnected();
    #else
    return ETH.linkUp();
    #endif
  }
  return false;
}

uint8_t EthLinkSpeed()
{
  if (EthEventData.ethInitSuccess) {
    return ETH.linkSpeed();
  }
  return 0;
}
#endif

#include "../ESPEasyCore/ESPEasyEth_ProcessEvent.h"

#if FEATURE_ETHERNET

# include "../../ESPEasy-Globals.h"

# include "../ESPEasyCore/ESPEasyEth.h"
# include "../ESPEasyCore/ESPEasyNetwork.h"
# include "../ESPEasyCore/ESPEasyWifi.h" // LogConnectionStatus

# include "../Globals/ESPEasyEthEvent.h"
# include "../Globals/ESPEasyWiFiEvent.h"
# include "../Globals/ESPEasy_Scheduler.h"
# include "../Globals/ESPEasy_time.h"
# include "../Globals/EventQueue.h"
# include "../Globals/MQTT.h"
# include "../Globals/NetworkState.h"
# include "../Globals/Settings.h"

# include "../Helpers/LongTermTimer.h"
# include "../Helpers/Network.h"
# include "../Helpers/Networking.h"
# include "../Helpers/PeriodicalActions.h"
# include "../Helpers/StringConverter.h"

# include <ETH.h>

void handle_unprocessedEthEvents() {
  if (EthEventData.unprocessedEthEvents()) {
    // Process disconnect events before connect events.
#if FEATURE_USE_IPV6
    if (!EthEventData.processedGotIP6) {
      processEthernetGotIPv6();
    }
#endif

    if (!EthEventData.processedDisconnect) {
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_DEBUG, F("Eth  : Entering processDisconnect()"));
      # endif // ifndef BUILD_NO_DEBUG
      processEthernetDisconnected();
    }

    // Must process the Ethernet Connected event regardless the active network medium.
    // It may happen by plugging in the cable while WiFi was active.
    if (!EthEventData.processedConnect) {
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_DEBUG, F("Eth  : Entering processConnect()"));
      # endif // ifndef BUILD_NO_DEBUG
      processEthernetConnected();
    }
  }

  if (active_network_medium == NetworkMedium_t::Ethernet) {
    const bool EthServices_was_initialized = EthEventData.EthServicesInitialized();

    if (!EthEventData.EthServicesInitialized() || EthEventData.unprocessedEthEvents())
    {
      if (!EthEventData.unprocessedEthEvents() && EthEventData.EthConnectAllowed()) {
        NetworkConnectRelaxed();
      }

      if (!EthEventData.processedGotIP) {
        # ifndef BUILD_NO_DEBUG
        addLog(LOG_LEVEL_DEBUG, F("Eth  : Entering processGotIP()"));
        # endif // ifndef BUILD_NO_DEBUG
        processEthernetGotIP();
      }

      if (!EthEventData.processedDHCPTimeout) {
        # ifndef BUILD_NO_DEBUG
        addLog(LOG_LEVEL_DEBUG, F("Eth  : DHCP timeout, Calling disconnect()"));
        # endif // ifndef BUILD_NO_DEBUG
        EthEventData.processedDHCPTimeout = true;

        // WifiDisconnect();
      }
    }
    EthEventData.setEthServicesInitialized();

    if (!EthServices_was_initialized && EthEventData.setEthServicesInitialized()) {
      registerEthEventHandler();
    }
  }
}

void check_Eth_DNS_valid() {
  // Check if DNS is still valid, as this may have been reset by the WiFi module turned off.
  // DNS records are shared between WiFi and Ethernet IP stack.
  // Turning off WiFi will clead DNS records for Ethernet
  if (EthEventData.EthServicesInitialized() &&
      (active_network_medium == NetworkMedium_t::Ethernet) &&
      EthEventData.ethInitSuccess &&
      !ethUseStaticIP()) {
    const bool has_cache = 
      valid_DNS_address(EthEventData.dns0_cache) || 
      valid_DNS_address(EthEventData.dns1_cache);

    if (has_cache) {
      const IPAddress dns0 = ETH.dnsIP(0);
      const IPAddress dns1 = ETH.dnsIP(1);

      if (!valid_DNS_address(dns0) && !valid_DNS_address(dns1)) {
        static uint32_t lastLog = 0;
        if (timePassedSince(lastLog) > 1000) {
          addLogMove(LOG_LEVEL_ERROR, concat(
            F("ETH  : DNS server was cleared, use cached DNS IP: "), 
            formatIP(EthEventData.dns0_cache)));
          lastLog = millis();
        }
        setDNS(0, EthEventData.dns0_cache);
        setDNS(1, EthEventData.dns1_cache);
      }
    }
  }
}

void processEthernetConnected() {
  if (EthEventData.processedConnect) { return; }

  // FIXME TD-er: Must differentiate among reconnects for WiFi and Ethernet.
  ++WiFiEventData.wifi_reconnects;
  addLog(LOG_LEVEL_INFO, F("processEthernetConnected()"));
  EthEventData.setEthConnected();
  EthEventData.processedConnect = true;

  if (Settings.UseRules)
  {
    eventQueue.add(F("Ethernet#LinkUp"));
  }
  setNetworkMedium(Settings.NetworkMedium);

  if (ethUseStaticIP()) { EthEventData.processedGotIP = false; }
}

void processEthernetDisconnected() {
  if (EthEventData.processedDisconnect) { return; }
  EthEventData.setEthDisconnected();
  EthEventData.processedDisconnect     = true;
  EthEventData.ethConnectAttemptNeeded = true;

  if (Settings.UseRules)
  {
    eventQueue.add(F("Ethernet#Disconnected"));
  }
}

void processEthernetGotIP() {
  if (EthEventData.processedGotIP || !EthEventData.ethInitSuccess) {
    return;
  }

  if (ethUseStaticIP()) {
    ethSetupStaticIPconfig();
  }
  const IPAddress ip = NetworkLocalIP();

  if (!ip) {
    return;
  }

  const IPAddress gw     = NetworkGatewayIP();
  const IPAddress subnet = NetworkSubnetMask();

  IPAddress dns0                              = ETH.dnsIP(0);
  IPAddress dns1                              = ETH.dnsIP(1);
  const LongTermTimer::Duration dhcp_duration = EthEventData.lastConnectMoment.timeDiff(EthEventData.lastGetIPmoment);
  #if ESP_IDF_VERSION_MAJOR >= 5
  const bool mustRequestDHCP = (!dns0 && !dns1) && !ethUseStaticIP();
  #endif

  if (!ethUseStaticIP()) {
    if (!dns0 && !dns1) {
      addLog(LOG_LEVEL_ERROR, F("ETH  : No DNS server received via DHCP, use cached DNS IP"));
      if (EthEventData.dns0_cache) setDNS(0, EthEventData.dns0_cache);
      if (EthEventData.dns1_cache) setDNS(1, EthEventData.dns1_cache);
    } else {
      EthEventData.dns0_cache = dns0;
      EthEventData.dns1_cache = dns1;
    }
  }

  if (loglevelActiveFor(LOG_LEVEL_INFO))
  {
    String log;

    if (log.reserve(160)) {
      log  = F("ETH MAC: ");
      log += NetworkMacAddress().toString();
      log += ' ';

      if (ethUseStaticIP()) {
        log += F("Static");
      } else {
        log += F("DHCP");
      }
      log += F(" IP: ");
      log += formatIP(ip);
      log += ' ';
      log += wrap_braces(NetworkGetHostname());
      log += F(" GW: ");
      log += formatIP(gw);
      log += F(" SN: ");
      log += formatIP(subnet);
      log += F(" DNS: ");
      log += formatIP(dns0);
      log += '/';
      log += formatIP(dns1);

      if (EthLinkUp()) {
        if (EthFullDuplex()) {
          log += F(" FULL_DUPLEX");
        }
        log += ' ';
        log += EthLinkSpeed();
        log += F("Mbps");
      } else {
        log += F(" Link Down");
      }

      if ((dhcp_duration > 0ll) && (dhcp_duration < 30000000ll)) {
        // Just log times when they make sense.
        log += F("   duration: ");
        log += static_cast<int32_t>(dhcp_duration / 1000);
        log += F(" ms");
      }

      addLogMove(LOG_LEVEL_INFO, log);
    }
  }

  // First try to get the time, since that may be used in logs
  if (node_time.systemTimePresent()) {
    node_time.initTime();
  }
# if FEATURE_MQTT
  mqtt_reconnect_count        = 0;
  MQTTclient_should_reconnect = true;
  timermqtt_interval          = 100;
  Scheduler.setIntervalTimer(SchedulerIntervalTimer_e::TIMER_MQTT);
  scheduleNextMQTTdelayQueue();
# endif // if FEATURE_MQTT
  Scheduler.sendGratuitousARP_now();

  if (Settings.UseRules)
  {
    eventQueue.add(F("Ethernet#Connected"));
  }
  statusLED(true);
  logConnectionStatus();

  EthEventData.processedGotIP = true;
#if ESP_IDF_VERSION_MAJOR >= 5
  if (mustRequestDHCP /*&& EthEventData.lastConnectMoment.millisPassedSince() < 10000*/) {
    // FIXME TD-er: Must add some check other than fixed timeout here to not constantly make DHCP requests.
    // Force new DHCP request.
    ETH.config();
  }
#endif

  EthEventData.setEthGotIP();
  CheckRunningServices();
}

#if FEATURE_USE_IPV6
void processEthernetGotIPv6() {
  if (!EthEventData.processedGotIP6) {
    if (loglevelActiveFor(LOG_LEVEL_INFO))
      addLog(LOG_LEVEL_INFO, String(F("ETH event: Got IP6 ")) + EthEventData.unprocessed_IP6.toString(true));
    EthEventData.processedGotIP6 = true;
#if FEATURE_ESPEASY_P2P
//    updateUDPport(true);
#endif

  }
}
#endif

#endif // if FEATURE_ETHERNET

#include "../ESPEasyCore/ESPEasyEthEvent.h"

#ifdef ESP32
# if FEATURE_ETHERNET
#  include <ETH.h>

#include "../ESPEasyCore/ESPEasyEth.h"
#include "../Globals/ESPEasyEthEvent.h"


// ********************************************************************************
// Functions called on events.  ** WARNING **
// Make sure not to call anything in these functions that result in delay() or yield()
// ********************************************************************************

#  include <WiFi.h>


#  if ESP_IDF_VERSION_MAJOR > 3
void EthEvent(WiFiEvent_t event, arduino_event_info_t info) {
  switch (event) {
    case ARDUINO_EVENT_ETH_START:

      if (ethPrepare()) {
//      addLog(LOG_LEVEL_INFO, F("ETH event: Started"));
//    } else {
//      addLog(LOG_LEVEL_ERROR, F("ETH event: Could not prepare ETH!"));
      }
      break;
    case ARDUINO_EVENT_ETH_CONNECTED:
//      addLog(LOG_LEVEL_INFO, F("ETH event: Connected"));
      EthEventData.markConnected();
      break;
    case ARDUINO_EVENT_ETH_GOT_IP:
      EthEventData.markGotIP();
//      addLog(LOG_LEVEL_INFO,  F("ETH event: Got IP"));
      break;
    case ARDUINO_EVENT_ETH_DISCONNECTED:
//      addLog(LOG_LEVEL_ERROR, F("ETH event: Disconnected"));
      EthEventData.markDisconnect();
      break;
    case ARDUINO_EVENT_ETH_STOP:
//      addLog(LOG_LEVEL_INFO, F("ETH event: Stopped"));
      break;
    #   if ESP_IDF_VERSION_MAJOR > 3
    case ARDUINO_EVENT_ETH_GOT_IP6:
    #   else // if ESP_IDF_VERSION_MAJOR > 3
    case ARDUINO_EVENT_GOT_IP6:
    #   endif // if ESP_IDF_VERSION_MAJOR > 3
    #if FEATURE_USE_IPV6
    {
      ip_event_got_ip6_t * event = static_cast<ip_event_got_ip6_t*>(&info.got_ip6);
      IPAddress ip(IPv6, (const uint8_t*)event->ip6_info.ip.addr, event->ip6_info.ip.zone);
      EthEventData.markGotIPv6(ip);
//      addLog(LOG_LEVEL_INFO, String(F("ETH event: Got IP6 ")) + ip.toString(true));
    }
    #else
//    addLog(LOG_LEVEL_INFO, F("ETH event: Got IP6"));
    #endif
      break;
    default:
    {
      break;
    }
  }
}

#  endif // if ESP_IDF_VERSION_MAJOR > 3

# endif // if FEATURE_ETHERNET

#endif // ifdef ESP32

#include "../ESPEasyCore/Serial.h"


#include "../Commands/ExecuteCommand.h"

#include "../Globals/Cache.h"
#include "../Globals/ESPEasy_Console.h"
//#include "../Globals/Logging.h" //  For serialWriteBuffer
#include "../Globals/Settings.h"

#include "../Helpers/ESPEasy_time_calc.h"
#include "../Helpers/Memory.h"



void initSerial()
{
  ESPEasy_Console.init();
}

void serial()
{
  ESPEasy_Console.loop();
}

bool process_serialWriteBuffer()
{
  return ESPEasy_Console.process_serialWriteBuffer();
}

// For now, only send it to the serial buffer and try to process it.
// Later we may want to wrap it into a log.
void serialPrint(const __FlashStringHelper *text) {
  ESPEasy_Console.addToSerialBuffer(text);
}

void serialPrint(const String& text) {
  ESPEasy_Console.addToSerialBuffer(text);
}

void serialPrintln(const __FlashStringHelper *text) {
  ESPEasy_Console.addToSerialBuffer(text);
  serialPrintln();
}

void serialPrintln(const String& text) {
  ESPEasy_Console.addToSerialBuffer(text);
  serialPrintln();
}

void serialPrintln() {
  ESPEasy_Console.addNewlineToSerialBuffer();
  ESPEasy_Console.process_serialWriteBuffer();
}

// Do not add helper functions for other types, since those types can only be
// explicit matched at a constructor, not a function declaration.

/*
   void serialPrint(char c) {
   serialPrint(String(c));
   }


   void serialPrint(unsigned long value) {
   serialPrint(String(value));
   }

   void serialPrint(long value) {
   serialPrint(String(value));
   }

   void serialPrintln(unsigned long value) {
   serialPrintln(String(value));
   }
 */

#include "../ESPEasyCore/ESPEasy_backgroundtasks.h"

#include "../../ESPEasy_common.h"

#include "../../ESPEasy-Globals.h"
#include "../DataStructs/TimingStats.h"
#include "../ESPEasyCore/ESPEasyNetwork.h"
#include "../ESPEasyCore/Serial.h"
#include "../Globals/NetworkState.h"
#include "../Globals/Services.h"
#include "../Globals/Settings.h"
#if FEATURE_RTTTL && FEATURE_ANYRTTTL_LIB && FEATURE_ANYRTTTL_ASYNC
#include "../Helpers/Audio.h"
#endif // if FEATURE_RTTTL && FEATURE_ANYRTTTL_LIB && FEATURE_ANYRTTTL_ASYNC
#include "../Helpers/ESPEasy_time_calc.h"
#include "../Helpers/Network.h"
#include "../Helpers/Networking.h"


#if FEATURE_ARDUINO_OTA
#include "../Helpers/OTA.h"
#endif



/*********************************************************************************************\
* run background tasks
\*********************************************************************************************/
bool runningBackgroundTasks = false;
void backgroundtasks()
{
  // checkRAM(F("backgroundtasks"));
  // always start with a yield
  delay(0);

  /*
     // Remove this watchdog feed for now.
     // See https://github.com/letscontrolit/ESPEasy/issues/1722#issuecomment-419659193

   #ifdef ESP32
     // Have to find a similar function to call ESP32's esp_task_wdt_feed();
   #else
     ESP.wdtFeed();
   #endif
   */

  // prevent recursion!
  if (runningBackgroundTasks)
  {
    return;
  }

  // Rate limit calls to run backgroundtasks
  static uint32_t lastRunBackgroundTasks = 0;
  if (timePassedSince(lastRunBackgroundTasks) < 10) return;
  lastRunBackgroundTasks = millis();

  START_TIMER
  #if FEATURE_MDNS || FEATURE_ESPEASY_P2P
  const bool networkConnected = NetworkConnected();
  #else
  NetworkConnected();
  #endif

  runningBackgroundTasks = true;

  /*
     // Not needed anymore, see: https://arduino-esp8266.readthedocs.io/en/latest/faq/readme.html#how-to-clear-tcp-pcbs-in-time-wait-state
     if (networkConnected) {
   #if defined(ESP8266)
      tcpCleanup();
   #endif
     }
   */

  process_serialWriteBuffer();

  if (!UseRTOSMultitasking) {
    serial();

//    if (webserverRunning) {
/*
    {
      START_TIMER
      web_server.handleClient();
      STOP_TIMER(WEBSERVER_HANDLE_CLIENT);
    }
*/
    #if FEATURE_ESPEASY_P2P
    if (networkConnected) {
      checkUDP();
    }
    #endif
  }

  #if FEATURE_DNS_SERVER

  // process DNS, only used if the ESP has no valid WiFi config
  if (dnsServerActive) {
    dnsServer.processNextRequest();
  }
  #endif // if FEATURE_DNS_SERVER

  #if FEATURE_ARDUINO_OTA

  if (Settings.ArduinoOTAEnable) {
    ArduinoOTA_handle();
  }

  // once OTA is triggered, only handle that and dont do other stuff. (otherwise it fails)
  while (ArduinoOTAtriggered)
  {
    delay(0);

    ArduinoOTA_handle();
  }

  #endif // if FEATURE_ARDUINO_OTA

  #if FEATURE_MDNS

  // Allow MDNS processing
  if (networkConnected) {
    # ifdef ESP8266

    // ESP32 does not have an update() function
    MDNS.update();
    # endif // ifdef ESP8266
  }
  #endif // if FEATURE_MDNS

  delay(0);

  #if FEATURE_RTTTL && FEATURE_ANYRTTTL_LIB && FEATURE_ANYRTTTL_ASYNC
  update_rtttl();
  #endif // if FEATURE_RTTTL && FEATURE_ANYRTTTL_LIB && FEATURE_ANYRTTTL_ASYNC

  statusLED(false);

  runningBackgroundTasks = false;
  STOP_TIMER(BACKGROUND_TASKS);
}

#include "../ESPEasyCore/ESPEasyWifi_ProcessEvent.h"

#include "../../ESPEasy-Globals.h"

#if FEATURE_ETHERNET
#include "../ESPEasyCore/ESPEasyEth_ProcessEvent.h"
#endif
#include "../ESPEasyCore/ESPEasyNetwork.h"
#include "../ESPEasyCore/ESPEasyWifi.h"

#include "../Globals/ESPEasyWiFiEvent.h"
#include "../Globals/ESPEasy_Scheduler.h"
#include "../Globals/ESPEasy_time.h"
#include "../Globals/EventQueue.h"
#include "../Globals/MQTT.h"
#include "../Globals/NetworkState.h"
#include "../Globals/RTC.h"
#include "../Globals/SecuritySettings.h"
#include "../Globals/Services.h"
#include "../Globals/Settings.h"
#include "../Globals/WiFi_AP_Candidates.h"

#include "../Helpers/Convert.h"
#include "../Helpers/ESPEasyRTC.h"
#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/Network.h"
#include "../Helpers/Networking.h"
#include "../Helpers/PeriodicalActions.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringGenerator_WiFi.h"
#include "../Helpers/StringProvider.h"

// #include "../ESPEasyCore/ESPEasyEth.h"
// #include "../ESPEasyCore/ESPEasyWiFiEvent.h"
// #include "../ESPEasyCore/ESPEasy_Log.h"
// #include "../Helpers/ESPEasy_time_calc.h"
// #include "../Helpers/Misc.h"
// #include "../Helpers/Scheduler.h"

#include "../WebServer/ESPEasy_WebServer.h"


// ********************************************************************************
// Called from the loop() to make sure events are processed as soon as possible.
// These functions are called from Setup() or Loop() and thus may call delay() or yield()
// ********************************************************************************
void handle_unprocessedNetworkEvents()
{
#if FEATURE_ETHERNET
  handle_unprocessedEthEvents();
#endif

  if (active_network_medium == NetworkMedium_t::WIFI) {
    const bool should_be_initialized = (WiFiEventData.WiFiGotIP() && WiFiEventData.WiFiConnected()) || NetworkConnected();
    if (WiFiEventData.WiFiServicesInitialized() != should_be_initialized)
    {
      if (!WiFiEventData.WiFiServicesInitialized()) {
        WiFiEventData.processedDHCPTimeout  = true;  // FIXME TD-er:  Find out when this happens  (happens on ESP32 sometimes)
        if (WiFiConnected()) {
          if (!WiFiEventData.WiFiGotIP()) {
            # ifndef BUILD_NO_DEBUG
            addLog(LOG_LEVEL_DEBUG, F("WiFi : Missed gotIP event"));
            #endif
            WiFiEventData.processedGotIP = false;
            processGotIP();
          }
          if (!WiFiEventData.WiFiConnected()) {
            # ifndef BUILD_NO_DEBUG
            addLog(LOG_LEVEL_DEBUG, F("WiFi : Missed connected event"));
            #endif
            WiFiEventData.processedConnect = false;
            processConnect();
          }
          // Apparently we are connected, so no need to process any late disconnect event
          WiFiEventData.processedDisconnect = true;
        }        
        WiFiEventData.setWiFiServicesInitialized();
//#ifdef ESP32
        setWebserverRunning(false);
        delay(1);
        setWebserverRunning(true);
        delay(1);
/*        
#else
        CheckRunningServices();
#endif
*/
      }
    }
  }

  if (WiFiEventData.unprocessedWifiEvents()) {
    // Process disconnect events before connect events.
    if (!WiFiEventData.processedDisconnect) {
      #ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_DEBUG, F("WIFI : Entering processDisconnect()"));
      #endif // ifndef BUILD_NO_DEBUG
      processDisconnect();
    }
  }

  if (active_network_medium == NetworkMedium_t::WIFI) {
    if ((!WiFiEventData.WiFiServicesInitialized()) || WiFiEventData.unprocessedWifiEvents() || WiFiEventData.wifiConnectAttemptNeeded) {
      // WiFi connection is not yet available, so introduce some extra delays to
      // help the background tasks managing wifi connections
      delay(0);
      
      if (!WiFiEventData.processedConnect) {
        #ifndef BUILD_NO_DEBUG
        addLog(LOG_LEVEL_DEBUG, F("WIFI : Entering processConnect()"));
        #endif // ifndef BUILD_NO_DEBUG
        processConnect();
      }

      if (!WiFiEventData.processedGotIP) {
        #ifndef BUILD_NO_DEBUG
        addLog(LOG_LEVEL_DEBUG, F("WIFI : Entering processGotIP()"));
        #endif // ifndef BUILD_NO_DEBUG
        processGotIP();
      }

      if (!WiFiEventData.processedDHCPTimeout) {
        #ifndef BUILD_NO_DEBUG
        addLog(LOG_LEVEL_INFO, F("WIFI : DHCP timeout, Calling disconnect()"));
        #endif // ifndef BUILD_NO_DEBUG
        WiFiEventData.processedDHCPTimeout = true;
        WifiDisconnect();
      }

      if (WiFi.status() == WL_DISCONNECTED && WiFiEventData.wifiConnectInProgress) {
        if (WiFiEventData.last_wifi_connect_attempt_moment.isSet() && 
            WiFiEventData.last_wifi_connect_attempt_moment.timeoutReached(DEFAULT_WIFI_CONNECTION_TIMEOUT)) {
          logConnectionStatus();
          resetWiFi();
        }
        if (!WiFiEventData.last_wifi_connect_attempt_moment.isSet()) {
          WiFiEventData.wifiConnectInProgress = false;
        }
        delay(10);
      }

      if (!WiFiEventData.wifiConnectInProgress) {
        WiFiEventData.wifiConnectAttemptNeeded = true;
        NetworkConnectRelaxed();
      }
    }


    if (WiFiEventData.WiFiDisconnected()) {
      #ifndef BUILD_NO_DEBUG

      if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
        static LongTermTimer lastDisconnectMoment_log;
        static uint8_t lastWiFiStatus_log = 0;
        uint8_t cur_wifi_status = WiFi.status();
        if (WiFiEventData.lastDisconnectMoment.get() != lastDisconnectMoment_log.get() || 
            lastWiFiStatus_log != cur_wifi_status) {
          lastDisconnectMoment_log.set(WiFiEventData.lastDisconnectMoment.get());
          lastWiFiStatus_log = cur_wifi_status;
          String wifilog = F("WIFI : Disconnected: WiFi.status() = ");
          wifilog += WiFiEventData.ESPeasyWifiStatusToString();
          wifilog += F(" RSSI: ");
          wifilog += String(WiFi.RSSI());
          wifilog += F(" status: ");
          #ifdef ESP8266
          station_status_t status = wifi_station_get_connect_status();
          wifilog += SDKwifiStatusToString(status);
          #endif
          #ifdef ESP32
          wifilog += ArduinoWifiStatusToString(WiFi.status());
          #endif
          addLogMove(LOG_LEVEL_DEBUG, wifilog);
        }
      }
      #endif // ifndef BUILD_NO_DEBUG

      // While connecting to WiFi make sure the device has ample time to do so
      delay(10);
    }

    if (!WiFiEventData.processedDisconnectAPmode) { processDisconnectAPmode(); }

    if (!WiFiEventData.processedConnectAPmode) { processConnectAPmode(); }

    if (WiFiEventData.timerAPoff.isSet()) { processDisableAPmode(); }

    if (!WiFiEventData.processedScanDone) { processScanDone(); }

    if (WiFiEventData.wifi_connect_attempt > 0) {
      // We only want to clear this counter if the connection is currently stable.
      if (WiFiEventData.WiFiServicesInitialized()) {
        if (WiFiEventData.lastConnectMoment.isSet() && WiFiEventData.lastConnectMoment.timeoutReached(WIFI_CONNECTION_CONSIDERED_STABLE)) {
          // Connection considered stable
          WiFiEventData.wifi_connect_attempt = 0;
          WiFiEventData.wifi_considered_stable = true;
          WiFi_AP_Candidates.markCurrentConnectionStable();

          if (WiFi.getAutoReconnect() != Settings.SDK_WiFi_autoreconnect()) {
            WiFi.setAutoReconnect(Settings.SDK_WiFi_autoreconnect());
            delay(1);
          }
        } else {
          if (WiFi.getAutoReconnect()) {
            WiFi.setAutoReconnect(false);
            delay(1);
          }
        }
      }
    }
  }
#if FEATURE_ETHERNET
  check_Eth_DNS_valid();
#endif // if FEATURE_ETHERNET

#if FEATURE_ESPEASY_P2P
  updateUDPport(false);
#endif
}

// ********************************************************************************
// Functions to process the data gathered from the events.
// These functions are called from Setup() or Loop() and thus may call delay() or yield()
// ********************************************************************************
void processDisconnect() {
  if (WiFiEventData.processedDisconnect) { return; }

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    String log = strformat(
      F("WIFI : Disconnected! Reason: '%s'"), 
      getLastDisconnectReason().c_str());

    if (WiFiEventData.lastConnectedDuration_us > 0) {
      log += concat(
        F(" Connected for "), 
        format_msec_duration(WiFiEventData.lastConnectedDuration_us / 1000ll));
    }
    addLogMove(LOG_LEVEL_INFO, log);
  }
  logConnectionStatus();

  if (WiFiEventData.processingDisconnect.isSet()) {
    if (WiFiEventData.processingDisconnect.millisPassedSince() > 5000 || WiFiEventData.processedDisconnect) {
      WiFiEventData.processedDisconnect = true;
      WiFiEventData.processingDisconnect.clear();
    }
  }


  if (WiFiEventData.processedDisconnect || 
      WiFiEventData.processingDisconnect.isSet()) { return; }
  WiFiEventData.processingDisconnect.setNow();
  WiFiEventData.setWiFiDisconnected();
  WiFiEventData.wifiConnectAttemptNeeded = true;
  delay(100); // FIXME TD-er: See https://github.com/letscontrolit/ESPEasy/issues/1987#issuecomment-451644424

  if (Settings.UseRules) {
    eventQueue.add(F("WiFi#Disconnected"));
  }


  // FIXME TD-er: With AutoReconnect enabled, WiFi must be reset or else we completely loose track of the actual WiFi state
  bool mustRestartWiFi = Settings.WiFiRestart_connection_lost() || WiFi.getAutoReconnect();
  if (WiFiEventData.lastConnectedDuration_us > 0 && (WiFiEventData.lastConnectedDuration_us / 1000) < 5000) {
    if (!WiFi_AP_Candidates.getBestCandidate().usable())
//      addLog(LOG_LEVEL_INFO, F("WIFI : !getBestCandidate().usable()  => mustRestartWiFi = true"));

      mustRestartWiFi = true;
  }
  
  if (WiFi.status() == WL_IDLE_STATUS) {
//    addLog(LOG_LEVEL_INFO, F("WIFI : WiFi.status() == WL_IDLE_STATUS  => mustRestartWiFi = true"));
    mustRestartWiFi = true;
  }


  #ifdef USES_ESPEASY_NOW
  if (use_EspEasy_now) {
//    mustRestartWiFi = true;
  }
  #endif
  //WifiDisconnect(); // Needed or else node may not reconnect reliably.

  if (mustRestartWiFi) {
    WiFiEventData.processedDisconnect = true;
    resetWiFi();
//    WifiScan(false);
//    delay(100);
//    setWifiMode(WIFI_OFF);
//    initWiFi();
//    delay(100);
  }
//  delay(500);
  logConnectionStatus();
  WiFiEventData.processedDisconnect = true;
  WiFiEventData.processingDisconnect.clear();
}

void processConnect() {
  if (WiFiEventData.processedConnect) { return; }
  //delay(100); // FIXME TD-er: See https://github.com/letscontrolit/ESPEasy/issues/1987#issuecomment-451644424
  if (checkAndResetWiFi()) {
    return;
  }
  WiFiEventData.processedConnect = true;
  if (WiFi.status() == WL_DISCONNECTED) {
    // Apparently not really connected
    return;
  }

  WiFiEventData.setWiFiConnected();
  ++WiFiEventData.wifi_reconnects;

  if (WiFi_AP_Candidates.getCurrent().bits.isEmergencyFallback) {
    #ifdef CUSTOM_EMERGENCY_FALLBACK_RESET_CREDENTIALS
    const bool mustResetCredentials = CUSTOM_EMERGENCY_FALLBACK_RESET_CREDENTIALS;
    #else
    const bool mustResetCredentials = false;
    #endif
    #ifdef CUSTOM_EMERGENCY_FALLBACK_START_AP
    const bool mustStartAP = CUSTOM_EMERGENCY_FALLBACK_START_AP;
    #else
    const bool mustStartAP = false;
    #endif
    if (mustStartAP) {
      int allowedUptimeMinutes = 10;
      #ifdef CUSTOM_EMERGENCY_FALLBACK_ALLOW_MINUTES_UPTIME
      allowedUptimeMinutes = CUSTOM_EMERGENCY_FALLBACK_ALLOW_MINUTES_UPTIME;
      #endif
      if (getUptimeMinutes() < allowedUptimeMinutes) {
        WiFiEventData.timerAPstart.setNow();
      }
    }
    if (mustResetCredentials && !WiFiEventData.performedClearWiFiCredentials) {
      WiFiEventData.performedClearWiFiCredentials = true;
      SecuritySettings.clearWiFiCredentials();
      SaveSecuritySettings();
      WiFiEventData.markDisconnect(WIFI_DISCONNECT_REASON_AUTH_EXPIRE);
      WiFi_AP_Candidates.force_reload();
    }
  }

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    const LongTermTimer::Duration connect_duration = WiFiEventData.last_wifi_connect_attempt_moment.timeDiff(WiFiEventData.lastConnectMoment);
    String log = strformat(
      F("WIFI : Connected! AP: %s (%s) Ch: %d"),
      WiFi.SSID().c_str(),
      WiFi.BSSIDstr().c_str(),
      RTC.lastWiFiChannel);

    if ((connect_duration > 0ll) && (connect_duration < 30000000ll)) {
      // Just log times when they make sense.
      log += strformat(
        F(" Duration: %d ms"),
        static_cast<int32_t>(connect_duration / 1000));
    }
    addLogMove(LOG_LEVEL_INFO, log);
  }

//  WiFiEventData.last_wifi_connect_attempt_moment.clear();

  if (Settings.UseRules) {
    if (WiFiEventData.bssid_changed) {
      eventQueue.add(F("WiFi#ChangedAccesspoint"));
    }

    if (WiFiEventData.channel_changed) {
      eventQueue.add(F("WiFi#ChangedWiFichannel"));
    }
  } 

  if (useStaticIP()) {
    WiFiEventData.markGotIP(); // in static IP config the got IP event is never fired.
  }
  saveToRTC();

  logConnectionStatus();
}

void processGotIP() {
  if (WiFiEventData.processedGotIP) {
    return;
  }
  if (checkAndResetWiFi()) {
    return;
  }

  IPAddress ip = NetworkLocalIP();

  if (!useStaticIP()) {
    #ifdef ESP8266
    if (!ip.isSet()) {
    #else
    if (ip[0] == 0 && ip[1] == 0 && ip[2] == 0 && ip[3] == 0) {
    #endif
      return;
    }
  }
  const IPAddress gw       = WiFi.gatewayIP();
  const IPAddress subnet   = WiFi.subnetMask();
  const LongTermTimer::Duration dhcp_duration = WiFiEventData.lastConnectMoment.timeDiff(WiFiEventData.lastGetIPmoment);
  WiFiEventData.dns0_cache = WiFi.dnsIP(0);
  WiFiEventData.dns1_cache = WiFi.dnsIP(1);

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    String log = strformat(
      F("WIFI : %s (%s) GW: %s SN: %s DNS: %s"),
      concat(useStaticIP() ? F("Static IP: ") : F("DHCP IP: "), formatIP(ip)).c_str(),
      NetworkGetHostname().c_str(),
      formatIP(gw).c_str(),
      formatIP(subnet).c_str(),
      getValue(LabelType::DNS).c_str());

    if ((dhcp_duration > 0ll) && (dhcp_duration < 30000000ll)) {
      // Just log times when they make sense.
      log += strformat(F("   duration: %d ms"), static_cast<int32_t>(dhcp_duration / 1000));
    }
    addLogMove(LOG_LEVEL_INFO, log);
  }

  // Might not work in core 2.5.0
  // See https://github.com/esp8266/Arduino/issues/5839
  if ((Settings.IP_Octet != 0) && (Settings.IP_Octet != 255))
  {
    ip[3] = Settings.IP_Octet;

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, concat(F("IP   : Fixed IP octet:"), formatIP(ip)));
    }
    WiFi.config(ip, gw, subnet, WiFiEventData.dns0_cache, WiFiEventData.dns1_cache);
  }

#if FEATURE_MQTT
  mqtt_reconnect_count        = 0;
  MQTTclient_should_reconnect = true;
  timermqtt_interval          = 100;
  Scheduler.setIntervalTimer(SchedulerIntervalTimer_e::TIMER_MQTT);
  scheduleNextMQTTdelayQueue();
#endif // if FEATURE_MQTT
  Scheduler.sendGratuitousARP_now();

  if (Settings.UseRules)
  {
    eventQueue.add(F("WiFi#Connected"));
  }
  statusLED(true);

  //  WiFi.scanDelete();

  if (WiFiEventData.wifiSetup) {
    // Wifi setup was active, Apparently these settings work.
    WiFiEventData.wifiSetup = false;
    SaveSecuritySettings();
  }

  if ((WiFiEventData.WiFiConnected() || WiFi.isConnected()) && hasIPaddr()) {
    WiFiEventData.setWiFiGotIP();
  }
  #if FEATURE_ESPEASY_P2P
  refreshNodeList();
  #endif
  logConnectionStatus();
}

#if FEATURE_USE_IPV6
void processGotIPv6() {
  if (!WiFiEventData.processedGotIP6) {
    WiFiEventData.processedGotIP6 = true;
    if (loglevelActiveFor(LOG_LEVEL_INFO))
      addLog(LOG_LEVEL_INFO, String(F("WIFI : STA got IP6 ")) + WiFiEventData.unprocessed_IP6.toString(true));
#if FEATURE_ESPEASY_P2P
//    updateUDPport(true);
#endif
  }
}
#endif

// A client disconnected from the AP on this node.
void processDisconnectAPmode() {
  if (WiFiEventData.processedDisconnectAPmode) { return; }
  WiFiEventData.processedDisconnectAPmode = true;

#ifndef BUILD_NO_DEBUG
  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    const int nrStationsConnected = WiFi.softAPgetStationNum();
    String    log                 = F("AP Mode: Client disconnected: ");
    log += WiFiEventData.lastMacDisconnectedAPmode.toString();
    log += F(" Connected devices: ");
    log += nrStationsConnected;
    addLogMove(LOG_LEVEL_INFO, log);
  }
#endif  
}

// Client connects to AP on this node
void processConnectAPmode() {
  if (WiFiEventData.processedConnectAPmode) { return; }
  WiFiEventData.processedConnectAPmode = true;
  // Extend timer to switch off AP.
  WiFiEventData.timerAPoff.setMillisFromNow(WIFI_AP_OFF_TIMER_DURATION);
#ifndef BUILD_NO_DEBUG
  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    String log = F("AP Mode: Client connected: ");
    log += WiFiEventData.lastMacConnectedAPmode.toString();
    log += F(" Connected devices: ");
    log += WiFi.softAPgetStationNum();
    addLogMove(LOG_LEVEL_INFO, log);
  }
#endif

  #if FEATURE_DNS_SERVER
  // Start DNS, only used if the ESP has no valid WiFi config
  // It will reply with it's own address on all DNS requests
  // (captive portal concept)
  if (!dnsServerActive) {
    dnsServerActive = true;
    dnsServer.start(DNS_PORT, "*", apIP);
  }
  #endif // if FEATURE_DNS_SERVER
}

// Switch of AP mode when timeout reached and no client connected anymore.
void processDisableAPmode() {
  if (!WiFiEventData.timerAPoff.isSet()) { return; }

  if (!WifiIsAP(WiFi.getMode())) {
    return;
  }
  // disable AP after timeout and no clients connected.
  if (WiFiEventData.timerAPoff.timeReached() && (WiFi.softAPgetStationNum() == 0)) {
    setAP(false);
  }

  if (!WifiIsAP(WiFi.getMode())) {
    WiFiEventData.timerAPoff.clear();
    if (WiFiEventData.wifiConnectAttemptNeeded) {
      // Force a reconnect cycle
      WifiDisconnect();
    }
  }
}

void processScanDone() {
  WiFi_AP_Candidates.load_knownCredentials();
  if (WiFiEventData.processedScanDone) { return; }



  // Better act on the scan done event, as it may get triggered for normal wifi begin calls.
  int8_t scanCompleteStatus = WiFi.scanComplete();
  switch (scanCompleteStatus) {
    case 0: // Nothing (yet) found
      if (WiFiEventData.lastGetScanMoment.timeoutReached(5000)) {
        WiFi.scanDelete();
        WiFiEventData.processedScanDone = true;
      }
      return;
    case -1: // WIFI_SCAN_RUNNING
      // FIXME TD-er: Set timeout...
      if (WiFiEventData.lastGetScanMoment.timeoutReached(5000)) {
        # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_ERROR, F("WiFi : Scan Running Timeout"));
      #endif
        WiFi.scanDelete();
        WiFiEventData.processedScanDone = true;
      }
      return;
    case -2: // WIFI_SCAN_FAILED
      addLog(LOG_LEVEL_ERROR, F("WiFi : Scan failed"));
      WiFi.scanDelete();
      WiFiEventData.processedScanDone = true;
      return;
  }

  WiFiEventData.lastGetScanMoment.setNow();
  WiFiEventData.processedScanDone = true;
# ifndef BUILD_NO_DEBUG
  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO, concat(F("WiFi : Scan finished, found: "), scanCompleteStatus));
  }
#endif

#if !FEATURE_ESP8266_DIRECT_WIFI_SCAN
  WiFi_AP_Candidates.process_WiFiscan(scanCompleteStatus);
#endif
  WiFi_AP_Candidates.load_knownCredentials();

  if (WiFi_AP_Candidates.addedKnownCandidate() && !NetworkConnected()) {
    if (!WiFiEventData.wifiConnectInProgress) {
      WiFiEventData.wifiConnectAttemptNeeded = true;
      # ifndef BUILD_NO_DEBUG
      if (WiFi_AP_Candidates.addedKnownCandidate()) {
        addLog(LOG_LEVEL_INFO, F("WiFi : Added known candidate, try to connect"));
      }
      #endif
#ifdef ESP32
//      setSTA(false);
#endif
      NetworkConnectRelaxed();
#ifdef USES_ESPEASY_NOW
      temp_disable_EspEasy_now_timer = millis() + 20000;
#endif
    }
  } else if (!WiFiEventData.wifiConnectInProgress && !NetworkConnected()) {
    WiFiEventData.timerAPstart.setNow();
  }

}





#include "../ESPEasyCore/ESPEasyRules.h"

#include "../../_Plugin_Helper.h"

#include "../Commands/ExecuteCommand.h"
#include "../DataStructs/TimingStats.h"
#include "../DataTypes/EventValueSource.h"
#include "../ESPEasyCore/ESPEasy_backgroundtasks.h"
#include "../ESPEasyCore/Serial.h"
#include "../Globals/Cache.h"
#include "../Globals/Device.h"
#include "../Globals/EventQueue.h"
#include "../Globals/Plugins.h"
#include "../Globals/Plugins_other.h"
#include "../Globals/RulesCalculate.h"
#include "../Globals/Settings.h"
#include "../Helpers/CRC_functions.h"
#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/ESPEasy_time_calc.h"
#include "../Helpers/FS_Helper.h"
#include "../Helpers/Misc.h"
#include "../Helpers/Numerical.h"
#include "../Helpers/RulesHelper.h"
#include "../Helpers/RulesMatcher.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringParser.h"


#include <math.h>
#include <vector>

#ifdef WEBSERVER_NEW_RULES
String EventToFileName(const String& eventName) {
  int size  = eventName.length();
  int index = eventName.indexOf('=');

  if (index > -1) {
    size = index;
  }
#if defined(ESP8266)
  String fileName = F("rules/");
#endif // if defined(ESP8266)
#if defined(ESP32)
  String fileName = F("/rules/");
#endif // if defined(ESP32)
  fileName += eventName.substring(0, size);
  fileName.replace('#', RULE_FILE_SEPARAROR);
  fileName.toLowerCase();
  return fileName;
}

String FileNameToEvent(const String& fileName) {
#if defined(ESP8266)
  String eventName = fileName.substring(6);
#endif // if defined(ESP8266)
#if defined(ESP32)
  String eventName = fileName.substring(7);
#endif // if defined(ESP32)
  eventName.replace(RULE_FILE_SEPARAROR, '#');
  return eventName;
}
#endif

void checkRuleSets() {
  Cache.rulesHelper.closeAllFiles();
}

/********************************************************************************************\
   Process next event from event queue
 \*********************************************************************************************/
bool processNextEvent() {
  if (Settings.UseRules)
  {
    String nextEvent;

    if (eventQueue.getNext(nextEvent)) {
      rulesProcessing(nextEvent);
      return true;
    }
  }

  // Just make sure any (accidentally) added or remaining events are not kept.
  eventQueue.clear();
  return false;
}

/********************************************************************************************\
   Rules processing
 \*********************************************************************************************/
void rulesProcessing(const String& event) {
  if (!Settings.UseRules) {
    return;
  }
  START_TIMER
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("rulesProcessing"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
#ifndef BUILD_NO_DEBUG
  const unsigned long timer = millis();
#endif // ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO, concat(F("EVENT: "), event));
  }

  if (Settings.OldRulesEngine()) {
    bool eventHandled = false;

    if (Settings.EnableRulesCaching()) {
      String filename;
      size_t pos = 0;
      if (Cache.rulesHelper.findMatchingRule(event, filename, pos)) {
        const bool startOnMatched = true; // We already matched the event
        eventHandled = rulesProcessingFile(filename, event, pos, startOnMatched);
      }
    } else {
      for (uint8_t x = 0; x < RULESETS_MAX && !eventHandled; x++) {
        eventHandled = rulesProcessingFile(getRulesFileName(x), event);
      }
    }
  } else {
    #ifdef WEBSERVER_NEW_RULES
    String fileName = EventToFileName(event);

    // if exists processed the rule file
    if (fileExists(fileName)) {
      rulesProcessingFile(fileName, event);
    }
    # ifndef BUILD_NO_DEBUG
    else {
      addLog(LOG_LEVEL_DEBUG, strformat(F("EVENT: %s is ingnored. File %s not found."),
             event.c_str(), fileName.c_str()));
    }
    # endif    // ifndef BUILD_NO_DEBUG
    #endif // WEBSERVER_NEW_RULES
  }

#ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLogMove(LOG_LEVEL_DEBUG, strformat(F("EVENT: %s Processing: %d ms"), event.c_str(), timePassedSince(timer)));
  }
#endif // ifndef BUILD_NO_DEBUG
  STOP_TIMER(RULES_PROCESSING);
  backgroundtasks();
}

/********************************************************************************************\
   Rules processing
 \*********************************************************************************************/
bool rulesProcessingFile(const String& fileName, 
                         const String& event, 
                         size_t pos,
                         bool   startOnMatched) {
  if (!Settings.UseRules || !fileExists(fileName)) {
    return false;
  }
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("rulesProcessingFile"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
#ifndef BUILD_NO_DEBUG

  if (Settings.SerialLogLevel == LOG_LEVEL_DEBUG_DEV) {
    serialPrint(F("RuleDebug Processing:"));
    serialPrintln(fileName);
    serialPrintln(F("     flags CMI  parse output:"));
  }
#endif // ifndef BUILD_NO_DEBUG

  static uint8_t nestingLevel = 0;

  nestingLevel++;

  if (nestingLevel > RULES_MAX_NESTING_LEVEL) {
    addLog(LOG_LEVEL_ERROR, F("EVENT: Error: Nesting level exceeded!"));
    nestingLevel--;
    return false;
  }


  bool match     = false;
  bool codeBlock = false;
  bool isCommand = false;
  bool condition[RULES_IF_MAX_NESTING_LEVEL];
  bool ifBranche[RULES_IF_MAX_NESTING_LEVEL];
  uint8_t ifBlock     = 0;
  uint8_t fakeIfBlock = 0;


  bool moreAvailable = true;
  bool eventHandled = false;
  while (moreAvailable && !eventHandled) {
    const bool searchNextOnBlock = !codeBlock && !match;
    String line = Cache.rulesHelper.readLn(fileName, pos, moreAvailable, searchNextOnBlock);

    // Parse the line and extract the action (if there is any)
    String action;
    {
      START_TIMER
      const bool matched_before_parse = match;
      bool isOneLiner = false;
      parseCompleteNonCommentLine(line, event, action, match, codeBlock,
                                  isCommand, isOneLiner, condition, ifBranche, ifBlock,
                                  fakeIfBlock, startOnMatched);
      if ((matched_before_parse && !match) || isOneLiner) {
        // We were processing a matching event and now crossed the "endon"
        // Or just dealing with a oneliner.
        // So we're done processing
        eventHandled = true;
        backgroundtasks();
      }
      STOP_TIMER(RULES_PARSE_LINE);
    }

    if (match) // rule matched for one action or a block of actions
    {
      START_TIMER
      processMatchedRule(action, event,
                         isCommand, condition,
                         ifBranche, ifBlock, fakeIfBlock);
      STOP_TIMER(RULES_PROCESS_MATCHED);
    }
  }

/*
  if (f) {
    f.close();
  }
*/

  nestingLevel--;
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("rulesProcessingFile2"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  backgroundtasks();
  return eventHandled; // && nestingLevel == 0;
}


/********************************************************************************************\
   Parse string commands
 \*********************************************************************************************/
bool get_next_inner_bracket(const String& line, unsigned int& startIndex, int& closingIndex, char closingBracket)
{
  if (line.length() <= 1) {
    // Not possible to have opening and closing bracket on a line this short.
    return false;
  }
  char openingBracket = closingBracket;

  switch (closingBracket) {
    case ']': openingBracket = '['; break;
    case '}': openingBracket = '{'; break;
    case ')': openingBracket = '('; break;
    default:
      // unknown bracket type
      return false;
  }
  // Closing bracket should not be found on the first position.
  closingIndex = line.indexOf(closingBracket, startIndex + 1);

  if (closingIndex == -1) { 
    // not found
    return false; 
  }

  for (int i = (closingIndex - 1); (i >= static_cast<int>(startIndex)) && (i >= 0); --i) {
    if (line[i] == openingBracket) {
      startIndex = i;
      return true;
    }
  }
  return false;
}

bool get_next_argument(const String& fullCommand, int& index, String& argument, char separator)
{
  if (index == -1) {
    return false;
  }
  int newIndex = fullCommand.indexOf(separator, index);

  if (newIndex == -1) {
    argument = fullCommand.substring(index);
  } else {
    argument = fullCommand.substring(index, newIndex);
  }

  if (argument.startsWith(String(separator))) {
    argument = argument.substring(1);
  }

  //  addLog(LOG_LEVEL_INFO, String("get_next_argument: ") + String(index) + " " + fullCommand + " " + argument);
  index = newIndex;

  if (index != -1) {
    ++index;
  }
  return argument.length() > 0;
}

const char bitwise_functions[] PROGMEM = "bitread|bitset|bitclear|bitwrite|xor|and|or";
enum class bitwise_functions_e {
  bitread,
  bitset,
  bitclear,
  bitwrite,
  xor_e,  // protected keywords, thus appended _e
  and_e,
  or_e
};


bool parse_bitwise_functions(const String& cmd_s_lower, const String& arg1, const String& arg2, const String& arg3, int64_t& result) {
  #ifndef BUILD_NO_DEBUG
  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    String log = F("Bitwise: {");
    log += wrapIfContains(cmd_s_lower, ':', '\"');
    log += ':';
    log += wrapIfContains(arg1, ':', '\"');

    if (arg2.length() > 0) {
      log += ':';
      log += wrapIfContains(arg2, ':', '\"');

      if (arg3.length() > 0) {
        log += ':';
        log += wrapIfContains(arg3, ':', '\"');
      }
    }
    log += '}';
    addLogMove(LOG_LEVEL_DEBUG, log);
  }
  #endif

  if (cmd_s_lower.length() < 2) {
    return false;
  }

  int command_i = GetCommandCode(cmd_s_lower.c_str(), bitwise_functions);
  if (command_i == -1) {
    // No matching function found
    return false;
  }
  
  if (cmd_s_lower.startsWith(F("bit"))) {
    uint32_t bitnr = 0;
    uint64_t iarg2 = 0;

    if (!validUIntFromString(arg1, bitnr) || !validUInt64FromString(arg2, iarg2)) {
      return false;
    }

    switch(static_cast<bitwise_functions_e>(command_i)) {
      case bitwise_functions_e::bitread:
        // Syntax like {bitread:0:123} to get a single decimal '1'
        result = bitRead(iarg2, bitnr);
        break;
      case bitwise_functions_e::bitset:
        // Syntax like {bitset:0:122} to set least significant bit of the given nr '122' to '1' => '123'
        result = iarg2;
        bitSetULL(result, bitnr);
        break;
      case bitwise_functions_e::bitclear:
        // Syntax like {bitclear:0:123} to set least significant bit of the given nr '123' to '0' => '122'
        result = iarg2;
        bitClearULL(result, bitnr);
        break;
      case bitwise_functions_e::bitwrite:
      {
        uint32_t iarg3 = 0;
        // Syntax like {bitwrite:0:122:1} to set least significant bit of the given nr '122' to '1' => '123'
        if (validUIntFromString(arg3, iarg3)) {
          const int bitvalue = (iarg3 & 1); // Only use the last bit of the given parameter
          result = iarg2;
          bitWriteULL(result, bitnr, bitvalue);
        } else {
          // Need 3 parameters, but 3rd one is not a valid uint
          return false;
        }
        break;
      }
      default: 
        return false;
    }

    // all functions starting with "bit" are checked
    return true;
  }

  uint64_t iarg1, iarg2 = 0;

  if (!validUInt64FromString(arg1, iarg1) || !validUInt64FromString(arg2, iarg2)) {
    return false;
  }

  switch(static_cast<bitwise_functions_e>(command_i)) {
    case bitwise_functions_e::xor_e:
      // Syntax like {xor:127:15} to XOR the binary values 1111111 and 1111 => 1110000
      result = iarg1 ^ iarg2;
      break;
    case bitwise_functions_e::and_e:
      // Syntax like {and:254:15} to AND the binary values 11111110 and 1111 => 1110
      result = iarg1 & iarg2;
      break;
    case bitwise_functions_e::or_e:
      // Syntax like {or:254:15} to OR the binary values 11111110 and 1111 => 11111111
      result = iarg1 | iarg2;
      break;
    default: 
      return false;

  }
  return true;
}

bool parse_math_functions(const String& cmd_s_lower, const String& arg1, const String& arg2, const String& arg3, ESPEASY_RULES_FLOAT_TYPE& result) {
  ESPEASY_RULES_FLOAT_TYPE farg1;
  float  farg2, farg3 = 0.0f;

  if (!cmd_s_lower.startsWith("crc") && !validDoubleFromString(arg1, farg1)) {
    return false;
  }

  if (equals(cmd_s_lower, F("constrain"))) {
    // Contrain a value X to be within range of A to B
    // Syntax like {constrain:x:a:b} to constrain x in range a...b
    if (validFloatFromString(arg2, farg2) && validFloatFromString(arg3, farg3)) {
      if (farg2 > farg3) {
        const float tmp = farg2;
        farg2 = farg3;
        farg3 = tmp;
      }
      result = constrain(farg1, farg2, farg3);
    } else {
      return false;
    }
  } else if (cmd_s_lower.startsWith("crc")) {
    std::vector<uint8_t> argument = parseHexTextData(arg1, 1);
    const String crctype          = cmd_s_lower.substring(3);

    if (argument.size() > 0) {
      if (equals(crctype, F("8"))) {
        result = calc_CRC8(&argument[0], argument.size());
      // } else if (equals(crctype, F("16"))) { // FIXME crc16 not supported until needed/used/tested
      //   result = calc_CRC16((const char *)argument.data(), argument.size());
      } else if (equals(crctype, F("32"))) {
        result = calc_CRC32(&argument[0], argument.size());
      } else {
        return false;
      }

      if (!arg2.isEmpty() && validDoubleFromString(arg2, farg1)) { // Optional expected crc value
        result = essentiallyEqual(result, farg1) ? 1.0 : 0.0; // Return 1 if the calculated crc == expected crc
      }
    } else {
      return false;
    }
  } else {
    // No matching function found
    return false;
  }
  return true;
}

const char string_commands[] PROGMEM = "substring|indexof|indexof_ci|equals|equals_ci|timetomin|timetosec|strtol|tobin|tohex|ord|urlencode"
  #if FEATURE_STRING_VARIABLES
  "|lookup"
  #endif // if FEATURE_STRING_VARIABLES
  ;
enum class string_commands_e {
  substring,
  indexof,
  indexof_ci,
  equals,
  equals_ci,
  timetomin,
  timetosec,
  strtol,
  tobin,
  tohex,
  ord,
  urlencode,
  #if FEATURE_STRING_VARIABLES
  lookup,
  #endif // if FEATURE_STRING_VARIABLES
};


void parse_string_commands(String& line) {
  unsigned int startIndex = 0;
  int closingIndex;

  bool mustReplaceMaskedChars = false;
  bool mustReplaceEscapedBracket = false;
  bool mustReplaceEscapedCurlyBracket = false;
  String MaskEscapedBracket;

  if (hasEscapedCharacter(line,'(') || hasEscapedCharacter(line,')')) {
    // replace the \( and \) with other characters to mask the escaped brackets so we can continue parsing.
    // We have to unmask then after we're finished.
    MaskEscapedBracket = static_cast<char>(0x11); // ASCII 0x11 = Device control 1
    line.replace(F("\\("), MaskEscapedBracket);
    MaskEscapedBracket = static_cast<char>(0x12); // ASCII 0x12 = Device control 2
    line.replace(F("\\)"), MaskEscapedBracket);
    mustReplaceEscapedBracket = true;
  }
  if (hasEscapedCharacter(line,'{') || hasEscapedCharacter(line,'}')) {
    // replace the \{ and \} with other characters to mask the escaped curly brackets so we can continue parsing.
    // We have to unmask then after we're finished.
    MaskEscapedBracket = static_cast<char>(0x13); // ASCII 0x13 = Device control 3
    line.replace(F("\\{"), MaskEscapedBracket);
    MaskEscapedBracket = static_cast<char>(0x14); // ASCII 0x14 = Device control 4
    line.replace(F("\\}"), MaskEscapedBracket);
    mustReplaceEscapedCurlyBracket = true;
  }

  while (get_next_inner_bracket(line, startIndex, closingIndex, '}')) {
    // Command without opening and closing brackets.
    const String fullCommand = line.substring(startIndex + 1, closingIndex);
    const String cmd_s_lower = parseString(fullCommand, 1, ':');
    const String arg1        = parseStringKeepCaseNoTrim(fullCommand, 2, ':');
    const String arg2        = parseStringKeepCaseNoTrim(fullCommand, 3, ':');
    const String arg3        = parseStringKeepCaseNoTrim(fullCommand, 4, ':');

    if (cmd_s_lower.length() > 0) {
      String replacement; // maybe just replace with empty to avoid looping?
      uint64_t iarg1, iarg2 = 0;
      ESPEASY_RULES_FLOAT_TYPE fresult{};
      int64_t  iresult = 0;
      int32_t startpos, endpos = -1;
      const bool arg1valid = validIntFromString(arg1, startpos);
      const bool arg2valid = validIntFromString(arg2, endpos);

      if (parse_math_functions(cmd_s_lower, arg1, arg2, arg3, fresult)) {
        const bool trimTrailingZeros = true;
        #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
        replacement = doubleToString(fresult, maxNrDecimals_fpType(fresult), trimTrailingZeros);
        #else
        replacement = floatToString(fresult, maxNrDecimals_fpType(fresult), trimTrailingZeros);
        #endif
      } else if (parse_bitwise_functions(cmd_s_lower, arg1, arg2, arg3, iresult)) {
        replacement = ull2String(iresult);
      } else {

        int command_i = GetCommandCode(cmd_s_lower.c_str(), string_commands);
        if (command_i != -1) {
          const string_commands_e command = static_cast<string_commands_e>(command_i);

              //  addLog(LOG_LEVEL_INFO, strformat(F("parse_string_commands cmd: %s %s %s %s"), 
              // cmd_s_lower.c_str(), arg1.c_str(), arg2.c_str(), arg3.c_str()));

          switch (command) {
            case string_commands_e::substring:
              // substring arduino style (first char included, last char excluded)
              // Syntax like 12345{substring:8:12:ANOTHER HELLO WORLD}67890

              if (arg1valid) {
                if (arg2valid){
                  replacement = arg3.substring(startpos, endpos);
                } else {
                  replacement = arg3.substring(startpos);
                }
              }
              break;
            #if FEATURE_STRING_VARIABLES
            case string_commands_e::lookup:
              if (arg1valid && arg2valid && startpos > -1 && endpos > -1) {
                replacement = arg3.substring(startpos * endpos, (startpos + 1) * endpos);
              }
              break;
            #endif // if FEATURE_STRING_VARIABLES
            case string_commands_e::indexof:
            case string_commands_e::indexof_ci:
              // indexOf arduino style (0-based position of first char returned, -1 if not found, case sensitive), 3rd argument is search-offset
              // indexOf_ci : case-insensitive
              // Syntax like {indexof:HELLO:"ANOTHER HELLO WORLD"} => 8, {indexof:hello:"ANOTHER HELLO WORLD"} => -1, {indexof_ci:Hello:"ANOTHER HELLO WORLD"} => 8
              // or like {indexof_ci:hello:"ANOTHER HELLO WORLD":10} => -1

              if (!arg1.isEmpty()
                  && !arg2.isEmpty()) {
                uint32_t offset = 0;
                validUIntFromString(arg3, offset);
                if (command == string_commands_e::indexof_ci) {
                  String arg1copy(arg1);
                  String arg2copy(arg2);
                  arg1copy.toLowerCase();
                  arg2copy.toLowerCase();
                  replacement = arg2copy.indexOf(arg1copy, offset);
                } else {
                  replacement = arg2.indexOf(arg1, offset);
                }
              }
              break;
            case string_commands_e::equals:
            case string_commands_e::equals_ci:
              // equals: compare strings 1 = equal, 0 = unequal (case sensitive)
              // equals_ci: case-insensitive compare
              // Syntax like {equals:HELLO:HELLO} => 1, {equals:hello:HELLO} => 0, {equals_ci:hello:HELLO} => 1, {equals_ci:hello:BLA} => 0

              if (!arg1.isEmpty()
                  && !arg2.isEmpty()) {
                if (command == string_commands_e::equals_ci) {
                  replacement = arg2.equalsIgnoreCase(arg1);
                } else {
                  replacement = arg2.equals(arg1);
                }
              }
              break;
            case string_commands_e::timetomin:
            case string_commands_e::timetosec:
              // time to minutes, transform a substring hh:mm to minutes
              // time to seconds, transform a substring hh:mm:ss to seconds
              // syntax similar to substring

              if (arg1valid
                  && arg2valid) {
                int timeSeconds = 0;
                String timeString;
                if(timeStringToSeconds(arg3.substring(startpos, endpos), timeSeconds, timeString)) {
                  if (command == string_commands_e::timetosec) {
                    replacement = timeSeconds;
                  } else { // timetomin
                    replacement = timeSeconds / 60;
                  }
                }
              }
              break;
            case string_commands_e::strtol:
              // string to long integer (from cstdlib)
              // Syntax like 1234{strtol:16:38}7890
              if (validUInt64FromString(arg1, iarg1)
                  && validUInt64FromString(arg2, iarg2)) {
                replacement = String(strtoul(arg2.c_str(), nullptr, iarg1));
              }
              break;
            case string_commands_e::tobin:
              // Convert to binary string
              // Syntax like 1234{tobin:15}7890
              if (validUInt64FromString(arg1, iarg1)) {
                replacement = ull2String(iarg1, BIN);
              }
              break;
            case string_commands_e::tohex:
              // Convert to HEX string
              // Syntax like 1234{tohex:15[,minHexDigits]}7890
              if (validUInt64FromString(arg1, iarg1)) {
                if (!validUInt64FromString(arg2, iarg2)) {
                  iarg2 = 0;
                }
                replacement = formatToHex_no_prefix(iarg1, iarg2);
              }
              break;
            case string_commands_e::ord:
              {
                // Give the ordinal/integer value of the first character of a string
                // Syntax like let 1,{ord:B}
                uint8_t uval = arg1.c_str()[0];
                replacement = String(uval);
              }
              break;
            case string_commands_e::urlencode:
              // Convert to url-encoded string
              // Syntax like {urlencode:"string to/encode"}
              if (!arg1.isEmpty()) {
                replacement = URLEncode(arg1);
              }
              break;
          }
        }
      }

      if (replacement.isEmpty()) {
        // part in braces is not a supported command.
        // replace the {} with other characters to mask the braces so we can continue parsing.
        // We have to unmask then after we're finished.
        // See: https://github.com/letscontrolit/ESPEasy/issues/2932#issuecomment-596139096
        replacement = line.substring(startIndex, closingIndex + 1);
        replacement.replace('{', static_cast<char>(0x02));
        replacement.replace('}', static_cast<char>(0x03));
        mustReplaceMaskedChars = true;
      }

      // Replace the full command including opening and closing brackets.
      line.replace(line.substring(startIndex, closingIndex + 1), replacement);

      /*
         if (replacement.length() > 0) {
         addLog(LOG_LEVEL_INFO, strformat(F("parse_string_commands cmd: %s -> %s"), fullCommand.c_str(), replacement.c_str());
         }
       */
    }
  }

  if (mustReplaceMaskedChars) {
    // We now have to check if we did mask some parts and unmask them.
    // Let's hope we don't mess up any Unicode here.
    line.replace(static_cast<char>(0x02), '{');
    line.replace(static_cast<char>(0x03), '}');
  }

  if (mustReplaceEscapedBracket) {
    // We now have to check if we did mask some escaped bracket and unmask them.
    // Let's hope we don't mess up any Unicode here.
    MaskEscapedBracket = static_cast<char>(0x11); // ASCII 0x11 = Device control 1
    line.replace(MaskEscapedBracket, F("\\("));
    MaskEscapedBracket = static_cast<char>(0x12); // ASCII 0x12 = Device control 2
    line.replace(MaskEscapedBracket, F("\\)"));
  }

  if (mustReplaceEscapedCurlyBracket) {
    // We now have to check if we did mask some escaped curly bracket and unmask them.
    // Let's hope we don't mess up any Unicode here.
    MaskEscapedBracket = static_cast<char>(0x13); // ASCII 0x13 = Device control 3
    line.replace(MaskEscapedBracket, F("\\{"));
    MaskEscapedBracket = static_cast<char>(0x14); // ASCII 0x14 = Device control 4
    line.replace(MaskEscapedBracket, F("\\}"));
  }
}

void substitute_eventvalue(String& line, const String& event) {
  if (substitute_eventvalue_CallBack_ptr != nullptr) {
    substitute_eventvalue_CallBack_ptr(line, event);
  }

  if (line.indexOf(F("%event")) != -1) {
    const int equalsPos = event.indexOf('=');

    if (event.charAt(0) == '!') {
      line.replace(F("%eventvalue%"), event); // substitute %eventvalue% with
                                              // literal event string if
                                              // starting with '!'
    } else {

      String argString;

      if (equalsPos > 0) {
        argString = event.substring(equalsPos + 1);
      }

      // Replace %eventvalueX% with the actual value of the event.
      // For compatibility reasons also replace %eventvalue%  (argc = 0)
      line.replace(F("%eventvalue%"), F("%eventvalue1%"));
      int eventvalue_pos = line.indexOf(F("%eventvalue"));

      while (eventvalue_pos != -1) {
        const int percent_pos = line.indexOf('%', eventvalue_pos + 1);

        if (percent_pos == -1) {
          // Found "%eventvalue" without closing %
          if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
            String log = F("Rules : Syntax error, missing '%' in '%eventvalue%': '");
            log += line;
            log += F("' %-pos: ");
            log += percent_pos;
            addLog(LOG_LEVEL_ERROR, log);
          }
          line.replace(F("%eventvalue"), F(""));
        } else {
          // Find the optional part for a default value when the asked for eventvalue does not exist.
          // Syntax: %eventvalueX|Y%
          // With: X = event value nr, Y = default value when eventvalue does not exist.
          String defaultValue('0');
          int or_else_pos = line.indexOf('|', eventvalue_pos);
          if ((or_else_pos == -1) || (or_else_pos > percent_pos)) {
            or_else_pos = percent_pos;
          } else {
            defaultValue = line.substring(or_else_pos + 1, percent_pos);
          }
          const String nr         = line.substring(eventvalue_pos + 11, or_else_pos);
          const String eventvalue = line.substring(eventvalue_pos, percent_pos + 1);
          int argc                = -1;

          if (equals(nr, '0')) {
            // Replace %eventvalue0% with the entire list of arguments.
            line.replace(eventvalue, argString);
          } else {
            argc = nr.toInt();
            
            // argc will be 0 on invalid int (0 was already handled)
            if (argc > 0) {
              String tmpParam;

              if (!GetArgv(argString.c_str(), tmpParam, argc)) {
                // Replace with default value for non existing event values
                tmpParam = parseTemplate(defaultValue);
              }
              line.replace(eventvalue, tmpParam);
            } else {
              // Just remove the invalid eventvalue variable
              if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
                addLog(LOG_LEVEL_ERROR, concat(F("Rules : Syntax error, invalid variable: "), eventvalue));
              }
              line.replace(eventvalue, EMPTY_STRING);
            }
          }
        }
        eventvalue_pos = line.indexOf(F("%eventvalue"));
      }
    }

    if ((line.indexOf(F("%eventname%")) != -1) ||
        (line.indexOf(F("%eventpar%")) != -1)) {
      const String eventName = equalsPos == -1 ? event : event.substring(0, equalsPos);

      // Replace %eventname% with the literal event
      line.replace(F("%eventname%"), eventName);

      // Part of %eventname% after the # char
      const int hash_pos = eventName.indexOf('#');
      line.replace(F("%eventpar%"), hash_pos == -1 ? EMPTY_STRING : eventName.substring(hash_pos + 1));
    }
  }
}

void parseCompleteNonCommentLine(String& line, const String& event,
                                 String& action, bool& match,
                                 bool& codeBlock, bool& isCommand, bool& isOneLiner,
                                 bool condition[], bool ifBranche[],
                                 uint8_t& ifBlock, uint8_t& fakeIfBlock,
                                 bool   startOnMatched) {
  if (line.length() == 0) {
    return;
  }
  const bool lineStartsWith_on = line.substring(0, 3).equalsIgnoreCase(F("on "));

  if (!codeBlock && !match) {
    // We're looking for a new code block.
    // Continue with next line if none was found on current line.
    if (!lineStartsWith_on) {
      return;
    }
  }

  if (line.equalsIgnoreCase(F("endon"))) // Check if action block has ended, then we will
                                           // wait for a new "on" rule
  {
    isCommand   = false;
    codeBlock   = false;
    match       = false;
    ifBlock     = 0;
    fakeIfBlock = 0;
    return;
  }

  const bool lineStartsWith_pct_event = line.startsWith(F("%event"));;

  isCommand = true;

  if (match || !codeBlock) {
    // only parse [xxx#yyy] if we have a matching ruleblock or need to eval the
    // "on" (no codeBlock)
    // This to avoid wasting CPU time...
    if (match && !fakeIfBlock) {
      // substitution of %eventvalue% is made here so it can be used on if
      // statement too
      substitute_eventvalue(line, event);
    }

    if (match || lineStartsWith_on) {
      // Only parseTemplate when we are actually doing something with the line.
      // When still looking for the "on ... do" part, do not change it before we found the block.
      line = parseTemplate(line);
    }
  }

  if (!codeBlock) // do not check "on" rules if a block of actions is to be
                  // processed
  {
    action.clear();
    if (lineStartsWith_on) {
      ifBlock     = 0;
      fakeIfBlock = 0;

      String ruleEvent;
      if (getEventFromRulesLine(line, ruleEvent, action)) {
        START_TIMER
        match = startOnMatched || ruleMatch(event, ruleEvent);
        STOP_TIMER(RULES_MATCH);
      } else {
        match = false;
      }

      if (action.length() > 0) // single on/do/action line, no block
      {
        isCommand  = true;
        isOneLiner = true;
        codeBlock  = false;
      } else {
        isCommand = false;
        codeBlock = true;
      }
    }
  } else {
    #ifndef BUILD_NO_DEBUG
    if (loglevelActiveFor(LOG_LEVEL_DEBUG_DEV)) {
      // keep the line for the log
      action = line;
    } else {
      action = std::move(line);  
    }
    #else
    action = std::move(line);
    #endif
  }

  if (isCommand && lineStartsWith_pct_event) {
    action = concat(F("restrict,"), action);
    if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
      addLogMove(LOG_LEVEL_ERROR, 
        concat(F("Rules : Prefix command with 'restrict': "), action));
    }
  }

#ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG_DEV)) {
    String log = F("RuleDebug: ");
    log += codeBlock ? 0 : 1;
    log += match ? 0 : 1;
    log += isCommand ? 0 : 1;
    log += F(": ");
    log += line;
    addLogMove(LOG_LEVEL_DEBUG_DEV, log);
  }
#endif // ifndef BUILD_NO_DEBUG
}

void processMatchedRule(String& action, const String& event,
                        bool& isCommand, bool condition[], bool ifBranche[],
                        uint8_t& ifBlock, uint8_t& fakeIfBlock) {
  String lcAction = action;

  lcAction.toLowerCase();
  lcAction.trim();

  if (fakeIfBlock) {
    isCommand = false;
  }
  else if (ifBlock) {
    if (condition[ifBlock - 1] != ifBranche[ifBlock - 1]) {
      isCommand = false;
    }
  }
  int split =
    lcAction.startsWith(F("elseif ")) ? 0 : -1; // check for optional "elseif" condition

  if (split != -1) {
    // Found "elseif" condition
    isCommand = false;

    if (ifBlock && !fakeIfBlock) {
      if (ifBranche[ifBlock - 1]) {
        if (condition[ifBlock - 1]) {
          ifBranche[ifBlock - 1] = false;
        }
        else {
          String check = lcAction.substring(split + 7);
          check.trim();
          condition[ifBlock - 1] = conditionMatchExtended(check);
#ifndef BUILD_NO_DEBUG

          if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
            String log  = F("Lev.");
            log += String(ifBlock);
            log += F(": [elseif ");
            log += check;
            log += F("]=");
            log += boolToString(condition[ifBlock - 1]);
            addLogMove(LOG_LEVEL_DEBUG, log);
          }
#endif // ifndef BUILD_NO_DEBUG
        }
      }
    }
  } else {
    // check for optional "if" condition
    split = lcAction.startsWith(F("if ")) ? 0 : -1;

    if (split != -1) {
      if (ifBlock < RULES_IF_MAX_NESTING_LEVEL) {
        if (isCommand) {
          ifBlock++;
          String check = lcAction.substring(split + 3);
          check.trim();
          condition[ifBlock - 1] = conditionMatchExtended(check);
          ifBranche[ifBlock - 1] = true;
#ifndef BUILD_NO_DEBUG

          if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
            String log  = F("Lev.");
            log += String(ifBlock);
            log += F(": [if ");
            log += check;
            log += F("]=");
            log += boolToString(condition[ifBlock - 1]);
            addLogMove(LOG_LEVEL_DEBUG, log);
          }
#endif // ifndef BUILD_NO_DEBUG
        } else {
          fakeIfBlock++;
        }
      } else {
        fakeIfBlock++;

        if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
          String log  = F("Lev.");
          log += String(ifBlock);
          log += F(": Error: IF Nesting level exceeded!");
          addLogMove(LOG_LEVEL_ERROR, log);
        }
      }
      isCommand = false;
    }
  }

  if ((equals(lcAction, F("else"))) && !fakeIfBlock) // in case of an "else" block of
                                               // actions, set ifBranche to
                                               // false
  {
    ifBranche[ifBlock - 1] = false;
    isCommand              = false;
#ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      String log  = F("Lev.");
      log += String(ifBlock);
      log += F(": [else]=");
      log += boolToString(condition[ifBlock - 1] == ifBranche[ifBlock - 1]);
      addLogMove(LOG_LEVEL_DEBUG, log);
    }
#endif // ifndef BUILD_NO_DEBUG
  }

  if (equals(lcAction, F("endif"))) // conditional block ends here
  {
    if (fakeIfBlock) {
      fakeIfBlock--;
    }
    else if (ifBlock) {
      ifBlock--;
    }
    isCommand = false;
  }

  // process the action if it's a command and unconditional, or conditional and
  // the condition matches the if or else block.
  if (isCommand) {
    substitute_eventvalue(action, event);

    const bool executeRestricted = equals(parseString(action, 1), F("restrict"));

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      String actionlog = executeRestricted ? F("ACT  : (restricted) ") : F("ACT  : ");
      actionlog += action;
      addLogMove(LOG_LEVEL_INFO, actionlog);
    }

    if (executeRestricted) {
      ExecuteCommand_all({EventValueSource::Enum::VALUE_SOURCE_RULES_RESTRICTED, parseStringToEndKeepCase(action, 2)});
    } else {
      // Use action.c_str() here as we need to preserve the action string.
      ExecuteCommand_all({EventValueSource::Enum::VALUE_SOURCE_RULES, action.c_str()});
    }
    delay(0);
  }
}

/********************************************************************************************\
   Check if an event matches to a given rule
 \*********************************************************************************************/


/********************************************************************************************\
   Check expression
 \*********************************************************************************************/
bool conditionMatchExtended(String& check) {
  int  condAnd   = -1;
  int  condOr    = -1;
  bool rightcond = false;
  bool leftcond  = conditionMatch(check); // initial check

  #ifndef BUILD_NO_DEBUG
  String debugstr;

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    debugstr += boolToString(leftcond);
  }
  #endif // ifndef BUILD_NO_DEBUG

  do {
    #ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      String log = F("conditionMatchExtended: ");
      log += debugstr;
      log += ' ';
      log += wrap_String(check, '"');
      addLogMove(LOG_LEVEL_DEBUG, log);
    }
    #endif // ifndef BUILD_NO_DEBUG
    condAnd = check.indexOf(F(" and "));
    condOr  = check.indexOf(F(" or "));

    if ((condAnd > 0) || (condOr > 0)) {                             // we got AND/OR
      if ((condAnd > 0) && (((condOr < 0) /*&& (condOr < condAnd)*/) ||
                            ((condOr > 0) && (condOr > condAnd)))) { // AND is first
        check     = check.substring(condAnd + 5);
        rightcond = conditionMatch(check);
        leftcond  = (leftcond && rightcond);

        #ifndef BUILD_NO_DEBUG

        if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
          debugstr += F(" && ");
        }
        #endif // ifndef BUILD_NO_DEBUG
      } else { // OR is first
        check     = check.substring(condOr + 4);
        rightcond = conditionMatch(check);
        leftcond  = (leftcond || rightcond);

        #ifndef BUILD_NO_DEBUG

        if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
          debugstr += F(" || ");
        }
        #endif // ifndef BUILD_NO_DEBUG
      }

      #ifndef BUILD_NO_DEBUG

      if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
        debugstr += boolToString(rightcond);
      }
      #endif // ifndef BUILD_NO_DEBUG
    }
  } while (condAnd > 0 || condOr > 0);

  #ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    check = debugstr;
  }
  #endif // ifndef BUILD_NO_DEBUG
  return leftcond;
}


void logtimeStringToSeconds(const String& tBuf, int hours, int minutes, int seconds, bool valid)
{
  #ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    String log;
    log  = F("timeStringToSeconds: ");
    log += wrap_String(tBuf, '"');
    log += F(" --> ");
    if (valid) {
      log += formatIntLeadingZeroes(hours, 2);
      log += ':';
      log += formatIntLeadingZeroes(minutes, 2);
      log += ':';
      log += formatIntLeadingZeroes(seconds, 2);
    } else {
      log += F("invalid");
    }
    addLogMove(LOG_LEVEL_DEBUG, log);
  }

  #endif // ifndef BUILD_NO_DEBUG
}

// convert old and new time string to nr of seconds
// return whether it should be considered a time string.
bool timeStringToSeconds(const String& tBuf, int& time_seconds, String& timeString) {
  {
    // Make sure we only check for expected characters
    // e.g. if 10:11:12 > 7:07 and 10:11:12 < 20:09:04
    // Should only try to match "7:07", not "7:07 and 10:11:12" 
    // Or else it will find "7:07:11"
    bool done = false;
    for (uint8_t pos = 0; !done && timeString.length() < 8 && pos < tBuf.length(); ++pos) {
      char c = tBuf[pos];
      if (isdigit(c) || c == ':') {
        timeString += c;
      } else {
        done = true;
      }
    }
  }

  time_seconds = -1;
  int32_t hours   = 0;
  int32_t minutes = 0;
  int32_t seconds = 0;

  int tmpIndex = 0;
  String hours_str, minutes_str, seconds_str;
  bool   validTime = false;

  if (get_next_argument(timeString, tmpIndex, hours_str, ':')) {
    if (validIntFromString(hours_str, hours)) {
      validTime = true;

      if ((hours < 0) || (hours > 24)) {
        validTime = false;
      } else {
        time_seconds = hours * 60 * 60;
      }

      if (validTime && get_next_argument(timeString, tmpIndex, minutes_str, ':')) {
        if (validIntFromString(minutes_str, minutes)) {
          if ((minutes < 0) || (minutes > 59)) {
            validTime = false;
          } else {
            time_seconds += minutes * 60;
          }

          if (validTime && get_next_argument(timeString, tmpIndex, seconds_str, ':')) {
            // New format, only HH:MM:SS
            if (validIntFromString(seconds_str, seconds)) {
              if ((seconds < 0) || (seconds > 59)) {
                validTime = false;
              } else {
                time_seconds += seconds;
              }
            }
          } else {
            // Old format, only HH:MM
          }
        }
      } else {
        // It is a valid time string, but could also be just a numerical.
        // We mark it here as invalid, meaning the 'other' time to compare it to must contain more than just the hour.
        validTime = false;
      }
    }
  }
  logtimeStringToSeconds(timeString, hours, minutes, seconds, validTime);
  return validTime;
}

// Balance the count of parentheses (aka round braces) by adding the missing left or right parentheses, if any
// Returns the number of added parentheses, < 0 is left parentheses added, > 0 is right parentheses added
int balanceParentheses(String& string) {
  int left = 0;
  int right = 0;
  for (unsigned int i = 0; i < string.length(); i++) {
    if (string[i] == '(') {
      left++;
    } else if (string[i] == ')') {
      right++;
    }
  }
  if (left != right) {
    string.reserve(string.length() + abs(right - left)); // Re-allocate max. once
  }
  if (left > right) {
    for (int i = 0; i < left - right; i++) {
      string += ')';
    }
  } else if (right > left) {
    for (int i = 0; i < right - left; i++) {
      string = String('(') + string; // This is quite 'expensive'
    }
  }
  return left - right;
}

bool conditionMatch(const String& check) {
  int  posStart, posEnd;
  char compare;

  if (!findCompareCondition(check, compare, posStart, posEnd)) {
    return false;
  }

  String tmpCheck1 = check.substring(0, posStart);
  String tmpCheck2 = check.substring(posEnd);

  tmpCheck1.trim();
  tmpCheck2.trim();
  ESPEASY_RULES_FLOAT_TYPE Value1{};
  ESPEASY_RULES_FLOAT_TYPE Value2{};

  int  timeInSec1 = 0;
  int  timeInSec2 = 0;
  String timeString1, timeString2;
  bool validTime1 = timeStringToSeconds(tmpCheck1, timeInSec1, timeString1);
  bool validTime2 = timeStringToSeconds(tmpCheck2, timeInSec2, timeString2);
  bool result     = false;

  bool compareTimes = false;

  if ((validTime1 || validTime2) && (timeInSec1 != -1) && (timeInSec2 != -1))
  {
    // At least one is a time containing ':' separator
    // AND both can be considered a time, so use it as a time and compare seconds.
    compareTimes = true;
    result       = compareIntValues(compare, timeInSec1, timeInSec2);
    tmpCheck1    = timeString1;
    tmpCheck2    = timeString2;
  } else {
    int condAnd = tmpCheck2.indexOf(F(" and "));
    int condOr  = tmpCheck2.indexOf(F(" or "));
    if (condAnd > -1 || condOr > -1) {            // Only parse first condition, rest will be parsed 'later'
      if (condAnd > -1 && (condOr == -1 || condAnd < condOr)) {
        tmpCheck2 = tmpCheck2.substring(0, condAnd);
      } else if (condOr > -1) {
        tmpCheck2 = tmpCheck2.substring(0, condOr);
      }
      tmpCheck2.trim();
    }
    balanceParentheses(tmpCheck1);
    balanceParentheses(tmpCheck2);
    if (isError(Calculate(tmpCheck1, Value1
                          #if FEATURE_STRING_VARIABLES
                          , false // suppress logging specific errors when parsing strings
                          #endif // if FEATURE_STRING_VARIABLES
                         )) ||
        isError(Calculate(tmpCheck2, Value2
                          #if FEATURE_STRING_VARIABLES
                          , false // suppress logging specific errors when parsing strings
                          #endif // if FEATURE_STRING_VARIABLES
                         )))
    {
      #if FEATURE_STRING_VARIABLES
      result = compareStringValues(compare, tmpCheck1, tmpCheck2);
      #else // if FEATURE_STRING_VARIABLES
      return false;
      #endif // if FEATURE_STRING_VARIABLES
    }
    else {
      result = compareDoubleValues(compare, Value1, Value2);
    }
  }

  #ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    String log = F("conditionMatch: ");
    log += wrap_String(check, '"');
    log += F(" --> ");

    log += wrap_String(tmpCheck1, '"');
    log += wrap_String(check.substring(posStart, posEnd), ' '); // Compare
    log += wrap_String(tmpCheck2, '"');

    log += F(" --> ");
    log += boolToString(result);
    log += ' ';

    log += '(';
    const bool trimTrailingZeros = true;
    log += compareTimes ? String(timeInSec1) : 
#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
    doubleToString(Value1, 6, trimTrailingZeros);
#else
    floatToString(Value1, 6, trimTrailingZeros);
#endif
    log += wrap_String(check.substring(posStart, posEnd), ' '); // Compare
    log += compareTimes ? String(timeInSec2) : 
#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
    doubleToString(Value2, 6, trimTrailingZeros);
#else
    floatToString(Value2, 6, trimTrailingZeros);
#endif    
    log += ')';
    addLogMove(LOG_LEVEL_DEBUG, log);
  }
  #else // ifndef BUILD_NO_DEBUG
  (void)compareTimes; // To avoid compiler warning
  #endif // ifndef BUILD_NO_DEBUG
  return result;
}

/********************************************************************************************\
   Generate rule events based on task refresh
 \*********************************************************************************************/
void createRuleEvents(struct EventStruct *event) {
  if (!Settings.UseRules) {
    return;
  }
  const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(event->TaskIndex);

  if (!validDeviceIndex(DeviceIndex)) { return; }

  const uint8_t valueCount = getValueCountForTask(event->TaskIndex);
  String taskName = getTaskDeviceName(event->TaskIndex);
  #if FEATURE_STRING_VARIABLES
  String postfix;
  const String search = getDerivedValueSearchAndPostfix(taskName, postfix);
  #endif // if FEATURE_STRING_VARIABLES

  // Small optimization as sensor type string may result in large strings
  // These also only yield a single value, so no need to check for combining task values.
  if (event->getSensorType() == Sensor_VType::SENSOR_TYPE_STRING) {
    size_t expectedSize = 2 + getTaskDeviceName(event->TaskIndex).length();
    expectedSize += Cache.getTaskDeviceValueName(event->TaskIndex, 0).length();
   
    bool appendCompleteStringvalue = false;

    String eventString;
    if (reserve_special(eventString, expectedSize + event->String2.length())) {
      appendCompleteStringvalue = true;
    } else if (!reserve_special(eventString, expectedSize + 24)) {
      // No need to continue as we can't even allocate the event, we probably also cannot process it
      addLog(LOG_LEVEL_ERROR, F("Not enough memory for event"));
      return;
    }
    eventString += taskName;
    eventString += '#';
    eventString += Cache.getTaskDeviceValueName(event->TaskIndex, 0);
    eventString += '=';
    eventString += '`';
    if (appendCompleteStringvalue) {
      eventString += event->String2;
    } else {
      eventString += event->String2.substring(0, 10);
      eventString += F("...");
      eventString += event->String2.substring(event->String2.length() - 10);
    }
    eventString += '`';
    eventQueue.addMove(std::move(eventString));    
  } else if (Settings.CombineTaskValues_SingleEvent(event->TaskIndex)) {
    String eventvalues;
    reserve_special(eventvalues, 32); // Enough for most use cases, prevent lots of memory allocations.

    uint8_t varNr = 0;
    for (; varNr < valueCount; ++varNr) {
      if (varNr != 0) {
        eventvalues += ',';
      }
      eventvalues += formatUserVarNoCheck(event, varNr);
    }
    #if FEATURE_STRING_VARIABLES
    if (Settings.EventAndLogDerivedTaskValues(event->TaskIndex)) {

      auto it = customStringVar.begin();
      while (it != customStringVar.end()) {
        if (it->first.startsWith(search) && it->first.endsWith(postfix)) {
          if (!it->second.isEmpty()) {
            String value(it->second);
            value = parseTemplateAndCalculate(value);
            if (varNr != 0) {
              eventvalues += ',';
            }
            eventvalues += value;
            ++varNr;
          }
        }
        else if (it->first.substring(0, search.length()).compareTo(search) > 0) {
          break;
        }
        ++it;
      }
    }
    #endif // if FEATURE_STRING_VARIABLES
    eventQueue.add(event->TaskIndex, F("All"), eventvalues);
  } else {
    for (uint8_t varNr = 0; varNr < valueCount; varNr++) {
      eventQueue.add(event->TaskIndex, Cache.getTaskDeviceValueName(event->TaskIndex, varNr), formatUserVarNoCheck(event, varNr));
    }
    #if FEATURE_STRING_VARIABLES
    if (Settings.EventAndLogDerivedTaskValues(event->TaskIndex)) {
      taskName.toLowerCase();

      auto it = customStringVar.begin();
      while (it != customStringVar.end()) {
        if (it->first.startsWith(search) && it->first.endsWith(postfix)) {
          String valueName = it->first.substring(search.length(), it->first.indexOf('-'));
          const String vname2 = getDerivedValueName(taskName, valueName);
          if (!vname2.isEmpty()) {
            valueName = vname2;
          }
          if (!it->second.isEmpty()) {
            String value(it->second);
            value = parseTemplateAndCalculate(value);
            eventQueue.add(event->TaskIndex, valueName, value);
          }
        }
        else if (it->first.substring(0, search.length()).compareTo(search) > 0) {
          break;
        }
        ++it;
      }
    }
    #endif // if FEATURE_STRING_VARIABLES
  }
}

#include "../ESPEasyCore/ESPEasyWifi.h"

#include "../../ESPEasy-Globals.h"
#include "../DataStructs/TimingStats.h"
#include "../ESPEasyCore/ESPEasyNetwork.h"
#include "../ESPEasyCore/ESPEasyWiFiEvent.h"
#include "../ESPEasyCore/ESPEasyWifi_ProcessEvent.h"
#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../ESPEasyCore/Serial.h"
#include "../Globals/ESPEasyWiFiEvent.h"
#include "../Globals/EventQueue.h"
#include "../Globals/NetworkState.h"
#include "../Globals/Nodes.h"
#include "../Globals/RTC.h"
#include "../Globals/SecuritySettings.h"
#include "../Globals/Services.h"
#include "../Globals/Settings.h"
#include "../Globals/WiFi_AP_Candidates.h"
#include "../Helpers/ESPEasy_time_calc.h"
#include "../Helpers/Hardware_defines.h"
#include "../Helpers/Misc.h"
#include "../Helpers/Networking.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringGenerator_WiFi.h"
#include "../Helpers/StringProvider.h"

#ifdef ESP32
#include <WiFiGeneric.h>
#include <esp_wifi.h> // Needed to call ESP-IDF functions like esp_wifi_....

#include <esp_phy_init.h>
#endif

// FIXME TD-er: Cleanup of WiFi code
#ifdef ESPEASY_WIFI_CLEANUP_WORK_IN_PROGRESS
bool ESPEasyWiFi_t::begin() {
  return true;
}

void ESPEasyWiFi_t::end() {


}


void ESPEasyWiFi_t::loop() {
  switch (_state) {
    case WiFiState_e::OFF:
    break;
    case WiFiState_e::AP_only:
    break;
    case WiFiState_e::ErrorRecovery:
    // Wait for timeout to expire
    // Start again from scratch
    break;
    case WiFiState_e::STA_Scanning:
    case WiFiState_e::STA_AP_Scanning:
    // Check if scanning is finished
    // When scanning per channel, call for scanning next channel
    break;
    case WiFiState_e::STA_Connecting:
    case WiFiState_e::STA_Reconnecting:
    // Check if (re)connecting has finished
    break;
    case WiFiState_e::STA_Connected:
    // Check if still connected
    // Reconnect if not.
    // Else mark last timestamp seen as connected
    break;
  }


  {
    // Check if we need to start AP
    // Flag captive portal in webserver and/or whether we might be in setup mode
  }

#ifdef USE_IMPROV
  {
    // Check for Improv mode.
  }
#endif


}


IPAddress  ESPEasyWiFi_t::getIP() const {

  IPAddress res;


  return res;
}

void  ESPEasyWiFi_t::disconnect() {

}


void ESPEasyWiFi_t::checkConnectProgress() {

}

void ESPEasyWiFi_t::startScanning() {
  _state = WiFiState_e::STA_Scanning;
  WifiScan(true);
  _last_state_change.setNow();
}


bool ESPEasyWiFi_t::connectSTA() {
  if (!WiFi_AP_Candidates.hasCandidateCredentials()) {
    if (!WiFiEventData.warnedNoValidWiFiSettings) {
      addLog(LOG_LEVEL_ERROR, F("WIFI : No valid wifi settings"));
      WiFiEventData.warnedNoValidWiFiSettings = true;
    }
    WiFiEventData.last_wifi_connect_attempt_moment.clear();
    WiFiEventData.wifi_connect_attempt     = 1;
    WiFiEventData.wifiConnectAttemptNeeded = false;

    // No need to wait longer to start AP mode.
    if (!Settings.DoNotStartAP()) {
      setAP(true);
    }
    return false;
  }
  WiFiEventData.warnedNoValidWiFiSettings = false;
  setSTA(true);
  #if defined(ESP8266)
  wifi_station_set_hostname(NetworkCreateRFCCompliantHostname().c_str());

  #endif // if defined(ESP8266)
  #if defined(ESP32)
  WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE);
  #endif // if defined(ESP32)
  setConnectionSpeed();
  setupStaticIPconfig();



    // Start the process of connecting or starting AP
    if (WiFi_AP_Candidates.getNext(true)) {
      // Try to connect to AP

    } else {
      // No (known) AP, start scanning
      startScanning();
    }


  return true;
}

#endif // ESPEASY_WIFI_CLEANUP_WORK_IN_PROGRESS


// ********************************************************************************
// WiFi state
// ********************************************************************************

/*
   WiFi STA states:
   1 STA off                 => ESPEASY_WIFI_DISCONNECTED
   2 STA connecting
   3 STA connected           => ESPEASY_WIFI_CONNECTED
   4 STA got IP              => ESPEASY_WIFI_GOT_IP
   5 STA connected && got IP => ESPEASY_WIFI_SERVICES_INITIALIZED

   N.B. the states are flags, meaning both "connected" and "got IP" must be set
        to be considered ESPEASY_WIFI_SERVICES_INITIALIZED

   The flag wifiConnectAttemptNeeded indicates whether a new connect attempt is needed.
   This is set to true when:
   - Security settings have been saved with AP mode enabled. FIXME TD-er, this may not be the best check.
   - WiFi connect timeout reached  &  No client is connected to the AP mode of the node.
   - Wifi is reset
   - WiFi setup page has been loaded with SSID/pass values.


   WiFi AP mode states:
   1 AP on                        => reset AP disable timer
   2 AP client connect/disconnect => reset AP disable timer
   3 AP off                       => AP disable timer = 0;

   AP mode will be disabled when both apply:
   - AP disable timer (timerAPoff) expired
   - No client is connected to the AP.

   AP mode will be enabled when at least one applies:
   - No valid WiFi settings
   - Start AP timer (timerAPstart) expired

   Start AP timer is set or cleared at:
   - Set timerAPstart when "valid WiFi connection" state is observed.
   - Disable timerAPstart when ESPEASY_WIFI_SERVICES_INITIALIZED wifi state is reached.

   For the first attempt to connect after a cold boot (RTC values are 0), a WiFi scan will be 
   performed to find the strongest known SSID.
   This will set RTC.lastBSSID and RTC.lastWiFiChannel
   
   Quick reconnect (using BSSID/channel of last connection) when both apply:
   - If wifi_connect_attempt < 3
   - RTC.lastBSSID is known
   - RTC.lastWiFiChannel != 0

   Change of wifi settings when both apply:
   - "other" settings valid
   - (wifi_connect_attempt % 2) == 0

   Reset of wifi_connect_attempt to 0 when both apply:
   - connection successful
   - Connection stable (connected for > 5 minutes)

 */


// ********************************************************************************
// Check WiFi connected status
// This is basically the state machine to switch between states:
// - Initiate WiFi reconnect
// - Start/stop of AP mode
// ********************************************************************************
bool WiFiConnected() {
  START_TIMER;

  static bool recursiveCall = false;

  static uint32_t lastCheckedTime = 0;
  static bool lastState = false;

#if FEATURE_USE_IPV6
  if (!WiFiEventData.processedGotIP6) {
    processGotIPv6();
  }
#endif

  if (!WifiIsSTA(WiFi.getMode())) {
    lastState = false;
    return lastState;
  }


  const int32_t timePassed = timePassedSince(lastCheckedTime);
  if (lastCheckedTime != 0) {
    if (timePassed < 100) {
      if (WiFiEventData.lastDisconnectMoment.isSet() &&
          WiFiEventData.lastDisconnectMoment.millisPassedSince() > timePassed)
      {
        // Try to rate-limit the nr of calls to this function or else it will be called 1000's of times a second.
        return lastState;
      }
    }
    if (timePassed < 10) {
      // Rate limit time spent in WiFiConnected() to max. 100x per sec to process the rest of this function
      return lastState;
    }
  }



  if (WiFiEventData.unprocessedWifiEvents()) { return false; }

  bool wifi_isconnected = WiFi.isConnected();
  #ifdef ESP8266
  // Perform check on SDK function, see: https://github.com/esp8266/Arduino/issues/7432
  station_status_t status = wifi_station_get_connect_status();
  switch(status) {
    case STATION_GOT_IP:
      wifi_isconnected = true;
      break;
    case STATION_NO_AP_FOUND:
    case STATION_CONNECT_FAIL:
    case STATION_WRONG_PASSWORD:
      wifi_isconnected = false;
      break;
    case STATION_IDLE:
    case STATION_CONNECTING:
      break;

    default:
      wifi_isconnected = false;
      break;
  }
  #endif

  if (recursiveCall) return wifi_isconnected;
  recursiveCall = true;


  // For ESP82xx, do not rely on WiFi.status() with event based wifi.
  const int32_t wifi_rssi = WiFi.RSSI();
  bool validWiFi = (wifi_rssi < 0) && wifi_isconnected && hasIPaddr();
  /*
  if (validWiFi && WiFi.channel() != WiFiEventData.usedChannel) {
    validWiFi = false;
  }
  */
  if (validWiFi != WiFiEventData.WiFiServicesInitialized()) {
    // else wifiStatus is no longer in sync.
    if (checkAndResetWiFi()) {
      // Wifi has been reset, so no longer valid WiFi
      validWiFi = false;
    }
  }

  if (validWiFi) {
    // Connected, thus disable any timer to start AP mode. (except when in WiFi setup mode)
    if (!WiFiEventData.wifiSetupConnect) {
      WiFiEventData.timerAPstart.clear();
    }
    STOP_TIMER(WIFI_ISCONNECTED_STATS);
    recursiveCall = false;
    // Only return true after some time since it got connected.
#if FEATURE_SET_WIFI_TX_PWR
    SetWiFiTXpower();
#endif
    lastState = WiFiEventData.wifi_considered_stable || WiFiEventData.lastConnectMoment.timeoutReached(100);
    lastCheckedTime = millis();
    return lastState;
  }

  if ((WiFiEventData.timerAPstart.isSet()) && WiFiEventData.timerAPstart.timeReached()) {
    if (WiFiEventData.timerAPoff.isSet() && !WiFiEventData.timerAPoff.timeReached()) {
      if (!Settings.DoNotStartAP()) {
        // Timer reached, so enable AP mode.
        if (!WifiIsAP(WiFi.getMode())) {
          if (!WiFiEventData.wifiConnectAttemptNeeded) {
            #ifndef BUILD_MINIMAL_OTA
            addLog(LOG_LEVEL_INFO, F("WiFi : WiFiConnected(), start AP"));
            #endif
            WifiScan(false);
            setSTA(false); // Force reset WiFi + reduce power consumption
            setAP(true);
          }
        }
      }
    } else {
      WiFiEventData.timerAPstart.clear();
      WiFiEventData.timerAPoff.clear();
    }
  }


  // When made this far in the code, we apparently do not have valid WiFi connection.
  if (!WiFiEventData.timerAPstart.isSet() && !WifiIsAP(WiFi.getMode())) {
    // First run we do not have WiFi connection any more, set timer to start AP mode
    // Only allow the automatic AP mode in the first N minutes after boot.
    if (getUptimeMinutes() < WIFI_ALLOW_AP_AFTERBOOT_PERIOD) {
      WiFiEventData.timerAPstart.setMillisFromNow(WIFI_RECONNECT_WAIT);
      // Fixme TD-er: Make this more elegant as it now needs to know about the extra time needed for the AP start timer.
      WiFiEventData.timerAPoff.setMillisFromNow(WIFI_RECONNECT_WAIT + WIFI_AP_OFF_TIMER_DURATION);
    }
  }

  const bool timeoutReached = WiFiEventData.last_wifi_connect_attempt_moment.isSet() && 
                              WiFiEventData.last_wifi_connect_attempt_moment.timeoutReached(2 * DEFAULT_WIFI_CONNECTION_TIMEOUT);

  if (timeoutReached && !WiFiEventData.wifiSetup) {
    // It took too long to make a connection, set flag we need to try again
    //if (!wifiAPmodeActivelyUsed()) {
      WiFiEventData.wifiConnectAttemptNeeded = true;
    //}
    WiFiEventData.wifiConnectInProgress = false;
    if (!WiFiEventData.WiFiDisconnected()) {
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_INFO, F("WiFi : wifiConnectTimeoutReached"));
      #endif
      WifiDisconnect();
    }
  }
  delay(0);
  STOP_TIMER(WIFI_NOTCONNECTED_STATS);
  recursiveCall = false;
  return false;
}

void WiFiConnectRelaxed() {
  if (!WiFiEventData.processedDisconnect) {
    processDisconnect();
  }
  if (!WiFiEventData.WiFiConnectAllowed() || WiFiEventData.wifiConnectInProgress) {
    if (WiFiEventData.wifiConnectInProgress) {
      if (WiFiEventData.last_wifi_connect_attempt_moment.isSet()) { 
        if (WiFiEventData.last_wifi_connect_attempt_moment.timeoutReached(WIFI_PROCESS_EVENTS_TIMEOUT)) {
          WiFiEventData.wifiConnectInProgress = false;
        }
      }
    }

    if (WiFiEventData.wifiConnectInProgress) {
      return; // already connected or connect attempt in progress need to disconnect first
    }
  }
  if (!WiFiEventData.processedScanDone) {
    // Scan is still active, so do not yet connect.
    return;
  }

  if (WiFiEventData.unprocessedWifiEvents()) {
    # ifndef BUILD_NO_DEBUG
    if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
      String log = F("WiFi : Connecting not possible, unprocessed WiFi events: ");
      if (!WiFiEventData.processedConnect) {
        log += F(" conn");
      }
      if (!WiFiEventData.processedDisconnect) {
        log += F(" disconn");
      }
      if (!WiFiEventData.processedGotIP) {
        log += F(" gotIP");
      }
#if FEATURE_USE_IPV6
      if (!WiFiEventData.processedGotIP6) {
        log += F(" gotIP6");
      }
#endif

      if (!WiFiEventData.processedDHCPTimeout) {
        log += F(" DHCP_t/o");
      }
      
      addLogMove(LOG_LEVEL_ERROR, log);
      logConnectionStatus();
    }
    #endif
    return;
  }

  if (!WiFiEventData.wifiSetupConnect && wifiAPmodeActivelyUsed()) {
    return;
  }


  // FIXME TD-er: Should not try to prepare when a scan is still busy.
  // This is a logic error which may lead to strange issues if some kind of timeout happens and/or RF calibration was not OK.
  // Split this function into separate parts, with the last part being the actual connect attempt either after a scan is complete or quick connect is possible.

  AttemptWiFiConnect();
}

void AttemptWiFiConnect() {
  if (!WiFiEventData.wifiConnectAttemptNeeded) {
    return;
  }

  if (WiFiEventData.wifiConnectInProgress) {
    return;
  }

  setNetworkMedium(NetworkMedium_t::WIFI);
  if (active_network_medium != NetworkMedium_t::WIFI) 
  {
    return;
  }


  if (WiFiEventData.wifiSetupConnect) {
    // wifiSetupConnect is when run from the setup page.
    RTC.clearLastWiFi(); // Force slow connect
    WiFiEventData.wifi_connect_attempt = 0;
    WiFiEventData.wifiSetupConnect     = false;
    if (WiFiEventData.timerAPoff.isSet()) {
      WiFiEventData.timerAPoff.setMillisFromNow(WIFI_RECONNECT_WAIT + WIFI_AP_OFF_TIMER_DURATION);
    }
  }

  if (WiFiEventData.last_wifi_connect_attempt_moment.isSet()) {
    if (!WiFiEventData.last_wifi_connect_attempt_moment.timeoutReached(DEFAULT_WIFI_CONNECTION_TIMEOUT)) {
      return;
    }
  }

  if (WiFiEventData.unprocessedWifiEvents()) {
    return;
  }
  setSTA(false);

  setSTA(true);

  if (WiFi_AP_Candidates.getNext(WiFiScanAllowed())) {
    const WiFi_AP_Candidate candidate = WiFi_AP_Candidates.getCurrent();

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, strformat(
        F("WIFI : Connecting %s attempt #%u"),
        candidate.toString().c_str(),
        WiFiEventData.wifi_connect_attempt));
    }
    WiFiEventData.markWiFiBegin();
    if (prepareWiFi()) {
      setNetworkMedium(NetworkMedium_t::WIFI);
      RTC.clearLastWiFi();
      RTC.lastWiFiSettingsIndex = candidate.index;
      
      float tx_pwr = 0; // Will be set higher based on RSSI when needed.
      // FIXME TD-er: Must check WiFiEventData.wifi_connect_attempt to increase TX power
#if FEATURE_SET_WIFI_TX_PWR
      if (Settings.UseMaxTXpowerForSending()) {
        tx_pwr = Settings.getWiFi_TX_power();
      }
      SetWiFiTXpower(tx_pwr, candidate.rssi);
#endif
      // Start connect attempt now, so no longer needed to attempt new connection.
      WiFiEventData.wifiConnectAttemptNeeded = false;
      WiFiEventData.wifiConnectInProgress = true;
      const String key = WiFi_AP_CandidatesList::get_key(candidate.index);

#if FEATURE_USE_IPV6
      if (Settings.EnableIPv6()) {
        WiFi.enableIPv6(true);
      }
#endif

#ifdef ESP32
      if (Settings.IncludeHiddenSSID()) {
        wifi_country_t config = {
          .cc = "01",
          .schan = 1,
          .nchan = 14,
          .policy = WIFI_COUNTRY_POLICY_MANUAL,
        };
        esp_wifi_set_country(&config);
      }
#endif


      if ((Settings.HiddenSSID_SlowConnectPerBSSID() || !candidate.bits.isHidden)
           && candidate.allowQuickConnect()) {
        WiFi.begin(candidate.ssid.c_str(), key.c_str(), candidate.channel, candidate.bssid.mac);
      } else {
        WiFi.begin(candidate.ssid.c_str(), key.c_str());
      }
#ifdef ESP32
  // Always wait for a second on ESP32
      WiFi.waitForConnectResult(1000);  // https://github.com/arendst/Tasmota/issues/14985
#else
      if (Settings.WaitWiFiConnect() || candidate.bits.isHidden) {
//        WiFi.waitForConnectResult(candidate.isHidden ? 3000 : 1000);  // https://github.com/arendst/Tasmota/issues/14985
        WiFi.waitForConnectResult(1000);  // https://github.com/arendst/Tasmota/issues/14985
      }
#endif
      delay(1);
    } else {
      WiFiEventData.wifiConnectInProgress = false;
    }
  } else {
    if (!wifiAPmodeActivelyUsed() || WiFiEventData.wifiSetupConnect) {
      if (!prepareWiFi()) {
        //return;
      }

      if (WiFiScanAllowed()) {
        // Maybe not scan async to give the ESP some slack in power consumption?
        const bool async = false;
        WifiScan(async);
      }
      // Limit nr of attempts as we don't have any AP candidates.
      WiFiEventData.last_wifi_connect_attempt_moment.setMillisFromNow(60000);
      WiFiEventData.timerAPstart.setNow();
    }
  }

  logConnectionStatus();
}

// ********************************************************************************
// Set Wifi config
// ********************************************************************************
bool prepareWiFi() {
  #if defined(ESP32)
  registerWiFiEventHandler();
  #endif

  if (!WiFi_AP_Candidates.hasCandidateCredentials()) {
    if (!WiFiEventData.warnedNoValidWiFiSettings) {
      addLog(LOG_LEVEL_ERROR, F("WIFI : No valid wifi settings"));
      WiFiEventData.warnedNoValidWiFiSettings = true;
    }
//    WiFiEventData.last_wifi_connect_attempt_moment.clear();
    WiFiEventData.wifi_connect_attempt     = 1;
    WiFiEventData.wifiConnectAttemptNeeded = false;

    // No need to wait longer to start AP mode.
    if (!Settings.DoNotStartAP()) {
      WifiScan(false);
//      setAP(true);
    }
    return false;
  }
  WiFiEventData.warnedNoValidWiFiSettings = false;
  setSTA(true);

  #if defined(ESP8266)
  wifi_station_set_hostname(NetworkCreateRFCCompliantHostname().c_str());

  #endif // if defined(ESP8266)
  #if defined(ESP32)
  WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE);
  #endif // if defined(ESP32)
  setConnectionSpeed();
  setupStaticIPconfig();
  WiFiEventData.wifiConnectAttemptNeeded = true;

  return true;
}

bool checkAndResetWiFi() {
  #ifdef ESP8266
  station_status_t status = wifi_station_get_connect_status();

  switch(status) {
    case STATION_GOT_IP:
      if (WiFi.RSSI() < 0 && WiFi.localIP().isSet()) {
        //if (WiFi.channel() == WiFiEventData.usedChannel || WiFiEventData.usedChannel == 0) {
          // This is a valid status, no need to reset
          return false;
        //}
      }
      break;
    case STATION_NO_AP_FOUND:
    case STATION_CONNECT_FAIL:
    case STATION_WRONG_PASSWORD:
      // Reason to reset WiFi
      break;
    case STATION_IDLE:
    case STATION_CONNECTING:
      if (WiFiEventData.last_wifi_connect_attempt_moment.isSet() && !WiFiEventData.last_wifi_connect_attempt_moment.timeoutReached(DEFAULT_WIFI_CONNECTION_TIMEOUT)) {
        return false;
      }
      break;
  }
  #endif
  #ifdef ESP32
  if (WiFi.isConnected()) {
    //if (WiFi.channel() == WiFiEventData.usedChannel || WiFiEventData.usedChannel == 0) {
      return false;
    //}
  }
  if (WiFiEventData.last_wifi_connect_attempt_moment.isSet() && !WiFiEventData.last_wifi_connect_attempt_moment.timeoutReached(DEFAULT_WIFI_CONNECTION_TIMEOUT)) {
    return false;
  }
  #endif
  # ifndef BUILD_NO_DEBUG
  String log = F("WiFi : WiFiConnected() out of sync: ");
  log += WiFiEventData.ESPeasyWifiStatusToString();
  log += F(" RSSI: ");
  log += String(WiFi.RSSI());
  #ifdef ESP8266
  log += F(" status: ");
  log += SDKwifiStatusToString(status);
  #endif
  #endif

  // Call for reset first, to make sure a syslog call will not try to send.
  resetWiFi();
  # ifndef BUILD_NO_DEBUG
  addLogMove(LOG_LEVEL_INFO, log);
  #endif
  return true;
}


void resetWiFi() {
  //if (wifiAPmodeActivelyUsed()) return;
  if (WiFiEventData.lastWiFiResetMoment.isSet() && !WiFiEventData.lastWiFiResetMoment.timeoutReached(1000)) {
    // Don't reset WiFi too often
    return;
  }
  FeedSW_watchdog();
  WiFiEventData.clearAll();
  WifiDisconnect();

  // Send this log only after WifiDisconnect() or else sending to syslog may cause issues
  #ifndef BUILD_MINIMAL_OTA
  addLog(LOG_LEVEL_INFO, F("Reset WiFi."));
  #endif

  //  setWifiMode(WIFI_OFF);

  initWiFi();
}

#ifdef ESP32
void removeWiFiEventHandler()
{
  WiFi.removeEvent(WiFiEventData.wm_event_id);
  WiFiEventData.wm_event_id = 0;
}

void registerWiFiEventHandler()
{
  if (WiFiEventData.wm_event_id != 0) {
    removeWiFiEventHandler();
  }
  WiFiEventData.wm_event_id = WiFi.onEvent(WiFiEvent);
}
#endif


void initWiFi()
{
#ifdef ESP8266

  // See https://github.com/esp8266/Arduino/issues/5527#issuecomment-460537616
  // FIXME TD-er: Do not destruct WiFi object, it may cause crashes with queued UDP traffic.
//  WiFi.~ESP8266WiFiClass();
//  WiFi = ESP8266WiFiClass();
#endif // ifdef ESP8266
#ifdef ESP32
  removeWiFiEventHandler();
#endif


  WiFi.persistent(false); // Do not use SDK storage of SSID/WPA parameters
  // The WiFi.disconnect() ensures that the WiFi is working correctly. If this is not done before receiving WiFi connections,
  // those WiFi connections will take a long time to make or sometimes will not work at all.
  WiFi.disconnect(false);
  delay(1);
  if (active_network_medium != NetworkMedium_t::NotSet) {
    setSTA(true);
    WifiScan(false);
  }
  setWifiMode(WIFI_OFF);

#if defined(ESP32)
  registerWiFiEventHandler();
#endif
#ifdef ESP8266
  // WiFi event handlers
  static bool handlers_initialized = false;
  if (!handlers_initialized) {
    stationConnectedHandler = WiFi.onStationModeConnected(onConnected);
    stationDisconnectedHandler = WiFi.onStationModeDisconnected(onDisconnect);
    stationGotIpHandler = WiFi.onStationModeGotIP(onGotIP);
    stationModeDHCPTimeoutHandler = WiFi.onStationModeDHCPTimeout(onDHCPTimeout);
    stationModeAuthModeChangeHandler = WiFi.onStationModeAuthModeChanged(onStationModeAuthModeChanged);
    APModeStationConnectedHandler = WiFi.onSoftAPModeStationConnected(onConnectedAPmode);
    APModeStationDisconnectedHandler = WiFi.onSoftAPModeStationDisconnected(onDisconnectedAPmode);
    handlers_initialized = true;
  }
#endif
  delay(100);
}

// ********************************************************************************
// Configure WiFi TX power
// ********************************************************************************
#if FEATURE_SET_WIFI_TX_PWR
void SetWiFiTXpower() {
  SetWiFiTXpower(0); // Just some minimal value, will be adjusted in SetWiFiTXpower
}

void SetWiFiTXpower(float dBm) { 
  SetWiFiTXpower(dBm, WiFi.RSSI());
}

void SetWiFiTXpower(float dBm, float rssi) {
  const WiFiMode_t cur_mode = WiFi.getMode();
  if (cur_mode == WIFI_OFF) {
    return;
  }

  if (Settings.UseMaxTXpowerForSending()) {
    dBm = 30; // Just some max, will be limited later
  }

  // Range ESP32  : -1dBm - 20dBm
  // Range ESP8266: 0dBm - 20.5dBm
  float maxTXpwr;
  float threshold = GetRSSIthreshold(maxTXpwr);
  #ifdef ESP8266
  float minTXpwr{};
  #endif
  #ifdef ESP32
  float minTXpwr = -1.0f;
  #endif

  threshold += Settings.WiFi_sensitivity_margin; // Margin in dBm on top of threshold

  // Assume AP sends with max set by ETSI standard.
  // 2.4 GHz: 100 mWatt (20 dBm)
  // US and some other countries allow 1000 mW (30 dBm)
  // We cannot send with over 20 dBm, thus it makes no sense to force higher TX power all the time.
  const float newrssi = rssi - 20;
  if (newrssi < threshold) {
    minTXpwr = threshold - newrssi;
  }
  if (minTXpwr > maxTXpwr) {
    minTXpwr = maxTXpwr;
  }
  if (dBm > maxTXpwr) {
    dBm = maxTXpwr;
  } else if (dBm < minTXpwr) {
    dBm = minTXpwr;
  }

  #ifdef ESP32
  int8_t power = dBm * 4;
  if (esp_wifi_set_max_tx_power(power) == ESP_OK)  {
    if (esp_wifi_get_max_tx_power(&power) == ESP_OK)  {
      dBm = static_cast<float>(power) / 4.0f;
    }
  }
  #endif

  #ifdef ESP8266
  WiFi.setOutputPower(dBm);
  #endif

  if (WiFiEventData.wifi_TX_pwr < dBm) {
    // Will increase the TX power, give power supply of the unit some rest
    delay(1);
  }

  WiFiEventData.wifi_TX_pwr = dBm;

  delay(0);
  #ifndef BUILD_NO_DEBUG
  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    const int TX_pwr_int = WiFiEventData.wifi_TX_pwr * 4;
    const int maxTXpwr_int = maxTXpwr * 4;
    if (TX_pwr_int != maxTXpwr_int) {
      static int last_log = -1;
      if (TX_pwr_int != last_log) {
        last_log = TX_pwr_int;
        String log = strformat(
          F("WiFi : Set TX power to %ddBm sensitivity: %ddBm"),
          static_cast<int>(dBm),
          static_cast<int>(threshold));
        if (rssi < 0) {
          log += strformat(F(" RSSI: %ddBm"), static_cast<int>(rssi));
        }
        addLogMove(LOG_LEVEL_DEBUG, log);
      }
    }
  }
  #endif
}
#endif




float GetRSSIthreshold(float& maxTXpwr) {
  maxTXpwr = Settings.getWiFi_TX_power();
  float threshold = WIFI_SENSITIVITY_n;
  switch (getConnectionProtocol()) {
    case WiFiConnectionProtocol::WiFi_Protocol_11b:
      threshold = WIFI_SENSITIVITY_11b;
      if (maxTXpwr > MAX_TX_PWR_DBM_11b) maxTXpwr = MAX_TX_PWR_DBM_11b;
      break;
    case WiFiConnectionProtocol::WiFi_Protocol_11g:
      threshold = WIFI_SENSITIVITY_54g;
      if (maxTXpwr > MAX_TX_PWR_DBM_54g) maxTXpwr = MAX_TX_PWR_DBM_54g;
      break;
#ifdef ESP8266
    case WiFiConnectionProtocol::WiFi_Protocol_11n:
#else
    case WiFiConnectionProtocol::WiFi_Protocol_HT20:
    case WiFiConnectionProtocol::WiFi_Protocol_HT40:
    case WiFiConnectionProtocol::WiFi_Protocol_HE20:
#endif

      threshold = WIFI_SENSITIVITY_n;
      if (maxTXpwr > MAX_TX_PWR_DBM_n) maxTXpwr = MAX_TX_PWR_DBM_n;
      break;
#ifdef ESP32
#if ESP_IDF_VERSION > ESP_IDF_VERSION_VAL(5, 2, 0)
    case WiFiConnectionProtocol::WiFi_Protocol_11a:
    case WiFiConnectionProtocol::WiFi_Protocol_VHT20:
      // FIXME TD-er: Must determine max. TX power for these 5 GHz modi
#endif
    case WiFiConnectionProtocol::WiFi_Protocol_LR:
#endif
    case WiFiConnectionProtocol::Unknown:
      break;
  }
  return threshold;
}

int GetRSSI_quality() {
  long rssi = WiFi.RSSI();

  if (-50 < rssi) { return 10; }

  if (rssi <= -98) { return 0;  }
  rssi = rssi + 97; // Range 0..47 => 1..9
  return (rssi / 5) + 1;
}

WiFiConnectionProtocol getConnectionProtocol() {
  if (WiFi.RSSI() < 0) {
    #ifdef ESP8266
    switch (wifi_get_phy_mode()) {
      case PHY_MODE_11B:
        return WiFiConnectionProtocol::WiFi_Protocol_11b;
      case PHY_MODE_11G:
        return WiFiConnectionProtocol::WiFi_Protocol_11g;
      case PHY_MODE_11N:
        return WiFiConnectionProtocol::WiFi_Protocol_11n;
    }
    #endif
    #ifdef ESP32

    wifi_phy_mode_t phymode;
    esp_wifi_sta_get_negotiated_phymode(&phymode);
    switch (phymode) {
      case WIFI_PHY_MODE_11B:   return WiFiConnectionProtocol::WiFi_Protocol_11b;
      case WIFI_PHY_MODE_11G:   return WiFiConnectionProtocol::WiFi_Protocol_11g;
      case WIFI_PHY_MODE_HT20:  return WiFiConnectionProtocol::WiFi_Protocol_HT20;
      case WIFI_PHY_MODE_HT40:  return WiFiConnectionProtocol::WiFi_Protocol_HT40;
      case WIFI_PHY_MODE_HE20:  return WiFiConnectionProtocol::WiFi_Protocol_HE20;
      case WIFI_PHY_MODE_LR:    return WiFiConnectionProtocol::WiFi_Protocol_LR;
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
      // 5 GHz
      case WIFI_PHY_MODE_11A:   return WiFiConnectionProtocol::WiFi_Protocol_11a;
      case WIFI_PHY_MODE_VHT20: return WiFiConnectionProtocol::WiFi_Protocol_VHT20;
#endif
    }
    #endif
  }
  return WiFiConnectionProtocol::Unknown;
}

#ifdef ESP32
int64_t WiFi_get_TSF_time()
{
  return esp_wifi_get_tsf_time(WIFI_IF_STA);
}
#endif


// ********************************************************************************
// Disconnect from Wifi AP
// ********************************************************************************
void WifiDisconnect()
{
  if (!WiFiEventData.processedDisconnect || 
       WiFiEventData.processingDisconnect.isSet()) {
    return;
  }
  if (WiFi.status() == WL_DISCONNECTED) {
    return;
  }
  // Prevent recursion
  static LongTermTimer processingDisconnectTimer;
  if (processingDisconnectTimer.isSet() && 
     !processingDisconnectTimer.timeoutReached(200)) return;
  processingDisconnectTimer.setNow();
  # ifndef BUILD_NO_DEBUG
  addLog(LOG_LEVEL_INFO, F("WiFi : WifiDisconnect()"));
  #endif
  #ifdef ESP32
  removeWiFiEventHandler();
  WiFi.disconnect();
  delay(100);
  {
    const IPAddress ip;
    const IPAddress gw;
    const IPAddress subnet;
    const IPAddress dns;
    WiFi.config(ip, gw, subnet, dns);
  }
  #endif
  #ifdef ESP8266
  // Only call disconnect when STA is active
  if (WifiIsSTA(WiFi.getMode())) {
    wifi_station_disconnect();
  }
  station_config conf{};
  memset(&conf, 0, sizeof(conf));
  ETS_UART_INTR_DISABLE();
  wifi_station_set_config_current(&conf);
  ETS_UART_INTR_ENABLE();
  #endif
  WiFiEventData.setWiFiDisconnected();
  WiFiEventData.markDisconnect(WIFI_DISCONNECT_REASON_UNSPECIFIED);
  /*
  if (!Settings.UseLastWiFiFromRTC()) {
    RTC.clearLastWiFi();
  }
  */
  delay(100);
  WiFiEventData.processingDisconnect.clear();
  WiFiEventData.processedDisconnect = false;
  processDisconnect();
  processingDisconnectTimer.clear();
}

// ********************************************************************************
// Scan WiFi network
// ********************************************************************************
bool WiFiScanAllowed() {
  if (WiFi_AP_Candidates.scanComplete() == WIFI_SCAN_RUNNING) {
    return false;
  }
  if (!WiFiEventData.processedScanDone) { 
    processScanDone(); 
  }
  if (!WiFiEventData.processedDisconnect) {
    processDisconnect();
  }

  if (WiFiEventData.wifiConnectInProgress) {
    return false;
  }

  if (WiFiEventData.intent_to_reboot) {
    return false;
  }

  if (WiFiEventData.unprocessedWifiEvents()) {
    # ifndef BUILD_NO_DEBUG
    if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
      String log = F("WiFi : Scan not allowed, unprocessed WiFi events: ");
      if (!WiFiEventData.processedConnect) {
        log += F(" conn");
      }
      if (!WiFiEventData.processedDisconnect) {
        log += F(" disconn");
      }
      if (!WiFiEventData.processedGotIP) {
        log += F(" gotIP");
      }
      if (!WiFiEventData.processedDHCPTimeout) {
        log += F(" DHCP_t/o");
      }
      
      addLogMove(LOG_LEVEL_ERROR, log);
      logConnectionStatus();
    }
    #endif
    return false;
  }
  /*
  if (!wifiAPmodeActivelyUsed() && !NetworkConnected()) {
    return true;
  }
  */
  WiFi_AP_Candidates.purge_expired();
  if (WiFiEventData.wifiConnectInProgress) {
    return false;
  }
  if (WiFiEventData.lastScanMoment.isSet()) {
    if (NetworkConnected() && WiFi_AP_Candidates.getBestCandidate().usable()) {
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_ERROR, F("WiFi : Scan not needed, good candidate present"));
      #endif
      return false;
    }
  }

  if (WiFiEventData.lastDisconnectMoment.isSet() && WiFiEventData.lastDisconnectMoment.millisPassedSince() < WIFI_RECONNECT_WAIT) {
    if (!NetworkConnected()) {
      return WiFiEventData.processedConnect;
    }
  }
  if (WiFiEventData.lastScanMoment.isSet()) {
    const LongTermTimer::Duration scanInterval = wifiAPmodeActivelyUsed() ? WIFI_SCAN_INTERVAL_AP_USED : WIFI_SCAN_INTERVAL_MINIMAL;
    if (WiFiEventData.lastScanMoment.millisPassedSince() < scanInterval) {
      return false;
    }
  }
  return WiFiEventData.processedConnect;
}


void WifiScan(bool async, uint8_t channel) {
  setSTA(true);
  if (!WiFiScanAllowed()) {
    return;
  }
#ifdef ESP32
  // TD-er: Don't run async scan on ESP32.
  // Since IDF 4.4 it seems like the active channel may be messed up when running async scan
  // Perform a disconnect after scanning.
  // See: https://github.com/letscontrolit/ESPEasy/pull/3579#issuecomment-967021347
  async = false;

  if (Settings.IncludeHiddenSSID()) {
    wifi_country_t config = {
      .cc = "01",
      .schan = 1,
      .nchan = 14,
      .policy = WIFI_COUNTRY_POLICY_MANUAL,
    };
    esp_wifi_set_country(&config);
  }


#endif

  START_TIMER;
  WiFiEventData.lastScanMoment.setNow();
  # ifndef BUILD_NO_DEBUG
  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    if (channel == 0) {
      addLog(LOG_LEVEL_INFO, F("WiFi : Start network scan all channels"));
    } else {
      addLogMove(LOG_LEVEL_INFO, strformat(F("WiFi : Start network scan ch: %d "), channel));
    }
  }
  #endif
  bool show_hidden         = true;
  WiFiEventData.processedScanDone = false;
  WiFiEventData.lastGetScanMoment.setNow();
  WiFiEventData.lastScanChannel = channel;

  unsigned int nrScans = 1 + (async ? 0 : Settings.NumberExtraWiFiScans);
  while (nrScans > 0) {
    if (!async) {
      WiFi_AP_Candidates.begin_sync_scan();
      FeedSW_watchdog();
    }
    --nrScans;
#ifdef ESP8266
#if FEATURE_ESP8266_DIRECT_WIFI_SCAN
    {
      static bool FIRST_SCAN = true;

      struct scan_config config;
      memset(&config, 0, sizeof(config));
      config.ssid = nullptr;
      config.bssid = nullptr;
      config.channel = channel;
      config.show_hidden = show_hidden ? 1 : 0;;
      config.scan_type = WIFI_SCAN_TYPE_ACTIVE;
      if (FIRST_SCAN) {
        config.scan_time.active.min = 100;
        config.scan_time.active.max = 200;
      } else {
        config.scan_time.active.min = 400;
        config.scan_time.active.max = 500;
      }
      FIRST_SCAN = false;
      wifi_station_scan(&config, &onWiFiScanDone);
      if (!async) {
        // will resume when SYSTEM_EVENT_SCAN_DONE event is fired
        do {
          delay(0);
        } while (!WiFiEventData.processedScanDone);
      }
 
    }
#else
    WiFi.scanNetworks(async, show_hidden, channel);
#endif
#endif
#ifdef ESP32
    const bool passive = Settings.PassiveWiFiScan();
    const uint32_t max_ms_per_chan = 120;
    WiFi.setScanTimeout(14 * max_ms_per_chan * 2);
    WiFi.scanNetworks(async, show_hidden, passive, max_ms_per_chan /*, channel */);
#endif
    if (!async) {
      FeedSW_watchdog();
      processScanDone();
    }
  }
#if FEATURE_TIMING_STATS
  if (async) {
    STOP_TIMER(WIFI_SCAN_ASYNC);
  } else {
    STOP_TIMER(WIFI_SCAN_SYNC);
  }
#endif

#ifdef ESP32
#if ESP_IDF_VERSION_MAJOR<5
  RTC.clearLastWiFi();
  if (WiFiConnected()) {
    # ifndef BUILD_NO_DEBUG
    addLog(LOG_LEVEL_INFO, F("WiFi : Disconnect after scan"));
    #endif

    const bool needReconnect = WiFiEventData.wifiConnectAttemptNeeded;
    WifiDisconnect();
    WiFiEventData.wifiConnectAttemptNeeded = needReconnect;
  }
#endif
#endif

}

// ********************************************************************************
// Scan all Wifi Access Points
// ********************************************************************************
void WiFiScan_log_to_serial()
{
  // Direct Serial is allowed here, since this function will only be called from serial input.
  serialPrintln(F("WIFI : SSID Scan start"));
  if (WiFi_AP_Candidates.scanComplete() <= 0) {
    WiFiMode_t cur_wifimode = WiFi.getMode();
    WifiScan(false);
    setWifiMode(cur_wifimode);
  }

  const int8_t scanCompleteStatus = WiFi_AP_Candidates.scanComplete();
  if (scanCompleteStatus <= 0) {
    serialPrintln(F("WIFI : No networks found"));
  }
  else
  {
    serialPrint(F("WIFI : "));
    serialPrint(String(scanCompleteStatus));
    serialPrintln(F(" networks found"));

    int i = 0;

    for (auto it = WiFi_AP_Candidates.scanned_begin(); it != WiFi_AP_Candidates.scanned_end(); ++it)
    {
      ++i;
      // Print SSID and RSSI for each network found
      serialPrint(F("WIFI : "));
      serialPrint(String(i));
      serialPrint(": ");
      serialPrintln(it->toString());
      delay(10);
    }
  }
  serialPrintln("");
}

// ********************************************************************************
// Manage Wifi Modes
// ********************************************************************************
void setSTA(bool enable) {
  switch (WiFi.getMode()) {
    case WIFI_OFF:

      if (enable) { setWifiMode(WIFI_STA); }
      break;
    case WIFI_STA:

      if (!enable) { setWifiMode(WIFI_OFF); }
      break;
    case WIFI_AP:

      if (enable) { setWifiMode(WIFI_AP_STA); }
      break;
    case WIFI_AP_STA:

      if (!enable) { setWifiMode(WIFI_AP); }
      break;
    default:
      break;
  }
}

void setAP(bool enable) {
  WiFiMode_t wifimode = WiFi.getMode();

  switch (wifimode) {
    case WIFI_OFF:

      if (enable) { 
        setWifiMode(WIFI_AP); 
      }
      break;
    case WIFI_STA:

      if (enable) { setWifiMode(WIFI_AP_STA); }
      break;
    case WIFI_AP:

      if (!enable) { setWifiMode(WIFI_OFF); }
      break;
    case WIFI_AP_STA:

      if (!enable) { setWifiMode(WIFI_STA); }
      break;
    default:
      break;
  }
}

// Only internal scope
void setAPinternal(bool enable)
{
  if (enable) {
    // create and store unique AP SSID/PW to prevent ESP from starting AP mode with default SSID and No password!
    // setup ssid for AP Mode when needed
    String softAPSSID = NetworkCreateRFCCompliantHostname();
    String pwd        = SecuritySettings.WifiAPKey;
    IPAddress subnet(DEFAULT_AP_SUBNET);

    if (!WiFi.softAPConfig(apIP, apIP, subnet)) {
      addLog(LOG_LEVEL_ERROR, strformat(
        ("WIFI : [AP] softAPConfig failed! IP: %s, GW: %s, SN: %s"),
        apIP.toString().c_str(), 
        apIP.toString().c_str(), 
        subnet.toString().c_str()
      )
      );
    }

    int channel = 1;
    if (WifiIsSTA(WiFi.getMode()) && WiFiConnected()) {
      channel = WiFi.channel();
    }

    if (WiFi.softAP(softAPSSID.c_str(), pwd.c_str(), channel)) {
      eventQueue.add(F("WiFi#APmodeEnabled"));
      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLogMove(LOG_LEVEL_INFO, strformat(
          F("WIFI : AP Mode enabled. SSID: %s IP: %s ch: %d"),
          softAPSSID.c_str(),
          formatIP(WiFi.softAPIP()).c_str(),
          channel));
      }
    } else {
      if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
        addLogMove(LOG_LEVEL_ERROR, strformat(
          F("WIFI : Error while starting AP Mode with SSID: %s IP: %s"),
          softAPSSID.c_str(),
          formatIP(apIP).c_str()));
      }
    }
    #ifdef ESP32

    #else // ifdef ESP32

    if (wifi_softap_dhcps_status() != DHCP_STARTED) {
      if (!wifi_softap_dhcps_start()) {
        addLog(LOG_LEVEL_ERROR, F("WIFI : [AP] wifi_softap_dhcps_start failed!"));
      }
    }
    #endif // ifdef ESP32
    WiFiEventData.timerAPoff.setMillisFromNow(WIFI_AP_OFF_TIMER_DURATION);
  } else {
    #if FEATURE_DNS_SERVER
    if (dnsServerActive) {
      dnsServerActive = false;
      dnsServer.stop();
    }
    #endif // if FEATURE_DNS_SERVER
  }
}

const __FlashStringHelper * getWifiModeString(WiFiMode_t wifimode)
{
  switch (wifimode) {
    case WIFI_OFF:   return F("OFF");
    case WIFI_STA:   return F("STA");
    case WIFI_AP:    return F("AP");
    case WIFI_AP_STA: return F("AP+STA");
    default:
      break;
  }
  return F("Unknown");
}

void setWifiMode(WiFiMode_t new_mode) {
  const WiFiMode_t cur_mode = WiFi.getMode();
  static WiFiMode_t processing_wifi_mode = cur_mode;
  if (cur_mode == new_mode) {
    return;
  }
  if (processing_wifi_mode == new_mode) {
    // Prevent loops
    return;
  }
  processing_wifi_mode = new_mode;

  if (cur_mode == WIFI_OFF) {
    #if defined(ESP32)
    // Needs to be set while WiFi is off
    WiFi.hostname(NetworkCreateRFCCompliantHostname());
    #endif
    WiFiEventData.markWiFiTurnOn();
  }
  if (new_mode != WIFI_OFF) {
    #ifdef ESP8266
    // See: https://github.com/esp8266/Arduino/issues/6172#issuecomment-500457407
    WiFi.forceSleepWake(); // Make sure WiFi is really active.
    #endif
    delay(100);
  } else {
    WifiDisconnect();
//    delay(100);
    processDisconnect();
    WiFiEventData.clear_processed_flags();
  }

  addLog(LOG_LEVEL_INFO, concat(F("WIFI : Set WiFi to "), getWifiModeString(new_mode)));

  int retry = 2;
  while (!WiFi.mode(new_mode) && retry > 0) {
    #ifndef BUILD_MINIMAL_OTA
    addLog(LOG_LEVEL_INFO, F("WIFI : Cannot set mode!!!!!"));
    #endif
    delay(100);
    --retry;
  }
  retry = 2;
  while (WiFi.getMode() != new_mode && retry > 0) {
    #ifndef BUILD_MINIMAL_OTA
    addLog(LOG_LEVEL_INFO, F("WIFI : mode not yet set"));
    #endif
    delay(100);
    --retry;
  }


  if (new_mode == WIFI_OFF) {
    WiFiEventData.markWiFiTurnOn();
    #if defined(ESP32)
    // Needs to be set while WiFi is off
    WiFi.hostname(NetworkCreateRFCCompliantHostname());
    #endif
    delay(100);
    #if defined(ESP32)
    esp_wifi_set_ps(WIFI_PS_NONE); 
//    esp_wifi_set_ps(WIFI_PS_MAX_MODEM);
    #endif
    #ifdef ESP8266
    WiFi.forceSleepBegin();
    #endif // ifdef ESP8266
    delay(1);
  } else {
    #ifdef ESP32
    if (cur_mode == WIFI_OFF) {
      registerWiFiEventHandler();
    }
    #endif
    // Only set power mode when AP is not enabled
    // When AP is enabled, the sleep mode is already set to WIFI_NONE_SLEEP
    if (!WifiIsAP(new_mode)) {
      if (Settings.WifiNoneSleep()) {
        #ifdef ESP8266
        WiFi.setSleepMode(WIFI_NONE_SLEEP);
        #endif
        #ifdef ESP32
        WiFi.setSleep(WIFI_PS_NONE);
        #endif
      } else if (Settings.EcoPowerMode()) {
        // Allow light sleep during idle times
        #ifdef ESP8266
        WiFi.setSleepMode(WIFI_LIGHT_SLEEP);
        #endif
        #ifdef ESP32
        // Maximum modem power saving. 
        // In this mode, interval to receive beacons is determined by the listen_interval parameter in wifi_sta_config_t
        // FIXME TD-er: Must test if this is desired behavior in ESP32.
        WiFi.setSleep(WIFI_PS_MAX_MODEM);
        #endif
      } else {
        // Default
        #ifdef ESP8266
        WiFi.setSleepMode(WIFI_MODEM_SLEEP);
        #endif
        #ifdef ESP32
        // Minimum modem power saving. 
        // In this mode, station wakes up to receive beacon every DTIM period
        WiFi.setSleep(WIFI_PS_MIN_MODEM);
        #endif
      }
    }
#if FEATURE_SET_WIFI_TX_PWR
    SetWiFiTXpower();
#endif
    if (WifiIsSTA(new_mode)) {
//      WiFi.setAutoConnect(Settings.SDK_WiFi_autoreconnect());
      WiFi.setAutoReconnect(Settings.SDK_WiFi_autoreconnect());
    }
    delay(100); // Must allow for some time to init.
  }
  const bool new_mode_AP_enabled = WifiIsAP(new_mode);

  if (WifiIsAP(cur_mode) && !new_mode_AP_enabled) {
    eventQueue.add(F("WiFi#APmodeDisabled"));
  }

  if (WifiIsAP(cur_mode) != new_mode_AP_enabled) {
    // Mode has changed
    setAPinternal(new_mode_AP_enabled);
  }
  #if FEATURE_MDNS
  #ifdef ESP8266
  // notifyAPChange() is not present in the ESP32 MDNSResponder
  MDNS.notifyAPChange();
  #endif
  #endif
}

bool WifiIsAP(WiFiMode_t wifimode)
{
  #if defined(ESP32)
  return (wifimode == WIFI_MODE_AP) || (wifimode == WIFI_MODE_APSTA);
  #else // if defined(ESP32)
  return (wifimode == WIFI_AP) || (wifimode == WIFI_AP_STA);
  #endif // if defined(ESP32)
}

bool WifiIsSTA(WiFiMode_t wifimode)
{
  #if defined(ESP32)
  return (wifimode & WIFI_MODE_STA) != 0;
  #else // if defined(ESP32)
  return (wifimode & WIFI_STA) != 0;
  #endif // if defined(ESP32)
}

bool WiFiUseStaticIP() {
  return Settings.IP[0] != 0 && Settings.IP[0] != 255;
}

bool wifiAPmodeActivelyUsed()
{
  if (!WifiIsAP(WiFi.getMode()) || (!WiFiEventData.timerAPoff.isSet())) {
    // AP not active or soon to be disabled in processDisableAPmode()
    return false;
  }
  return WiFi.softAPgetStationNum() != 0;

  // FIXME TD-er: is effectively checking for AP active enough or must really check for connected clients to prevent automatic wifi
  // reconnect?
}

void setConnectionSpeed() {
  #ifdef ESP8266
  // ESP8266 only supports 802.11g mode when running in STA+AP
  const bool forcedByAPmode = WifiIsAP(WiFi.getMode());
  WiFiPhyMode_t phyMode = (Settings.ForceWiFi_bg_mode() || forcedByAPmode) ? WIFI_PHY_MODE_11G : WIFI_PHY_MODE_11N;
  if (!forcedByAPmode) {
    const WiFi_AP_Candidate candidate = WiFi_AP_Candidates.getCurrent();
    if (candidate.phy_known() && (candidate.bits.phy_11g != candidate.bits.phy_11n)) {
      if ((WIFI_PHY_MODE_11G == phyMode) && !candidate.bits.phy_11g) {
        phyMode = WIFI_PHY_MODE_11N;
        #ifndef BUILD_MINIMAL_OTA
        addLog(LOG_LEVEL_INFO, F("WIFI : AP is set to 802.11n only"));
        #endif
      } else if ((WIFI_PHY_MODE_11N == phyMode) && !candidate.bits.phy_11n) {
        phyMode = WIFI_PHY_MODE_11G;
        #ifndef BUILD_MINIMAL_OTA
        addLog(LOG_LEVEL_INFO, F("WIFI : AP is set to 802.11g only"));
        #endif
      }      
    } else {
      bool useAlternate = WiFiEventData.connectionFailures > 10;
      if (useAlternate) {
        phyMode = (WIFI_PHY_MODE_11G == phyMode) ? WIFI_PHY_MODE_11N : WIFI_PHY_MODE_11G;
      }
    }
  } else {
    // No need to perform a next attempt.
    WiFi_AP_Candidates.markAttempt();
  }

  if (WiFi.getPhyMode() == phyMode) {
    return;
  }
  #ifndef BUILD_NO_DEBUG
  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    String log = concat(F("WIFI : Set to 802.11"), (WIFI_PHY_MODE_11G == phyMode) ? 'g' : 'n');
    if (forcedByAPmode) {
      log += (F(" (AP+STA mode)"));
    }
    if (Settings.ForceWiFi_bg_mode()) {
      log += F(" Force B/G mode");
    }
    addLogMove(LOG_LEVEL_INFO, log);
  }
  #endif
  WiFi.setPhyMode(phyMode);
  #endif // ifdef ESP8266

  // Does not (yet) work, so commented out.
  #ifdef ESP32

  // HT20 = 20 MHz channel width.
  // HT40 = 40 MHz channel width.
  // In theory, HT40 can offer upto 150 Mbps connection speed.
  // However since HT40 is using nearly all channels on 2.4 GHz WiFi,
  // Thus you are more likely to experience disturbances.
  // The response speed and stability is better at HT20 for ESP units.
  esp_wifi_set_bandwidth(WIFI_IF_STA, WIFI_BW_HT20);

  uint8_t protocol = WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G; // Default to BG

  if (!Settings.ForceWiFi_bg_mode() || (WiFiEventData.connectionFailures > 10)) {
    // Set to use BGN
    protocol |= WIFI_PROTOCOL_11N;
    #ifdef ESP32C6
    protocol |= WIFI_PROTOCOL_11AX;
    #endif
  }

  const WiFi_AP_Candidate candidate = WiFi_AP_Candidates.getCurrent();
  if (candidate.phy_known()) {
    // Check to see if the access point is set to "N-only"
    if ((protocol & WIFI_PROTOCOL_11N) == 0) {
      if (!candidate.bits.phy_11b && !candidate.bits.phy_11g && candidate.bits.phy_11n) {
        if (candidate.bits.phy_11n) {
          // Set to use BGN
          protocol |= WIFI_PROTOCOL_11N;
          addLog(LOG_LEVEL_INFO, F("WIFI : AP is set to 802.11n only"));
        }
#ifdef ESP32C6
        if (candidate.bits.phy_11ax) {
          // Set to use WiFi6
          protocol |= WIFI_PROTOCOL_11AX;
          addLog(LOG_LEVEL_INFO, F("WIFI : AP is set to 802.11ax"));
        }
#endif
      }
    }
  }


  if (WifiIsSTA(WiFi.getMode())) {
    // Set to use "Long GI" making it more resilliant to reflections
    // See: https://www.tp-link.com/us/configuration-guides/q_a_basic_wireless_concepts/?configurationId=2958#_idTextAnchor038
    esp_wifi_config_80211_tx_rate(WIFI_IF_STA, WIFI_PHY_RATE_MCS3_LGI);
    esp_wifi_set_protocol(WIFI_IF_STA, protocol);
  }

  if (WifiIsAP(WiFi.getMode())) {
    esp_wifi_set_protocol(WIFI_IF_AP, protocol);
  }
  #endif // ifdef ESP32
}

void setupStaticIPconfig() {
  setUseStaticIP(WiFiUseStaticIP());

  if (!WiFiUseStaticIP()) { return; }
  const IPAddress ip     (Settings.IP);
  const IPAddress gw     (Settings.Gateway);
  const IPAddress subnet (Settings.Subnet);
  const IPAddress dns    (Settings.DNS);

  WiFiEventData.dns0_cache = dns;

  WiFi.config(ip, gw, subnet, dns);

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO, strformat(
      F("IP   : Static IP : %s GW: %s SN: %s DNS: %s"),
      formatIP(ip).c_str(),
      formatIP(gw).c_str(),
      formatIP(subnet).c_str(),
      getValue(LabelType::DNS).c_str()));
  }
}

// ********************************************************************************
// Formatting WiFi related strings
// ********************************************************************************
String formatScanResult(int i, const String& separator) {
  int32_t rssi = 0;

  return formatScanResult(i, separator, rssi);
}

String formatScanResult(int i, const String& separator, int32_t& rssi) {
  WiFi_AP_Candidate tmp(i);
  rssi = tmp.rssi;
  return tmp.toString(separator);
}


void logConnectionStatus() {
  static unsigned long lastLog = 0;
  if (lastLog != 0 && timePassedSince(lastLog) < 1000) {
    return;
  }
  lastLog = millis();
#ifndef BUILD_NO_DEBUG
  #ifdef ESP8266
  const uint8_t arduino_corelib_wifistatus = WiFi.status();
  const uint8_t sdk_wifistatus             = wifi_station_get_connect_status();

  if ((arduino_corelib_wifistatus == WL_CONNECTED) != (sdk_wifistatus == STATION_GOT_IP)) {
    if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
      String log = F("WiFi : SDK station status differs from Arduino status. SDK-status: ");
      log += SDKwifiStatusToString(sdk_wifistatus);
      log += F(" Arduino status: ");
      log += ArduinoWifiStatusToString(arduino_corelib_wifistatus);
      addLogMove(LOG_LEVEL_ERROR, log);
    }
  }
  #endif

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO, strformat(
      F("WIFI : Arduino wifi status: %s ESPeasy internal wifi status: %s"),
      ArduinoWifiStatusToString(WiFi.status()).c_str(),
      WiFiEventData.ESPeasyWifiStatusToString().c_str()));
  }
/*
  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    String log;

    switch (WiFi.status()) {
      case WL_NO_SSID_AVAIL: {
        log = F("WIFI : No SSID found matching: ");
        break;
      }
      case WL_CONNECT_FAILED: {
        log = F("WIFI : Connection failed to: ");
        break;
      }
      case WL_DISCONNECTED: {
        log = F("WIFI : WiFi.status() = WL_DISCONNECTED  SSID: ");
        break;
      }
      case WL_IDLE_STATUS: {
        log = F("WIFI : Connection in IDLE state: ");
        break;
      }
      case WL_CONNECTED: {
        break;
      }
      default:
        break;
    }

    if (log.length() > 0) {
      const char *ssid = getLastWiFiSettingsSSID();
      log += ssid;
      addLog(LOG_LEVEL_INFO, log);
    }
  }
  */
#endif // ifndef BUILD_NO_DEBUG
}

#include "../ESPEasyCore/ESPEasy_setup.h"

#include "../../ESPEasy_fdwdecl.h" // Needed for PluginInit() and CPluginInit()

#include "../../ESPEasy-Globals.h"
#include "../../_Plugin_Helper.h"
#include "../Commands/InternalCommands_decoder.h"
#include "../CustomBuild/CompiletimeDefines.h"
#include "../ESPEasyCore/ESPEasyGPIO.h"
#include "../ESPEasyCore/ESPEasyNetwork.h"
#include "../ESPEasyCore/ESPEasyRules.h"
#include "../ESPEasyCore/ESPEasyWifi.h"
#include "../ESPEasyCore/ESPEasyWifi_ProcessEvent.h"
#include "../ESPEasyCore/Serial.h"
#include "../Globals/Cache.h"
#include "../Globals/ESPEasy_Console.h"
#include "../Globals/ESPEasyWiFiEvent.h"
#include "../Globals/ESPEasy_time.h"
#include "../Globals/NetworkState.h"
#include "../Globals/RTC.h"
#include "../Globals/Statistics.h"
#include "../Globals/WiFi_AP_Candidates.h"
#include "../Helpers/_CPlugin_init.h"
#include "../Helpers/_NPlugin_init.h"
#include "../Helpers/_Plugin_init.h"
#include "../Helpers/DeepSleep.h"
#include "../Helpers/ESPEasyRTC.h"
#include "../Helpers/ESPEasy_FactoryDefault.h"
#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/ESPEasy_checks.h"
#include "../Helpers/Hardware_device_info.h"
#include "../Helpers/Memory.h"
#include "../Helpers/Misc.h"
#include "../Helpers/StringGenerator_System.h"
#include "../WebServer/ESPEasy_WebServer.h"


#ifdef USE_RTOS_MULTITASKING
# include "../Helpers/Networking.h"
# include "../Helpers/PeriodicalActions.h"
#endif // ifdef USE_RTOS_MULTITASKING

#if FEATURE_ARDUINO_OTA
# include "../Helpers/OTA.h"
#endif // if FEATURE_ARDUINO_OTA

#ifdef ESP32

# if ESP_IDF_VERSION_MAJOR < 5
#  include <esp_pm.h>
#  include <soc/efuse_reg.h>
#  include <soc/boot_mode.h>
#  include <soc/gpio_reg.h>
# else // if ESP_IDF_VERSION_MAJOR < 5
#  include <hal/efuse_hal.h>
#  include <rom/gpio.h>
#  include <esp_pm.h>
# endif // if ESP_IDF_VERSION_MAJOR < 5

# if CONFIG_IDF_TARGET_ESP32
#  if ESP_IDF_VERSION_MAJOR < 5
#   include "hal/efuse_ll.h"
#   include "hal/efuse_hal.h"
#  else // if ESP_IDF_VERSION_MAJOR < 5
#   include <soc/efuse_defs.h>
#   include <bootloader_common.h>

#   if ESP_IDF_VERSION > ESP_IDF_VERSION_VAL(5, 2, 0)

// IDF5.3 fix esp_gpio_reserve used in init PSRAM.
#    include "esp_private/esp_gpio_reserve.h"
#   endif // if ESP_IDF_VERSION > ESP_IDF_VERSION_VAL(5, 2, 0)
#  endif // if ESP_IDF_VERSION_MAJOR < 5
# endif // if CONFIG_IDF_TARGET_ESP32

#endif // ifdef ESP32


#ifdef USE_RTOS_MULTITASKING

void RTOS_TaskServers(void *parameter)
{
  while (true) {
    delay(100);
    web_server.handleClient();
    # if FEATURE_ESPEASY_P2P
    checkUDP();
    # endif // if FEATURE_ESPEASY_P2P
  }
}

void RTOS_TaskSerial(void *parameter)
{
  while (true) {
    delay(100);
    serial();
  }
}

void RTOS_Task10ps(void *parameter)
{
  while (true) {
    delay(100);
    run10TimesPerSecond();
  }
}

void RTOS_HandleSchedule(void *parameter)
{
  while (true) {
    Scheduler.handle_schedule();
  }
}

#endif // ifdef USE_RTOS_MULTITASKING

/*********************************************************************************************\
* ISR call back function for handling the watchdog.
\*********************************************************************************************/
void sw_watchdog_callback(void *arg)
{
  yield(); // feed the WD
  ++sw_watchdog_callback_count;
}

/*********************************************************************************************\
* SETUP
\*********************************************************************************************/
void ESPEasy_setup()
{
#if defined(ESP8266_DISABLE_EXTRA4K) || defined(USE_SECOND_HEAP)

  //  disable_extra4k_at_link_time();
#endif // if defined(ESP8266_DISABLE_EXTRA4K) || defined(USE_SECOND_HEAP)
#ifdef PHASE_LOCKED_WAVEFORM
  enablePhaseLockedWaveform();
#endif // ifdef PHASE_LOCKED_WAVEFORM
#ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
#endif // ifdef USE_SECOND_HEAP
#ifdef ESP32
# ifdef DISABLE_ESP32_BROWNOUT
  DisableBrownout(); // Workaround possible weak LDO resulting in brownout detection during Wifi connection
# endif  // DISABLE_ESP32_BROWNOUT

# ifdef BOARD_HAS_PSRAM
  psramInit();
# endif // ifdef BOARD_HAS_PSRAM

# if CONFIG_IDF_TARGET_ESP32

  // restore GPIO16/17 if no PSRAM is found
  if (!FoundPSRAM()) {
    // test if the CPU is not pico
    #  if ESP_IDF_VERSION_MAJOR < 5
    uint32_t chip_ver    = REG_GET_FIELD(EFUSE_BLK0_RDATA3_REG, EFUSE_RD_CHIP_VER_PKG);
    uint32_t pkg_version = chip_ver & 0x7;
    #  else // if ESP_IDF_VERSION_MAJOR < 5
    uint32_t pkg_version = bootloader_common_get_chip_ver_pkg();
    uint32_t chip_ver    = REG_GET_FIELD(EFUSE_BLK0_RDATA3_REG, EFUSE_RD_CHIP_PACKAGE);
    #  endif // if ESP_IDF_VERSION_MAJOR < 5

    if (pkg_version <= 7) { // D0WD, S0WD, D2WD
#ifdef CORE32SOLO1
      gpio_num_t PSRAM_CLK = GPIO_NUM_NC; //GPIO_NUM_17;
      gpio_num_t PSRAM_CS  = GPIO_NUM_NC; //GPIO_NUM_16;
#else
      gpio_num_t PSRAM_CLK = static_cast<gpio_num_t>(CONFIG_D0WD_PSRAM_CLK_IO);
      gpio_num_t PSRAM_CS  = static_cast<gpio_num_t>(CONFIG_D0WD_PSRAM_CS_IO);

      switch (pkg_version)
      {
        case EFUSE_RD_CHIP_VER_PKG_ESP32U4WDH:
          // Very strange model as the same model exists as:
          // - Single core @160 MHz
          // - Dual   core @240 MHz
          // See: https://www.letscontrolit.com/forum/viewtopic.php?t=10735
          PSRAM_CLK = GPIO_NUM_NC; //GPIO_NUM_17;
          PSRAM_CS  = GPIO_NUM_NC; //GPIO_NUM_16;
          break;
        case EFUSE_RD_CHIP_VER_PKG_ESP32D2WDQ5:
          PSRAM_CLK = static_cast<gpio_num_t>(CONFIG_D2WD_PSRAM_CLK_IO);
          PSRAM_CS  = static_cast<gpio_num_t>(CONFIG_D2WD_PSRAM_CS_IO);
          break;
        case EFUSE_RD_CHIP_VER_PKG_ESP32PICOD4:
        case EFUSE_RD_CHIP_VER_PKG_ESP32PICOV302:
          PSRAM_CLK = static_cast<gpio_num_t>(GPIO_NUM_NC);
          PSRAM_CS  = static_cast<gpio_num_t>(CONFIG_PICO_PSRAM_CS_IO);
          break;
      }
#endif
#  if ESP_IDF_VERSION > ESP_IDF_VERSION_VAL(5, 2, 0)

      // Thanks Theo Arends from Tasmota
      if (PSRAM_CLK != GPIO_NUM_NC) {
        esp_gpio_revoke(BIT64(PSRAM_CLK) | BIT64(PSRAM_CS));
      } else {
        if (PSRAM_CS != GPIO_NUM_NC) {
          esp_gpio_revoke(BIT64(PSRAM_CS));
        }
      }
#  endif // if ESP_IDF_VERSION > ESP_IDF_VERSION_VAL(5, 2, 0)

      if (PSRAM_CLK != GPIO_NUM_NC) {
        gpio_reset_pin(PSRAM_CLK);
      }
      if (PSRAM_CS != GPIO_NUM_NC) {
        gpio_reset_pin(PSRAM_CS);
      }
    }
  }
# endif  // if CONFIG_IDF_TARGET_ESP32
  initADC();
#endif  // ESP32
#ifndef BUILD_NO_RAM_TRACKER
  lowestFreeStack = getFreeStackWatermark();
  lowestRAM       = FreeMem();
#endif // ifndef BUILD_NO_RAM_TRACKER

  /*
   #ifdef ESP32
     {
     ESPEasy_NVS_Helper preferences;
     ResetFactoryDefaultPreference.init(preferences);
     }
   #endif
   */
#ifndef BUILD_NO_DEBUG

  //  checkAll_internalCommands();
#endif // ifndef BUILD_NO_DEBUG

  PluginSetup();
  CPluginSetup();

  initWiFi();
  WiFiEventData.clearAll();

#ifndef BUILD_MINIMAL_OTA
  run_compiletime_checks();
#endif // ifndef BUILD_MINIMAL_OTA
#ifdef ESP8266

  //  ets_isr_attach(8, sw_watchdog_callback, nullptr);  // Set a callback for feeding the watchdog.
#endif // ifdef ESP8266


  // Read ADC at boot, before WiFi tries to connect.
  // see https://github.com/letscontrolit/ESPEasy/issues/2646
#if FEATURE_ADC_VCC
  vcc = ESP.getVcc() / 1000.0f;
#endif // if FEATURE_ADC_VCC
#ifdef ESP8266
  espeasy_analogRead(A0);
#endif // ifdef ESP8266

  initAnalogWrite();

  resetPluginTaskData();

  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("setup"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  ESPEasy_Console.begin(115200);

  // serialPrint("\n\n\nBOOOTTT\n\n\n");

  initLog();
  #ifndef BUILD_NO_RAM_TRACKER
  logMemUsageAfter(F("initLog()"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  #ifdef BOARD_HAS_PSRAM

  if (FoundPSRAM()) {
    if (UsePSRAM()) {
      addLog(LOG_LEVEL_INFO, F("Using PSRAM"));
    } else {
      addLog(LOG_LEVEL_ERROR, F("PSRAM found, unable to use"));
    }
  }
  #endif // ifdef BOARD_HAS_PSRAM

  if (SpiffsSectors() < 32)
  {
    serialPrintln(F("\nNo (or too small) FS area..\nSystem Halted\nPlease reflash with 128k FS minimum!"));

    while (true) {
      delay(1);
    }
  }

  emergencyReset();

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    String log = F("\n\n\rINIT : Booting version: ");
    log += getValue(LabelType::BINARY_FILENAME);
    log += F(", (");
    log += get_build_origin();
    log += F(") ");
    log += getValue(LabelType::GIT_BUILD);
    log += F(" (");
    log += getSystemLibraryString();
    log += ')';
    addLogMove(LOG_LEVEL_INFO, log);
    log  = F("INIT : Free RAM:");
    log += FreeMem();
    addLogMove(LOG_LEVEL_INFO, log);
  }

  readBootCause();

  {
    String log;

    if (readFromRTC())
    {
      RTC.bootFailedCount++;
      RTC.bootCounter++;
      lastMixedSchedulerId_beforereboot.mixed_id = RTC.lastMixedSchedulerId;
      readUserVarFromRTC();
      log = concat(F("INIT : "), getLastBootCauseString()) + 
            concat(F(" #"), RTC.bootCounter);

      #ifndef BUILD_NO_DEBUG
      log += F(" Last Action before Reboot: ");
      log += ESPEasy_Scheduler::decodeSchedulerId(lastMixedSchedulerId_beforereboot);
      log += F(" Last systime: ");
      log += RTC.lastSysTime;
      #endif // ifndef BUILD_NO_DEBUG
    }

    // cold boot (RTC memory empty)
    else
    {
      initRTC();

      // cold boot situation
      if (lastBootCause == BOOT_CAUSE_MANUAL_REBOOT) { // only set this if not set earlier during boot stage.
        lastBootCause = BOOT_CAUSE_COLD_BOOT;
      }
      log = F("INIT : Cold Boot");
    }

    log += concat(F(" - Restart Reason: "), getResetReasonString());

    RTC.deepSleepState = 0;
    saveToRTC();

    addLogMove(LOG_LEVEL_INFO, log);
  }
  #ifndef BUILD_NO_RAM_TRACKER
  logMemUsageAfter(F("RTC init"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  fileSystemCheck();
  #ifndef BUILD_NO_RAM_TRACKER
  logMemUsageAfter(F("fileSystemCheck()"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  //  progMemMD5check();
  LoadSettings();
#if FEATURE_DEFINE_SERIAL_CONSOLE_PORT
  ESPEasy_Console.reInit();
#endif // if FEATURE_DEFINE_SERIAL_CONSOLE_PORT

  #ifndef BUILD_NO_RAM_TRACKER
  logMemUsageAfter(F("LoadSettings()"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  addLog(LOG_LEVEL_INFO, concat(F("CPU Frequency: "), ESP.getCpuFreqMHz()));
  

#ifdef ESP32
#ifndef CORE32SOLO1

  // Configure dynamic frequency scaling:
  // maximum and minimum frequencies are set in sdkconfig,
  // automatic light sleep is enabled if tickless idle support is enabled.
  ESP_PM_CONFIG_T pm_config = {
    .max_freq_mhz = getCPU_MaxFreqMHz(),
    .min_freq_mhz = Settings.EcoPowerMode() ? getCPU_MinFreqMHz() : getCPU_MaxFreqMHz(),
# if CONFIG_FREERTOS_USE_TICKLESS_IDLE
    .light_sleep_enable = Settings.EcoPowerMode()
# else
    .light_sleep_enable = false
# endif // if CONFIG_FREERTOS_USE_TICKLESS_IDLE
  };
  esp_pm_configure(&pm_config);
#endif
#endif // ifdef ESP32


  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("hardwareInit"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  hardwareInit();
  #ifndef BUILD_NO_RAM_TRACKER
  logMemUsageAfter(F("hardwareInit()"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  node_time.restoreFromRTC();

  Settings.UseRTOSMultitasking = false; // For now, disable it, we experience heap corruption.

  if ((RTC.bootFailedCount > 10) && (RTC.bootCounter > 10)) {
    uint8_t toDisable = RTC.bootFailedCount - 10;
    toDisable = disablePlugin(toDisable);

    if (toDisable != 0) {
      toDisable = disableController(toDisable);
    }
    #if FEATURE_NOTIFIER

    if (toDisable != 0) {
      toDisable = disableNotification(toDisable);
    }
    #endif // if FEATURE_NOTIFIER

    if (toDisable != 0) {
      toDisable = disableRules(toDisable);
    }

    if (toDisable != 0) {
      toDisable = disableAllPlugins(toDisable);
    }

    if (toDisable != 0) {
      toDisable = disableAllControllers(toDisable);
    }
#if FEATURE_NOTIFIER

    if (toDisable != 0) {
      toDisable = disableAllNotifications(toDisable);
    }
#endif // if FEATURE_NOTIFIER
  }
  #if FEATURE_ETHERNET

  // This ensures, that changing WIFI OR ETHERNET MODE happens properly only after reboot. Changing without reboot would not be a good idea.
  // This only works after LoadSettings();
  // Do not call setNetworkMedium here as that may try to clean up settings.
  active_network_medium = Settings.NetworkMedium;
  #else // if FEATURE_ETHERNET

  if (Settings.NetworkMedium == NetworkMedium_t::Ethernet) {
    Settings.NetworkMedium = NetworkMedium_t::WIFI;
  }
  #endif // if FEATURE_ETHERNET

  setNetworkMedium(Settings.NetworkMedium);

  bool initWiFi = active_network_medium == NetworkMedium_t::WIFI;

  // FIXME TD-er: Must add another check for 'delayed start WiFi' for poorly designed ESP8266 nodes.


  if (initWiFi) {
#ifdef ESP32
    // FIXME TD-er: Disabled for now, as this may not return and thus block the ESP forever.
    // See: https://github.com/espressif/esp-idf/issues/15862
    //check_and_update_WiFi_Calibration();
#endif

    WiFi_AP_Candidates.clearCache();
    WiFi_AP_Candidates.load_knownCredentials();
    setSTA(true);

    if (!WiFi_AP_Candidates.hasCandidates()) {
      WiFiEventData.wifiSetup = true;
      RTC.clearLastWiFi(); // Must scan all channels
      // Wait until scan has finished to make sure as many as possible are found
      // We're still in the setup phase, so nothing else is taking resources of the ESP.
      WifiScan(false);
      WiFiEventData.lastScanMoment.clear();
    }

    // Always perform WiFi scan
    // It appears reconnecting from RTC may take just as long to be able to send first packet as performing a scan first and then connect.
    // Perhaps the WiFi radio needs some time to stabilize first?
    if (!WiFi_AP_Candidates.hasCandidates()) {
      WifiScan(false, RTC.lastWiFiChannel);
    }
    WiFi_AP_Candidates.clearCache();
    processScanDone();
    WiFi_AP_Candidates.load_knownCredentials();

    if (!WiFi_AP_Candidates.hasCandidates()) {
      #ifndef BUILD_MINIMAL_OTA
      addLog(LOG_LEVEL_INFO, F("Setup: Scan all channels"));
      #endif
      WifiScan(false);
    }

    //    setWifiMode(WIFI_OFF);
  }
  #ifndef BUILD_NO_RAM_TRACKER
  logMemUsageAfter(F("WifiScan()"));
  #endif // ifndef BUILD_NO_RAM_TRACKER


  //  setWifiMode(WIFI_STA);
  checkRuleSets();
  #ifndef BUILD_NO_RAM_TRACKER
  logMemUsageAfter(F("checkRuleSets()"));
  #endif // ifndef BUILD_NO_RAM_TRACKER


  // if different version, eeprom settings structure has changed. Full Reset needed
  // on a fresh ESP module eeprom values are set to 255. Version results into -1 (signed int)
  if ((Settings.Version != VERSION) || (Settings.PID != ESP_PROJECT_PID))
  {
    // Direct Serial is allowed here, since this is only an emergency task.
    serialPrint(F("\nPID:"));
    serialPrintln(String(Settings.PID));
    serialPrint(F("Version:"));
    serialPrintln(String(Settings.Version));
    serialPrintln(F("INIT : Incorrect PID or version!"));
    delay(1000);
    ResetFactory();
  }

  initSerial();
  #ifndef BUILD_NO_RAM_TRACKER
  logMemUsageAfter(F("initSerial()"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO, concat(F("INIT : Free RAM:"), FreeMem()));
  }

#ifndef BUILD_NO_DEBUG

  if (Settings.UseSerial && (Settings.SerialLogLevel >= LOG_LEVEL_DEBUG_MORE)) {
    ESPEasy_Console.setDebugOutput(true);
  }
#endif // ifndef BUILD_NO_DEBUG

  timermqtt_interval      = 100; // Interval for checking MQTT
  timerAwakeFromDeepSleep = millis();
  CPluginInit();
  #ifndef BUILD_NO_RAM_TRACKER
  logMemUsageAfter(F("CPluginInit()"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  #if FEATURE_NOTIFIER
  NPluginInit();
  # ifndef BUILD_NO_RAM_TRACKER
  logMemUsageAfter(F("NPluginInit()"));
  # endif
  #endif // if FEATURE_NOTIFIER

  PluginInit();

  initSerial(); // Plugins may have altered serial, so re-init serial

  #ifndef BUILD_NO_RAM_TRACKER
  logMemUsageAfter(F("PluginInit()"));
  #endif

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    String log;
    if (reserve_special(log, 80)) {
      log += concat(F("INFO : Plugins: "), getDeviceCount() + 1);
      log += ' ';
      log += getPluginDescriptionString();
      log += F(" (");
      log += getSystemLibraryString();
      log += ')';
      addLogMove(LOG_LEVEL_INFO, log);
    }
  }

  /*
     if ((getDeviceCount() + 1) >= PLUGIN_MAX) {
      addLog(LOG_LEVEL_ERROR, concat(F("Programming error! - Increase PLUGIN_MAX ("), getDeviceCount()) + ')');
     }
   */

  clearAllCaches();
  #ifndef BUILD_NO_RAM_TRACKER
  logMemUsageAfter(F("clearAllCaches()"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  if (Settings.UseRules && isDeepSleepEnabled())
  {
    String event = F("System#NoSleep=");
    event += Settings.deepSleep_wakeTime;
    rulesProcessing(event); // TD-er: Process events in the setup() now.
  }

  if (Settings.UseRules)
  {
    String event = F("System#Wake");
    rulesProcessing(event); // TD-er: Process events in the setup() now.
  }
  #ifdef ESP32

  if (Settings.UseRules)
  {
    const uint32_t gpio_strap =   GPIO_REG_READ(GPIO_STRAP_REG);

    //    BOOT_MODE_GET();

    // Event values:
    // ESP32   :  GPIO-5, GPIO-15, GPIO-4, GPIO-2, GPIO-0, GPIO-12
    // ESP32-C3:  bit 0: GPIO2, bit 2: GPIO8, bit 3: GPIO9
    // ESP32-S2: Unclear what bits represent which strapping state.
    // ESP32-S3: bit5 ~ bit2 correspond to strapping pins GPIO3, GPIO45, GPIO0, and GPIO46 respectively.
    String event = F("System#BootMode=");
    event += bitRead(gpio_strap, 0);
    event += ',';
    event += bitRead(gpio_strap, 1);
    event += ',';
    event += bitRead(gpio_strap, 2);
    event += ',';
    event += bitRead(gpio_strap, 3);
    event += ',';
    event += bitRead(gpio_strap, 4);
    event += ',';
    event += bitRead(gpio_strap, 5);
    rulesProcessing(event);
  }
  #endif // ifdef ESP32

  #if FEATURE_ETHERNET

  if (Settings.ETH_Pin_power_rst != -1) {
    GPIO_Write(PLUGIN_GPIO, Settings.ETH_Pin_power_rst, 1);
  }

  #endif // if FEATURE_ETHERNET

  NetworkConnectRelaxed();
  #ifndef BUILD_NO_RAM_TRACKER
  logMemUsageAfter(F("NetworkConnectRelaxed()"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  setWebserverRunning(true);
  #ifndef BUILD_NO_RAM_TRACKER
  logMemUsageAfter(F("setWebserverRunning()"));
  #endif // ifndef BUILD_NO_RAM_TRACKER


  #if FEATURE_REPORTING
  ReportStatus();
  #endif // if FEATURE_REPORTING

  #if FEATURE_ARDUINO_OTA
  ArduinoOTAInit();
  # ifndef BUILD_NO_RAM_TRACKER
  logMemUsageAfter(F("ArduinoOTAInit()"));
  # endif
  #endif // if FEATURE_ARDUINO_OTA

  if (node_time.systemTimePresent()) {
    node_time.initTime();
    #ifndef BUILD_NO_RAM_TRACKER
    logMemUsageAfter(F("node_time.initTime()"));
    #endif // ifndef BUILD_NO_RAM_TRACKER
  }

  if (Settings.UseRules)
  {
    String event = F("System#Boot");
    rulesProcessing(event); // TD-er: Process events in the setup() now.
    #ifndef BUILD_NO_RAM_TRACKER
    logMemUsageAfter(F("rulesProcessing(System#Boot)"));
    #endif // ifndef BUILD_NO_RAM_TRACKER
  }

  writeDefaultCSS();
  #ifndef BUILD_NO_RAM_TRACKER
  logMemUsageAfter(F("writeDefaultCSS()"));
  #endif // ifndef BUILD_NO_RAM_TRACKER


  #ifdef USE_RTOS_MULTITASKING
  UseRTOSMultitasking = Settings.UseRTOSMultitasking;

  if (UseRTOSMultitasking) {
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO, F("RTOS : Launching tasks"));
    }
    xTaskCreatePinnedToCore(RTOS_TaskServers, "RTOS_TaskServers", 16384, nullptr, 1, nullptr, 1);
    xTaskCreatePinnedToCore(RTOS_TaskSerial,  "RTOS_TaskSerial",  8192,  nullptr, 1, nullptr, 1);
    xTaskCreatePinnedToCore(RTOS_Task10ps,    "RTOS_Task10ps",    8192,  nullptr, 1, nullptr, 1);
    xTaskCreatePinnedToCore(
      RTOS_HandleSchedule,   /* Function to implement the task */
      "RTOS_HandleSchedule", /* Name of the task */
      16384,                 /* Stack size in words */
      nullptr,               /* Task input parameter */
      1,                     /* Priority of the task */
      nullptr,               /* Task handle. */
      1);                    /* Core where the task should run */
  }
  #endif // ifdef USE_RTOS_MULTITASKING

  // Start the interval timers at N msec from now.
  // Make sure to start them at some time after eachother,
  // since they will keep running at the same interval.
  Scheduler.setIntervalTimerOverride(SchedulerIntervalTimer_e::TIMER_20MSEC,     5);    // timer for periodic actions 50 x per/sec
  Scheduler.setIntervalTimerOverride(SchedulerIntervalTimer_e::TIMER_100MSEC,    66);   // timer for periodic actions 10 x per/sec
  Scheduler.setIntervalTimerOverride(SchedulerIntervalTimer_e::TIMER_1SEC,       777);  // timer for periodic actions once per/sec
  Scheduler.setIntervalTimerOverride(SchedulerIntervalTimer_e::TIMER_30SEC,      1333); // timer for watchdog once per 30 sec
  Scheduler.setIntervalTimerOverride(SchedulerIntervalTimer_e::TIMER_MQTT,       88);   // timer for interaction with MQTT
  Scheduler.setIntervalTimerOverride(SchedulerIntervalTimer_e::TIMER_STATISTICS, 2222);
  #ifndef BUILD_NO_RAM_TRACKER
  logMemUsageAfter(F("Scheduler.setIntervalTimerOverride"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
}


#include "../ESPEasyCore/ESPEasyWiFiEvent.h"

#if FEATURE_ETHERNET
#include <ETH.h>
#endif

#include "../DataStructs/RTCStruct.h"

#include "../DataTypes/ESPEasyTimeSource.h"

#include "../ESPEasyCore/ESPEasyEth.h"
#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../ESPEasyCore/ESPEasyNetwork.h"
#include "../ESPEasyCore/ESPEasyWifi.h"
#include "../ESPEasyCore/ESPEasyWifi_ProcessEvent.h"

#include "../Globals/ESPEasyWiFiEvent.h"
#include "../Globals/NetworkState.h"
#include "../Globals/RTC.h"
#include "../Globals/WiFi_AP_Candidates.h"

#include "../Helpers/ESPEasy_time_calc.h"


#if FEATURE_ETHERNET
#include "../Globals/ESPEasyEthEvent.h"
#endif


#ifdef ESP32
void setUseStaticIP(bool enabled) {
}

#endif // ifdef ESP32
#ifdef ESP8266
void WiFi_Access_Static_IP::set_use_static_ip(bool enabled) {
  _useStaticIp = enabled;
}
void setUseStaticIP(bool enabled) {
  WiFi_Access_Static_IP tmp_wifi;

  tmp_wifi.set_use_static_ip(enabled);
}

#endif // ifdef ESP8266




// ********************************************************************************
// Functions called on events.
// Make sure not to call anything in these functions that result in delay() or yield()
// ********************************************************************************
#ifdef ESP32
#include <WiFi.h>

static bool ignoreDisconnectEvent = false;

#if ESP_IDF_VERSION_MAJOR > 3
void WiFiEvent(WiFiEvent_t event, arduino_event_info_t info) {
  switch (event) {
    case ARDUINO_EVENT_WIFI_READY:
      // ESP32 WiFi ready
      break;
    case ARDUINO_EVENT_WIFI_STA_START:
    # ifndef BUILD_NO_DEBUG
      //addLog(LOG_LEVEL_INFO, F("WiFi : Event STA Started"));
    #endif
      break;
    case ARDUINO_EVENT_WIFI_STA_STOP:
    # ifndef BUILD_NO_DEBUG
      //addLog(LOG_LEVEL_INFO, F("WiFi : Event STA Stopped"));
    #endif
      break;
    case ARDUINO_EVENT_WIFI_AP_START:
    # ifndef BUILD_NO_DEBUG
      //addLog(LOG_LEVEL_INFO, F("WiFi : Event AP Started"));
    #endif
      break;
    case ARDUINO_EVENT_WIFI_AP_STOP:
    # ifndef BUILD_NO_DEBUG
      //addLog(LOG_LEVEL_INFO, F("WiFi : Event AP Stopped"));
    #endif
      break;
    case ARDUINO_EVENT_WIFI_STA_LOST_IP:
      // ESP32 station lost IP and the IP is reset to 0
      #if FEATURE_ETHERNET
      if (active_network_medium == NetworkMedium_t::Ethernet) {
        // DNS records are shared among WiFi and Ethernet (very bad design!)
        // So we must restore the DNS records for Ethernet in case we started with WiFi and then plugged in Ethernet.
        // As soon as WiFi is turned off, the DNS entry for Ethernet is cleared.
        EthEventData.markLostIP();
      }
      #endif // if FEATURE_ETHERNET
      WiFiEventData.markLostIP();
      # ifndef BUILD_NO_DEBUG
      //addLog(LOG_LEVEL_INFO, 
      /*
        active_network_medium == NetworkMedium_t::Ethernet ?
        F("ETH : Event Lost IP") :
      */
//         F("WiFi : Event Lost IP"));
      #endif
      break;

    case ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED:
      // Receive probe request packet in soft-AP interface
      // TODO TD-er: Must implement like onProbeRequestAPmode for ESP8266
      # ifndef BUILD_NO_DEBUG
      //addLog(LOG_LEVEL_INFO, F("WiFi : Event AP got probed"));
      #endif
      break;

    case ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE:
      #if ESP_IDF_VERSION_MAJOR > 3
      WiFiEventData.setAuthMode(info.wifi_sta_authmode_change.new_mode);
      #else
      WiFiEventData.setAuthMode(info.auth_change.new_mode);
      #endif
      break;

    case ARDUINO_EVENT_WIFI_STA_CONNECTED:
    {
      char ssid_copy[33]; // Ensure space for maximum len SSID (32) plus trailing 0
      #if ESP_IDF_VERSION_MAJOR > 3
      memcpy(ssid_copy, info.wifi_sta_connected.ssid, info.wifi_sta_connected.ssid_len);
      ssid_copy[32] = 0; // Potentially add 0-termination if none present earlier
      WiFiEventData.markConnected((const char*) ssid_copy, info.wifi_sta_connected.bssid, info.wifi_sta_connected.channel);
      WiFiEventData.setAuthMode(info.wifi_sta_connected.authmode);
      //addLog(LOG_LEVEL_INFO, F("WiFi : Event WIFI_STA_CONNECTED"));
      #else
      memcpy(ssid_copy, info.connected.ssid, info.connected.ssid_len);
      ssid_copy[32] = 0; // Potentially add 0-termination if none present earlier
      WiFiEventData.markConnected((const char*) ssid_copy, info.connected.bssid, info.connected.channel);
      #endif
      break;
    }
    case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
      if (!ignoreDisconnectEvent) {
        ignoreDisconnectEvent = true;
        #if ESP_IDF_VERSION_MAJOR > 3
        WiFiEventData.markDisconnect(static_cast<WiFiDisconnectReason>(info.wifi_sta_disconnected.reason));
        if (info.wifi_sta_disconnected.reason == WIFI_REASON_AUTH_EXPIRE) {
          // See: https://github.com/espressif/arduino-esp32/issues/8877#issuecomment-1807677897
          #if ESP_IDF_VERSION_MAJOR >= 5
          // FIXME TD-er: Should no longer be needed.
          WiFi.STA._setStatus(WL_CONNECTION_LOST);
          #else
          WiFiSTAClass::_setStatus(WL_CONNECTION_LOST);
          #endif
        }
        #else
        WiFiEventData.markDisconnect(static_cast<WiFiDisconnectReason>(info.disconnected.reason));
        if (info.disconnected.reason == WIFI_REASON_AUTH_EXPIRE) {
          // See: https://github.com/espressif/arduino-esp32/issues/8877#issuecomment-1807677897
          WiFiSTAClass::_setStatus(WL_CONNECTION_LOST);
        }
        #endif
        WiFi.persistent(false);
        WiFi.disconnect(true);
      }
      break;
    case ARDUINO_EVENT_WIFI_STA_GOT_IP:
      ignoreDisconnectEvent = false;
      WiFiEventData.markGotIP();
      break;
    #if FEATURE_USE_IPV6
    case ARDUINO_EVENT_WIFI_STA_GOT_IP6:
    {
      ip_event_got_ip6_t * event = static_cast<ip_event_got_ip6_t*>(&info.got_ip6);
      const IPAddress ip(IPv6, (const uint8_t*)event->ip6_info.ip.addr, event->ip6_info.ip.zone);
      WiFiEventData.markGotIPv6(ip);
      break;
    }
    case ARDUINO_EVENT_WIFI_AP_GOT_IP6:
      addLog(LOG_LEVEL_INFO, F("WIFI : AP got IP6"));
      break;
    #endif
    case ARDUINO_EVENT_WIFI_AP_STACONNECTED:
      #if ESP_IDF_VERSION_MAJOR > 3
      WiFiEventData.markConnectedAPmode(info.wifi_ap_staconnected.mac);
      #else
      WiFiEventData.markConnectedAPmode(info.sta_connected.mac);
      #endif
      break;
    case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED:
      #if ESP_IDF_VERSION_MAJOR > 3
      WiFiEventData.markDisconnectedAPmode(info.wifi_ap_stadisconnected.mac);
      #else
      WiFiEventData.markDisconnectedAPmode(info.sta_disconnected.mac);
      #endif
      break;
    case ARDUINO_EVENT_WIFI_SCAN_DONE:
      WiFiEventData.processedScanDone = false;
      break;
#if FEATURE_ETHERNET
    case ARDUINO_EVENT_ETH_START:
    case ARDUINO_EVENT_ETH_CONNECTED:
    case ARDUINO_EVENT_ETH_GOT_IP:
    case ARDUINO_EVENT_ETH_DISCONNECTED:
    case ARDUINO_EVENT_ETH_STOP:
    #if ESP_IDF_VERSION_MAJOR > 3
    case ARDUINO_EVENT_ETH_GOT_IP6:
    #else
    case ARDUINO_EVENT_GOT_IP6:
    #endif
      // Handled in EthEvent
      break;
#endif //FEATURE_ETHERNET
    default:
      {

        // addLogMove(LOG_LEVEL_ERROR, concat(F("UNKNOWN WIFI/ETH EVENT: "),  event));
      }
      break;
  }
}
#else
void WiFiEvent(system_event_id_t event, system_event_info_t info) {
  switch (event) {
    case SYSTEM_EVENT_WIFI_READY:
      // ESP32 WiFi ready
      break;
    case SYSTEM_EVENT_STA_START:
    # ifndef BUILD_NO_DEBUG
      //addLog(LOG_LEVEL_INFO, F("WiFi : Event STA Started"));
    #endif
      break;
    case SYSTEM_EVENT_STA_STOP:
    # ifndef BUILD_NO_DEBUG
      //addLog(LOG_LEVEL_INFO, F("WiFi : Event STA Stopped"));
    #endif
      break;
    case SYSTEM_EVENT_AP_START:
    # ifndef BUILD_NO_DEBUG
      //addLog(LOG_LEVEL_INFO, F("WiFi : Event AP Started"));
    #endif
      break;
    case SYSTEM_EVENT_AP_STOP:
    # ifndef BUILD_NO_DEBUG
      //addLog(LOG_LEVEL_INFO, F("WiFi : Event AP Stopped"));
    #endif
      break;
    case SYSTEM_EVENT_STA_LOST_IP:
      // ESP32 station lost IP and the IP is reset to 0
      #if FEATURE_ETHERNET
      if (active_network_medium == NetworkMedium_t::Ethernet) {
        EthEventData.markLostIP();
      }
      else
      #endif // if FEATURE_ETHERNET
      WiFiEventData.markLostIP();
      # ifndef BUILD_NO_DEBUG
      /*
      addLog(LOG_LEVEL_INFO, 
        active_network_medium == NetworkMedium_t::Ethernet ?
        F("ETH : Event Lost IP") : F("WiFi : Event Lost IP"));
      */
      #endif
      break;

    case SYSTEM_EVENT_AP_PROBEREQRECVED:
      // Receive probe request packet in soft-AP interface
      // TODO TD-er: Must implement like onProbeRequestAPmode for ESP8266
      # ifndef BUILD_NO_DEBUG
      //addLog(LOG_LEVEL_INFO, F("WiFi : Event AP got probed"));
      #endif
      break;

    case SYSTEM_EVENT_STA_AUTHMODE_CHANGE:
      #if ESP_IDF_VERSION_MAJOR > 3
      WiFiEventData.setAuthMode(info.wifi_sta_authmode_change.new_mode);
      #else
      WiFiEventData.setAuthMode(info.auth_change.new_mode);
      #endif
      break;

    case SYSTEM_EVENT_STA_CONNECTED:
    {
      char ssid_copy[33] = { 0 }; // Ensure space for maximum len SSID (32) plus trailing 0
      #if ESP_IDF_VERSION_MAJOR > 3
      memcpy(ssid_copy, info.wifi_sta_connected.ssid, info.wifi_sta_connected.ssid_len);
      ssid_copy[32] = 0; // Potentially add 0-termination if none present earlier
      WiFiEventData.markConnected((const char*) ssid_copy, info.wifi_sta_connected.bssid, info.wifi_sta_connected.channel);
      #else
      memcpy(ssid_copy, info.connected.ssid, info.connected.ssid_len);
      ssid_copy[32] = 0; // Potentially add 0-termination if none present earlier
      WiFiEventData.markConnected((const char*) ssid_copy, info.connected.bssid, info.connected.channel);
      #endif
      break;
    }
    case SYSTEM_EVENT_STA_DISCONNECTED:
      if (!ignoreDisconnectEvent) {
        ignoreDisconnectEvent = true;
        #if ESP_IDF_VERSION_MAJOR > 3
        WiFiEventData.markDisconnect(static_cast<WiFiDisconnectReason>(info.wifi_sta_disconnected.reason));
        #else
        WiFiEventData.markDisconnect(static_cast<WiFiDisconnectReason>(info.disconnected.reason));
        #endif
        WiFi.persistent(false);
        WiFi.disconnect(true);
      }
      break;
    case SYSTEM_EVENT_STA_GOT_IP:
      ignoreDisconnectEvent = false;
      WiFiEventData.markGotIP();
      break;
    case SYSTEM_EVENT_AP_STACONNECTED:
      #if ESP_IDF_VERSION_MAJOR > 3
      WiFiEventData.markConnectedAPmode(info.wifi_ap_staconnected.mac);
      #else
      WiFiEventData.markConnectedAPmode(info.sta_connected.mac);
      #endif
      break;
    case SYSTEM_EVENT_AP_STADISCONNECTED:
      #if ESP_IDF_VERSION_MAJOR > 3
      WiFiEventData.markDisconnectedAPmode(info.wifi_ap_stadisconnected.mac);
      #else
      WiFiEventData.markDisconnectedAPmode(info.sta_disconnected.mac);
      #endif
      break;
    case SYSTEM_EVENT_SCAN_DONE:
      WiFiEventData.processedScanDone = false;
      break;
#if FEATURE_ETHERNET
    case SYSTEM_EVENT_ETH_START:
      if (ethPrepare()) {
        //addLog(LOG_LEVEL_INFO, F("ETH event: Started"));
      } else {
        //addLog(LOG_LEVEL_ERROR, F("ETH event: Could not prepare ETH!"));
      }
      break;
    case SYSTEM_EVENT_ETH_CONNECTED:
      //addLog(LOG_LEVEL_INFO, F("ETH event: Connected"));
      EthEventData.markConnected();
      break;
    case SYSTEM_EVENT_ETH_GOT_IP:
      EthEventData.markGotIP();
      //addLog(LOG_LEVEL_INFO, F("ETH event: Got IP"));
      break;
    case SYSTEM_EVENT_ETH_DISCONNECTED:
      //addLog(LOG_LEVEL_ERROR, F("ETH event: Disconnected"));
      EthEventData.markDisconnect();
      break;
    case SYSTEM_EVENT_ETH_STOP:
      //addLog(LOG_LEVEL_INFO, F("ETH event: Stopped"));
      break;
    case SYSTEM_EVENT_GOT_IP6:
      //addLog(LOG_LEVEL_INFO, F("ETH event: Got IP6"));
      break;
#endif //FEATURE_ETHERNET
    default:
      {
        //addLogMove(LOG_LEVEL_ERROR, concat(F("UNKNOWN WIFI/ETH EVENT: "), event));
      }
      break;
  }
}

#endif

#endif // ifdef ESP32

#ifdef ESP8266

void onConnected(const WiFiEventStationModeConnected& event) {
  WiFiEventData.markConnected(event.ssid, event.bssid, event.channel);
}

void onDisconnect(const WiFiEventStationModeDisconnected& event) {
  WiFiEventData.markDisconnect(event.reason);
  if (WiFi.status() == WL_CONNECTED) {
    // See https://github.com/esp8266/Arduino/issues/5912
    WiFi.persistent(false);
    WiFi.disconnect(false);
    delay(0);
  }
}

void onGotIP(const WiFiEventStationModeGotIP& event) {
  WiFiEventData.markGotIP();
}

void onDHCPTimeout() {
  WiFiEventData.processedDHCPTimeout = false;
}

void onConnectedAPmode(const WiFiEventSoftAPModeStationConnected& event) {
  WiFiEventData.markConnectedAPmode(event.mac);
}

void onDisconnectedAPmode(const WiFiEventSoftAPModeStationDisconnected& event) {
  WiFiEventData.markDisconnectedAPmode(event.mac);
}

void onStationModeAuthModeChanged(const WiFiEventStationModeAuthModeChanged& event) {
  WiFiEventData.setAuthMode(event.newMode);
}

#if FEATURE_ESP8266_DIRECT_WIFI_SCAN
void onWiFiScanDone(void *arg, STATUS status) {
  if (status == OK) {
    auto *head = reinterpret_cast<bss_info *>(arg);
    int scanCount = 0;
    for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
      WiFi_AP_Candidates.process_WiFiscan(*it);
      ++scanCount;
    }
    WiFi_AP_Candidates.after_process_WiFiscan();
    WiFiEventData.lastGetScanMoment.setNow();
//    WiFiEventData.processedScanDone = true;
# ifndef BUILD_NO_DEBUG
/*
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, concat(F("WiFi : Scan finished (ESP8266), found: "), scanCount));
    }
*/
#endif
    WiFi_AP_Candidates.load_knownCredentials();
    if (WiFi_AP_Candidates.addedKnownCandidate() || !NetworkConnected()) {
      WiFiEventData.wifiConnectAttemptNeeded = true;
      # ifndef BUILD_NO_DEBUG
      if (WiFi_AP_Candidates.addedKnownCandidate()) {
        //addLog(LOG_LEVEL_INFO, F("WiFi : Added known candidate, try to connect"));
      }
      #endif
      NetworkConnectRelaxed();
    }

  }

  WiFiMode_t mode = WiFi.getMode();
  setWifiMode(WIFI_OFF);
  delay(1);
  setWifiMode(mode);
  delay(1);
}
#endif

#endif // ifdef ESP8266

#include "../ESPEasyCore/ESPEasyEth.h"

#if FEATURE_ETHERNET

#include "../CustomBuild/ESPEasyLimits.h"
#include "../ESPEasyCore/ESPEasyNetwork.h"
#include "../ESPEasyCore/ESPEasyWifi.h"
#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../ESPEasyCore/ESPEasyGPIO.h"
#include "../ESPEasyCore/ESPEasyEthEvent.h"
#include "../Globals/ESPEasyEthEvent.h"
#include "../Globals/NetworkState.h"
#include "../Globals/Settings.h"
#include "../Helpers/Hardware_GPIO.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/Networking.h"

#include <ETH.h>
#include <lwip/dns.h>
#if ESP_IDF_VERSION_MAJOR > 3
 #include <esp_eth_phy.h>
#else
 #include <eth_phy/phy.h>
#endif

#include <WiFi.h>

bool ethUseStaticIP() {
  return Settings.ETH_IP[0] != 0 && Settings.ETH_IP[0] != 255;
}

void ethSetupStaticIPconfig() {
  const IPAddress IP_zero(0, 0, 0, 0); 
  if (!ethUseStaticIP()) { 
    if (!ETH.config(IP_zero, IP_zero, IP_zero, IP_zero)) {
      addLog(LOG_LEVEL_ERROR, F("ETH  : Cannot set IP config"));
    }
    return; 
  }
  const IPAddress ip     = Settings.ETH_IP;
  const IPAddress gw     = Settings.ETH_Gateway;
  const IPAddress subnet = Settings.ETH_Subnet;
  const IPAddress dns    = Settings.ETH_DNS;

  EthEventData.dns0_cache = dns;
  EthEventData.dns1_cache = IP_zero;


  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    String log = F("ETH IP   : Static IP : ");
    log += formatIP(ip);
    log += F(" GW: ");
    log += formatIP(gw);
    log += F(" SN: ");
    log += formatIP(subnet);
    log += F(" DNS: ");
    log += formatIP(dns);
    addLogMove(LOG_LEVEL_INFO, log);
  }
  ETH.config(ip, gw, subnet, dns);
  setDNS(0, EthEventData.dns0_cache);
  setDNS(1, EthEventData.dns1_cache);
}

bool ethCheckSettings() {
  return isValid(Settings.ETH_Phy_Type) 
#if CONFIG_ETH_USE_ESP32_EMAC
      && (isValid(Settings.ETH_Clock_Mode)/* || isSPI_EthernetType(Settings.ETH_Phy_Type)*/)
#endif
      && isValid(Settings.NetworkMedium)
      && validGpio(Settings.ETH_Pin_mdc_cs)
      && (isSPI_EthernetType(Settings.ETH_Phy_Type) ||
          ( validGpio(Settings.ETH_Pin_mdio_irq) && 
            (validGpio(Settings.ETH_Pin_power_rst) || (Settings.ETH_Pin_power_rst == -1))
          )
        ); // Some boards have fixed power
}

bool ethPrepare() {
  char hostname[40];
  safe_strncpy(hostname, NetworkCreateRFCCompliantHostname().c_str(), sizeof(hostname));
  ETH.setHostname(hostname);
  ethSetupStaticIPconfig();
  return true;
}

void ethPrintSettings() {
  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    String log;
    if (log.reserve(115)) {
//    log += F("ETH/Wifi mode: ");
//    log += toString(active_network_medium);
      log += F("ETH PHY Type: ");
      log += toString(Settings.ETH_Phy_Type);
      log += F(" PHY Addr: ");
      log += Settings.ETH_Phy_Addr;

      if (!isSPI_EthernetType(Settings.ETH_Phy_Type)) {
        log += F(" Eth Clock mode: ");
        log += toString(Settings.ETH_Clock_Mode);
      }
      log += strformat(isSPI_EthernetType(Settings.ETH_Phy_Type) 
        ? F(" CS: %d IRQ: %d RST: %d") : F(" MDC: %d MIO: %d PWR: %d"),
        Settings.ETH_Pin_mdc_cs,
        Settings.ETH_Pin_mdio_irq,
        Settings.ETH_Pin_power_rst);
      addLogMove(LOG_LEVEL_INFO, log);
    }
  }
}

MAC_address ETHMacAddress() {
  MAC_address mac;
  if(!EthEventData.ethInitSuccess) {
    addLog(LOG_LEVEL_ERROR, F("Call NetworkMacAddress() only on connected Ethernet!"));
  } else {
    #if ESP_IDF_VERSION_MAJOR > 3
    ETH.macAddress(mac.mac);
    #else
    esp_eth_get_mac(mac.mac);
    #endif
  }
  return mac;
}

void removeEthEventHandler()
{
  WiFi.removeEvent(EthEventData.wm_event_id);
  EthEventData.wm_event_id = 0;
}

void registerEthEventHandler()
{
  if (EthEventData.wm_event_id != 0) {
    removeEthEventHandler();
  }
  EthEventData.wm_event_id = WiFi.onEvent(EthEvent);
}


bool ETHConnectRelaxed() {
  if (EthEventData.ethInitSuccess) {
    return EthLinkUp();
  }
  ethPrintSettings();
  if (!ethCheckSettings())
  {
    addLog(LOG_LEVEL_ERROR, F("ETH: Settings not correct!!!"));
    EthEventData.ethInitSuccess = false;
    return false;
  }
  // Re-register event listener
  removeEthEventHandler();

  ethPower(true);
  EthEventData.markEthBegin();

  // Re-register event listener
  registerEthEventHandler();

  if (!EthEventData.ethInitSuccess) {
#if ESP_IDF_VERSION_MAJOR < 5
    EthEventData.ethInitSuccess = ETH.begin( 
      Settings.ETH_Phy_Addr,
      Settings.ETH_Pin_power_rst,
      Settings.ETH_Pin_mdc_cs,
      Settings.ETH_Pin_mdio_irq,
      (eth_phy_type_t)Settings.ETH_Phy_Type,
      (eth_clock_mode_t)Settings.ETH_Clock_Mode);
#else
#if FEATURE_USE_IPV6
    if (Settings.EnableIPv6()) {
      ETH.enableIPv6(true);
    }
#endif

    if (isSPI_EthernetType(Settings.ETH_Phy_Type)) {
      spi_host_device_t SPI_host = Settings.getSPI_host();
      if (SPI_host == spi_host_device_t::SPI_HOST_MAX) {
        addLog(LOG_LEVEL_ERROR, F("SPI not enabled"));
        #ifdef ESP32C3
        // FIXME TD-er: Fallback for ETH01-EVO board
        SPI_host = spi_host_device_t::SPI2_HOST;
        Settings.InitSPI = static_cast<int>(SPI_Options_e::UserDefined);
        Settings.SPI_SCLK_pin = 7;
        Settings.SPI_MISO_pin = 3;
        Settings.SPI_MOSI_pin = 10;
        #endif
      }
      // else 
      {
#if ETH_SPI_SUPPORTS_CUSTOM
        EthEventData.ethInitSuccess = ETH.begin( 
          to_ESP_phy_type(Settings.ETH_Phy_Type),
          Settings.ETH_Phy_Addr,
          Settings.ETH_Pin_mdc_cs,
          Settings.ETH_Pin_mdio_irq,
          Settings.ETH_Pin_power_rst,
          SPI);
#else
        EthEventData.ethInitSuccess = ETH.begin( 
          to_ESP_phy_type(Settings.ETH_Phy_Type),
          Settings.ETH_Phy_Addr,
          Settings.ETH_Pin_mdc_cs,
          Settings.ETH_Pin_mdio_irq,
          Settings.ETH_Pin_power_rst,
          SPI_host,
          static_cast<int>(Settings.SPI_SCLK_pin),
          static_cast<int>(Settings.SPI_MISO_pin),
          static_cast<int>(Settings.SPI_MOSI_pin));
#endif
      }
    } else {
# if CONFIG_ETH_USE_ESP32_EMAC
    ethResetGPIOpins();
    EthEventData.ethInitSuccess = ETH.begin( 
      to_ESP_phy_type(Settings.ETH_Phy_Type),
      Settings.ETH_Phy_Addr,
      Settings.ETH_Pin_mdc_cs,
      Settings.ETH_Pin_mdio_irq,
      Settings.ETH_Pin_power_rst,
      (eth_clock_mode_t)Settings.ETH_Clock_Mode);
#endif
    }

#endif
  }
  if (EthEventData.ethInitSuccess) {
    // FIXME TD-er: Not sure if this is correctly set to false
    //EthEventData.ethConnectAttemptNeeded = false;

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
#if ESP_IDF_VERSION_MAJOR < 5
      addLog(LOG_LEVEL_INFO, strformat(
        F("ETH  : MAC: %s speed: %dM %s Link: %s"),
        ETH.macAddress().c_str(),
        ETH.linkSpeed(),
        String(ETH.fullDuplex() ? F("Full Duplex") : F("Half Duplex")).c_str(),
        String(ETH.linkUp() ? F("Up") : F("Down")).c_str()));
#else
      addLog(LOG_LEVEL_INFO, strformat(
        F("ETH  : MAC: %s phy addr: %d speed: %dM %s Link: %s"),
        ETH.macAddress().c_str(),
        ETH.phyAddr(),
        ETH.linkSpeed(),
        concat(
          ETH.fullDuplex() ? F("Full Duplex") : F("Half Duplex"),
          ETH.autoNegotiation() ? F("(auto)") : F("")).c_str(),
        String(ETH.linkUp() ? F("Up") : F("Down")).c_str()));
#endif
    }
    
    if (EthLinkUp()) {
      // We might miss the connected event, since we are already connected.
      EthEventData.markConnected();
    }
  } else {
    addLog(LOG_LEVEL_ERROR, F("ETH  : Failed to initialize ETH"));
  }
  return EthEventData.ethInitSuccess;
}

void ethPower(bool enable) {
  if (isSPI_EthernetType(Settings.ETH_Phy_Type)) 
    return;
  if (Settings.ETH_Pin_power_rst != -1) {
    if (GPIO_Internal_Read(Settings.ETH_Pin_power_rst) == enable) {
      // Already the desired state
      return;
    }
    addLog(LOG_LEVEL_INFO, enable ? F("ETH power ON") : F("ETH power OFF"));
    if (!enable) {
      EthEventData.ethInitSuccess = false;
      EthEventData.clearAll();
      #ifdef ESP_IDF_VERSION_MAJOR
      // FIXME TD-er: See: https://github.com/espressif/arduino-esp32/issues/6105
      // Need to store the last link state, as it will be cleared after destructing the object.
      EthEventData.setEthDisconnected();
      if (ETH.linkUp()) {
        EthEventData.setEthConnected();
      }
      #endif
//      ETH = ETHClass();
    }
    if (enable) {
//      ethResetGPIOpins();
    }
//    gpio_reset_pin((gpio_num_t)Settings.ETH_Pin_power_rst);

    GPIO_Write(PLUGIN_GPIO, Settings.ETH_Pin_power_rst, enable ? 1 : 0);
    if (!enable) {
      if (Settings.ETH_Clock_Mode == EthClockMode_t::Ext_crystal_osc) {
        delay(600); // Give some time to discharge any capacitors
        // Delay is needed to make sure no clock signal remains present which may cause the ESP to boot into flash mode.
      }
    } else {
      delay(400); // LAN chip needs to initialize before calling Eth.begin()
    }
  }
}

void ethResetGPIOpins() {
  if (isSPI_EthernetType(Settings.ETH_Phy_Type)) 
    return;

  // fix an disconnection issue after rebooting Olimex POE - this forces a clean state for all GPIO involved in RMII
  // Thanks to @s-hadinger and @Jason2866
  // Resetting state of power pin is done in ethPower()
  addLog(LOG_LEVEL_INFO, F("ethResetGPIOpins()"));
  gpio_reset_pin((gpio_num_t)Settings.ETH_Pin_mdc_cs);
  gpio_reset_pin((gpio_num_t)Settings.ETH_Pin_mdio_irq);
# if CONFIG_ETH_USE_ESP32_EMAC
  gpio_reset_pin(GPIO_NUM_19);    // EMAC_TXD0 - hardcoded
  gpio_reset_pin(GPIO_NUM_21);    // EMAC_TX_EN - hardcoded
  gpio_reset_pin(GPIO_NUM_22);    // EMAC_TXD1 - hardcoded
  gpio_reset_pin(GPIO_NUM_25);    // EMAC_RXD0 - hardcoded
  gpio_reset_pin(GPIO_NUM_26);    // EMAC_RXD1 - hardcoded
  gpio_reset_pin(GPIO_NUM_27);    // EMAC_RX_CRS_DV - hardcoded
#endif
  /*
  switch (Settings.ETH_Clock_Mode) {
    case EthClockMode_t::Ext_crystal_osc:       // ETH_CLOCK_GPIO0_IN
    case EthClockMode_t::Int_50MHz_GPIO_0:      // ETH_CLOCK_GPIO0_OUT
      gpio_reset_pin(GPIO_NUM_0);
      break;
    case EthClockMode_t::Int_50MHz_GPIO_16:     // ETH_CLOCK_GPIO16_OUT
      gpio_reset_pin(GPIO_NUM_16);
      break;
    case EthClockMode_t::Int_50MHz_GPIO_17_inv: // ETH_CLOCK_GPIO17_OUT
      gpio_reset_pin(GPIO_NUM_17);
      break;
  }
  */
  delay(1);
}

bool ETHConnected() {
  if (EthEventData.EthServicesInitialized()) {
    if (EthLinkUp()) {
      return true;
    }
    // Apparently we missed an event
    EthEventData.processedDisconnect = false;
  } else if (EthEventData.ethInitSuccess) {
    if (EthLinkUp()) {
      EthEventData.setEthConnected();
      if (NetworkLocalIP() != IPAddress(0, 0, 0, 0) && 
          !EthEventData.EthGotIP()) {
        EthEventData.processedGotIP = false;
      }
      if (EthEventData.lastConnectMoment.isSet()) {
        if (!EthEventData.EthServicesInitialized()) {
          if (EthEventData.lastConnectMoment.millisPassedSince() > 10000 &&
              EthEventData.lastGetIPmoment.isSet()) {
            EthEventData.processedGotIP = false;
            EthEventData.markLostIP();
          }
        }
      }
      return EthEventData.EthServicesInitialized();
    } else {
      if (EthEventData.last_eth_connect_attempt_moment.isSet() && 
          EthEventData.last_eth_connect_attempt_moment.millisPassedSince() < 5000) {
        return false;
      }
      setNetworkMedium(NetworkMedium_t::WIFI);
    }
  }
  return false;
}

#endif // if FEATURE_ETHERNET
#include "../ESPEasyCore/ESPEasy_Log.h"

#include "../DataStructs/LogStruct.h"
#include "../ESPEasyCore/Serial.h"
#include "../Globals/Cache.h"
#include "../Globals/ESPEasy_Console.h"
#include "../Globals/ESPEasyWiFiEvent.h"
#include "../Globals/Logging.h"
#include "../Globals/Settings.h"
#include "../Helpers/Networking.h"
#include "../Helpers/StringConverter.h"

#include <FS.h>

#if FEATURE_SD
#include <SD.h>
#include "../Helpers/ESPEasy_Storage.h"
#endif

#define UPDATE_LOGLEVEL_ACTIVE_CACHE_INTERVAL 5000

/********************************************************************************************\
  Init critical variables for logging (important during initial factory reset stuff )
  \*********************************************************************************************/
void initLog()
{
  //make sure addLog doesnt do any stuff before initalisation of Settings is complete.
  Settings.UseSerial=true;
  Settings.SyslogFacility=0;
  setLogLevelFor(LOG_TO_SYSLOG, 0);
  setLogLevelFor(LOG_TO_SERIAL, 2); //logging during initialisation
  setLogLevelFor(LOG_TO_WEBLOG, 2);
  setLogLevelFor(LOG_TO_SDCARD, 0);
}


/********************************************************************************************\
  Logging
  \*********************************************************************************************/
const __FlashStringHelper * getLogLevelDisplayString(int logLevel) {
  switch (logLevel) {
    case LOG_LEVEL_NONE:       return F("None");
    case LOG_LEVEL_ERROR:      return F("Error");
    case LOG_LEVEL_INFO:       return F("Info");
# ifndef BUILD_NO_DEBUG
    case LOG_LEVEL_DEBUG:      return F("Debug");
    case LOG_LEVEL_DEBUG_MORE: return F("Debug More");
    case LOG_LEVEL_DEBUG_DEV:  return F("Debug dev");
#endif

    default:
    break;
  }
  return F("");
}

const __FlashStringHelper * getLogLevelDisplayStringFromIndex(uint8_t index, int& logLevel) {
  switch (index) {
    case 0: logLevel = LOG_LEVEL_ERROR;      break;
    case 1: logLevel = LOG_LEVEL_INFO;       break;
# ifndef BUILD_NO_DEBUG
    case 2: logLevel = LOG_LEVEL_DEBUG;      break;
    case 3: logLevel = LOG_LEVEL_DEBUG_MORE; break;
    case 4: logLevel = LOG_LEVEL_DEBUG_DEV;  break;
#endif

    default: logLevel = -1; return F("");
  }
  return getLogLevelDisplayString(logLevel);
}

void disableSerialLog() {
  log_to_serial_disabled = true;
  setLogLevelFor(LOG_TO_SERIAL, 0);
}

void setLogLevelFor(uint8_t destination, uint8_t logLevel) {
  switch (destination) {
    case LOG_TO_SERIAL:
      if (!log_to_serial_disabled || logLevel == 0) {
        Settings.SerialLogLevel = logLevel; 
      }
      break;
    case LOG_TO_SYSLOG: Settings.SyslogLevel = logLevel;    break;
    case LOG_TO_WEBLOG: Settings.WebLogLevel = logLevel;    break;
    case LOG_TO_SDCARD: Settings.SDLogLevel = logLevel;     break;
    default:
      break;
  }
  updateLogLevelCache();
}

void updateLogLevelCache() {
  uint8_t max_lvl = 0;
  // FIXME TD-er: Must add check whether SW serial may be using the same pins as Serial0
  const bool useSerial = Settings.UseSerial && !activeTaskUseSerial0();
  if (log_to_serial_disabled) {
    if (useSerial) {
      ESPEasy_Console.setDebugOutput(false);
    }
  } else {
    max_lvl = _max(max_lvl, Settings.SerialLogLevel);
#ifndef BUILD_NO_DEBUG
    if (useSerial && Settings.SerialLogLevel >= LOG_LEVEL_DEBUG_MORE) {
      ESPEasy_Console.setDebugOutput(true);
    }
#endif
  }
  max_lvl = _max(max_lvl, Settings.SyslogLevel);
  if (Logging.logActiveRead()) {
    max_lvl = _max(max_lvl, Settings.WebLogLevel);
  }
#if FEATURE_SD
  max_lvl = _max(max_lvl, Settings.SDLogLevel);
#endif
  highest_active_log_level = max_lvl;
}

bool loglevelActiveFor(uint8_t logLevel) {
  #ifdef ESP32
  if (xPortInIsrContext()) {
    // When called from an ISR, you should not send out logs.
    // Allocating memory from within an ISR is a big no-no.
    // Also long-time blocking like sending logs (especially to a syslog server) 
    // is also really not a good idea from an ISR call.
    return false;
  }
  #endif
  static uint32_t lastUpdateLogLevelCache = 0;
  if (lastUpdateLogLevelCache == 0 || 
      timePassedSince(lastUpdateLogLevelCache) > UPDATE_LOGLEVEL_ACTIVE_CACHE_INTERVAL)
  {
    lastUpdateLogLevelCache = millis();
    updateLogLevelCache();
  }
  return logLevel <= highest_active_log_level;
}

uint8_t getSerialLogLevel() {
#if FEATURE_DEFINE_SERIAL_CONSOLE_PORT
  // FIXME TD-er: Must add check whether SW serial may be using the same pins as Serial0
  if (log_to_serial_disabled || !Settings.UseSerial) return 0;
#else
  if (log_to_serial_disabled || !Settings.UseSerial || activeTaskUseSerial0()) return 0;
#endif
  if (!(WiFiEventData.WiFiServicesInitialized())){
    if (Settings.SerialLogLevel < LOG_LEVEL_INFO) {
      return LOG_LEVEL_INFO;
    }
  }
  return Settings.SerialLogLevel;
}

uint8_t getWebLogLevel() {
  if (Logging.logActiveRead()) {
    return Settings.WebLogLevel;
  } 
  if (Settings.WebLogLevel != LOG_LEVEL_NONE) {
    updateLogLevelCache();
  }
  return LOG_LEVEL_NONE;
}

bool loglevelActiveFor(uint8_t destination, uint8_t logLevel) {
  #ifdef ESP32
  if (xPortInIsrContext()) {
    // When called from an ISR, you should not send out logs.
    // Allocating memory from within an ISR is a big no-no.
    // Also long-time blocking like sending logs (especially to a syslog server) 
    // is also really not a good idea from an ISR call.
    return false;
  }
  #endif

  uint8_t logLevelSettings = 0;
  switch (destination) {
    case LOG_TO_SERIAL: {
      logLevelSettings = getSerialLogLevel();
      break;
    }
    case LOG_TO_SYSLOG: {
      logLevelSettings = Settings.SyslogLevel;
      break;
    }
    case LOG_TO_WEBLOG: {
      logLevelSettings = getWebLogLevel();
      break;
    }
    case LOG_TO_SDCARD: {
      #if FEATURE_SD
      logLevelSettings = Settings.SDLogLevel;
      #endif
      break;
    }
    default:
      return false;
  }
  return logLevel <= logLevelSettings;
}

void addLog(uint8_t logLevel, const __FlashStringHelper *str)
{
  if (loglevelActiveFor(logLevel)) {
    String copy;
    if (!reserve_special(copy, strlen_P((PGM_P)str))) {
      return;
    }
    copy = str;
    addToLogMove(logLevel, std::move(copy));
  }
}

void addLog(uint8_t logLevel, const char *line)
{
  // Please note all functions called from here handling line must be PROGMEM aware.
  if (loglevelActiveFor(logLevel)) {

    String copy;
    #ifdef USE_SECOND_HEAP
    {
      // Allow to store the logs in 2nd heap if present.
      if (mmu_is_iram(line)) {
        size_t length = 0;
        const char* cur_char = line;
        bool copying = false;
        bool done = false;
        while (!done) {
          const uint8_t ch = mmu_get_uint8(cur_char++);
          if (ch == 0) {
            if (copying) {
              done = true;
            } else {
              if (!reserve_special(copy, length)) {
                return;
              }
              copying = true;
              cur_char = line;
            }
          } else {
            if (copying) {
              copy +=  (char)ch;
            } else {
              ++length;
            }
          }
        }
      } else {
        if (!reserve_special(copy, strlen_P((PGM_P)line))) {
          return;
        }
        copy = line;
      }
    }
    #else 
    if (!reserve_special(copy, strlen_P((PGM_P)line))) {
      return;
    }
    copy = line;
    #endif
    addToLogMove(logLevel, std::move(copy));
  }
}

void addLog(uint8_t logLevel, String&& string)
{
  addToLogMove(logLevel, std::move(string));
}


#ifndef LIMIT_BUILD_SIZE
#include "../Helpers/Memory.h"
#endif

void addToSerialLog(uint8_t logLevel, const String& string)
{
  if (loglevelActiveFor(LOG_TO_SERIAL, logLevel)) {
    ESPEasy_Console.addToSerialBuffer(format_msec_duration(millis()));
    #ifndef LIMIT_BUILD_SIZE
    ESPEasy_Console.addToSerialBuffer(strformat(F(" : (%d) "), FreeMem()));
    #endif
    {
      String loglevelDisplayString = getLogLevelDisplayString(logLevel);
      while (loglevelDisplayString.length() < 6) {
        loglevelDisplayString += ' ';
      }
      ESPEasy_Console.addToSerialBuffer(loglevelDisplayString);
    }
    ESPEasy_Console.addToSerialBuffer(F(" : "));
    ESPEasy_Console.addToSerialBuffer(string);
    ESPEasy_Console.addNewlineToSerialBuffer();
  }
}

void addToSysLog(uint8_t logLevel, const String& string)
{
  if (loglevelActiveFor(LOG_TO_SYSLOG, logLevel)) {
    sendSyslog(logLevel, string);
  }
}

void addToSDLog(uint8_t logLevel, const String& string)
{
#if FEATURE_SD
  if (!string.isEmpty() && loglevelActiveFor(LOG_TO_SDCARD, logLevel)) {
    String   logName = patch_fname(F("log.txt"));
    fs::File logFile = SD.open(logName, "a+");
    if (logFile) {
      const size_t stringLength = string.length();
      for (size_t i = 0; i < stringLength; ++i) {
        logFile.print(string[i]);
      }
      logFile.println();
    }
    logFile.close();
  }
#endif
}


void addLog(uint8_t logLevel, const String& string)
{
  #ifdef ESP32
  if (xPortInIsrContext()) {
    // When called from an ISR, you should not send out logs.
    // Allocating memory from within an ISR is a big no-no.
    // Also long-time blocking like sending logs (especially to a syslog server) 
    // is also really not a good idea from an ISR call.
    return;
  }
  #endif

  if (string.isEmpty()) return;
  addToSerialLog(logLevel, string);
  addToSysLog(logLevel, string);
  addToSDLog(logLevel, string);
  if (loglevelActiveFor(LOG_TO_WEBLOG, logLevel)) {
    Logging.add(logLevel, string);
  }
}

void addToLogMove(uint8_t logLevel, String&& string)
{
  #ifdef ESP32
  if (xPortInIsrContext()) {
    // When called from an ISR, you should not send out logs.
    // Allocating memory from within an ISR is a big no-no.
    // Also long-time blocking like sending logs (especially to a syslog server) 
    // is also really not a good idea from an ISR call.
    return;
  }
  #endif

  if (string.isEmpty()) return;
  String tmp;
  move_special(tmp, std::move(string));
  addToSerialLog(logLevel, tmp);
  addToSysLog(logLevel, tmp);
  addToSDLog(logLevel, tmp);

  // May clear the string, so call as last one.
  if (loglevelActiveFor(LOG_TO_WEBLOG, logLevel)) {
    Logging.add(logLevel, std::move(tmp));
  }
}


#include "../ESPEasyCore/ESPEasyGPIO.h"

/****************************************************************************/

// Central functions for GPIO handling
// **************************************************************************/
#include "../../_Plugin_Helper.h"
#include "../ESPEasyCore/ESPEasyRules.h"
#include "../Globals/GlobalMapPortStatus.h"
#include "../Helpers/PortStatus.h"


#include <GPIO_Direct_Access.h>


// ********************************************************************************
// Internal GPIO write
// ********************************************************************************
void GPIO_Internal_Write(int pin, uint8_t value)
{
  if (checkValidPortRange(PLUGIN_GPIO, pin)) {
    const uint32_t key = createKey(PLUGIN_GPIO, pin);
    auto it            = globalMapPortStatus.find(key);

    if (it != globalMapPortStatus.end()) {
      if (it->second.mode == PIN_MODE_PWM) {
        set_Gpio_PWM(pin, value);
      } else {
        // gpio_reset_pin(static_cast<gpio_num_t>(pin));
        pinMode(pin, OUTPUT);
        digitalWrite(pin, value);

        // DIRECT_PINMODE_OUTPUT(pin);
        // DIRECT_pinWrite(pin, value);
      }
    }
  }
}

// ********************************************************************************
// Internal GPIO read
// ********************************************************************************
bool GPIO_Internal_Read(int pin)
{
  if (checkValidPortRange(PLUGIN_GPIO, pin)) {
    return DIRECT_pinRead(pin) != 0u;
  }
  return false;
}

bool GPIO_Read_Switch_State(struct EventStruct *event) {
  const int pin = CONFIG_PIN1;

  if (checkValidPortRange(PLUGIN_GPIO, pin)) {
    const uint32_t key = createKey(PLUGIN_GPIO, pin);

    auto it = globalMapPortStatus.find(key);

    if (it != globalMapPortStatus.end()) {
      return GPIO_Read_Switch_State(pin, it->second.mode);
    }
  }
  return false;
}

bool GPIO_Read_Switch_State(int pin, uint8_t pinMode) {
  bool canRead = false;

  if (checkValidPortRange(PLUGIN_GPIO, pin)) {
    switch (pinMode)
    {
      case PIN_MODE_UNDEFINED:
      case PIN_MODE_INPUT:
      case PIN_MODE_INPUT_PULLUP:
      case PIN_MODE_OUTPUT:
        canRead = true;
        break;

      /*
            case PIN_MODE_PWM:
              break;
            case PIN_MODE_SERVO:
              break;
            case PIN_MODE_OFFLINE:
              break;
       */
      default:
        break;
    }
  }

  if (!canRead) { return false; }

  // Do not read from the pin while mode is set to PWM or servo.
  // See https://github.com/letscontrolit/ESPEasy/issues/2117#issuecomment-443516794
  return DIRECT_pinRead(pin) != 0u;
}

#ifdef USES_P009

// ********************************************************************************
// MCP23017 PIN read
// ********************************************************************************
int8_t GPIO_MCP_Read(int Par1)
{
  int8_t pinState = -1;

  if (checkValidPortRange(PLUGIN_MCP, Par1)) {
    uint8_t unit           = (Par1 - 1) / 16;
    uint8_t port           = Par1 - (unit * 16) - 1;
    uint8_t address        = 0x20 + unit;
    uint8_t IOBankValueReg = (port < 8) ? MCP23017_GPIOA : MCP23017_GPIOB;
    port = port % 8;

    uint8_t retValue;

    if (GPIO_MCP_ReadRegister(address, IOBankValueReg, &retValue)) {
      retValue = (retValue & (1 << port)) >> port;
      pinState = (retValue == 0) ? 0 : 1;

      /*
         // get the current pin status
         Wire.beginTransmission(address);
         Wire.write(IOBankValueReg); // IO data register
         Wire.endTransmission();
         Wire.requestFrom(address, (uint8_t)0x1);
         if (Wire.available())
         {
         state = (Wire.read() & (1 << port)) >> port;
         }
       */
    }
  }
  return pinState;
}

// ********************************************************************************
// MCP23017 read register
// ********************************************************************************

bool GPIO_MCP_ReadRegister(uint8_t mcpAddr, uint8_t regAddr, uint8_t *retValue) {
  bool success = false;

  // Read the register
  const uint8_t value = I2C_read8_reg(mcpAddr, regAddr, &success);

  if (success) {
    *retValue = value;
  }
  return success;
}

// ********************************************************************************
// MCP23017 write register
// ********************************************************************************

void GPIO_MCP_WriteRegister(uint8_t mcpAddr, uint8_t regAddr, uint8_t regValue) {
  // Write the register
  I2C_write8_reg(mcpAddr, regAddr, regValue);
}

// ********************************************************************************
// MCP23017 write pin
// ********************************************************************************
bool GPIO_MCP_Write(int Par1, uint8_t Par2)
{
  if (!checkValidPortRange(PLUGIN_MCP, Par1)) {
    return false;
  }
  bool success            = false;
  uint8_t unit            = (Par1 - 1) / 16;
  uint8_t port            = Par1 - (unit * 16) - 1;
  uint8_t address         = int((Par1 - 1) / 16) + 0x20;
  uint8_t IOBankConfigReg = (port < 8) ? MCP23017_IODIRA : MCP23017_IODIRB;
  uint8_t IOBankValueReg  = (port < 8) ? MCP23017_GPIOA : MCP23017_GPIOB;
  port = port % 8;

  uint8_t retValue;

  // turn this port into output, first read current config
  if (GPIO_MCP_ReadRegister(address, IOBankConfigReg, &retValue)) {
    retValue &= ~(1 << port); // change pin to output (0)

    // write new IO config
    GPIO_MCP_WriteRegister(address, IOBankConfigReg, retValue);

    if (GPIO_MCP_ReadRegister(address, IOBankValueReg, &retValue)) {
      (Par2 == 1) ? retValue |= (1 << port) : retValue &= ~(1 << port);

      // write new IO config
      GPIO_MCP_WriteRegister(address, IOBankValueReg, retValue);
      success = true;
    }
  }
  return success;

  /*
     uint8_t portvalue = 0;
     // turn this port into output, first read current config
     Wire.beginTransmission(address);
     Wire.write(IOBankConfigReg); // IO config register
     Wire.endTransmission();
     Wire.requestFrom(address, (uint8_t)0x1);
     if (Wire.available())
     {
      portvalue = Wire.read();
      portvalue &= ~(1 << port ); // change pin from (default) input to output

      // write new IO config
      Wire.beginTransmission(address);
      Wire.write(IOBankConfigReg); // IO config register
      Wire.write(portvalue);
      Wire.endTransmission();
     }
     // get the current pin status
     Wire.beginTransmission(address);
     Wire.write(IOBankValueReg); // IO data register
     Wire.endTransmission();
     Wire.requestFrom(address, (uint8_t)0x1);
     if (Wire.available())
     {
      portvalue = Wire.read();
      if (Par2 == 1)
        portvalue |= (1 << port);
      else
        portvalue &= ~(1 << port);

      // write back new data
      Wire.beginTransmission(address);
      Wire.write(IOBankValueReg);
      Wire.write(portvalue);
      Wire.endTransmission();
      success = true;
     }
     return(success);
   */
}

// ********************************************************************************
// MCP23017 config
// Par2: 0: Pullup disabled
// Par2: 1: Pullup enabled
// ********************************************************************************
bool setMCPInputAndPullupMode(uint8_t Par1, bool enablePullUp)
{
  if (!checkValidPortRange(PLUGIN_MCP, Par1)) {
    return false;
  }

  bool success            = false;
  uint8_t unit            = (Par1 - 1) / 16;
  uint8_t port            = Par1 - (unit * 16) - 1;
  uint8_t address         = 0x20 + unit;
  uint8_t IOBankPullUpReg = (port < 8) ? MCP23017_GPPUA : MCP23017_GPPUB;
  uint8_t IOBankIODirReg  = (port < 8) ? MCP23017_IODIRA : MCP23017_IODIRB;
  port = port % 8;

  uint8_t retValue;

  // set this port mode to INPUT (bit=1)
  if (GPIO_MCP_ReadRegister(address, IOBankIODirReg, &retValue)) {
    retValue |= (1 << port);
    GPIO_MCP_WriteRegister(address, IOBankIODirReg, retValue);

    // turn this port pullup on or off
    if (GPIO_MCP_ReadRegister(address, IOBankPullUpReg, &retValue)) {
      enablePullUp ? retValue |= (1 << port) : retValue &= ~(1 << port);
      GPIO_MCP_WriteRegister(address, IOBankPullUpReg, retValue);

      success = true;
    }
  }
  return success;
}

bool setMCPOutputMode(uint8_t Par1)
{
  if (!checkValidPortRange(PLUGIN_MCP, Par1)) {
    return false;
  }

  bool success = false;
  uint8_t retValue;
  uint8_t unit           = (Par1 - 1) / 16;
  uint8_t port           = Par1 - (unit * 16) - 1;
  uint8_t address        = 0x20 + unit;
  uint8_t IOBankIODirReg = (port < 8) ? MCP23017_IODIRA : MCP23017_IODIRB;
  port = port % 8;

  // set this port mode to OUTPUT (bit=0)
  if (GPIO_MCP_ReadRegister(address, IOBankIODirReg, &retValue)) {
    retValue &= ~(1 << port);
    GPIO_MCP_WriteRegister(address, IOBankIODirReg, retValue);
    success = true;
  }
  return success;
}

#endif // ifdef USES_P009

#ifdef USES_P019

// ********************************************************************************
// PCF8574 read pin
// ********************************************************************************
// @giig1967g-20181023: changed to int8_t
int8_t GPIO_PCF_Read(int Par1)
{
  int8_t state = -1;

  if (checkValidPortRange(PLUGIN_PCF, Par1)) {
    const uint8_t unit = (Par1 - 1) / 8;
    const uint8_t port = Par1 - (unit * 8) - 1;
    uint8_t address    = 0x20 + unit;

    if (unit > 7) { address += 0x10; }

    // get the current pin status
    bool is_ok          = false;
    const uint8_t value = I2C_read8(address, &is_ok);

    if (is_ok)
    {
      state = ((value & _BV(port)) >> (port));
    }
  }
  return state;
}

bool GPIO_PCF_ReadAllPins(uint8_t address, uint8_t *retValue)
{
  bool success = false;

  const uint8_t value = I2C_read8(address, &success);

  if (success) {
    *retValue = value;
  }
  return success;
}

// ********************************************************************************
// PCF8574 write pin
// *******************************************************************************
void GPIO_PCF_WriteAllPins(uint8_t address, uint8_t value)
{
  I2C_write8(address, value);
}

bool GPIO_PCF_Write(int Par1, uint8_t Par2)
{
  if (!checkValidPortRange(PLUGIN_PCF, Par1)) {
    return false;
  }
  uint8_t unit    = (Par1 - 1) / 8;
  uint8_t port    = Par1 - (unit * 8) - 1;
  uint8_t address = 0x20 + unit;

  if (unit > 7) { address += 0x10; }

  // generate bitmask
  int i            = 0;
  uint8_t portmask = 255;
  unit = unit * 8 + 1; // calculate first pin

  uint32_t key;

  // REMEMBER: all input pins must be set to 1 when writing to the unit
  for (i = 0; i < 8; i++) {
    key = createKey(PLUGIN_PCF, unit + i);

    auto it = globalMapPortStatus.find(key);

    if (it != globalMapPortStatus.end()) {
      if ((it->second.mode == PIN_MODE_OUTPUT) && (it->second.state == 0)) {
        portmask &= ~(1 << i); // set port i = 0
      }
    }
  }

  if (Par2 == 1) {
    portmask |= (1 << port);
  }
  else {
    portmask &= ~(1 << port);
  }

  GPIO_PCF_WriteAllPins(address, portmask);
  return true;
}

bool setPCFInputMode(uint8_t pin)
{
  if (!checkValidPortRange(PLUGIN_PCF, pin)) {
    return false;
  }
  uint8_t unit       = (pin - 1) / 8;
  const uint8_t port = pin - (unit * 8) - 1;
  uint8_t address    = 0x20 + unit;

  if (unit > 7) { address += 0x10; }

  // generate bitmask
  int i = 0;
  uint8_t portmask;

  if (GPIO_PCF_ReadAllPins(address, &portmask)) {
    unit = unit * 8 + 1; // calculate first pin
    uint32_t key;

    // REMEMBER: all input pins must be set to 1 when writing to the unit
    for (i = 0; i < 8; i++) {
      key = createKey(PLUGIN_PCF, unit + i);

      if (!existPortStatus(key) ||
          (existPortStatus(key) &&
           ((globalMapPortStatus[key].mode == PIN_MODE_INPUT_PULLUP) || (globalMapPortStatus[key].mode == PIN_MODE_INPUT))) ||
          (port == i)) {      // set to 1 the PIN to be set as INPUT
        portmask |= (1 << i); // set port i = 1
      }
    }

    GPIO_PCF_WriteAllPins(address, portmask);

    return true;
  } else {
    return false;
  }
}

#endif // ifdef USES_P019


// *********************************************************
// GPIO_Monitor10xSec:
// What it does:
// a) It updates the pinstate structure
// b) Returns an EVENT in case monitor is enabled
// c) Does not update the pinstate state if the port is defined in a task, as it will be picked up by thePLUGIN_TEN_PER_SECOND event
// called from: run10TimesPerSecond in ESPEasy.ino
// *********************************************************

void GPIO_Monitor10xSec()
{
  for (auto it = globalMapPortStatus.begin(); it != globalMapPortStatus.end(); ++it) {
    // only call monitor function if there is the need to
    if (it->second.monitor || it->second.command || it->second.init) {
      const uint16_t   gpioPort              = getPortFromKey(it->first);
      const pluginID_t pluginID              = getPluginFromKey(it->first);
      int8_t currentState                    = -1;
      const __FlashStringHelper *eventString = F("");
      bool caseFound                         = true;

      switch (pluginID.value)
      {
        case PLUGIN_GPIO_INT:
          currentState = GPIO_Read_Switch_State(gpioPort, it->second.mode);
          eventString  = F("GPIO");
          break;
#ifdef USES_P009
        case PLUGIN_MCP_INT:
          currentState = GPIO_MCP_Read(gpioPort);
          eventString  = F("MCP");
          break;
#endif // ifdef USES_P009
#ifdef USES_P019
        case PLUGIN_PCF_INT:
          currentState = GPIO_PCF_Read(gpioPort);
          eventString  = F("PCF");
          break;
#endif // ifdef USES_P019
        default:
          caseFound = false;
          break;
      }

      if (caseFound) {
        if (currentState == -1) {
          it->second.mode = PIN_MODE_OFFLINE;
        } else if (it->second.mode == PIN_MODE_OFFLINE) {
          it->second.mode = PIN_MODE_UNDEFINED;
        }

        if (((it->second.state != currentState) || (it->second.forceMonitor && it->second.monitor))) {
          // update state
          if (!it->second.task) {
            it->second.state = currentState; // update state ONLY if task flag=false otherwise it will not be picked up by 10xSEC function

            // send event if not task, otherwise is sent in the task PLUGIN_TEN_PER_SECOND
            if (it->second.monitor) {
              sendMonitorEvent(eventString, gpioPort, currentState);
            }
          }
        }
      }
      it->second.forceMonitor = 0; // reset flag
    }
  }
}

// prefix should be either "GPIO", "PCF", "MCP"
void sendMonitorEvent(const __FlashStringHelper *prefix, int port, int8_t state)
{
  String eventString = prefix;

  eventString += '#';
  eventString += port;
  eventString += '=';
  eventString += state;
  rulesProcessing(eventString);
}

bool checkValidPortRange(pluginID_t pluginID, int port)
{
  switch (pluginID.value)
  {
    case PLUGIN_GPIO_INT:
      return validGpio(port);

    case PLUGIN_MCP_INT:
    case PLUGIN_PCF_INT:
      return port >= 1 && port <= 128;
  }
  return false;
}

void setInternalGPIOPullupMode(uint8_t port)
{
  if (checkValidPortRange(PLUGIN_GPIO, port)) {
  #if defined(ESP8266)

    if (port == 16) {
      pinMode(port, INPUT_PULLDOWN_16);
    }
    else {
      pinMode(port, INPUT_PULLUP);
    }
  #else // if defined(ESP8266)
    pinMode(port, INPUT_PULLUP);
  #endif // if defined(ESP8266)
  }
}

bool GPIO_Write(pluginID_t pluginID, int port, uint8_t value, uint8_t pinMode)
{
  bool success = true;

  switch (pluginID.value)
  {
    case PLUGIN_GPIO_INT:

      if (pinMode == PIN_MODE_OUTPUT) {
        GPIO_Internal_Write(port, value);
      }
      else {
        success = false;
      }
      break;
    case PLUGIN_MCP_INT:
#ifdef USES_P009
      GPIO_MCP_Write(port, value);
#endif // ifdef USES_P009
      break;
    case PLUGIN_PCF_INT:
#ifdef USES_P019
      GPIO_PCF_Write(port, value);
#endif // ifdef USES_P019
      break;
    default:
      success = false;
  }

  if (success) {
    Scheduler.clearGPIOTimer(pluginID, port);
  }
  return success;
}

bool GPIO_Read(pluginID_t pluginID, int port, int8_t& value)
{
  bool success = true;

  switch (pluginID.value)
  {
    case PLUGIN_GPIO_INT:
      value = GPIO_Internal_Read(port);
      break;
    case PLUGIN_MCP_INT:
#ifdef USES_P009
      value = GPIO_MCP_Read(port);
#endif // ifdef USES_P009
      break;
    case PLUGIN_PCF_INT:
#ifdef USES_P019
      value = GPIO_PCF_Read(port);
#endif // ifdef USES_P019
      break;
    default:
      success = false;
  }
  return success;
}

#include "../ESPEasyCore/Controller.h"

#include "../../ESPEasy_common.h"
#include "../../ESPEasy-Globals.h"

#include "../../_Plugin_Helper.h"

#include "../ControllerQueue/MQTT_queue_element.h"

#include "../CustomBuild/Certificate_CA.h"

#include "../DataStructs/ControllerSettingsStruct.h"
#include "../DataStructs/ESPEasy_EventStruct.h"

#include "../DataTypes/ESPEasy_plugin_functions.h"
#include "../DataTypes/SPI_options.h"

#include "../ESPEasyCore/ESPEasyRules.h"
#include "../ESPEasyCore/Serial.h"

#include "../Globals/CPlugins.h"
#include "../Globals/Device.h"
#include "../Globals/ESPEasyWiFiEvent.h"
#include "../Globals/ESPEasy_Scheduler.h"
#include "../Globals/MQTT.h"
#include "../Globals/Plugins.h"
#include "../Globals/RulesCalculate.h"

#include "../Helpers/_CPlugin_Helper.h"

// #include "../Helpers/Memory.h"
#include "../Helpers/Misc.h"
#include "../Helpers/Network.h"
#include "../Helpers/PeriodicalActions.h"
#include "../Helpers/PortStatus.h"


constexpr pluginID_t PLUGIN_ID_MQTT_IMPORT(37);

// ********************************************************************************
// Interface for Sending to Controllers
// ********************************************************************************
void sendData(struct EventStruct *event, bool sendEvents)
{
  START_TIMER;
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("sendData"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  //  LoadTaskSettings(event->TaskIndex);

  if (Settings.UseRules && sendEvents) {
    createRuleEvents(event);
  }

  if (Settings.UseValueLogger && (Settings.InitSPI > static_cast<int>(SPI_Options_e::None)) && (Settings.Pin_sd_cs >= 0)) {
    SendValueLogger(event->TaskIndex);
  }

  //  LoadTaskSettings(event->TaskIndex); // could have changed during background tasks.

  for (controllerIndex_t x = 0; x < CONTROLLER_MAX; x++)
  {
    if (Settings.ControllerEnabled[x] &&
        Settings.TaskDeviceSendData[x][event->TaskIndex] &&
        Settings.Protocol[x])
    {
      event->ControllerIndex = x;
      const protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(event->ControllerIndex);
      event->idx = Settings.TaskDeviceID[x][event->TaskIndex];

      if (validUserVar(event)) {
        String dummy;
        CPluginCall(ProtocolIndex, CPlugin::Function::CPLUGIN_PROTOCOL_SEND, event, dummy);
      }
#ifndef BUILD_NO_DEBUG
      else {
        if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
          String log = F("Invalid value detected for controller ");
          log += getCPluginNameFromProtocolIndex(ProtocolIndex);
          addLogMove(LOG_LEVEL_DEBUG, log);
        }
      }
#endif // ifndef BUILD_NO_DEBUG
    }
  }

  lastSend = millis();
  STOP_TIMER(SEND_DATA_STATS);
}

bool validUserVar(struct EventStruct *event) {
  if (!validTaskIndex(event->TaskIndex)) { return false; }
  const Sensor_VType vtype = event->getSensorType();

  if (isIntegerOutputDataType(vtype) ||
      (vtype == Sensor_VType::SENSOR_TYPE_STRING)) // FIXME TD-er: Must look at length of event->String2 ?
  {
    return true;
  }
  const uint8_t valueCount = getValueCountForTask(event->TaskIndex);

  for (int i = 0; i < valueCount; ++i) {
    if (!UserVar.isValid(event->TaskIndex, i, vtype)) {
      return false;
    }
  }
  return true;
}

#if FEATURE_MQTT

/*********************************************************************************************\
* Handle incoming MQTT messages
\*********************************************************************************************/

// handle MQTT messages
void incoming_mqtt_callback(char *c_topic, uint8_t *b_payload, unsigned int length) {
  statusLED(true);
  controllerIndex_t enabledMqttController = firstEnabledMQTT_ControllerIndex();

  if (!validControllerIndex(enabledMqttController)) {
    addLog(LOG_LEVEL_ERROR, F("MQTT : No enabled MQTT controller"));
    return;
  }

  if (length > MQTT_MAX_PACKET_SIZE)
  {
    addLog(LOG_LEVEL_ERROR, F("MQTT : Ignored too big message"));
    return;
  }

  // TD-er: This one cannot set the TaskIndex, but that may seem to work out.... hopefully.
  protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(enabledMqttController);

  Scheduler.schedule_mqtt_controller_event_timer(
    ProtocolIndex,
    CPlugin::Function::CPLUGIN_PROTOCOL_RECV,
    c_topic, b_payload, length);

  deviceIndex_t DeviceIndex = getDeviceIndex(PLUGIN_ID_MQTT_IMPORT); // Check if P037_MQTTimport is present in the build

  if (validDeviceIndex(DeviceIndex)) {
    //  Here we loop over all tasks and call each 037 plugin with function PLUGIN_MQTT_IMPORT
    for (taskIndex_t taskIndex = 0; taskIndex < TASKS_MAX; taskIndex++)
    {
      if (Settings.TaskDeviceEnabled[taskIndex] && (Settings.getPluginID_for_task(taskIndex) == PLUGIN_ID_MQTT_IMPORT))
      {
        Scheduler.schedule_mqtt_plugin_import_event_timer(
          DeviceIndex, taskIndex, PLUGIN_MQTT_IMPORT,
          c_topic, b_payload, length);
      }
    }
  }
}

/*********************************************************************************************\
* Disconnect from MQTT message broker
\*********************************************************************************************/
void MQTTDisconnect()
{
  if (MQTTclient.connected()) {
    #if FEATURE_MQTT_CONNECT_BACKGROUND
    if (MQTT_task_data.taskHandle) {
      vTaskDelete(MQTT_task_data.taskHandle);
      MQTT_task_data.taskHandle = NULL;
    }
    MQTT_task_data.status = MQTT_connect_status_e::Disconnected;
    #endif // if FEATURE_MQTT_CONNECT_BACKGROUND
    MQTTclient.disconnect();
    addLog(LOG_LEVEL_INFO, F("MQTT : Disconnected from broker"));
  }
  updateMQTTclient_connected();
}

bool MQTTConnect_prepareClient(controllerIndex_t controller_idx) {
  MakeControllerSettings(ControllerSettings); // -V522

  if (!AllocatedControllerSettings()) {
    # ifndef BUILD_NO_DEBUG
    addLog(LOG_LEVEL_ERROR, F("MQTT : Cannot connect, out of RAM"));
    # endif // ifndef BUILD_NO_DEBUG
    return false;
  }
  LoadControllerSettings(controller_idx, *ControllerSettings);

  if (!ControllerSettings->checkHostReachable(true)) {
    return false;
  }

  if (MQTTclient.connected()) {
    #if FEATURE_MQTT_CONNECT_BACKGROUND
    if (MQTT_task_data.status != MQTT_connect_status_e::Connecting) {
      MQTT_task_data.status = MQTT_connect_status_e::Disconnected;
    }
    #endif // if FEATURE_MQTT_CONNECT_BACKGROUND
    MQTTclient.disconnect();
    # if FEATURE_MQTT_TLS

    if (mqtt_tls != nullptr) {
      delete mqtt_tls;
      mqtt_tls = nullptr;
    }
    mqtt_rootCA.clear();
    # endif // if FEATURE_MQTT_TLS
  }

  updateMQTTclient_connected();

  //  mqtt = WiFiClient(); // workaround see: https://github.com/esp8266/Arduino/issues/4497#issuecomment-373023864
  delay(0);
# if FEATURE_MQTT_TLS

  uint16_t mqttPort = ControllerSettings->Port;

  const TLS_types TLS_type = ControllerSettings->TLStype();

  if ((TLS_type != TLS_types::NoTLS) && (nullptr == mqtt_tls)) {
#  ifdef ESP32
  #   if MQTT_MAX_PACKET_SIZE > 2000
    mqtt_tls = new BearSSL::WiFiClientSecure_light(4096, 4096);
  #   else // if MQTT_MAX_PACKET_SIZE > 2000
    mqtt_tls = new BearSSL::WiFiClientSecure_light(2048, 2048);
  #   endif // if MQTT_MAX_PACKET_SIZE > 2000
#  else // ESP32 - ESP8266
    mqtt_tls = new BearSSL::WiFiClientSecure_light(1024, 1024);
#  endif // ifdef ESP32
    mqtt_rootCA.clear();

    if (mqtt_tls == nullptr) {
      mqtt_tls_last_errorstr = F("MQTT : Could not create TLS client, out of memory");
      addLog(LOG_LEVEL_ERROR, mqtt_tls_last_errorstr);
      return false;
    } else {
      mqtt_tls->setUtcTime_fcn(getUnixTime);
      mqtt_tls->setCfgTime_fcn(get_build_unixtime);
    }
    mqtt_tls_last_errorstr.clear();
    mqtt_tls_last_error = 0;
  }

  switch (TLS_type) {
    case TLS_types::NoTLS:
    {
      // Ignoring the ACK from the server is probably set for a reason.
      // For example because the server does not give an acknowledgement.
      // This way, we always need the set amount of timeout to handle the request.
      // Thus we should not make the timeout dynamic here if set to ignore ack.
      const uint32_t timeout = ControllerSettings->MustCheckReply
      ? WiFiEventData.getSuggestedTimeout(Settings.Protocol[controller_idx], ControllerSettings->ClientTimeout)
      : ControllerSettings->ClientTimeout;

  #  ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS

      // See: https://github.com/espressif/arduino-esp32/pull/6676
      mqtt.setTimeout((timeout + 500) / 1000); // in seconds!!!!
      Client *pClient = &mqtt;
      pClient->setTimeout(timeout);
  #  else // ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS
      mqtt.setTimeout(timeout); // in msec as it should be!
  #  endif // ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS
      MQTTclient.setClient(mqtt);
      MQTTclient.setKeepAlive(ControllerSettings->KeepAliveTime ? ControllerSettings->KeepAliveTime : CONTROLLER_KEEP_ALIVE_TIME_DFLT);
      MQTTclient.setSocketTimeout(timeout);
      break;
    }

    /*
       case TLS_types::TLS_PSK:
       {
       // if (mqtt_tls != nullptr)
       //  mqtt_tls->setPreSharedKey(const char *pskIdent, const char *psKey); // psKey in Hex
       break;
       }
     */
    case TLS_types::TLS_CA_CERT:
    {
      mqtt_rootCA.clear();

      /*
         // FIXME TD-er: Must convert rootCA from file to format accepted by bearSSL

         if (mqtt_rootCA.isEmpty() && (mqtt_tls != nullptr)) {
         LoadCertificate(ControllerSettings->getCertificateFilename(), mqtt_rootCA);

         if (mqtt_rootCA.isEmpty()) {
         // Fingerprint must be of some minimal length to continue.
         mqtt_tls_last_errorstr = F("MQTT : No TLS root CA");
         addLog(LOG_LEVEL_ERROR, mqtt_tls_last_errorstr);
         return false;
         }



         //mqtt_X509List.append(mqtt_rootCA.c_str());
         //        mqtt_tls->setTrustAnchors(&mqtt_X509List);
         }
       */
      if (mqtt_tls != nullptr) {
        mqtt_tls->setTrustAnchor(Tasmota_TA, Tasmota_TA_size);
      }
      break;
    }

    /*
       case TLS_types::TLS_CA_CLI_CERT:
       {
       //if (mqtt_tls != nullptr)
       //  mqtt_tls->setCertificate(const char *client_ca);
       break;
       }
     */
    case TLS_types::TLS_FINGERPRINT:
    {
      // Fingerprint is checked when making the connection.
      mqtt_rootCA.clear();
      mqtt_fingerprint.clear();
      LoadCertificate(ControllerSettings->getCertificateFilename(), mqtt_fingerprint, false);

      if (mqtt_fingerprint.length() < 32) {
        // Fingerprint must be of some minimal length to continue.
        mqtt_tls_last_errorstr = F("MQTT : Stored TLS fingerprint too small");
        addLog(LOG_LEVEL_ERROR, mqtt_tls_last_errorstr);
        return false;
      }

      if (mqtt_tls != nullptr) {
        mqtt_tls->setInsecure();
      }
      break;
    }
    case TLS_types::TLS_insecure:
    {
      mqtt_rootCA.clear();

      if (mqtt_tls != nullptr) {
        mqtt_tls->setTrustAnchor(Tasmota_TA, Tasmota_TA_size);
        mqtt_tls->setInsecure();
      }
      break;
    }
  }

  if ((TLS_type != TLS_types::NoTLS) && (mqtt_tls != nullptr)) {
    // Certificate expiry not enabled in Mbed TLS.
    //    mqtt_tls->setX509Time(node_time.getUnixTime());
    // Ignoring the ACK from the server is probably set for a reason.
    // For example because the server does not give an acknowledgement.
    // This way, we always need the set amount of timeout to handle the request.
    // Thus we should not make the timeout dynamic here if set to ignore ack.
    const uint32_t timeout = ControllerSettings->MustCheckReply
      ? WiFiEventData.getSuggestedTimeout(Settings.Protocol[controller_idx], ControllerSettings->ClientTimeout)
      : ControllerSettings->ClientTimeout;

    if (mqtt_tls_last_error == 296) {
      // in this special case of cipher mismatch, we force enable ECDSA
      // this would be the case for newer letsencrypt certificates now defaulting
      // to EC certificates requiring ECDSA instead of RSA
      mqtt_tls->setECDSA(true);
      addLog(LOG_LEVEL_INFO, F("MQTT : TLS now enabling ECDSA"));
    }


#  ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS

    // See: https://github.com/espressif/arduino-esp32/pull/6676
    mqtt_tls->setTimeout((timeout + 500) / 1000); // in seconds!!!!
    Client *pClient = mqtt_tls;
    pClient->setTimeout(timeout);
#  else // ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS
    mqtt_tls->setTimeout(timeout); // in msec as it should be!
#  endif // ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS

#  ifdef ESP8266
    mqtt_tls->setBufferSizes(1024, 1024);
#  endif // ifdef ESP8266
    MQTTclient.setClient(*mqtt_tls);
    MQTTclient.setKeepAlive(ControllerSettings->KeepAliveTime ? ControllerSettings->KeepAliveTime : CONTROLLER_KEEP_ALIVE_TIME_DFLT);
    MQTTclient.setSocketTimeout(timeout);


    if (mqttPort == 1883) {
      mqttPort = 8883;
    }
  } else {
    if (mqttPort == 8883) {
      mqttPort = 1883;
    }
  }

# else // if FEATURE_MQTT_TLS

  // Ignoring the ACK from the server is probably set for a reason.
  // For example because the server does not give an acknowledgement.
  // This way, we always need the set amount of timeout to handle the request.
  // Thus we should not make the timeout dynamic here if set to ignore ack.
  const uint32_t timeout = ControllerSettings->MustCheckReply
    ? WiFiEventData.getSuggestedTimeout(Settings.Protocol[controller_idx], ControllerSettings->ClientTimeout)
    : ControllerSettings->ClientTimeout;

#  ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS

  // See: https://github.com/espressif/arduino-esp32/pull/6676
  mqtt.setTimeout((timeout + 500) / 1000); // in seconds!!!!
  Client *pClient = &mqtt;
  pClient->setTimeout(timeout);
#  else // ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS
  mqtt.setTimeout(timeout); // in msec as it should be!
#  endif // ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS

  MQTTclient.setClient(mqtt);
  MQTTclient.setKeepAlive(ControllerSettings->KeepAliveTime ? ControllerSettings->KeepAliveTime : CONTROLLER_KEEP_ALIVE_TIME_DFLT);
  MQTTclient.setSocketTimeout(timeout);
# endif // if FEATURE_MQTT_TLS

  if (ControllerSettings->UseDNS) {
    #if !defined(BUILD_NO_DEBUG) && FEATURE_MQTT_CONNECT_BACKGROUND
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO, strformat(F("MQTT : Connecting to: %s:%u"), ControllerSettings->getHost().c_str(), ControllerSettings->Port));
    }
    #endif // if !defined(BUILD_NO_DEBUG) && FEATURE_MQTT_CONNECT_BACKGROUND
    MQTTclient.setServer(ControllerSettings->getHost().c_str(), ControllerSettings->Port);
  } else {
    #if !defined(BUILD_NO_DEBUG) && FEATURE_MQTT_CONNECT_BACKGROUND
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO, strformat(F("MQTT : Connecting to: %s:%u"), ControllerSettings->getIP().toString().c_str(), ControllerSettings->Port));
    }
    #endif // if !defined(BUILD_NO_DEBUG) && FEATURE_MQTT_CONNECT_BACKGROUND
    MQTTclient.setServer(ControllerSettings->getIP(), ControllerSettings->Port);
  }
  MQTTclient.setCallback(incoming_mqtt_callback);

  if (MQTTclient_should_reconnect) {
    addLog(LOG_LEVEL_ERROR, F("MQTT : Intentional reconnect"));
  }

  return true;
}

bool MQTTConnect_clientConnect(controllerIndex_t controller_idx) {
  MakeControllerSettings(ControllerSettings); // -V522

  if (!AllocatedControllerSettings()) {
    #ifndef BUILD_MINIMAL_OTA
    addLog(LOG_LEVEL_ERROR, F("MQTT : Cannot connect, out of RAM"));
    #endif
    return false;
  }
  LoadControllerSettings(controller_idx, *ControllerSettings);

  bool MQTTresult                   = false;

  // MQTT needs a unique clientname to subscribe to broker
  const String clientid = getMQTTclientID(*ControllerSettings);

  const String LWTTopic             = getLWT_topic(*ControllerSettings);
  const String LWTMessageDisconnect = getLWT_messageDisconnect(*ControllerSettings);
  const uint8_t willQos             = 0;
  const bool    willRetain          = ControllerSettings->mqtt_willRetain() && ControllerSettings->mqtt_sendLWT();
  // As suggested here: https://github.com/knolleary/pubsubclient/issues/458#issuecomment-493875150
  const bool    cleanSession        = ControllerSettings->mqtt_cleanSession();
  const bool    hasCredentials      = hasControllerCredentialsSet(controller_idx, *ControllerSettings);

  const uint64_t statisticsTimerStart(getMicros64());

  MQTTresult =
      MQTTclient.connect(clientid.c_str(),
                        hasCredentials ? getControllerUser(controller_idx, *ControllerSettings).c_str() : nullptr,
                        hasCredentials ? getControllerPass(controller_idx, *ControllerSettings).c_str() : nullptr,
                        ControllerSettings->mqtt_sendLWT() ? LWTTopic.c_str() : nullptr,
                        willQos,
                        willRetain,
                        ControllerSettings->mqtt_sendLWT() ? LWTMessageDisconnect.c_str() : nullptr,
                        cleanSession);
  delay(0);

  count_connection_results(
    MQTTresult,
    F("MQTT : Broker "),
    Settings.Protocol[controller_idx],
    statisticsTimerStart);

  return MQTTresult;
}

bool MQTTConnect_wrapUpConnect(controllerIndex_t controller_idx, bool MQTTresult) {
  MakeControllerSettings(ControllerSettings); // -V522

  if (!AllocatedControllerSettings()) {
    #ifndef BUILD_MINIMAL_OTA
    addLog(LOG_LEVEL_ERROR, F("MQTT : Cannot connect, out of RAM"));
    #endif
    return false;
  }
  LoadControllerSettings(controller_idx, *ControllerSettings);

  const String clientid   = getMQTTclientID(*ControllerSettings);
  const String LWTTopic   = getLWT_topic(*ControllerSettings);
  const bool   willRetain = ControllerSettings->mqtt_willRetain() && ControllerSettings->mqtt_sendLWT();

  # if FEATURE_MQTT_TLS

  const TLS_types TLS_type = ControllerSettings->TLStype();

  if (mqtt_tls != nullptr)
  {
    #  ifdef ESP32
    mqtt_tls_last_error = mqtt_tls->getLastError();
    mqtt_tls->clearLastError();
    mqtt_tls_last_cipher_suite = mqtt_tls->getLastCipherSuite();
    #  endif // ifdef ESP32

    // mqtt_tls_last_errorstr = buf;
    if (mqtt_tls_last_error == ERR_OOM) { mqtt_tls_last_errorstr = F("OutOfMemory"); }

    if (mqtt_tls_last_error == ERR_CANT_RESOLVE_IP) { mqtt_tls_last_errorstr = F("Can't resolve IP"); }

    if (mqtt_tls_last_error == ERR_TCP_CONNECT) { mqtt_tls_last_errorstr = F("TCP Connect error"); }

    //    if (mqtt_tls_last_error == ERR_MISSING_CA) mqtt_tls_last_errorstr = F("Missing CA");
    if (mqtt_tls_last_error == ERR_TLS_TIMEOUT) { mqtt_tls_last_errorstr = F("TLS Timeout"); }

#  ifndef BUILD_NO_DEBUG

    if (BR_TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 == mqtt_tls_last_cipher_suite) {
      addLog(LOG_LEVEL_DEBUG, F("TLS cipher suite: ECDHE_RSA_AES_128_GCM_SHA256"));
    } else if (BR_TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 == mqtt_tls_last_cipher_suite) {
      addLog(LOG_LEVEL_DEBUG, F("TLS cipher suite: ECDHE_ECDSA_AES_128_GCM_SHA256"));
    } else if (0 != mqtt_tls_last_cipher_suite) {
      addLog(LOG_LEVEL_DEBUG, strformat(F("TLS cipher suite: 0x%04X"), mqtt_tls_last_cipher_suite));
    }
#  endif // ifndef BUILD_NO_DEBUG
  }
  #  ifdef ESP32

  // FIXME TD-er: There seems to be no verify function in BearSSL used on ESP8266
  if (TLS_type == TLS_types::TLS_FINGERPRINT)
  {
    // Check fingerprint
    if (MQTTresult) {
      const int newlinepos = mqtt_fingerprint.indexOf('\n');
      String    fp;
      String    dn;

      if (ControllerSettings->UseDNS) { dn = ControllerSettings->getHost(); }

      if (newlinepos == -1) {
        fp = mqtt_fingerprint;
      } else {
        fp = mqtt_fingerprint.substring(0, newlinepos);
        const int newlinepos2 = mqtt_fingerprint.indexOf('\n', newlinepos);

        if (newlinepos2 == -1) {
          dn = mqtt_fingerprint.substring(newlinepos + 1);
        }
        else {
          dn = mqtt_fingerprint.substring(newlinepos + 1, newlinepos2);
        }
        dn.trim();
      }

      // FIXME TD-er: Must implement fingerprint verification

      /*
         if (mqtt_tls != nullptr) {
         if (!mqtt_tls->verify(
         fp.c_str(),
         dn.isEmpty() ? nullptr : dn.c_str()))
         {
         mqtt_tls_last_errorstr += F("TLS Fingerprint does not match");
         addLog(LOG_LEVEL_INFO, mqtt_fingerprint);
         MQTTresult = false;
         }
         }
       */
    }
  }
  #  endif // ifdef ESP32

  # endif  // if FEATURE_MQTT_TLS

  if (!MQTTresult) {
    # if FEATURE_MQTT_TLS

    if ((mqtt_tls_last_error != 0) && loglevelActiveFor(LOG_LEVEL_ERROR)) {
      String log = F("MQTT : TLS error code: ");
      log += mqtt_tls_last_error;
      log += ' ';
      log += mqtt_tls_last_errorstr;
      addLog(LOG_LEVEL_ERROR, log);
    }
    # endif // if FEATURE_MQTT_TLS

    #if FEATURE_MQTT_CONNECT_BACKGROUND
    if (MQTT_task_data.status != MQTT_connect_status_e::Connecting) {
      MQTT_task_data.status = MQTT_connect_status_e::Disconnected;
    }
    #endif // if FEATURE_MQTT_CONNECT_BACKGROUND
    MQTTclient.disconnect();
    # if FEATURE_MQTT_TLS

    if (mqtt_tls != nullptr) {
      mqtt_tls->stop();
    }
    # endif // if FEATURE_MQTT_TLS

    updateMQTTclient_connected();

    return false;
  }

  if (loglevelActiveFor(LOG_LEVEL_INFO))
  {
    addLogMove(LOG_LEVEL_INFO, concat(F("MQTT : Connected to broker with client ID: "), clientid));
  }

  # if FEATURE_MQTT_TLS
  #  ifdef ESP32

  // FIXME TD-er: Must get certificate info

  /*

     if ((mqtt_tls != nullptr) && loglevelActiveFor(LOG_LEVEL_INFO))
     {
     String log = F("MQTT : Peer certificate info: ");
     log += ControllerSettings->getHost();
     log += ' ';
     log += mqtt_tls->getPeerCertificateInfo();
     addLogMove(LOG_LEVEL_INFO, log);
     }
   */
  #  endif // ifdef ESP32
  # endif  // if FEATURE_MQTT_TLS

  MQTTparseSystemVariablesAndSubscribe(String(ControllerSettings->Subscribe));

  # if FEATURE_MQTT_DISCOVER

  if (ControllerSettings->mqtt_autoDiscovery()) {
    MQTTparseSystemVariablesAndSubscribe(String(ControllerSettings->MqttAutoDiscoveryTrigger));
  }
  # endif // if FEATURE_MQTT_DISCOVER

  updateMQTTclient_connected();
  statusLED(true);
  mqtt_reconnect_count = 0;

  // call all installed controller to publish autodiscover data
  if (MQTTclient_should_reconnect) { CPluginCall(CPlugin::Function::CPLUGIN_GOT_CONNECTED, 0); }
  MQTTclient_should_reconnect = false;

  if (ControllerSettings->mqtt_sendLWT()) {
    String LWTMessageConnect = getLWT_messageConnect(*ControllerSettings);

    if (!MQTTclient.publish(LWTTopic.c_str(), LWTMessageConnect.c_str(), willRetain)) {
      MQTTclient_must_send_LWT_connected = true;
    }
  }

  return true;
}

/*********************************************************************************************\
* Connect to MQTT message broker
\*********************************************************************************************/
bool MQTTConnect(controllerIndex_t controller_idx)
{
  if (MQTTclient_next_connect_attempt.isSet() && !MQTTclient_next_connect_attempt.timeoutReached(timermqtt_interval)) {
    return false;
  }
  MQTTclient_next_connect_attempt.setNow();
  ++mqtt_reconnect_count;

  if (!MQTTConnect_prepareClient(controller_idx)) {
    return false;
  }
  
  bool MQTTresult = MQTTConnect_clientConnect(controller_idx);

  return MQTTConnect_wrapUpConnect(controller_idx, MQTTresult);
}

void MQTTparseSystemVariablesAndSubscribe(String subscribeTo) {
  if (subscribeTo.isEmpty()) { return; }
  parseSystemVariables(subscribeTo, false);
  subscribeTo.trim();

  if (!subscribeTo.isEmpty()) {
    MQTTclient.subscribe(subscribeTo.c_str());
# ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, concat(F("MQTT : Subscribed to: "),  subscribeTo));
    }
# endif // ifndef BUILD_NO_DEBUG
  }
}

String getMQTTclientID(const ControllerSettingsStruct& ControllerSettings) {
  String clientid = ControllerSettings.ClientID;

  if (clientid.isEmpty()) {
    // Try to generate some default
    clientid = F(CONTROLLER_DEFAULT_CLIENTID);
  }
  parseSystemVariables(clientid, false);
  clientid.replace(' ', '_'); // Make sure no spaces are present in the client ID

  if ((WiFiEventData.wifi_reconnects >= 1) && ControllerSettings.mqtt_uniqueMQTTclientIdReconnect()) {
    // Work-around for 'lost connections' to the MQTT broker.
    // If the broker thinks the connection is still alive, a reconnect from the
    // client will be refused.
    // To overcome this issue, append the number of reconnects to the client ID to
    // make it different from the previous one.
    clientid += '_';
    clientid += WiFiEventData.wifi_reconnects;
  }
  return clientid;
}

#if FEATURE_MQTT_CONNECT_BACKGROUND
void MQTT_execute_connect_task(void *parameter)
{
  MQTT_connect_request*MQTT_task_data = static_cast<MQTT_connect_request *>(parameter);

  uint32_t timeout = MQTT_task_data->timeout;
  uint8_t  incr    = 5; // Increment timeout between connection attempts 5 times by 100 msec

  while (!MQTTConnect_clientConnect(MQTT_task_data->ControllerIndex) && !MQTTclient.connected()) {
    const TickType_t xDelay = timeout / portTICK_PERIOD_MS;
    vTaskDelay(xDelay); // Use regular controller timeout also for delay between connection attempts (range 10..4000)
    if (incr > 0) {
      timeout += 100; // Increment next few (5) times with 100 msec
      incr--;
    }
    if (timePassedSince(MQTT_task_data->startTime) > 120000) { // Quit after 120 seconds
      break;
    }
  }
  MQTT_task_data->result     = MQTTclient.connected();
  MQTT_task_data->status     = MQTT_task_data->result
                               ? MQTT_connect_status_e::Connected
                               : MQTT_connect_status_e::Failure;
  MQTT_task_data->endTime    = millis();
  MQTT_task_data->taskHandle = NULL;
  vTaskDelete(MQTT_task_data->taskHandle);
}

bool MQTTConnectInBackground(controllerIndex_t controller_idx, bool reportOnly) {
  if (!Settings.MQTTConnectInBackground()) { return false; }

  if ((MQTT_task_data.status == MQTT_connect_status_e::Connected) || (MQTT_task_data.status == MQTT_connect_status_e::Failure)) {
    MQTT_task_data.status = MQTT_connect_status_e::Ready; // Set status first to avoid re-entry

    const bool result = MQTTConnect_wrapUpConnect(MQTT_task_data.ControllerIndex, MQTT_task_data.result);

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO, strformat(F("MQTT : Background Connect request %s, took %d msec"),
                                       FsP(MQTT_task_data.result ? F("success") : F("FAILED")),
                                       MQTT_task_data.endTime - MQTT_task_data.startTime
                                      ));
    }

    return result && MQTT_task_data.result;
  }
  
  if ((MQTT_task_data.status == MQTT_connect_status_e::Ready) && MQTTclient.connected()) {
    if (!MQTT_task_data.logged && loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO, F("MQTT : Background Connect request Ready"));
    }
    MQTT_task_data.logged = true;
    return MQTT_task_data.result;
  }

  if (MQTT_task_data.status == MQTT_connect_status_e::Connecting) {
    if (timePassedSince(MQTT_task_data.loopTime) > 1000) {
      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLog(LOG_LEVEL_INFO, strformat(F("MQTT : Background waiting to Connect, %d sec"),
                                         timePassedSince(MQTT_task_data.startTime) / 1000
                                        ));
      }
      MQTT_task_data.loopTime = millis();
    }
    return false; // Not ready yet
  }

  if ((MQTT_task_data.status == MQTT_connect_status_e::Ready) &&
      !MQTTclient.connected() &&
      NetworkConnected(10)) { // Unexpected network disconnect and reconnect?
    MQTT_task_data.status = MQTT_connect_status_e::Disconnected;
    reportOnly            = false; // Reconnect ASAP
    if (CONTROLLER_MAX == controller_idx) {
      controller_idx = firstEnabledMQTT_ControllerIndex();
    }
  }

  if (!reportOnly && (MQTT_task_data.status == MQTT_connect_status_e::Disconnected) && (controller_idx < CONTROLLER_MAX)) {
    if (MQTTclient_next_connect_attempt.isSet() && !MQTTclient_next_connect_attempt.timeoutReached(timermqtt_interval)) {
      return false;
    }
    MQTTclient_next_connect_attempt.setNow();
    ++mqtt_reconnect_count;

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO, strformat(F("MQTT : Start background connect for controller %d"), controller_idx + 1));
    }
    MQTT_task_data.result          = false;

    if (MQTTConnect_prepareClient(controller_idx)) {
      MakeControllerSettings(ControllerSettings); // -V522

      if (!AllocatedControllerSettings()) {
        #ifndef BUILD_MINIMAL_OTA
        addLog(LOG_LEVEL_ERROR, F("MQTT : Cannot load Controller settings, out of RAM"));
        #endif
        return false;
      }
      LoadControllerSettings(controller_idx, *ControllerSettings);

      const uint32_t timeout = ControllerSettings->MustCheckReply
                               ? WiFiEventData.getSuggestedTimeout(Settings.Protocol[controller_idx], ControllerSettings->ClientTimeout)
                               : ControllerSettings->ClientTimeout;

      MQTT_task_data.status          = MQTT_connect_status_e::Connecting;
      MQTT_task_data.logged          = false;
      MQTT_task_data.ControllerIndex = controller_idx;
      MQTT_task_data.startTime       = millis();
      MQTT_task_data.loopTime        = millis();
      MQTT_task_data.timeout         = timeout;

      xTaskCreatePinnedToCore(
        MQTT_execute_connect_task,   // Function that should be called
        "MQTTClient.connect()",      // Name of the task (for debugging)
        8192,                        // Stack size (bytes)
        &MQTT_task_data,             // Parameter to pass
        1,                           // Task priority
        &MQTT_task_data.taskHandle,  // Task handle
        xPortGetCoreID()             // Core you want to run the task on (0 or 1)
        );
    } else {
      MQTT_task_data.status = MQTT_connect_status_e::Failure;
    }
  }
  return false;
}
#endif // if FEATURE_MQTT_CONNECT_BACKGROUND

/*********************************************************************************************\
* Check connection MQTT message broker
\*********************************************************************************************/
bool MQTTCheck(controllerIndex_t controller_idx)
{
  if (!NetworkConnected(10)) {
    return false;
  }
  protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(controller_idx);

  if (!validProtocolIndex(ProtocolIndex)) {
    return false;
  }

  if (getProtocolStruct(ProtocolIndex).usesMQTT)
  {
    bool   mqtt_sendLWT = false;
    String LWTTopic, LWTMessageConnect;
    bool   willRetain = false;
    {
      MakeControllerSettings(ControllerSettings); // -V522

      if (!AllocatedControllerSettings()) {
        addLog(LOG_LEVEL_ERROR, F("MQTT : Cannot check, out of RAM"));
        return false;
      }

      LoadControllerSettings(controller_idx, *ControllerSettings);

      // FIXME TD-er: Is this still needed?

      /*
       #ifdef USES_ESPEASY_NOW
         if (!MQTTclient.connected()) {
         if (ControllerSettings->enableESPEasyNowFallback()) {
         return true;
         }
         }
       #endif
       */

      if (!ControllerSettings->isSet()) {
        return true;
      }

      if (ControllerSettings->mqtt_sendLWT()) {
        mqtt_sendLWT      = true;
        LWTTopic          = getLWT_topic(*ControllerSettings);
        LWTMessageConnect = getLWT_messageConnect(*ControllerSettings);
        willRetain        = ControllerSettings->mqtt_willRetain();
      }
    }

    if (MQTTclient_should_reconnect || !MQTTclient.connected())
    {
      #if FEATURE_MQTT_CONNECT_BACKGROUND
      if (Settings.MQTTConnectInBackground()) {
        return MQTTConnectInBackground(controller_idx, false);
      }
      #endif // ifdef ESP32
      return MQTTConnect(controller_idx);
    }

    if (MQTTclient_must_send_LWT_connected) {
      if (mqtt_sendLWT) {
        if (MQTTclient.publish(LWTTopic.c_str(), LWTMessageConnect.c_str(), willRetain)) {
          MQTTclient_must_send_LWT_connected = false;
        }
      } else {
        MQTTclient_must_send_LWT_connected = false;
      }
    }
  }

  // When no MQTT protocol is enabled, all is fine.
  return true;
}

String getLWT_topic(const ControllerSettingsStruct& ControllerSettings) {
  String LWTTopic;

  if (ControllerSettings.mqtt_sendLWT()) {
    LWTTopic = ControllerSettings.MQTTLwtTopic;

    if (LWTTopic.isEmpty())
    {
      LWTTopic  = ControllerSettings.Subscribe;
      LWTTopic += F("/LWT");
    }
    LWTTopic.replace(F("/#"), F("/status"));
    parseSystemVariables(LWTTopic, false);
  }
  return LWTTopic;
}

String getLWT_messageConnect(const ControllerSettingsStruct& ControllerSettings) {
  String LWTMessageConnect;

  if (ControllerSettings.mqtt_sendLWT()) {
    LWTMessageConnect = ControllerSettings.LWTMessageConnect;

    if (LWTMessageConnect.isEmpty()) {
      LWTMessageConnect = F(DEFAULT_MQTT_LWT_CONNECT_MESSAGE);
    }
    parseSystemVariables(LWTMessageConnect, false);
  }
  return LWTMessageConnect;
}

String getLWT_messageDisconnect(const ControllerSettingsStruct& ControllerSettings) {
  String LWTMessageDisconnect;

  if (ControllerSettings.mqtt_sendLWT()) {
    LWTMessageDisconnect = ControllerSettings.LWTMessageDisconnect;

    if (LWTMessageDisconnect.isEmpty()) {
      LWTMessageDisconnect = F(DEFAULT_MQTT_LWT_DISCONNECT_MESSAGE);
    }
    parseSystemVariables(LWTMessageDisconnect, false);
  }
  return LWTMessageDisconnect;
}

#endif // if FEATURE_MQTT

/*********************************************************************************************\
* Send status info to request source
\*********************************************************************************************/
void SendStatusOnlyIfNeeded(struct EventStruct *event, bool param1, uint32_t key, const String& param2, int16_t param3) {
  if (SourceNeedsStatusUpdate(event->Source)) {
    SendStatus(event, getPinStateJSON(param1, key, param2, param3));
    printToWeb = false; // SP: 2020-06-12: to avoid to add more info to a JSON structure
  }
}

bool SourceNeedsStatusUpdate(EventValueSource::Enum eventSource)
{
  switch (eventSource) {
    case EventValueSource::Enum::VALUE_SOURCE_HTTP:
    case EventValueSource::Enum::VALUE_SOURCE_SERIAL:
    case EventValueSource::Enum::VALUE_SOURCE_MQTT:
    case EventValueSource::Enum::VALUE_SOURCE_WEB_FRONTEND:
      return true;

    default:
      break;
  }
  return false;
}

void SendStatus(struct EventStruct *event, const __FlashStringHelper *status)
{
  SendStatus(event, String(status));
}

void SendStatus(struct EventStruct *event, const String& status)
{
  if (status.isEmpty()) { return; }

  switch (event->Source)
  {
    case EventValueSource::Enum::VALUE_SOURCE_HTTP:
    case EventValueSource::Enum::VALUE_SOURCE_WEB_FRONTEND:

      if (printToWeb) {
        printWebString += status;
      }
      break;
#if FEATURE_MQTT
    case EventValueSource::Enum::VALUE_SOURCE_MQTT:
      MQTTStatus(event, status);
      break;
#endif // if FEATURE_MQTT
    case EventValueSource::Enum::VALUE_SOURCE_SERIAL:
      serialPrintln(status);
      break;

    default:
      break;
  }
}

#if FEATURE_MQTT

controllerIndex_t firstEnabledMQTT_ControllerIndex() {
  for (controllerIndex_t i = 0; i < CONTROLLER_MAX; ++i) {
    protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(i);

    if (validProtocolIndex(ProtocolIndex)) {
      if (getProtocolStruct(ProtocolIndex).usesMQTT && Settings.ControllerEnabled[i]) {
        return i;
      }
    }
  }
  return INVALID_CONTROLLER_INDEX;
}

bool MQTT_queueFull(controllerIndex_t controller_idx) {
  if (MQTTDelayHandler == nullptr) {
    return true;
  }

  if (MQTTDelayHandler->queueFull(controller_idx)) {
    // The queue is full, try to make some room first.
    processMQTTdelayQueue();
    return MQTTDelayHandler->queueFull(controller_idx);
  }
  return false;
}

bool MQTTpublish(controllerIndex_t controller_idx,
                 taskIndex_t       taskIndex,
                 const char       *topic,
                 const char       *payload,
                 bool              retained,
                 bool              callbackTask)
{
  if (MQTTDelayHandler == nullptr) {
    return false;
  }

  if (MQTT_queueFull(controller_idx)) {
    return false;
  }
  String topic_str;
  String payload_str;

  if (!reserve_special(topic_str, strlen_P(topic)) ||
      !reserve_special(payload_str, strlen_P(payload))) {
    return false;
  }
  topic_str   = topic;
  payload_str = payload;

  bool success = false;

  constexpr unsigned size = sizeof(MQTT_queue_element);
  void *ptr               = special_calloc(1, size);

  if (ptr != nullptr) {
    success =
      MQTTDelayHandler->addToQueue(
        std::unique_ptr<MQTT_queue_element>(
          new (ptr) MQTT_queue_element(
            controller_idx, taskIndex,
            std::move(topic_str),
            std::move(payload_str), retained,
            callbackTask)));
  }

  scheduleNextMQTTdelayQueue();
  return success;
}

bool MQTTpublish(controllerIndex_t controller_idx,
                 taskIndex_t       taskIndex,
                 String         && topic,
                 String         && payload,
                 bool              retained,
                 bool              callbackTask) {
  if (MQTTDelayHandler == nullptr) {
    return false;
  }

  if (MQTT_queueFull(controller_idx)) {
    return false;
  }

  bool success = false;

  constexpr unsigned size = sizeof(MQTT_queue_element);
  void *ptr               = special_calloc(1, size);

  if (ptr != nullptr) {
    success =
      MQTTDelayHandler->addToQueue(
        std::unique_ptr<MQTT_queue_element>(
          new (ptr) MQTT_queue_element(
            controller_idx, taskIndex,
            std::move(topic),
            std::move(payload), retained,
            callbackTask)));
  }
  scheduleNextMQTTdelayQueue();
  return success;
}

/*********************************************************************************************\
* Send status info back to channel where request came from
\*********************************************************************************************/
void MQTTStatus(struct EventStruct *event, const String& status)
{
  controllerIndex_t enabledMqttController = firstEnabledMQTT_ControllerIndex();

  if (validControllerIndex(enabledMqttController)) {
    controllerIndex_t DomoticzMQTT_controllerIndex = findFirstEnabledControllerWithId(2);

    if (DomoticzMQTT_controllerIndex == enabledMqttController) {
      // Do not send MQTT status updates to Domoticz
      return;
    }
    String pubname;
    bool   mqtt_retainFlag;
    {
      // Place the ControllerSettings in a scope to free the memory as soon as we got all relevant information.
      MakeControllerSettings(ControllerSettings); // -V522

      if (!AllocatedControllerSettings()) {
        addLog(LOG_LEVEL_ERROR, F("MQTT : Cannot send status, out of RAM"));
        return;
      }

      LoadControllerSettings(enabledMqttController, *ControllerSettings);
      pubname         = ControllerSettings->Publish;
      mqtt_retainFlag = ControllerSettings->mqtt_retainFlag();
    }

    // FIXME TD-er: Why check for "/#" suffix on a publish topic?
    // It makes no sense to have a subscribe wildcard on a publish topic.
    pubname.replace(F("/#"), F("/status"));

    parseSingleControllerVariable(pubname, event, 0, false);
    parseControllerVariables(pubname, event, false);


    if (!pubname.endsWith(F("/status"))) {
      pubname += F("/status");
    }

    MQTTpublish(enabledMqttController, event->TaskIndex, pubname.c_str(), status.c_str(), mqtt_retainFlag);
  }
}

# if FEATURE_MQTT_TLS

bool GetTLSfingerprint(String& fp)
{
  #  ifdef ESP32

  if (MQTTclient_connected && (mqtt_tls != nullptr)) {
    const uint8_t *recv_fingerprint = mqtt_tls->getRecvPubKeyFingerprint();

    if (recv_fingerprint != nullptr) {
      fp.reserve(64);

      for (size_t i = 0; i < 21; ++i) {
        const String tmp(recv_fingerprint[i], HEX);

        switch (tmp.length()) {
          case 0:
            fp += '0';

          // fall through
          case 1:
            fp += '0';
            break;
        }
        fp += tmp;
      }
      fp.toLowerCase();
      return true;
    }
  }
  #  endif // ifdef ESP32
  return false;
}

bool GetTLS_Certificate(String& cert, bool caRoot)
{
  #  ifdef ESP32

  // FIXME TD-er: Implement retrieval of certificate

  /*

     if (MQTTclient_connected && (mqtt_tls != nullptr)) {
     String subject;

     if (mqtt_tls->getPeerCertificate(cert, subject, caRoot) == 0) {
     return true;
     }
     }
   */
  #  endif // ifdef ESP32
  return false;
}

# endif // if FEATURE_MQTT_TLS

#endif  // if FEATURE_MQTT

/*********************************************************************************************\
* send specific sensor task data, effectively calling PluginCall(PLUGIN_READ...)
\*********************************************************************************************/
void SensorSendTask(struct EventStruct *event, unsigned long timestampUnixTime) {
  SensorSendTask(event, timestampUnixTime, millis());
}

void SensorSendTask(struct EventStruct *event, unsigned long timestampUnixTime, unsigned long lasttimer)
{
  if (!validTaskIndex(event->TaskIndex)) {
    return;
  }

  // FIXME TD-er: Should a 'disabled' task be rescheduled?
  // If not, then it should be rescheduled after the check to see if it is enabled.
  Scheduler.reschedule_task_device_timer(event->TaskIndex, lasttimer);

  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("SensorSendTask"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  if (Settings.TaskDeviceEnabled[event->TaskIndex])
  {
    START_TIMER;
    const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(event->TaskIndex);

    if (!validDeviceIndex(DeviceIndex)) { return; }

    struct EventStruct TempEvent(event->TaskIndex);
    TempEvent.Source        = event->Source;
    TempEvent.timestamp_sec = timestampUnixTime;
    checkDeviceVTypeForTask(&TempEvent);

    String dummy;

    if (PluginCall(PLUGIN_READ, &TempEvent, dummy)) {
      sendData(&TempEvent);
    }
    STOP_TIMER(SENSOR_SEND_TASK);
  }
}

#include "../Globals/Device.h"

// TODO TD-er: Move often used functions like determine valueCount to here.
#ifdef ESP8266
int deviceCount = -1;
#else
DeviceCount_t deviceCount;
#endif

int getDeviceCount()
{
#ifdef ESP8266
    return deviceCount;
#else
    return deviceCount.value;
#endif
}

DeviceVector Device;
#include "../Globals/ESPEasy_time.h"

ESPEasy_time node_time;

uint32_t getUnixTime() {
    return node_time.getUnixTime();
}
#include "../Globals/CRCValues.h"

CRCStruct CRCValues;
#include "../Globals/NPlugins.h"

#if FEATURE_NOTIFIER

#include "../DataStructs/ESPEasy_EventStruct.h"
#include "../DataStructs/NotificationStruct.h"
#include "../Globals/Settings.h"


nprotocolIndex_t INVALID_NPROTOCOL_INDEX = NPLUGIN_MAX;


bool (*NPlugin_ptr[NPLUGIN_MAX])(NPlugin::Function,
                                    struct EventStruct *,
                                    String&);
npluginID_t NPlugin_id[NPLUGIN_MAX];

NotificationStruct Notification[NPLUGIN_MAX];

int notificationCount = -1;


bool NPluginCall(NPlugin::Function Function, struct EventStruct *event)
{
  #ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  #endif
  int x;
  struct EventStruct TempEvent;

  if (event == 0) {
    event = &TempEvent;
  }

  switch (Function)
  {
    // Unconditional calls to all plugins
    case NPlugin::Function::NPLUGIN_PROTOCOL_ADD:

      for (x = 0; x < NPLUGIN_MAX; x++) {
        if (validNPluginID(NPlugin_id[x])) {
          String dummy;
          NPlugin_ptr[x](Function, event, dummy);
        }
      }
      return true;
      break;

    case NPlugin::Function::NPLUGIN_GET_DEVICENAME:
    case NPlugin::Function::NPLUGIN_WEBFORM_SAVE:
    case NPlugin::Function::NPLUGIN_WEBFORM_LOAD:
    case NPlugin::Function::NPLUGIN_WRITE:
    case NPlugin::Function::NPLUGIN_NOTIFY:
      break;
  }

  return false;
}


bool validNProtocolIndex(nprotocolIndex_t index) {
  return index != INVALID_NPROTOCOL_INDEX;
}

bool validNotifierIndex(notifierIndex_t index)
{
  return index < NOTIFICATION_MAX;
}

bool validNPluginID(npluginID_t npluginID)
{
  if (npluginID == INVALID_N_PLUGIN_ID) {
    return false;
  }

  // FIXME TD-er: Must search to all included plugins
  return true;
}

String getNPluginNameFromNotifierIndex(notifierIndex_t NotifierIndex) {
  String name;

  if (validNPluginID(NPlugin_id[NotifierIndex])) {
    NPlugin_ptr[NotifierIndex](NPlugin::Function::NPLUGIN_GET_DEVICENAME, nullptr, name);
  }
  return name;
}

/********************************************************************************************\
   Get notificatoin protocol index (plugin index), by NPlugin_id
 \*********************************************************************************************/
nprotocolIndex_t getNProtocolIndex(npluginID_t Number)
{
  for (uint8_t x = 0; x <= notificationCount; x++) {
    if (Notification[x].Number == Number.value) {
      return x;
    }
  }
  return INVALID_NPROTOCOL_INDEX;
}

nprotocolIndex_t getNProtocolIndex_from_NotifierIndex(notifierIndex_t index) {
  if (validNotifierIndex(index)) {
    return getNProtocolIndex(npluginID_t::toPluginID(Settings.Notification[index]));
  }
  return INVALID_NPROTOCOL_INDEX;
}

bool addNPlugin(/*npluginID_t*/ uint8_t npluginID, nprotocolIndex_t x) {
  if (x < NPLUGIN_MAX) { 
    // FIXME TD-er: Must add lookup for notification plugins too
//    ProtocolIndex_to_NPlugin_id[x] = npluginID; 
//    NPlugin_id_to_ProtocolIndex[npluginID] = x;

    NPlugin_id[x] = npluginID_t::toPluginID(npluginID);
    return true;
  }
  /*
  {
    String log = F("System: Error - Too many N-Plugins. NPLUGIN_MAX = ");
    log += NPLUGIN_MAX;
    addLog(LOG_LEVEL_ERROR, log);
  }
  */
  return false;
}

#endif
#include "../Globals/Services.h"

#if FEATURE_ARDUINO_OTA
  bool ArduinoOTAtriggered = false;
#endif


#ifdef ESP8266  
  ESP8266WebServer web_server(80);
  #ifndef NO_HTTP_UPDATER
  ESP8266HTTPUpdateServer httpUpdater(true);
  #endif
#endif

#ifdef ESP32
  WebServer web_server(80);
  #ifndef NO_HTTP_UPDATER
  ESP32HTTPUpdateServer httpUpdater(true);
  #endif
#endif


#if FEATURE_DNS_SERVER
  DNSServer  dnsServer;
  bool dnsServerActive = false;
#endif // if FEATURE_DNS_SERVER

#include "../Globals/TXBuffer.h"

#include "../DataStructs/Web_StreamingBuffer.h"


Web_StreamingBuffer TXBuffer;
#include "../Globals/MQTT.h"

#include "../../ESPEasy_common.h"

#if FEATURE_MQTT


// MQTT client
WiFiClient mqtt;
# if FEATURE_MQTT_TLS
String  mqtt_tls_last_errorstr;
int32_t mqtt_tls_last_error = 0;

#  ifdef ESP32
BearSSL::WiFiClientSecure_light*mqtt_tls{};
#  endif // ifdef ESP32
#  ifdef ESP8266
BearSSL::WiFiClientSecure*mqtt_tls;
BearSSL::X509List mqtt_X509List;
#  endif // ifdef ESP8266
int32_t mqtt_tls_last_cipher_suite{};
String mqtt_rootCA;
String mqtt_fingerprint;
# endif  // if FEATURE_MQTT_TLS

PubSubClient MQTTclient(mqtt);
bool MQTTclient_should_reconnect        = true;
bool MQTTclient_must_send_LWT_connected = false;
bool MQTTclient_connected               = false;
int  mqtt_reconnect_count               = 0;
LongTermTimer MQTTclient_next_connect_attempt;

# if FEATURE_MQTT_CONNECT_BACKGROUND
MQTT_connect_request MQTT_task_data;
# endif // if FEATURE_MQTT_CONNECT_BACKGROUND

# if FEATURE_MQTT_DISCOVER

controllerIndex_t mqttDiscoveryController = INVALID_CONTROLLER_INDEX;
taskIndex_t mqttDiscoverOnlyTask          = INVALID_TASK_INDEX;
uint32_t    mqttDiscoveryTimeout          = 0; // Decremented in 10 per second, random timeout before discovery is sent, for broker load
                                               // distribution
# endif // if FEATURE_MQTT_DISCOVER
#endif // if FEATURE_MQTT

#ifdef USES_P037

// mqtt import status
bool P037_MQTTImport_connected = false;
#endif // ifdef USES_P037

#include "../Globals/ESPEasy_Console.h"

EspEasy_Console_t ESPEasy_Console;


#include "../Globals/I2Cdev.h"


I2Cdev i2cdev;

#include "../Globals/Plugins_other.h"


 
 void (*parseTemplate_CallBack_ptr)(String& tmpString, bool useURLencode) = nullptr;
 void (*substitute_eventvalue_CallBack_ptr)(String& line, const String& event) = nullptr;
#include "../Globals/Logging.h"

#include "../DataStructs/LogStruct.h"


LogStruct Logging;

uint8_t highest_active_log_level = 0;
bool log_to_serial_disabled = false;


#include "../Globals/ExtraTaskSettings.h"


// TODO TD-er: Move fuctions to manage the ExtraTaskSettings cache to this file.

ExtraTaskSettingsStruct ExtraTaskSettings;
#include "../Globals/C016_ControllerCache.h"

#ifdef USES_C016


# include "../ControllerQueue/C016_queue_element.h"

ControllerCache_struct ControllerCache;

void C016_flush() {
  ControllerCache.flush();
}

bool C016_CacheInitialized() {
  return ControllerCache.isInitialized();
}

String C016_getCacheFileName(int& fileNr, bool& islast) {
  return ControllerCache.getNextCacheFileName(fileNr, islast);
}

bool C016_deleteOldestCacheBlock() {
  return ControllerCache.deleteOldestCacheBlock();
}

bool C016_deleteAllCacheBlocks() {
  return ControllerCache.deleteAllCacheBlocks();
}

bool C016_getTaskSample(C016_binary_element& element) {
  return ControllerCache.peek((uint8_t *)&element, sizeof(element));
}

struct EventStruct C016_getTaskSample(
  unsigned long& timestamp,
  uint8_t      & valueCount,
  float        & val1,
  float        & val2,
  float        & val3,
  float        & val4)
{
  C016_binary_element element;

  if (!ControllerCache.peek((uint8_t *)&element, sizeof(element))) {
    return EventStruct();
  }

  timestamp  = element.unixTime;
  valueCount = element.valueCount;
  val1       = element.values.getFloat(0);
  val2       = element.values.getFloat(1);
  val3       = element.values.getFloat(2);
  val4       = element.values.getFloat(3);

  EventStruct event(element.TaskIndex);

  // FIXME TD-er: Is this needed?
  //  event.ControllerIndex = element._controller_idx;
  event.sensorType = element.sensorType;

  return event;
}

#endif // ifdef USES_C016

#include "../Globals/NetworkState.h"

#include "../../ESPEasy_common.h"


// Ethernet Connection status
NetworkMedium_t active_network_medium = NetworkMedium_t::NotSet;

bool webserverRunning(false);
bool webserver_init(false);

#if FEATURE_MDNS
bool mDNS_init(false);
#endif


// NTP status
bool statusNTPInitialized = false;


// Setup DNS, only used if the ESP has no valid WiFi config
const uint8_t DNS_PORT = 53;
IPAddress  apIP(DEFAULT_AP_IP);



// udp protocol stuff (syslog, global sync, node info list, ntp time)
WiFiUDP portUDP;

#include "../Globals/ESPEasyEthEvent.h"

#include "../../ESPEasy_common.h"

#if FEATURE_ETHERNET
EthernetEventData_t EthEventData;
#endif


#include "../Globals/MainLoopCommand.h"

uint8_t cmd_within_mainloop = 0;
#include "../Globals/ESPEasyWiFi.h"

#ifdef ESPEASY_WIFI_CLEANUP_WORK_IN_PROGRESS
ESPEasyWiFi_t ESPEasyWiFi;
#endif // ifdef ESPEASY_WIFI_CLEANUP_WORK_IN_PROGRESS

#include "../Globals/Settings.h"


SettingsStruct Settings;
#include "../Globals/CPlugins.h"

#include "../../_Plugin_Helper.h"
#include "../DataStructs/ESPEasy_EventStruct.h"
#include "../DataStructs/TimingStats.h"
#include "../DataTypes/ESPEasy_plugin_functions.h"
#include "../ESPEasyCore/ESPEasy_Log.h"
#if FEATURE_MQTT_DISCOVER
# include "../Globals/MQTT.h"
#endif // if FEATURE_MQTT_DISCOVER
#include "../Globals/Settings.h"
#include "../Helpers/_CPlugin_init.h"


/********************************************************************************************\
   Call CPlugin functions
 \*********************************************************************************************/
bool CPluginCall(CPlugin::Function Function, struct EventStruct *event) {
  #ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  #endif // ifdef USE_SECOND_HEAP

  String dummy;

  return CPluginCall(Function, event, dummy);
}

bool CPluginCall(CPlugin::Function Function, struct EventStruct *event, String& str)
{
  #ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  #endif // ifdef USE_SECOND_HEAP

  struct EventStruct TempEvent;

  if (event == 0) {
    event = &TempEvent;
  }

  switch (Function)
  {
    case CPlugin::Function::CPLUGIN_PROTOCOL_ADD:
      // only called from CPluginSetup() directly using protocolIndex
      break;

    // calls to all active controllers
    case CPlugin::Function::CPLUGIN_INIT_ALL:
    case CPlugin::Function::CPLUGIN_UDP_IN:
    case CPlugin::Function::CPLUGIN_INTERVAL:      // calls to send stats information
    case CPlugin::Function::CPLUGIN_GOT_CONNECTED: // calls to send autodetect information
    case CPlugin::Function::CPLUGIN_GOT_INVALID:   // calls to mark unit as invalid
    case CPlugin::Function::CPLUGIN_FLUSH:
    case CPlugin::Function::CPLUGIN_TEN_PER_SECOND:
    case CPlugin::Function::CPLUGIN_FIFTY_PER_SECOND:
    case CPlugin::Function::CPLUGIN_WRITE:
    {
      const bool success = Function != CPlugin::Function::CPLUGIN_WRITE;

      if (Function == CPlugin::Function::CPLUGIN_INIT_ALL) {
        Function = CPlugin::Function::CPLUGIN_INIT;
      }

      for (controllerIndex_t x = 0; x < CONTROLLER_MAX; x++) {
        if ((Settings.Protocol[x] != 0) && Settings.ControllerEnabled[x]) {
          event->ControllerIndex = x;
          String command;

          if (Function == CPlugin::Function::CPLUGIN_WRITE) {
            command = str;
          }

          if (CPluginCall(
                getProtocolIndex_from_ControllerIndex(x),
                Function,
                event,
                command)) {
            if (Function == CPlugin::Function::CPLUGIN_WRITE) {
              // Need to stop when write call was handled
              return true;
            }
          }
        }
      }
      return success;
    }

    // calls to specific controller
    case CPlugin::Function::CPLUGIN_INIT:
    case CPlugin::Function::CPLUGIN_EXIT:
    case CPlugin::Function::CPLUGIN_PROTOCOL_TEMPLATE:
    case CPlugin::Function::CPLUGIN_PROTOCOL_SEND:
    case CPlugin::Function::CPLUGIN_PROTOCOL_RECV:
    case CPlugin::Function::CPLUGIN_GET_DEVICENAME:
    case CPlugin::Function::CPLUGIN_WEBFORM_LOAD:
    case CPlugin::Function::CPLUGIN_WEBFORM_SAVE:
    case CPlugin::Function::CPLUGIN_GET_PROTOCOL_DISPLAY_NAME:
    case CPlugin::Function::CPLUGIN_TASK_CHANGE_NOTIFICATION:
    case CPlugin::Function::CPLUGIN_WEBFORM_SHOW_HOST_CONFIG:
    {
      const controllerIndex_t controllerindex = event->ControllerIndex;
      bool success                            = false;

      if (validControllerIndex(controllerindex)) {
        if (Settings.ControllerEnabled[controllerindex] && supportedCPluginID(Settings.Protocol[controllerindex]))
        {
          if (Function == CPlugin::Function::CPLUGIN_PROTOCOL_SEND) {
            checkDeviceVTypeForTask(event);
          }
          success = CPluginCall(
            getProtocolIndex_from_ControllerIndex(controllerindex),
            Function,
            event,
            str);
        }
        #ifdef ESP32

        if (Function == CPlugin::Function::CPLUGIN_EXIT) {
          Cache.clearControllerSettings(controllerindex);
        }
        #endif // ifdef ESP32
        #if FEATURE_MQTT_DISCOVER

        if (Function == CPlugin::Function::CPLUGIN_EXIT) {
          if (mqttDiscoveryController == controllerindex) {
            mqttDiscoveryController = INVALID_CONTROLLER_INDEX; // Reset
            mqttDiscoverOnlyTask    = INVALID_TASK_INDEX;
          }
        }
        #endif // if FEATURE_MQTT_DISCOVER
      }
      return success;
    }

    case CPlugin::Function::CPLUGIN_ACKNOWLEDGE: // calls to send acknowledge back to controller

      for (controllerIndex_t x = 0; x < CONTROLLER_MAX; x++) {
        if (Settings.ControllerEnabled[x] && supportedCPluginID(Settings.Protocol[x])) {
          CPluginCall(
            getProtocolIndex_from_ControllerIndex(x),
            Function,
            event,
            str);
        }
      }
      return true;
    case CPlugin::Function::CPLUGIN_CONNECT_SUCCESS:
    case CPlugin::Function::CPLUGIN_CONNECT_FAIL:
      break;
  }

  return false;
}

// Check if there is any controller enabled.
bool anyControllerEnabled() {
  for (controllerIndex_t x = 0; x < CONTROLLER_MAX; x++) {
    if (Settings.ControllerEnabled[x] && supportedCPluginID(Settings.Protocol[x])) {
      return true;
    }
  }
  return false;
}

// Find first enabled controller index with this protocol
controllerIndex_t findFirstEnabledControllerWithId(cpluginID_t cpluginid) {
  if (supportedCPluginID(cpluginid)) {
    for (controllerIndex_t i = 0; i < CONTROLLER_MAX; i++) {
      if ((Settings.Protocol[i] == cpluginid) && Settings.ControllerEnabled[i]) {
        return i;
      }
    }
  }
  return INVALID_CONTROLLER_INDEX;
}

bool validProtocolIndex(protocolIndex_t index)
{
  return validProtocolIndex_init(index);
}

/*
   bool validControllerIndex(controllerIndex_t index)
   {
   return index < CONTROLLER_MAX;
   }
 */
bool validCPluginID(cpluginID_t cpluginID)
{
  return getProtocolIndex_from_CPluginID_(cpluginID) != INVALID_PROTOCOL_INDEX;
}

bool supportedCPluginID(cpluginID_t cpluginID)
{
  return validProtocolIndex(getProtocolIndex_from_CPluginID_(cpluginID));
}

protocolIndex_t getProtocolIndex_from_ControllerIndex(controllerIndex_t index) {
  if (validControllerIndex(index)) {
    return getProtocolIndex_from_CPluginID_(Settings.Protocol[index]);
  }
  return INVALID_PROTOCOL_INDEX;
}

protocolIndex_t getProtocolIndex_from_CPluginID(cpluginID_t cpluginID) {
  return getProtocolIndex_from_CPluginID_(cpluginID);
}

cpluginID_t getCPluginID_from_ProtocolIndex(protocolIndex_t index) {
  return getCPluginID_from_ProtocolIndex_(index);
}

cpluginID_t getCPluginID_from_ControllerIndex(controllerIndex_t index) {
  const protocolIndex_t protocolIndex = getProtocolIndex_from_ControllerIndex(index);

  return getCPluginID_from_ProtocolIndex(protocolIndex);
}

String getCPluginNameFromProtocolIndex(protocolIndex_t ProtocolIndex) {
  String controllerName;

  if (validProtocolIndex(ProtocolIndex)) {
    CPluginCall(ProtocolIndex, CPlugin::Function::CPLUGIN_GET_DEVICENAME, nullptr, controllerName);
  }
  return controllerName;
}

String getCPluginNameFromCPluginID(cpluginID_t cpluginID) {
  protocolIndex_t protocolIndex = getProtocolIndex_from_CPluginID_(cpluginID);

  if (!validProtocolIndex(protocolIndex)) {
    return strformat(F("CPlugin %d not included in build"), cpluginID);
  }
  return getCPluginNameFromProtocolIndex(protocolIndex);
}

#include "../Globals/ESPEasy_Scheduler.h"

ESPEasy_Scheduler Scheduler;
#include "../Globals/SecuritySettings.h"


SecurityStruct SecuritySettings;
ExtendedControllerCredentialsStruct ExtendedControllerCredentials;
#include "../Globals/ResetFactoryDefaultPref.h"
#include "../DataStructs/FactoryDefaultPref.h"

ResetFactoryDefaultPreference_struct ResetFactoryDefaultPreference;
#include "../Globals/RamTracker.h"

#ifndef BUILD_NO_RAM_TRACKER

#if FEATURE_TIMING_STATS
#include "../DataStructs/TimingStats.h"
#endif

#include "../ESPEasyCore/ESPEasy_Log.h"

#include "../Globals/Settings.h"
#include "../Globals/Statistics.h"

#include "../Helpers/Memory.h"
#include "../Helpers/Misc.h"
#include "../Helpers/StringConverter.h"

RamTracker myRamTracker;

/********************************************************************************************\
   Global convenience functions calling RamTracker
 \*********************************************************************************************/

void checkRAMtoLog(void){
  myRamTracker.getTraceBuffer();
}

void checkRAM(const String &flashString, int a ) {
  checkRAM(flashString, String(a));
}

void checkRAM(const __FlashStringHelper * flashString, int a) {
  checkRAM(String(flashString), String(a));
}


void checkRAM(const __FlashStringHelper * flashString, const String& a) {
  checkRAM(String(flashString), a);
}

void checkRAM(const __FlashStringHelper * flashString, const __FlashStringHelper * a) {
  checkRAM_values values;
  if (!values.mustContinue()) return;
  String s = flashString;
  s += F(" (");
  s += a;
  s += ')';
  checkRAM(values, s);
}

void checkRAM(const String& flashString, const String &a ) {
  checkRAM_values values;
  if (!values.mustContinue()) return;
  String s = flashString;
  s += F(" (");
  s += a;
  s += ')';
  checkRAM(values, s);
}

void checkRAM(const __FlashStringHelper * descr ) {
  checkRAM_values values;
  if (values.mustContinue()) 
    checkRAM(values, String(descr));
}

void checkRAM_PluginCall_task(uint8_t taskIndex, uint8_t Function) {
  checkRAM_values values;
  if (!values.mustContinue()) return;
  String s = concat(F("PluginCall_task_"), taskIndex + 1);

  s += F(" (");
  #if FEATURE_TIMING_STATS
  s += getPluginFunctionName(Function);
  #else // if FEATURE_TIMING_STATS
  s += String(Function);
  #endif // if FEATURE_TIMING_STATS
  s += ')';
  checkRAM(values, s);
}

void checkRAM(const String& descr ) {
  checkRAM_values values;
  checkRAM(values, descr);
}

checkRAM_values::checkRAM_values() {
  freeStack = getFreeStackWatermark();
#ifdef ESP32
  freeRAM = ESP.getMinFreeHeap();
#else
  freeRAM = FreeMem();
#endif
}

bool checkRAM_values::mustContinue() const {
  // Here we simply check to see if it is desired to continue creating a description string.
  // When no description string is created, it would still be nice to get some idea of the lowest stack/ram while we're here.
  if (Settings.EnableRAMTracking()) {
    return freeStack <= lowestFreeStack ||
           freeRAM <= lowestRAM;
  }

  if (freeStack <= lowestFreeStack) {
    lowestFreeStack = freeStack;
  }

  if (freeRAM <= lowestRAM)
  {
    lowestRAM = freeRAM;
  }
  return false;
}

void checkRAM(const checkRAM_values & values, const String& descr)
{
  if (Settings.EnableRAMTracking())
    myRamTracker.registerRamState(descr);

  if (values.freeStack <= lowestFreeStack) {
    lowestFreeStack = values.freeStack;
    lowestFreeStackfunction = descr;
  }

  if (values.freeRAM <= lowestRAM)
  {
    lowestRAM = values.freeRAM;
    lowestRAMfunction = std::move(descr);
  }
}

/********************************************************************************************\
   RamTracker class
 \*********************************************************************************************/



// find highest the trace with the largest minimum memory (gets replaced by worse one)
unsigned int RamTracker::bestCaseTrace(void) {
  unsigned int lowestMemoryInTrace      = 0;
  unsigned int lowestMemoryInTraceIndex = 0;

  for (int i = 0; i < TRACES; i++) {
    if (tracesMemory[i] > lowestMemoryInTrace) {
      lowestMemoryInTrace      = tracesMemory[i];
      lowestMemoryInTraceIndex = i;
    }
  }

  // serialPrintln(lowestMemoryInTraceIndex);
  return lowestMemoryInTraceIndex;
}

RamTracker::RamTracker(void) {
  readPtr  = 0;
  writePtr = 0;

  for (int i = 0; i < TRACES; i++) {
    free_string(traces[i]);
    tracesMemory[i] = 0xffffffff; // init with best case memory values, so they get replaced if memory goes lower
  }

  for (int i = 0; i < TRACEENTRIES; i++) {
    nextAction[i]            = "startup";
    nextActionStartMemory[i] = ESP.getFreeHeap(); // init with best case memory values, so they get replaced if memory goes lower
  }
}

void RamTracker::registerRamState(const String& s) {   // store function
  nextAction[writePtr]            = s;                 // name and mem
  nextActionStartMemory[writePtr] = ESP.getFreeHeap(); // in cyclic buffer.
  int bestCase = bestCaseTrace();                      // find best case memory trace

  if (ESP.getFreeHeap() < tracesMemory[bestCase]) {    // compare to current memory value
    free_string(traces[bestCase]);
    readPtr          = writePtr + 1;                   // read out buffer, oldest value first

    if (readPtr >= TRACEENTRIES) { 
      readPtr = 0;        // read pointer wrap around
    }
    tracesMemory[bestCase] = ESP.getFreeHeap();        // store new lowest value of that trace

    for (int i = 0; i < TRACEENTRIES; i++) {           // tranfer cyclic buffer strings and mem values to this trace
      traces[bestCase] += nextAction[readPtr];
      traces[bestCase] += F("-> ");
      traces[bestCase] += String(nextActionStartMemory[readPtr]);
      traces[bestCase] += ' ';
      readPtr++;

      if (readPtr >= TRACEENTRIES) { readPtr = 0; // wrap around read pointer
      }
    }
  }
  writePtr++;

  if (writePtr >= TRACEENTRIES) { writePtr = 0; // inc write pointer and wrap around too.
  }
}

 // return giant strings, one line per trace. Add stremToWeb method to avoid large strings.
void RamTracker::getTraceBuffer() {
#ifndef BUILD_NO_DEBUG
  if (Settings.EnableRAMTracking() && loglevelActiveFor(LOG_LEVEL_DEBUG_DEV)) {
    String retval = F("Memtrace\n");

    for (int i = 0; i < TRACES; i++) {
      retval += String(i);
      retval += F(": lowest: ");
      retval += String(tracesMemory[i]);
      retval += ' ';
      retval += traces[i];
      addLogMove(LOG_LEVEL_DEBUG_DEV, retval);
      retval.clear();
    }
  }
#endif // ifndef BUILD_NO_DEBUG
}

#else // BUILD_NO_RAM_TRACKER
/*

void checkRAMtoLog(void) {}

void checkRAM(const String& flashString,
              int           a) {}

void checkRAM(const String& flashString,
              const String& a) {}

void checkRAM(const String& descr) {}
*/

#endif // BUILD_NO_RAM_TRACKER
#include "../Globals/ESPEasyWiFiEvent.h"

#include "../../ESPEasy_common.h"


#ifdef ESP8266
WiFiEventHandler stationConnectedHandler;
WiFiEventHandler stationDisconnectedHandler;
WiFiEventHandler stationGotIpHandler;
WiFiEventHandler stationModeDHCPTimeoutHandler;
WiFiEventHandler stationModeAuthModeChangeHandler;
WiFiEventHandler APModeStationConnectedHandler;
WiFiEventHandler APModeStationDisconnectedHandler;
#endif // ifdef ESP8266

WiFiEventData_t WiFiEventData;



#include "../Globals/GlobalMapPortStatus.h"

MapPortStatus globalMapPortStatus;
#include "../Globals/Cache.h"

#include "../DataStructs/Caches.h"

void clearAllCaches()
{
  Cache.clearAllCaches();
}

void clearAllButTaskCaches()
{
  Cache.clearAllButTaskCaches();
}

void clearTaskCache(taskIndex_t TaskIndex)
{
  Cache.clearTaskCache(TaskIndex);
}

void clearFileCaches()
{
  Cache.clearFileCaches();
}

void updateActiveTaskUseSerial0()
{
  Cache.updateActiveTaskUseSerial0();
}

bool activeTaskUseSerial0()
{
  return Cache.activeTaskUseSerial0;
}

Caches Cache;

#include "../Globals/Statistics.h"


#ifndef BUILD_NO_RAM_TRACKER
uint32_t lowestRAM = 0;
String   lowestRAMfunction;
uint32_t lowestFreeStack = 0;
String   lowestFreeStackfunction;
#endif

uint8_t lastBootCause                           = BOOT_CAUSE_MANUAL_REBOOT;
SchedulerTimerID lastMixedSchedulerId_beforereboot(0);

unsigned long idle_msec_per_sec = 0;
unsigned long elapsed10ps       = 0;
unsigned long elapsed10psU      = 0;
unsigned long elapsed50ps       = 0;
unsigned long loopCounter       = 0;
unsigned long loopCounterLast   = 0;
unsigned long loopCounterMax    = 1;
uint32_t      lastLoopStart     = 0;
unsigned long shortestLoop      = 10000000;
unsigned long longestLoop       = 0;
unsigned long loopCounter_full  = 1;
float loop_usec_duration_total  = 0.0f;


unsigned long dailyResetCounter                   = 0;

ESPEASY_VOLATILE(unsigned long) sw_watchdog_callback_count{};


I2C_bus_state I2C_state = I2C_bus_state::OK;
unsigned long I2C_bus_cleared_count = 0;

#include "../Globals/RulesCalculate.h"

#include "../DataStructs/TimingStats.h"
#include "../Helpers/Numerical.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringConverter_Numerical.h"

RulesCalculate_t RulesCalculate{};

/*******************************************************************************************
* Helper functions to actually interact with the rules calculation functions.
* *****************************************************************************************/
int64_t CalculateParam(const String& TmpStr, int64_t errorValue) {
  int64_t returnValue = errorValue;

  if (TmpStr.length() == 0) {
    return returnValue;
  }

  // Minimize calls to the Calulate function.
  // Only if TmpStr starts with '=' then call Calculate(). Otherwise do not call it
  if (TmpStr[0] != '=') {
    if (!validInt64FromString(TmpStr, returnValue)) {
      return errorValue;
    }
  } else {
    ESPEASY_RULES_FLOAT_TYPE param{};

    // Starts with an '=', so Calculate starting at next position
    CalculateReturnCode returnCode = Calculate(TmpStr.substring(1), param);

    if (!isError(returnCode)) {
#ifndef BUILD_NO_DEBUG

      if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
        addLogMove(LOG_LEVEL_DEBUG,
                   strformat(F("CALCULATE PARAM: %s = %.6g"), TmpStr.c_str(), roundf(param)));
      }
#endif // ifndef BUILD_NO_DEBUG
      // return integer only as it's valid only for variable indices and device/task id
      #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
      return round(param);
      #else
      return roundf(param);
      #endif
    }
  }
  return returnValue;
}

CalculateReturnCode Calculate_preProcessed(const String            & preprocessd_input,
                                           ESPEASY_RULES_FLOAT_TYPE& result)
{
  START_TIMER;
  CalculateReturnCode returnCode = RulesCalculate.doCalculate(
    preprocessd_input.c_str(),
    &result);

  STOP_TIMER(COMPUTE_STATS);
  return returnCode;
}

CalculateReturnCode Calculate(const String& input,
                              ESPEASY_RULES_FLOAT_TYPE& result
                              #if           FEATURE_STRING_VARIABLES
                              , const bool  logStringErrors
                              #endif // if FEATURE_STRING_VARIABLES
                              )
{
  CalculateReturnCode returnCode = Calculate_preProcessed(
    RulesCalculate_t::preProces(input),
    result);

  #ifndef LIMIT_BUILD_SIZE
  # if FEATURE_STRING_VARIABLES
  bool skipError = false;
  # endif // if FEATURE_STRING_VARIABLES

  if (isError(returnCode)) {
    if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
      String log = F("Calculate: ");

      switch (returnCode) {
        case CalculateReturnCode::ERROR_STACK_OVERFLOW:
          log += F("Stack Overflow");
          break;
        case CalculateReturnCode::ERROR_BAD_OPERATOR:
          log += F("Bad Operator");
          break;
        case CalculateReturnCode::ERROR_PARENTHESES_MISMATCHED:
          log += F("Parenthesis mismatch");
          break;
        case CalculateReturnCode::ERROR_UNKNOWN_TOKEN:
          log += F("Unknown token");
          # if FEATURE_STRING_VARIABLES
          skipError = !logStringErrors;
          # endif // if FEATURE_STRING_VARIABLES
          break;
        case CalculateReturnCode::ERROR_TOKEN_LENGTH_EXCEEDED:
          log += strformat(F("Exceeded token length (%d)"), TOKEN_LENGTH);
          # if FEATURE_STRING_VARIABLES
          skipError = !logStringErrors;
          # endif // if FEATURE_STRING_VARIABLES
          break;
        case CalculateReturnCode::OK:
          // Already handled, but need to have all cases here so the compiler can warn if we're missing one.
          break;
      }

      # if FEATURE_STRING_VARIABLES

      if (!skipError)
      # endif // if FEATURE_STRING_VARIABLES
      {
        # ifndef BUILD_NO_DEBUG
        log += F(" input: ");
        log += input;
        log += F(" = ");

        const bool trimTrailingZeros = true;
        #  if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
        log += doubleToString(result, 6, trimTrailingZeros);
        #  else // if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
        log += floatToString(result, 6, trimTrailingZeros);
        #  endif // if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
        # endif // ifndef BUILD_NO_DEBUG

        addLogMove(LOG_LEVEL_ERROR, log);
      }
    }
  }
  #endif // ifndef LIMIT_BUILD_SIZE
  return returnCode;
}

#include "../Globals/EventQueue.h"


EventQueueStruct eventQueue;
#include "../Globals/Nodes.h"

#if FEATURE_ESPEASY_P2P

NodesHandler Nodes;

#endif
#include "../Globals/RTC.h"

#include "../DataStructs/RTCStruct.h"


RTCStruct RTC;


#include "../Globals/RuntimeData.h"


std::map<String, ESPEASY_RULES_FLOAT_TYPE> customFloatVar;

#if FEATURE_STRING_VARIABLES
std::map<String, String> customStringVar;
#endif // if FEATURE_STRING_VARIABLES
//float UserVar[VARS_PER_TASK * TASKS_MAX];

UserVarStruct UserVar;


ESPEASY_RULES_FLOAT_TYPE getCustomFloatVar(String indexName, ESPEASY_RULES_FLOAT_TYPE defaultValue) {
  indexName.toLowerCase();
  auto it = customFloatVar.find(indexName);

  if (it != customFloatVar.end()) {
    return it->second;
  }
  return defaultValue;
}

void setCustomFloatVar(String indexName, const ESPEASY_RULES_FLOAT_TYPE& value) {
  indexName.toLowerCase();
  // std::map doesn't handle 2nd heap well, so make sure we keep using the default heap.
  # ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  # endif // ifdef USE_SECOND_HEAP

  customFloatVar[indexName] = value;
}

bool getNextCustomFloatVar(String& indexName, ESPEASY_RULES_FLOAT_TYPE& value) {
  String valueName(indexName);
  valueName.toLowerCase();
  auto it = customFloatVar.find(valueName);

  if (it == customFloatVar.end()) { return false; }
  ++it;

  if (it == customFloatVar.end()) { return false; }
  indexName = it->first;
  value = it->second;
  return true;
}

#if FEATURE_STRING_VARIABLES
String getCustomStringVar(String indexName, String defaultValue) {
  indexName.toLowerCase();
  auto it = customStringVar.find(indexName);

  if (it != customStringVar.end()) {
    return it->second;
  }
  return defaultValue;
}

void setCustomStringVar(String indexName, const String& value) {
  indexName.toLowerCase();
  // std::map doesn't handle 2nd heap well, so make sure we keep using the default heap.
  # ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  # endif // ifdef USE_SECOND_HEAP

  customStringVar[indexName] = value;
}

void clearCustomStringVar(String indexName) {
  indexName.toLowerCase();
  // std::map doesn't handle 2nd heap well, so make sure we keep using the default heap.
  # ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  # endif // ifdef USE_SECOND_HEAP

  auto it = customStringVar.find(indexName);

  if (it != customStringVar.end()) {
    customStringVar.erase(it);
  }
}

bool hasCustomStringVar(String indexName) {
  indexName.toLowerCase();
  auto it = customStringVar.find(indexName);

  return it != customStringVar.end();
}

bool getNextCustomStringVar(String& indexName, String& value) {
  String valueName(indexName);
  valueName.toLowerCase();
  auto it = customStringVar.find(valueName);

  if (it == customStringVar.end()) { return false; }
  ++it;

  if (it == customStringVar.end()) { return false; }
  indexName = it->first;
  value = it->second;
  return true;
}
#endif // if FEATURE_STRING_VARIABLES

#include "../Globals/TimeZone.h"

ESPEasy_time_zone time_zone;
#include "../Globals/Plugins.h"

#include "../CustomBuild/ESPEasyLimits.h"

#include "../../_Plugin_Helper.h"

#include "../DataStructs/ESPEasy_EventStruct.h"
#include "../DataStructs/TimingStats.h"

#include "../DataTypes/ESPEasy_plugin_functions.h"

#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../ESPEasyCore/Serial.h"

#include "../Globals/Cache.h"
#include "../Globals/Device.h"
#include "../Globals/ESPEasy_Scheduler.h"
#include "../Globals/ExtraTaskSettings.h"
#include "../Globals/EventQueue.h"
#include "../Globals/GlobalMapPortStatus.h"
#include "../Globals/NetworkState.h"
#include "../Globals/Settings.h"
#include "../Globals/Statistics.h"

#if FEATURE_DEFINE_SERIAL_CONSOLE_PORT
# include "../Helpers/_Plugin_Helper_serial.h"
#endif // if FEATURE_DEFINE_SERIAL_CONSOLE_PORT

#include "../Helpers/ESPEasyRTC.h"
#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/Hardware_I2C.h"
#include "../Helpers/Misc.h"
#include "../Helpers/_Plugin_init.h"
#include "../Helpers/PortStatus.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringParser.h"

#include <vector>


bool validDeviceIndex(deviceIndex_t index) {
  return validDeviceIndex_init(index);
}

/*
   bool validTaskIndex(taskIndex_t index) {
   return index < TASKS_MAX;
   }

   bool validPluginID(pluginID_t pluginID) {
   return (pluginID != INVALID_PLUGIN_ID);
   }
 */
bool validPluginID_fullcheck(pluginID_t pluginID) {
  return getDeviceIndex_from_PluginID(pluginID) != INVALID_DEVICE_INDEX;
}

/*
   bool validUserVarIndex(userVarIndex_t index) {
   return index < USERVAR_MAX_INDEX;
   }

   bool validTaskVarIndex(taskVarIndex_t index) {
   return index < VARS_PER_TASK;
   }
 */
bool supportedPluginID(pluginID_t pluginID) {
  return validDeviceIndex(getDeviceIndex(pluginID));
}

deviceIndex_t getDeviceIndex_from_TaskIndex(taskIndex_t taskIndex) {
  if (validTaskIndex(taskIndex)) {
    return getDeviceIndex(Settings.getPluginID_for_task(taskIndex));
  }
  return INVALID_DEVICE_INDEX;
}

/*********************************************************************************************
 * get the taskPluginID with required checks, INVALID_PLUGIN_ID when invalid
 ********************************************************************************************/
pluginID_t getPluginID_from_TaskIndex(taskIndex_t taskIndex) {
  if (validTaskIndex(taskIndex)) {
    const pluginID_t pluginID = Settings.getPluginID_for_task(taskIndex);

    if (supportedPluginID(pluginID)) {
      return pluginID;
    }
  }
  return INVALID_PLUGIN_ID;
}

#if FEATURE_PLUGIN_PRIORITY
bool isPluginI2CPowerManager_from_TaskIndex(taskIndex_t taskIndex, uint8_t i2cBus) {
  if (validTaskIndex(taskIndex)) {
    # if FEATURE_I2C_MULTIPLE

    if (Settings.getI2CInterface(taskIndex) != i2cBus) {
      return false;
    }
    # endif // if FEATURE_I2C_MULTIPLE
    deviceIndex_t deviceIndex = getDeviceIndex_from_TaskIndex(taskIndex);

    if (validDeviceIndex(deviceIndex)) {
      return (Device[deviceIndex].Type == DEVICE_TYPE_I2C) &&
             Device[deviceIndex].PowerManager &&
             Settings.isPowerManagerTask(taskIndex);
    }
  }
  return false;
}

#endif // if FEATURE_PLUGIN_PRIORITY

deviceIndex_t getDeviceIndex(pluginID_t pluginID)
{
  return getDeviceIndex_from_PluginID(pluginID);
}

/********************************************************************************************\
   Find name of plugin given the plugin device index..
 \*********************************************************************************************/
String getPluginNameFromDeviceIndex(deviceIndex_t deviceIndex) {
  #ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  #endif // ifdef USE_SECOND_HEAP

  String deviceName;

  if (validDeviceIndex(deviceIndex)) {
    PluginCall(deviceIndex, PLUGIN_GET_DEVICENAME, nullptr, deviceName);
  }
  return deviceName;
}

String getPluginNameFromPluginID(pluginID_t pluginID) {
  deviceIndex_t deviceIndex = getDeviceIndex(pluginID);

  if (!validDeviceIndex(deviceIndex)) {
    return strformat(F("Plugin %d not included in build"), pluginID.value);
  }
  return getPluginNameFromDeviceIndex(deviceIndex);
}

#if FEATURE_I2C_DEVICE_SCAN
bool checkPluginI2CAddressFromDeviceIndex(deviceIndex_t deviceIndex, uint8_t i2cAddress) {
  bool hasI2CAddress = false;

  if (validDeviceIndex(deviceIndex)) {
    String dummy;
    struct EventStruct TempEvent;
    TempEvent.Par1 = i2cAddress;
    hasI2CAddress  = PluginCall(deviceIndex, PLUGIN_I2C_HAS_ADDRESS, &TempEvent, dummy);
  }
  return hasI2CAddress;
}

#endif // if FEATURE_I2C_DEVICE_SCAN

bool getPluginDisplayParametersFromTaskIndex(taskIndex_t taskIndex, uint16_t& x, uint16_t& y, uint16_t& r, uint16_t& colorDepth) {
  if (!validTaskIndex(taskIndex)) { return false; }
  const deviceIndex_t deviceIndex = getDeviceIndex_from_TaskIndex(taskIndex);

  if (validDeviceIndex(deviceIndex)) {
    const pluginID_t pluginID = getPluginID_from_DeviceIndex(deviceIndex);

    if (validPluginID(pluginID)) {
      String dummy;
      struct EventStruct TempEvent;
      TempEvent.setTaskIndex(taskIndex);

      if (PluginCall(deviceIndex, PLUGIN_GET_DISPLAY_PARAMETERS, &TempEvent, dummy)) {
        x          = TempEvent.Par1;
        y          = TempEvent.Par2;
        r          = TempEvent.Par3;
        colorDepth = TempEvent.Par4;
        return true;
      }
    }
  }
  return false;
}

#if FEATURE_I2C_GET_ADDRESS
uint8_t getTaskI2CAddress(taskIndex_t taskIndex) {
  uint8_t getI2CAddress           = 0;
  const deviceIndex_t deviceIndex = getDeviceIndex_from_TaskIndex(taskIndex);

  if (validTaskIndex(taskIndex) && validDeviceIndex(deviceIndex)) {
    String dummy;
    struct EventStruct TempEvent;
    TempEvent.setTaskIndex(taskIndex);
    TempEvent.Par1 = 0;

    if (PluginCall(deviceIndex, PLUGIN_I2C_GET_ADDRESS, &TempEvent, dummy)) {
      getI2CAddress = TempEvent.Par1;
    }
  }
  return getI2CAddress;
}

#endif // if FEATURE_I2C_GET_ADDRESS


// ********************************************************************************
// Functions to assist changing I2C multiplexer port or clock speed
// when addressing a task
// ********************************************************************************

bool prepare_I2C_by_taskIndex(taskIndex_t taskIndex, deviceIndex_t DeviceIndex) {
  if (!validTaskIndex(taskIndex) || !validDeviceIndex(DeviceIndex)) {
    return false;
  }

  if (Device[DeviceIndex].Type != DEVICE_TYPE_I2C) {
    return true; // No I2C task, so consider all-OK
  }

  if (!Settings.isI2CEnabled(Settings.getI2CInterface(taskIndex))) {
    return false; // Plugin-selected I2C bus is not configured, fail
  }

  if (I2C_state != I2C_bus_state::OK) {
    return false; // Bus state is not OK, so do not consider task runnable
  }

  #if FEATURE_I2C_MULTIPLE
  const uint8_t i2cBus = Settings.getI2CInterface(taskIndex);
  #else // if FEATURE_I2C_MULTIPLE
  const uint8_t i2cBus = 0;
  #endif // if FEATURE_I2C_MULTIPLE

  if (bitRead(Settings.I2C_Flags[taskIndex], I2C_FLAGS_SLOW_SPEED)) {
    I2CSelectLowClockSpeed(i2cBus);  // Set to slow, also switch the bus
  } else {
    I2CSelectHighClockSpeed(i2cBus); // Set to normal, also switch the bus
  }

  #if FEATURE_I2CMULTIPLEXER
  I2CMultiplexerSelectByTaskIndex(taskIndex);

  // Output is selected after this write, so now we must make sure the
  // frequency is set before anything else is sent.
  #endif // if FEATURE_I2CMULTIPLEXER

  return true;
}

void post_I2C_by_taskIndex(taskIndex_t taskIndex, deviceIndex_t DeviceIndex) {
  if (!validTaskIndex(taskIndex) || !validDeviceIndex(DeviceIndex)) {
    return;
  }

  if (Device[DeviceIndex].Type != DEVICE_TYPE_I2C) {
    return;
  }
  #if FEATURE_I2C_MULTIPLE
  const uint8_t i2cBus = Settings.getI2CInterface(taskIndex);
  #else // if FEATURE_I2C_MULTIPLE
  const uint8_t i2cBus = 0;
  #endif // ifdef ESP32
  #if FEATURE_I2CMULTIPLEXER
  I2CMultiplexerOff(i2cBus);
  #endif // if FEATURE_I2CMULTIPLEXER

  I2CSelectHighClockSpeed(i2cBus); // Reset, stay on current bus
}

// Add an event to the event queue.
// event value 1 = taskIndex (first task = 1)
// event value 2 = return value of the plugin function
// Example:  TaskInit#bme=1,0    (taskindex = 0, return value = 0)
void queueTaskEvent(const String& eventName, taskIndex_t taskIndex, const String& value_str) {
  if (Settings.UseRules) {
    String event = strformat(
      F("%s#%s=%d"),
      eventName.c_str(),
      getTaskDeviceName(taskIndex).c_str(),
      taskIndex + 1);

    if (value_str.length() > 0) {
      event += ',';
      event += wrapWithQuotesIfContainsParameterSeparatorChar(value_str);
    }
    eventQueue.addMove(std::move(event));
  }
}

void queueTaskEvent(const String& eventName, taskIndex_t taskIndex, const int& value1) {
  queueTaskEvent(eventName, taskIndex, String(value1));
}

void queueTaskEvent(const __FlashStringHelper *eventName, taskIndex_t taskIndex, const String& value1) {
  queueTaskEvent(String(eventName), taskIndex, value1);
}

void queueTaskEvent(const __FlashStringHelper *eventName, taskIndex_t taskIndex, const int& value1) {
  queueTaskEvent(String(eventName), taskIndex, String(value1));
}

void loadDefaultTaskValueNames_ifEmpty(taskIndex_t TaskIndex) {
  String  oldNames[VARS_PER_TASK];
  uint8_t oldNrDec[VARS_PER_TASK];

  LoadTaskSettings(TaskIndex);

  for (uint8_t i = 0; i < VARS_PER_TASK; ++i) {
    oldNames[i] = ExtraTaskSettings.TaskDeviceValueNames[i];
    oldNrDec[i] = ExtraTaskSettings.TaskDeviceValueDecimals[i];
    oldNames[i].trim();
  }

  struct EventStruct TempEvent(TaskIndex);
  String dummy;

  // the plugin call should populate ExtraTaskSettings with its default values.
  PluginCall(PLUGIN_GET_DEVICEVALUENAMES, &TempEvent, dummy);

  // Restore the settings that were already set by the user
  for (uint8_t i = 0; i < VARS_PER_TASK; ++i) {
    const bool isDefault = oldNames[i].isEmpty();
    ExtraTaskSettings.isDefaultTaskVarName(i, isDefault);

    if (!isDefault) {
      ExtraTaskSettings.setTaskDeviceValueName(i, oldNames[i]);
      ExtraTaskSettings.TaskDeviceValueDecimals[i] = oldNrDec[i];
    }
  }
}

/**
 * Call the plugin of 1 task for 1 function, with standard EventStruct and optional command string
 */
bool PluginCallForTask(taskIndex_t taskIndex, uint8_t Function, EventStruct *TempEvent, String& command, EventStruct *event = nullptr) {
  #ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  #endif // ifdef USE_SECOND_HEAP

  bool retval                    = false;
  const bool considerTaskEnabled = Settings.TaskDeviceEnabled[taskIndex];

  // || (Settings.TaskDeviceEnabled[taskIndex].enabled && Function == PLUGIN_INIT);

  if (considerTaskEnabled) {
    if (validPluginID_fullcheck(Settings.getPluginID_for_task(taskIndex)))
    {
      const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(taskIndex);

      if (validDeviceIndex(DeviceIndex)) {
        if (Function == PLUGIN_INIT) {
          UserVar.clear_computed(taskIndex);
          LoadTaskSettings(taskIndex);
        }

        if (Settings.TaskDeviceDataFeed[taskIndex] == 0) // these calls only to tasks with local feed
        {
          TempEvent->setTaskIndex(taskIndex);

          // Need to 'clear' the sensorType first, before calling getSensorType()
          TempEvent->sensorType = Sensor_VType::SENSOR_TYPE_NOT_SET;
          TempEvent->getSensorType();

          if (event != nullptr) {
            TempEvent->OriginTaskIndex = event->TaskIndex;
          }

          if (!prepare_I2C_by_taskIndex(taskIndex, DeviceIndex)) {
            return false;
          }
          #ifndef BUILD_NO_RAM_TRACKER

          if (Settings.EnableRAMTracking()) {
            switch (Function) {
              case PLUGIN_WRITE:         // First set
              case PLUGIN_REQUEST:
              case PLUGIN_ONCE_A_SECOND: // Second set
              case PLUGIN_TEN_PER_SECOND:
              case PLUGIN_FIFTY_PER_SECOND:
              case PLUGIN_INIT:          // Second set, instead of PLUGIN_INIT_ALL
              case PLUGIN_CLOCK_IN:
              case PLUGIN_TIME_CHANGE:
            # if FEATURE_PLUGIN_PRIORITY
              case PLUGIN_PRIORITY_INIT:
            # endif // if FEATURE_PLUGIN_PRIORITY
              {
                checkRAM(F("PluginCall_s"), taskIndex);
                break;
              }
            }
          }
          #endif // ifndef BUILD_NO_RAM_TRACKER
          #if FEATURE_I2C_DEVICE_CHECK
          bool i2cStatusOk = true;

          if ((Function == PLUGIN_INIT) && (Device[DeviceIndex].Type == DEVICE_TYPE_I2C) && !Device[DeviceIndex].I2CNoDeviceCheck) {
            const uint8_t i2cAddr = getTaskI2CAddress(taskIndex);

            if (i2cAddr > 0) {
              START_TIMER;
              i2cStatusOk = I2C_deviceCheck(i2cAddr);
              STOP_TIMER_TASK(DeviceIndex, PLUGIN_I2C_GET_ADDRESS);
            }
          }

          if (i2cStatusOk) {
          #endif // if FEATURE_I2C_DEVICE_CHECK
            #ifndef BUILD_NO_RAM_TRACKER

          if (Settings.EnableRAMTracking()) {
            switch (Function) {
              case PLUGIN_WRITE:         // First set
              case PLUGIN_REQUEST:
              case PLUGIN_ONCE_A_SECOND: // Second set
              case PLUGIN_TEN_PER_SECOND:
              case PLUGIN_FIFTY_PER_SECOND:
              case PLUGIN_INIT:          // Second set, instead of PLUGIN_INIT_ALL
              case PLUGIN_CLOCK_IN:
              case PLUGIN_TIME_CHANGE:
              {
                checkRAM(F("PluginCall_s"), taskIndex);
                break;
              }
            }
          }
            #endif // ifndef BUILD_NO_RAM_TRACKER

          if (Function == PLUGIN_INIT) {
            // Schedule the plugin to be read.
            // Do this before actual init, to allow the plugin to schedule a specific first read.
            Scheduler.schedule_task_device_timer_at_init(TempEvent->TaskIndex);
          }

          START_TIMER;
          retval = (PluginCall(DeviceIndex, Function, TempEvent, command));

          STOP_TIMER_TASK(DeviceIndex, Function);

          if (Function == PLUGIN_INIT) {
              #if FEATURE_PLUGIN_STATS

            if (Device[DeviceIndex].PluginStats) {
              PluginTaskData_base *taskData = getPluginTaskData(taskIndex);

              if (taskData == nullptr) {
                // Plugin apparently does not have PluginTaskData.
                // Create Plugin Task data if it has "Stats" checked.
                LoadTaskSettings(taskIndex);

                if (ExtraTaskSettings.anyEnabledPluginStats()) {
                  special_initPluginTaskData(taskIndex, _StatsOnly_data_struct);
                }
              }
            }
              #endif // if FEATURE_PLUGIN_STATS
            queueTaskEvent(F("TaskInit"), taskIndex, retval);
          }
          #if FEATURE_I2C_DEVICE_CHECK
        }
          #endif // if FEATURE_I2C_DEVICE_CHECK

          post_I2C_by_taskIndex(taskIndex, DeviceIndex);
          delay(0); // SMY: call delay(0) unconditionally
        } else {
          #if FEATURE_PLUGIN_STATS

          if ((Function == PLUGIN_INIT) && Device[DeviceIndex].PluginStats) {
            PluginTaskData_base *taskData = getPluginTaskData(taskIndex);

            if (taskData == nullptr) {
              // Plugin apparently does not have PluginTaskData.
              // Create Plugin Task data if it has "Stats" checked.
              LoadTaskSettings(taskIndex);

              if (ExtraTaskSettings.anyEnabledPluginStats()) {
                // Try to allocate in PSRAM or 2nd heap if possible
                special_initPluginTaskData(taskIndex, _StatsOnly_data_struct);
              }
            }
          }
          #endif // if FEATURE_PLUGIN_STATS
        }
      }
    }
  }
  return retval;
}

/*********************************************************************************************\
* Function call to all or specific plugins
\*********************************************************************************************/
bool PluginCall(uint8_t Function, struct EventStruct *event, String& str)
{
  #ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  #endif // ifdef USE_SECOND_HEAP

  struct EventStruct TempEvent;

  if (event == nullptr) {
    event = &TempEvent;
  }
  else {
    TempEvent.deep_copy(*event);
  }

  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("PluginCall"), Function);
  #endif // ifndef BUILD_NO_RAM_TRACKER

  switch (Function)
  {
    // Unconditional calls to all plugins
    // FIXME TD-er: PLUGIN_UNCONDITIONAL_POLL is not being used at the moment

    /*
       case PLUGIN_UNCONDITIONAL_POLL:
       {

       const unsigned maxDeviceIndex = getNrBuiltInDeviceIndex();

       for (deviceIndex_t x; x < maxDeviceIndex; ++x) {
          START_TIMER;
          PluginCall(x, Function, event, str);
          STOP_TIMER_TASK(x, Function);
          delay(0); // SMY: call delay(0) unconditionally
       }
       return true;
       }
     */

    case PLUGIN_MONITOR:

      for (auto it = globalMapPortStatus.begin(); it != globalMapPortStatus.end(); ++it) {
        // only call monitor function if there the need to
        if (it->second.monitor || it->second.command || it->second.init) {
          TempEvent.Par1 = getPortFromKey(it->first);

          // initialize the "x" variable to synch with the pluginNumber if second.x == -1
          if (!validDeviceIndex(it->second.x)) { it->second.x = getDeviceIndex(getPluginFromKey(it->first)); }

          const deviceIndex_t DeviceIndex = it->second.x;

          if (validDeviceIndex(DeviceIndex))  {
            START_TIMER;
            #if FEATURE_I2C_MULTIPLE
            const uint8_t i2cBus = Settings.getI2CInterfacePCFMCP();
            I2CSelectHighClockSpeed(i2cBus); // Switch to requested bus, no need to switch back,
                                             // next I2C plugin call will switch to desired bus
            #endif // if FEATURE_I2C_MULTIPLE
            PluginCall(DeviceIndex, Function, &TempEvent, str);
            STOP_TIMER_TASK(DeviceIndex, Function);
          }
        }
      }
      return true;


    // Call to all plugins. Return at first match
    case PLUGIN_WRITE:
      //    case PLUGIN_REQUEST: @giig1967g: replaced by new function getGPIOPluginValues()
    {
      taskIndex_t firstTask = 0;
      taskIndex_t lastTask  = TASKS_MAX;
      String command        = String(str);           // Local copy to avoid warning in ExecuteCommand
      int dotPos            = command.indexOf('.');  // Find first period

      if ((Function == PLUGIN_WRITE)                 // Only applicable on PLUGIN_WRITE function
          && (dotPos > -1)) {                        // First precondition is just a quick check for a period (fail-fast strategy)
        const String arg0 = parseString(command, 1); // Get first argument
        dotPos = arg0.indexOf('.');

        if (dotPos > -1) {
          String thisTaskName = parseString(arg0, 1, '.'); // Extract taskname prefix
          removeChar(thisTaskName, '[');                   // Remove the optional square brackets
          removeChar(thisTaskName, ']');

          if (thisTaskName.length() > 0) {                 // Second precondition
            taskIndex_t thisTask = findTaskIndexByName(thisTaskName);

            if (!validTaskIndex(thisTask)) {               // Taskname not found or invalid, check for a task number?
              thisTask = static_cast<taskIndex_t>(atoi(thisTaskName.c_str()));

              if ((thisTask == 0) || (thisTask > TASKS_MAX)) {
                thisTask = INVALID_TASK_INDEX;
              } else {
                thisTask--; // 0-based
              }
            }

            if (validTaskIndex(thisTask)) {            // Known taskindex?
#ifdef USES_P022                                       // Exclude P022 as it has rather explicit differences in commands when used with the
                                                       // [<TaskName>]. prefix
              const pluginID_t pluginID = Settings.getPluginID_for_task(thisTask);

              if (Settings.TaskDeviceEnabled[thisTask] // and internally needs to know wether it was called with the taskname prefixed
                  && validPluginID_fullcheck(pluginID)
                  && (Settings.TaskDeviceDataFeed[thisTask] == 0)) {
                const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(thisTask);
                constexpr pluginID_t P022_PCA9685_PLUGIN_ID(22);

                if (validDeviceIndex(DeviceIndex) &&
                    (pluginID == P022_PCA9685_PLUGIN_ID) /* PLUGIN_ID_022 define no longer available, 'assume' 22 for now */) {
                  thisTask = INVALID_TASK_INDEX;
                }
              }

              if (validTaskIndex(thisTask)) {
#endif // ifdef USES_P022
              firstTask = thisTask;
              lastTask  = thisTask + 1;                  // Add 1 to satisfy the for condition
              command   = command.substring(dotPos + 1); // Remove [<TaskName>]. prefix
#ifdef USES_P022
            }
#endif // ifdef USES_P022
            }
          }
        }
      }

      // String info = F("PLUGIN_WRITE first: "); // To remove
      // info += firstTask;
      // info += F(" last: ");
      // info += lastTask;
      // addLog(LOG_LEVEL_INFO, info);

      for (taskIndex_t task = firstTask; task < lastTask; task++)
      {
        bool retval = PluginCallForTask(task, Function, &TempEvent, command);

        if (!retval) {
          if (1 == (lastTask - firstTask)) {
            // These plugin task data commands are generic, so only apply them on a specific task.
            // Don't try to match them on the first task that may have such data.
            PluginTaskData_base *taskData = getPluginTaskDataBaseClassOnly(task);

            if (nullptr != taskData) {
              if (taskData->plugin_write_base(event, command)) {
                retval = true;
              }
            }
          }
        }

        if (retval) {
          EventStruct CPlugin_ack_event;
          CPlugin_ack_event.deep_copy(TempEvent);
          CPlugin_ack_event.setTaskIndex(task);
          CPluginCall(CPlugin::Function::CPLUGIN_ACKNOWLEDGE, &CPlugin_ack_event, command);
          return true;
        }
      }

      /*
            if (Function == PLUGIN_REQUEST) {
              // @FIXME TD-er: work-around as long as gpio command is still performed in P001_switch.
              for (deviceIndex_t deviceIndex = 0; validDeviceIndex(deviceIndex); deviceIndex++) {
                if (PluginCall(deviceIndex, Function, event, str)) {
                  delay(0); // SMY: call delay(0) unconditionally
                  CPluginCall(CPlugin::Function::CPLUGIN_ACKNOWLEDGE, event, str);
                  return true;
                }
              }
            }
       */
      break;
    }

    // Call to all plugins used in a task. Return at first match
    case PLUGIN_SERIAL_IN:
    case PLUGIN_UDP_IN:
    {
      for (taskIndex_t taskIndex = 0; taskIndex < TASKS_MAX; taskIndex++)
      {
        if (Settings.TaskDeviceEnabled[taskIndex]) {
          if (PluginCallForTask(taskIndex, Function, &TempEvent, str)) {
            #ifndef BUILD_NO_RAM_TRACKER
            checkRAM(F("PluginCallUDP"), taskIndex);
            #endif // ifndef BUILD_NO_RAM_TRACKER
            return true;
          }
        }
      }
      return false;
    }

    // Call to all plugins that are used in a task
    case PLUGIN_ONCE_A_SECOND:
    case PLUGIN_TEN_PER_SECOND:
    case PLUGIN_FIFTY_PER_SECOND:
    case PLUGIN_INIT_ALL:
    case PLUGIN_CLOCK_IN:
    case PLUGIN_TIME_CHANGE:
    {
      if (Function == PLUGIN_INIT_ALL) {
        Function = PLUGIN_INIT;
      }
      bool result = true;

      for (taskIndex_t taskIndex = 0; taskIndex < TASKS_MAX; taskIndex++)
      {
        #ifndef BUILD_NO_DEBUG
        int freemem_begin{};

        if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
          freemem_begin = ESP.getFreeHeap();
        }
        #endif // ifndef BUILD_NO_DEBUG

        bool retval = PluginCallForTask(taskIndex, Function, &TempEvent, str, event);

        if (Function == PLUGIN_INIT) {
          UserVar.clear_computed(taskIndex);

          if (!retval && (Settings.TaskDeviceDataFeed[taskIndex] == 0)) {
            // Disable temporarily as PLUGIN_INIT failed
            // FIXME TD-er: Should reschedule call to PLUGIN_INIT????
            // What interval?  (see: PR #4793)
            // Settings.TaskDeviceEnabled[taskIndex].setRetryInit();
            // Scheduler.setPluginTaskTimer(10000, taskIndex, PLUGIN_INIT);
            Settings.TaskDeviceEnabled[taskIndex] = false;
            result                                = false;
          }
          #ifndef BUILD_NO_DEBUG

          if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
            // See also logMemUsageAfter()
            const int freemem_end = ESP.getFreeHeap();
            String log;

            if (reserve_special(log, 128)) {
              log  = F("After PLUGIN_INIT ");
              log += F(" task: ");

              if (taskIndex < 9) { log += ' '; }
              log += taskIndex + 1;

              while (log.length() < 30) { log += ' '; }
              log += F("Free mem after: ");
              log += freemem_end;

              while (log.length() < 53) { log += ' '; }
              log += F("plugin: ");
              log += freemem_begin - freemem_end;

              while (log.length() < 67) { log += ' '; }

              log += Settings.TaskDeviceEnabled[taskIndex] ? F("[ena]") : F("[dis]");

              while (log.length() < 73) { log += ' '; }
              log += getPluginNameFromDeviceIndex(getDeviceIndex_from_TaskIndex(taskIndex));

              addLogMove(LOG_LEVEL_DEBUG, log);
            }
          }
          #endif // ifndef BUILD_NO_DEBUG
        }
      }

      return result;
    }

    #if FEATURE_PLUGIN_PRIORITY
    case PLUGIN_PRIORITY_INIT_ALL:
    {
      if (Function == PLUGIN_PRIORITY_INIT_ALL) {
        addLogMove(LOG_LEVEL_INFO, F("INIT : Check for Priority tasks"));
        PluginInit(true); // Priority only, load plugins but don't initialize them yet
        Function = PLUGIN_PRIORITY_INIT;
      }

      for (taskIndex_t taskIndex = 0; taskIndex < TASKS_MAX; taskIndex++) {
        bool isPriority = PluginCallForTask(taskIndex, Function, &TempEvent, str, event);

        if ((Function == PLUGIN_PRIORITY_INIT) && isPriority) { // If this is a priority task, then initialize it, next PLUGIN_INIT call
                                                                // must be self-ignored by plugin!
          clearPluginTaskData(taskIndex);                       // Make sure any task data is actually cleared.

          if (PluginCallForTask(taskIndex, PLUGIN_INIT, &TempEvent, str, event) &&
              loglevelActiveFor(LOG_LEVEL_INFO)) {
            addLog(LOG_LEVEL_INFO, strformat(F("INIT : Started Priority task %d, [%s] %s"),
                                             taskIndex + 1,
                                             getTaskDeviceName(taskIndex).c_str(),
                                             getPluginNameFromDeviceIndex(getDeviceIndex_from_TaskIndex(taskIndex)).c_str()));
          }
        }
      }

      return true;
    }
    #endif // if FEATURE_PLUGIN_PRIORITY

    // Call to specific task which may interact with the hardware
    case PLUGIN_INIT:
    case PLUGIN_EXIT:
    case PLUGIN_WEBFORM_LOAD:
    case PLUGIN_WEBFORM_LOAD_ALWAYS:
    case PLUGIN_WEBFORM_LOAD_OUTPUT_SELECTOR:
    case PLUGIN_READ:
    case PLUGIN_GET_PACKED_RAW_DATA:
    case PLUGIN_TASKTIMER_IN:
    case PLUGIN_PROCESS_CONTROLLER_DATA:
    {
      // FIXME TD-er: Code duplication with PluginCallForTask
      if (!validTaskIndex(event->TaskIndex)) {
        return false;
      }

      if ((Function == PLUGIN_READ) || (Function == PLUGIN_INIT) || (Function == PLUGIN_PROCESS_CONTROLLER_DATA)) {
        if (!Settings.TaskDeviceEnabled[event->TaskIndex]) {
          return false;
        }

        if (Function == PLUGIN_INIT) {
          clearTaskCache(event->TaskIndex);
          UserVar.clear_computed(event->TaskIndex);
        }
      }
      const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(event->TaskIndex);

      if (validDeviceIndex(DeviceIndex)) {
        if (ExtraTaskSettings.TaskIndex != event->TaskIndex) {
          if ((Function == PLUGIN_READ) && Device[DeviceIndex].ErrorStateValues) {
            // PLUGIN_READ should not need to access ExtraTaskSettings except for what's already being cached.
            // Only exception is when ErrorStateValues is needed.
            // Therefore only need to call LoadTaskSettings for those tasks with ErrorStateValues
            LoadTaskSettings(event->TaskIndex);
          } else if ((Function == PLUGIN_INIT) || (Function == PLUGIN_WEBFORM_LOAD) || (Function == PLUGIN_WEBFORM_LOAD_ALWAYS)) {
            // LoadTaskSettings may call PLUGIN_GET_DEVICEVALUENAMES.
            LoadTaskSettings(event->TaskIndex);
          }
        }
        event->BaseVarIndex = event->TaskIndex * VARS_PER_TASK;

        #ifndef BUILD_NO_RAM_TRACKER
        checkRAM_PluginCall_task(event->TaskIndex, Function);
        #endif // ifndef BUILD_NO_RAM_TRACKER

        if (!prepare_I2C_by_taskIndex(event->TaskIndex, DeviceIndex)) {
          return false;
        }
        bool retval                  = false;
        const bool performPluginCall =
          (Function != PLUGIN_READ && Function != PLUGIN_INIT) ||
          (Settings.TaskDeviceDataFeed[event->TaskIndex] == 0);
        #if FEATURE_I2C_DEVICE_CHECK
        bool i2cStatusOk = true;

        if (Settings.TaskDeviceDataFeed[event->TaskIndex] == 0) {
          // Only for locally connected sensors, not virtual ones via p2p.
          if (((Function == PLUGIN_INIT) || (Function == PLUGIN_READ))
              && (Device[DeviceIndex].Type == DEVICE_TYPE_I2C) && !Device[DeviceIndex].I2CNoDeviceCheck) {
            const uint8_t i2cAddr = getTaskI2CAddress(event->TaskIndex);

            if (i2cAddr > 0) {
              START_TIMER;

              // Disable task when device is unreachable for 10 PLUGIN_READs or 1 PLUGIN_INIT
              i2cStatusOk = I2C_deviceCheck(i2cAddr, event->TaskIndex, Function == PLUGIN_INIT ? 1 : 10);
              STOP_TIMER_TASK(DeviceIndex, PLUGIN_I2C_GET_ADDRESS);
            }
          }
        }

        if (i2cStatusOk) {
        #endif // if FEATURE_I2C_DEVICE_CHECK
        START_TIMER;

        if (((Function == PLUGIN_INIT) ||
             (Function == PLUGIN_WEBFORM_LOAD)) &&
            Device[DeviceIndex].ErrorStateValues) { // Only when we support ErrorStateValues
          // FIXME TD-er: Not sure if this should be called here.
          // It may be better if ranges are set in the call for default values and error values set via PLUGIN_INIT.
          // Also these may be plugin specific so perhaps create a helper function to load/save these values and call these helpers from the
          // plugin code.
          PluginCall(DeviceIndex, PLUGIN_INIT_VALUE_RANGES, event, str); // Initialize value range(s)
        }

        if ((Function == PLUGIN_INIT)
              #if FEATURE_PLUGIN_PRIORITY
            && !Settings.isPriorityTask(event->TaskIndex) // Don't clear already initialized PriorityTask data
              #endif // if FEATURE_PLUGIN_PRIORITY
            ) {
          // Make sure any task data is actually cleared.
          clearPluginTaskData(event->TaskIndex);

          /*
           #if FEATURE_DEFINE_SERIAL_CONSOLE_PORT
             if (Device[DeviceIndex].isSerial()) {
              checkSerialConflict(
                serialHelper_getSerialType(event),
                serialHelper_getRxPin(event),
                serialHelper_getTxPin(event));
             }
           #endif
           */
        }

        if (performPluginCall) {
          retval = PluginCall(DeviceIndex, Function, event, str);
        } else {
          retval = event->Source == EventValueSource::Enum::VALUE_SOURCE_UDP;
        }

        if (Function == PLUGIN_READ) {
          if (!retval) {
            String errorStr;

            if (PluginCall(DeviceIndex, PLUGIN_READ_ERROR_OCCURED, event, errorStr))
            {
              // Apparently the last read call resulted in an error
              // Send event indicating the error.
              queueTaskEvent(F("TaskError"), event->TaskIndex, errorStr);
            }
          } else {
            // Must be done as soon as there are new values, so we can keep a copy of the previous value
            // This previous value may be needed in formulas using %pvalue%
            UserVar.markPluginRead(event->TaskIndex);
              #if FEATURE_PLUGIN_STATS
            PluginTaskData_base *taskData = getPluginTaskDataBaseClassOnly(event->TaskIndex);

            if (taskData != nullptr) {
              // FIXME TD-er: Must make this flag configurable
              const bool onlyUpdateTimestampWhenSame = true;
              const bool trackpeaks                  =
                Settings.TaskDeviceDataFeed[event->TaskIndex] != 0 || // Receive data from remote node
                !Device[DeviceIndex].TaskLogsOwnPeaks;

              taskData->pushPluginStatsValues(
                event,
                trackpeaks,
                onlyUpdateTimestampWhenSame);
            }
              #endif // if FEATURE_PLUGIN_STATS
            saveUserVarToRTC();
          }
        }

        if (Function == PLUGIN_INIT) {
          if (!retval && (Settings.TaskDeviceDataFeed[event->TaskIndex] == 0)) {
            // Disable temporarily as PLUGIN_INIT failed
            // FIXME TD-er: Should reschedule call to PLUGIN_INIT????
            Settings.TaskDeviceEnabled[event->TaskIndex] = false;
          } else {
              #if FEATURE_PLUGIN_STATS

            if (Device[DeviceIndex].PluginStats) {
              PluginTaskData_base *taskData = getPluginTaskData(event->TaskIndex);

              if (taskData == nullptr) {
                // Plugin apparently does not have PluginTaskData.
                // Create Plugin Task data if it has "Stats" checked.
                LoadTaskSettings(event->TaskIndex);

                if (ExtraTaskSettings.anyEnabledPluginStats()) {
                  special_initPluginTaskData(event->TaskIndex, _StatsOnly_data_struct);
                }
              }
            }
              #endif // if FEATURE_PLUGIN_STATS
            // Schedule the plugin to be read.
            Scheduler.schedule_task_device_timer_at_init(TempEvent.TaskIndex);
            queueTaskEvent(F("TaskInit"), event->TaskIndex, retval);
          }
        }

        if (Function == PLUGIN_EXIT) {
          UserVar.clear_computed(event->TaskIndex);
          clearPluginTaskData(event->TaskIndex);

          //          clearTaskCache(event->TaskIndex);

          //            initSerial();
          queueTaskEvent(F("TaskExit"), event->TaskIndex, retval);
          updateActiveTaskUseSerial0();
        }
        STOP_TIMER_TASK(DeviceIndex, Function);
        #if FEATURE_I2C_DEVICE_CHECK
      }
        #endif // if FEATURE_I2C_DEVICE_CHECK
        post_I2C_by_taskIndex(event->TaskIndex, DeviceIndex);
        delay(0); // SMY: call delay(0) unconditionally

        return retval;
      }
      return false;
    }

    // Call to specific task not interacting with hardware
    case PLUGIN_GET_CONFIG_VALUE:
    case PLUGIN_GET_DEVICEVALUENAMES:
    case PLUGIN_GET_DEVICEGPIONAMES:
    case PLUGIN_WEBFORM_SAVE:
    case PLUGIN_WEBFORM_SHOW_VALUES:
    case PLUGIN_WEBFORM_SHOW_CONFIG:
    case PLUGIN_WEBFORM_SHOW_I2C_PARAMS:
    case PLUGIN_WEBFORM_PRE_SERIAL_PARAMS:
    case PLUGIN_WEBFORM_SHOW_SERIAL_PARAMS:
    case PLUGIN_WEBFORM_SHOW_GPIO_DESCR:
    #if FEATURE_PLUGIN_STATS
    case PLUGIN_WEBFORM_LOAD_SHOW_STATS:
    #endif // if FEATURE_PLUGIN_STATS
    case PLUGIN_SET_CONFIG:
    case PLUGIN_SET_DEFAULTS:
    case PLUGIN_I2C_HAS_ADDRESS:
    case PLUGIN_WEBFORM_SHOW_ERRORSTATE_OPT:
    case PLUGIN_INIT_VALUE_RANGES:
    #ifdef USES_ESPEASY_NOW
    case PLUGIN_FILTEROUT_CONTROLLER_DATA:
    #endif // ifdef USES_ESPEASY_NOW
    #if FEATURE_MQTT_DISCOVER || FEATURE_CUSTOM_TASKVAR_VTYPE
    case PLUGIN_GET_DISCOVERY_VTYPES:
    #endif // if FEATURE_MQTT_DISCOVER || FEATURE_CUSTOM_TASKVAR_VTYPE
    #if FEATURE_TASKVALUE_UNIT_OF_MEASURE
    case PLUGIN_GET_UOM_GROUPS:
    #endif // if FEATURE_TASKVALUE_UNIT_OF_MEASURE

    #if FEATURE_MQTT
    case PLUGIN_MQTT_CONNECTION_STATE:
    case PLUGIN_MQTT_IMPORT:
    #endif // if FEATURE_MQTT
    {
      START_TIMER;
      const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(event->TaskIndex);

      if (validDeviceIndex(DeviceIndex)) {
        if ((Function == PLUGIN_GET_DEVICEVALUENAMES) ||
            (Function == PLUGIN_WEBFORM_SAVE) ||
            (Function == PLUGIN_WEBFORM_LOAD) ||
            (Function == PLUGIN_WEBFORM_LOAD_ALWAYS) ||
            (Function == PLUGIN_SET_DEFAULTS) ||
            (Function == PLUGIN_INIT_VALUE_RANGES) ||
            (Function == PLUGIN_WEBFORM_SHOW_SERIAL_PARAMS) ||
            (Function == PLUGIN_WEBFORM_PRE_SERIAL_PARAMS)
            #if FEATURE_MQTT_DISCOVER || FEATURE_CUSTOM_TASKVAR_VTYPE
            || (Function == PLUGIN_GET_DISCOVERY_VTYPES)
            #endif // if FEATURE_MQTT_DISCOVER || FEATURE_CUSTOM_TASKVAR_VTYPE
            #if FEATURE_TASKVALUE_UNIT_OF_MEASURE
            || (Function == PLUGIN_GET_UOM_GROUPS)
            #endif // if FEATURE_TASKVALUE_UNIT_OF_MEASURE
            ) {
          LoadTaskSettings(event->TaskIndex);
        }
        event->BaseVarIndex = event->TaskIndex * VARS_PER_TASK;

        #ifndef BUILD_NO_RAM_TRACKER
        checkRAM_PluginCall_task(event->TaskIndex, Function);
        #endif // ifndef BUILD_NO_RAM_TRACKER

        if (Function == PLUGIN_SET_DEFAULTS) {
          for (int i = 0; i < VARS_PER_TASK; ++i) {
            UserVar.setFloat(event->TaskIndex, i, 0.0f);
          }
        }

        bool retval =  PluginCall(DeviceIndex, Function, event, str);

        // Calls may have updated ExtraTaskSettings, so validate them.
        ExtraTaskSettings.validate();

        if ((Function == PLUGIN_GET_DEVICEVALUENAMES) ||
            (Function == PLUGIN_WEBFORM_SAVE) ||
            (Function == PLUGIN_SET_DEFAULTS) ||
            (Function == PLUGIN_INIT_VALUE_RANGES) ||
            ((Function == PLUGIN_SET_CONFIG) && retval)) {
          // Each of these may update ExtraTaskSettings, but it may not have been saved yet.
          // Thus update the cache just in case something from it is requested from the cache.
          Cache.updateExtraTaskSettingsCache();
        }

        if (Function == PLUGIN_SET_DEFAULTS) {
          saveUserVarToRTC();
        }

        if ((Function == PLUGIN_GET_CONFIG_VALUE) && !retval) {
          // Try to match a statistical property of a task value.
          // e.g.: [taskname#valuename.avg]
          PluginTaskData_base *taskData = getPluginTaskDataBaseClassOnly(event->TaskIndex);

          if (nullptr != taskData) {
            if (taskData->plugin_get_config_value_base(event, str)) {
              retval = true;
            }
          }
        }


        STOP_TIMER_TASK(DeviceIndex, Function);
        delay(0); // SMY: call delay(0) unconditionally
        return retval;
      }
      return false;
    }

    // Frequently made call to specific task not interacting with hardware
    case PLUGIN_GET_DEVICEVALUECOUNT:
    case PLUGIN_GET_DEVICEVTYPE:
    case PLUGIN_FORMAT_USERVAR:
    {
      START_TIMER;
      const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(event->TaskIndex);

      if (validDeviceIndex(DeviceIndex)) {
        event->BaseVarIndex = event->TaskIndex * VARS_PER_TASK;

        #ifndef BUILD_NO_RAM_TRACKER

        //        checkRAM_PluginCall_task(event->TaskIndex, Function);
        #endif // ifndef BUILD_NO_RAM_TRACKER

        if (Function == PLUGIN_GET_DEVICEVALUECOUNT) {
          event->Par1 = Device[DeviceIndex].ValueCount;
        }

        if (Function == PLUGIN_GET_DEVICEVTYPE) {
          event->sensorType = Device[DeviceIndex].VType;
        }
        bool retval =  PluginCall(DeviceIndex, Function, event, str);

        if (Function == PLUGIN_GET_DEVICEVALUECOUNT) {
          // Check if we have a valid value count.
          if (Output_Data_type_t::Simple == Device[DeviceIndex].OutputDataType) {
            if ((event->Par1 < 1) || (event->Par1 > VARS_PER_TASK)) {
              // Output_Data_type_t::Simple only allows for 1 .. 4 output types.
              // Apparently the value is not correct, so use the default.
              event->Par1 = Device[DeviceIndex].ValueCount;
            }
          }
        }
        STOP_TIMER_TASK(DeviceIndex, Function);
        delay(0); // SMY: call delay(0) unconditionally
        return retval;
      }
      return false;
    }
  } // case
  return false;
}

#include "../Globals/WiFi_AP_Candidates.h"

WiFi_AP_CandidatesList WiFi_AP_Candidates;

#include "../Helpers/BusCmd_Handler_I2C.h"

/**
 * Constructor BusCmd_Handler_I2C
 */
BusCmd_Handler_I2C::BusCmd_Handler_I2C(uint8_t  i2cAddress,
                                       TwoWire *wire)
  : _i2cAddress(i2cAddress), _wire(wire)
{
  //
}

bool BusCmd_Handler_I2C::init() {
  return nullptr != _wire && _i2cAddress > 0 && _i2cAddress < 128;
}

uint8_t BusCmd_Handler_I2C::read8u() {
  return I2C_read8(_i2cAddress, &_ok);
}

uint16_t BusCmd_Handler_I2C::read16u() {
  return I2C_read16(_i2cAddress, &_ok);
}

uint32_t BusCmd_Handler_I2C::read24u() {
  return I2C_read24(_i2cAddress, &_ok);
}

uint32_t BusCmd_Handler_I2C::read32u() {
  return I2C_read32(_i2cAddress, &_ok);
}

uint8_t BusCmd_Handler_I2C::read8uREG(uint16_t reg,
                                      bool     wideReg) {
  return I2C_read8_reg(_i2cAddress, reg, &_ok);
}

uint16_t BusCmd_Handler_I2C::read16uREG(uint16_t reg,
                                        bool     wideReg) {
  return I2C_read16_reg(_i2cAddress, reg, &_ok);
}

uint32_t BusCmd_Handler_I2C::read24uREG(uint16_t reg,
                                        bool     wideReg) {
  return I2C_read24_reg(_i2cAddress, reg, &_ok);
}

uint32_t BusCmd_Handler_I2C::read32uREG(uint16_t reg,
                                        bool     wideReg) {
  return I2C_read32_reg(_i2cAddress, reg, &_ok);
}

std::vector<uint8_t>BusCmd_Handler_I2C::read8uB(uint32_t size) {
  std::vector<uint8_t> data_b;

  #ifdef ESP32

  if (size > I2C_BUFFER_LENGTH) {
    if (!_wire->setBufferSize(size)) { // 0 return on error
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_ERROR, strformat(F("I2C  : Error resizing buffer to size: %u"), size));
      # endif // ifndef BUILD_NO_DEBUG
      return data_b;
    }
  }
  #endif // ifdef ESP32
  data_b.reserve(size);

  _wire->requestFrom(_i2cAddress, size);

  for (uint8_t i = 0; i < size; ++i) {
    data_b.push_back(_wire->read());
  }
  #ifdef ESP32

  if (size > I2C_BUFFER_LENGTH) {
    _wire->setBufferSize(I2C_BUFFER_LENGTH); // Restore default
  }
  #endif // ifdef ESP32
  return data_b;
}

std::vector<uint16_t>BusCmd_Handler_I2C::read16uW(uint32_t size) {
  std::vector<uint16_t> data_w;

  #ifdef ESP32

  if ((size * 2) > I2C_BUFFER_LENGTH) {
    if (!_wire->setBufferSize(size * 2)) { // 0 return on error
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_ERROR, strformat(F("I2C  : Error resizing buffer to size: %u"), size * 2));
      # endif // ifndef BUILD_NO_DEBUG
      return data_w;
    }
  }
  #endif // ifdef ESP32
  data_w.reserve(size);

  _wire->requestFrom(_i2cAddress, size * 2);

  for (uint8_t i = 0; i < size; ++i) {
    data_w.push_back((_wire->read() << 8) | _wire->read());
  }
  #ifdef ESP32

  if ((size * 2) > I2C_BUFFER_LENGTH) {
    _wire->setBufferSize(I2C_BUFFER_LENGTH); // Restore default
  }
  #endif // ifdef ESP32
  return data_w;
}

std::vector<uint8_t>BusCmd_Handler_I2C::read8uBREG(uint16_t reg,
                                                   uint32_t size,
                                                   bool     wideReg) {
  std::vector<uint8_t> data_b;

  #ifdef ESP32

  if (size > I2C_BUFFER_LENGTH) {
    if (!_wire->setBufferSize(size)) { // 0 return on error
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_ERROR, strformat(F("I2C  : Error resizing buffer to size: %u"), size));
      # endif // ifndef BUILD_NO_DEBUG
      return data_b;
    }
  }
  #endif // ifdef ESP32
  data_b.reserve(size);

  if (I2C_write8(_i2cAddress, reg) && (_wire->requestFrom(_i2cAddress, size) == size)) {
    for (uint8_t i = 0; i < size; ++i) {
      data_b.push_back(_wire->read());
    }
  }
  #ifdef ESP32

  if (size > I2C_BUFFER_LENGTH) {
    _wire->setBufferSize(I2C_BUFFER_LENGTH); // Restore default
  }
  #endif // ifdef ESP32
  return data_b;
}

std::vector<uint16_t>BusCmd_Handler_I2C::read16uWREG(uint16_t reg,
                                                     uint32_t size,
                                                     bool     wideReg) {
  std::vector<uint16_t> data_w;

  #ifdef ESP32

  if ((size * 2) > I2C_BUFFER_LENGTH) {
    if (!_wire->setBufferSize(size * 2)) { // 0 return on error
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_ERROR, strformat(F("I2C  : Error resizing buffer to size: %u"), size * 2));
      # endif // ifndef BUILD_NO_DEBUG
      return data_w;
    }
  }
  #endif // ifdef ESP32
  data_w.reserve(size);

  if (I2C_write8(_i2cAddress, reg) && (_wire->requestFrom(_i2cAddress, size * 2) == size * 2)) {
    for (uint8_t i = 0; i < size; ++i) {
      data_w.push_back((_wire->read() << 8) | _wire->read());
    }
  }
  #ifdef ESP32

  if ((size * 2) > I2C_BUFFER_LENGTH) {
    _wire->setBufferSize(I2C_BUFFER_LENGTH); // Restore default
  }
  #endif // ifdef ESP32
  return data_w;
}

#if FEATURE_BUSCMD_STRING
String BusCmd_Handler_I2C::readString(uint32_t len) {
  String result;

  # ifdef ESP32

  if (len > I2C_BUFFER_LENGTH) {
    if (!_wire->setBufferSize(len)) { // 0 return on error
      #  ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_ERROR, strformat(F("I2C  : Error resizing buffer to size: %u"), len));
      #  endif // ifndef BUILD_NO_DEBUG
      return result;
    }
  }
  # endif // ifdef ESP32
  result.reserve(len);

  # ifdef ESP32
  const size_t maxBytesPerChunk = len;
  # endif // ifdef ESP32
  # ifdef ESP8266
  constexpr size_t maxBytesPerChunk = 128; // Chuncked in 32 byte blocks
  # endif // ifdef ESP8266
  size_t index = 0;

  while (index < len) {
    const size_t bytesRemaining = len - index;
    const size_t bytesThisChunk = min(bytesRemaining, maxBytesPerChunk);
    const size_t endIndex       = index + bytesThisChunk;
    const bool   isLastChunk    = (bytesRemaining <= maxBytesPerChunk);
    _wire->requestFrom(_i2cAddress, bytesThisChunk, isLastChunk);

    for (; index < endIndex; ++index) {
      # ifndef BUILD_NO_DEBUG

      if (_wire->available() < 1) {
        addLog(LOG_LEVEL_ERROR, strformat(F("I2C  : Error reading data, got just: %u from %u requested bytes"), index, bytesThisChunk));
        break;
      }
      # endif // ifndef BUILD_NO_DEBUG
      const uint8_t c = _wire->read();

      if (c != 0) {
        result += static_cast<char>(c);
      }
    }
  }

  # ifdef ESP32

  if (len > I2C_BUFFER_LENGTH) {
    _wire->setBufferSize(I2C_BUFFER_LENGTH); // Restore default
  }
  # endif // ifdef ESP32
  return result;
}

String BusCmd_Handler_I2C::readStringREG(uint16_t reg,
                                         uint32_t len,
                                         bool     wideReg) {
  String result;

  # ifdef ESP32

  if (len > I2C_BUFFER_LENGTH) {
    if (!_wire->setBufferSize(len)) { // 0 return on error
      #  ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_ERROR, strformat(F("I2C  : Error resizing buffer to size: %u"), len));
      #  endif // ifndef BUILD_NO_DEBUG
      return result;
    }
  }
  # endif // ifdef ESP32

  result.reserve(len);
  _wire->requestFrom(_i2cAddress, len);

  if (I2C_write8(_i2cAddress, reg) && (_wire->requestFrom(_i2cAddress, len) == len)) {
    for (uint8_t i = 0; i < len; ++i) {
      result += static_cast<char>(_wire->read());
    }
  }
  # ifdef ESP32

  if (len > I2C_BUFFER_LENGTH) {
    _wire->setBufferSize(I2C_BUFFER_LENGTH); // Restore default
  }
  # endif // ifdef ESP32
  return result;
}

#endif // if FEATURE_BUSCMD_STRING

bool BusCmd_Handler_I2C::write8u(uint8_t data) {
  return I2C_write8(_i2cAddress, data);
}

bool BusCmd_Handler_I2C::write16u(uint16_t data) {
  return I2C_write16(_i2cAddress, data);
}

bool BusCmd_Handler_I2C::write24u(uint32_t data) {
  return I2C_write24(_i2cAddress, data);
}

bool BusCmd_Handler_I2C::write32u(uint32_t data) {
  return I2C_write32(_i2cAddress, data);
}

bool BusCmd_Handler_I2C::write8uREG(uint16_t reg,
                                    uint8_t  data,
                                    bool     wideReg) {
  return I2C_write8_reg(_i2cAddress, reg, data);
}

bool BusCmd_Handler_I2C::write16uREG(uint16_t reg,
                                     uint16_t data,
                                     bool     wideReg) {
  return I2C_write16_reg(_i2cAddress, reg, data);
}

bool BusCmd_Handler_I2C::write24uREG(uint16_t reg,
                                     uint32_t data,
                                     bool     wideReg) {
  return I2C_write24_reg(_i2cAddress, reg, data);
}

bool BusCmd_Handler_I2C::write32uREG(uint16_t reg,
                                     uint32_t data,
                                     bool     wideReg) {
  return I2C_write32_reg(_i2cAddress, reg, data);
}

uint32_t BusCmd_Handler_I2C::write8uB(std::vector<uint8_t>data) {
  const size_t size = data.size();

  #ifdef ESP32

  if (size > I2C_BUFFER_LENGTH) {
    if (!_wire->setBufferSize(size)) { // 0 return on error
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_ERROR, strformat(F("I2C  : Error resizing buffer to size: %u"), size));
      # endif // ifndef BUILD_NO_DEBUG
      return 0;
    }
  }
  #endif // ifdef ESP32
  _wire->beginTransmission(_i2cAddress);

  for (size_t itb = 0; itb < size; ++itb) {
    _wire->write(data[itb]);
  }
  #ifdef ESP32

  if (size > I2C_BUFFER_LENGTH) {
    _wire->setBufferSize(I2C_BUFFER_LENGTH); // Restore default
  }
  #endif // ifdef ESP32
  return _wire->endTransmission() == 0;
}

uint32_t BusCmd_Handler_I2C::write16uW(std::vector<uint16_t>data) {
  const size_t size = data.size();

  #ifdef ESP32

  if ((size * 2) > I2C_BUFFER_LENGTH) {
    if (!_wire->setBufferSize(size * 2)) { // 0 return on error
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_ERROR, strformat(F("I2C  : Error resizing buffer to size: %u"), size * 2));
      # endif // ifndef BUILD_NO_DEBUG
      return 0;
    }
  }
  #endif // ifdef ESP32
  _wire->beginTransmission(_i2cAddress);

  for (size_t itw = 0; itw < size; ++itw) {
    _wire->write((uint8_t)(data[itw] << 8));
    _wire->write((uint8_t)(data[itw] & 0xFF));
  }
  #ifdef ESP32

  if ((size * 2) > I2C_BUFFER_LENGTH) {
    _wire->setBufferSize(I2C_BUFFER_LENGTH); // Restore default
  }
  #endif // ifdef ESP32
  return _wire->endTransmission() == 0;
}

uint32_t BusCmd_Handler_I2C::write8uBREG(uint16_t            reg,
                                         std::vector<uint8_t>data,
                                         bool                wideReg) {
  const size_t size = data.size();

  #ifdef ESP32

  if (size > I2C_BUFFER_LENGTH) {
    if (!_wire->setBufferSize(size)) { // 0 return on error
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_ERROR, strformat(F("I2C  : Error resizing buffer to size: %u"), size));
      # endif // ifndef BUILD_NO_DEBUG
      return 0;
    }
  }
  #endif // ifdef ESP32
  _wire->beginTransmission(_i2cAddress);
  _wire->write((uint8_t)reg);

  for (size_t itb = 0; itb < size; ++itb) {
    _wire->write(data[itb]);
  }
  #ifdef ESP32

  if (size > I2C_BUFFER_LENGTH) {
    _wire->setBufferSize(I2C_BUFFER_LENGTH); // Restore default
  }
  #endif // ifdef ESP32
  return _wire->endTransmission() == 0;
}

uint32_t BusCmd_Handler_I2C::write16uWREG(uint16_t             reg,
                                          std::vector<uint16_t>data,
                                          bool                 wideReg) {
  const size_t size = data.size();

  #ifdef ESP32

  if ((size * 2) > I2C_BUFFER_LENGTH) {
    if (!_wire->setBufferSize(size * 2)) { // 0 return on error
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_ERROR, strformat(F("I2C  : Error resizing buffer to size: %u"), size * 2));
      # endif // ifndef BUILD_NO_DEBUG
      return 0;
    }
  }
  #endif // ifdef ESP32
  _wire->beginTransmission(_i2cAddress);
  _wire->write((uint8_t)reg);

  for (size_t itw = 0; itw < size; ++itw) {
    _wire->write((uint8_t)(data[itw] << 8));
    _wire->write((uint8_t)(data[itw] & 0xFF));
  }
  #ifdef ESP32

  if ((size * 2) > I2C_BUFFER_LENGTH) {
    _wire->setBufferSize(I2C_BUFFER_LENGTH); // Restore default
  }
  #endif // ifdef ESP32
  return _wire->endTransmission() == 0;
}

#if FEATURE_BUSCMD_STRING
uint32_t BusCmd_Handler_I2C::writeString(const String& data) {
  const size_t len = data.length();

  # ifdef ESP32

  if (len > I2C_BUFFER_LENGTH) {
    if (!_wire->setBufferSize(len)) { // 0 return on error
      #  ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_ERROR, strformat(F("I2C  : Error resizing buffer to size: %u"), len));
      #  endif // ifndef BUILD_NO_DEBUG
      return 0;
    }
  }
  # endif // ifdef ESP32
  _wire->beginTransmission(_i2cAddress);

  for (size_t its = 0; its < len; ++its) {
    _wire->write(data[its]);
  }
  # ifdef ESP32

  if (len > I2C_BUFFER_LENGTH) {
    _wire->setBufferSize(I2C_BUFFER_LENGTH); // Restore default
  }
  # endif // ifdef ESP32
  return _wire->endTransmission() == 0;
}

uint32_t BusCmd_Handler_I2C::writeStringReg(uint16_t      reg,
                                            const String& data,
                                            bool          wideReg) {
  const size_t len = data.length();

  # ifdef ESP32

  if (len > I2C_BUFFER_LENGTH) {
    if (!_wire->setBufferSize(len)) { // 0 return on error
      #  ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_ERROR, strformat(F("I2C  : Error resizing buffer to size: %u"), len));
      #  endif // ifndef BUILD_NO_DEBUG
      return 0;
    }
  }
  # endif // ifdef ESP32
  _wire->beginTransmission(_i2cAddress);
  _wire->write((uint8_t)reg);

  for (size_t its = 0; its < len; ++its) {
    _wire->write(data[its]);
  }
  # ifdef ESP32

  if (len > I2C_BUFFER_LENGTH) {
    _wire->setBufferSize(I2C_BUFFER_LENGTH); // Restore default
  }
  # endif // ifdef ESP32
  return _wire->endTransmission() == 0;
}

#endif // if FEATURE_BUSCMD_STRING

#include "../Helpers/CUL_interval_filter.h"


#ifdef USES_P094

# include "../ESPEasyCore/ESPEasy_Log.h"
# include "../Globals/ESPEasy_time.h"
# include "../Globals/TimeZone.h"
# include "../Helpers/ESPEasy_time_calc.h"
# include "../Helpers/StringConverter.h"


CUL_time_filter_struct::CUL_time_filter_struct(uint32_t checksum, unsigned long UnixTimeExpiration)
  : _checksum(checksum), _UnixTimeExpiration(UnixTimeExpiration) {}

String CUL_interval_filter_getExpiration_log_str(const P094_filter& filter)
{
  const unsigned long expiration = filter.computeUnixTimeExpiration();

  if ((expiration != 0) && (expiration != 0xFFFFFFFF)) {
    struct tm exp_tm;
    breakTime(time_zone.toLocal(expiration), exp_tm);

    return concat(F(" Expiration: "), formatDateTimeString(exp_tm));
  }
  return EMPTY_STRING;
}

bool CUL_interval_filter::filter(const mBusPacket_t& packet, const P094_filter& filter)
{
  if (!enabled) {
    return true;
  }

  if (filter.getFilterWindow() == P094_Filter_Window::None) {
    // Will always be rejected, so no need to keep track of the message
    return false;
  }

  if (filter.getFilterWindow() == P094_Filter_Window::All) {
    // Will always be allowed, so no need to keep track of the message
    return true;
  }

  const uint32_t key = packet.deviceID_to_map_key();
  auto it            = _mBusFilterMap.find(key);

  if (it != _mBusFilterMap.end()) {
    // Already present
    if (node_time.getUnixTime() < it->second._UnixTimeExpiration) {
      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        String log = concat(F("CUL   : Interval filtered: "), packet.toString());
        log += CUL_interval_filter_getExpiration_log_str(filter);
        addLogMove(LOG_LEVEL_INFO, log);
      }
      return false;
    }

    if (packet._checksum == it->second._checksum) {
      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLogMove(LOG_LEVEL_INFO, concat(F("CUL   : Interval Same Checksum: "), packet.toString()));
      }
      return false;
    }

    // Has expired, so remove from filter map
    _mBusFilterMap.erase(it);
  }

  const unsigned long expiration = filter.computeUnixTimeExpiration();

  CUL_time_filter_struct item(packet._checksum, expiration);

  _mBusFilterMap[key] = item;

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    String log = concat(F("CUL   : Add to IntervalFilter: "), packet.toString());
    log += CUL_interval_filter_getExpiration_log_str(filter);

    addLogMove(LOG_LEVEL_INFO, log);
  }

  return true;
}

void CUL_interval_filter::purgeExpired()
{
  auto it = _mBusFilterMap.begin();

  const unsigned long currentTime = node_time.getUnixTime();

  for (; it != _mBusFilterMap.end();) {
    if (currentTime > it->second._UnixTimeExpiration) {
      it = _mBusFilterMap.erase(it);
    } else {
      ++it;
    }
  }
}

#endif // ifdef USES_P094

#include "../Helpers/Hardware_device_info.h"

#ifdef ESP32C2

# include <soc/soc.h>
# include <soc/efuse_reg.h>
# include <soc/spi_reg.h>
# include <soc/spi_pins.h>
# include <soc/rtc.h>
# include <esp_chip_info.h>
# include <bootloader_common.h>


bool isFlashInterfacePin_ESPEasy(int gpio) {
  // GPIO-11: Flash voltage selector
  // For chip variants with a SiP flash built in, GPIO11~ GPIO17 are dedicated to connecting SiP flash, not for other uses
  //  return (gpio) >= 12 && (gpio) <= 17;
  switch (gpio) {
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 0)
    case SPI_IOMUX_PIN_NUM_HD:
    case SPI_IOMUX_PIN_NUM_CS:
    case SPI_IOMUX_PIN_NUM_MOSI:
    case SPI_IOMUX_PIN_NUM_CLK:
    case SPI_IOMUX_PIN_NUM_MISO:
    case SPI_IOMUX_PIN_NUM_WP:
#else
    case MSPI_IOMUX_PIN_NUM_HD:
    case MSPI_IOMUX_PIN_NUM_WP:
    case MSPI_IOMUX_PIN_NUM_CS0:
    case MSPI_IOMUX_PIN_NUM_CLK:
    case MSPI_IOMUX_PIN_NUM_MOSI:
    case MSPI_IOMUX_PIN_NUM_MISO:
#endif
      return true;
  }
  return false;
}

bool flashVddPinCanBeUsedAsGPIO()
{
  return false;
}

int32_t getEmbeddedFlashSize()
{
  // ESP32-C2 doesn't have eFuse field FLASH_CAP.
  // Can't get info about the flash chip.
  return 0;
}

int32_t getEmbeddedPSRAMSize()
{
  // Doesn't have PSRAM
  return 0;
}

# ifndef isPSRAMInterfacePin
bool isPSRAMInterfacePin(int gpio) {
  return false;
}

# endif // ifndef isPSRAMInterfacePin


const __FlashStringHelper* getChipModel(uint32_t chip_model, uint32_t chip_revision, uint32_t pkg_version, bool single_core)
{
  switch (pkg_version) {
    case 0: return F("ESP32-C2");
    case 1: return F("ESP32-C2");
  }
  return F("ESP32-C2");
}

#endif // ifdef ESP32C2

#include "../Helpers/StringGenerator_System.h"

#include "../Helpers/ESPEasy_time_calc.h"
#include "../Helpers/Hardware_device_info.h"
#include "../Helpers/StringConverter.h"

/*********************************************************************************************\
   ESPEasy specific strings
\*********************************************************************************************/

#include "../CustomBuild/CompiletimeDefines.h"

#if defined(ESP32)
  #ifdef ESP32S2
    #include <esp32s2/rom/rtc.h>
  #elif defined(ESP32S3)
    #include <esp32s3/rom/rtc.h>
  #elif defined(ESP32C2)
    #include <esp32c2/rom/rtc.h>
  #elif defined(ESP32C3)
    #include <esp32c3/rom/rtc.h>
  #elif defined(ESP32C6)
    #include <esp32c6/rom/rtc.h>
  # elif defined(ESP32_CLASSIC)
    #if ESP_IDF_VERSION_MAJOR > 3
      #include <esp32/rom/rtc.h>
    #else
      #include <rom/rtc.h>
    #endif
  # else

    static_assert(false, "Implement processor architecture");

  #endif
#endif

#if FEATURE_MQTT

//#include <PubSubClient.h>
#include "../Globals/MQTT.h"

const __FlashStringHelper * getMQTT_state() {
  switch (MQTTclient.state()) {
    case MQTT_CONNECTION_TIMEOUT: return F("Connection timeout");
    case MQTT_CONNECTION_LOST: return F("Connection lost");
    case MQTT_CONNECT_FAILED: return F("Connect failed");
    case MQTT_DISCONNECTED: return F("Disconnected");
    case MQTT_CONNECTED: return F("Connected");
    case MQTT_CONNECT_BAD_PROTOCOL: return F("Connect bad protocol");
    case MQTT_CONNECT_BAD_CLIENT_ID: return F("Connect bad client_id");
    case MQTT_CONNECT_UNAVAILABLE: return F("Connect unavailable");
    case MQTT_CONNECT_BAD_CREDENTIALS: return F("Connect bad credentials");
    case MQTT_CONNECT_UNAUTHORIZED: return F("Connect unauthorized");
    default: break;
  }
  return F("");
}

#endif // if FEATURE_MQTT

/********************************************************************************************\
   Get system information
 \*********************************************************************************************/
const __FlashStringHelper * getLastBootCauseString() {
  switch (lastBootCause)
  {
    case BOOT_CAUSE_MANUAL_REBOOT: return F("Manual Reboot");
    case BOOT_CAUSE_DEEP_SLEEP:    return F("Deep Sleep");
    case BOOT_CAUSE_COLD_BOOT:     return F("Cold Boot");
    case BOOT_CAUSE_EXT_WD:        return F("External Watchdog");
    case BOOT_CAUSE_SOFT_RESTART:  return F("Soft Reboot");
    case BOOT_CAUSE_SW_WATCHDOG:   return F("SW Watchdog");
    case BOOT_CAUSE_EXCEPTION:     return F("Exception");
    case BOOT_CAUSE_POWER_UNSTABLE: return F("PWR Unstable"); // ESP32 only
  }
  return F("Unknown");
}

#ifdef ESP32

// See https://github.com/espressif/esp-idf/blob/master/components/esp32/include/rom/rtc.h
const __FlashStringHelper * getResetReasonString_f(uint8_t icore, bool& isDEEPSLEEP_RESET) {
  isDEEPSLEEP_RESET = false;

  #ifdef ESP32S2

  // See tools\sdk\esp32\include\esp_rom\include\esp32s2\rom\rtc.h
  switch (rtc_get_reset_reason(icore)) {
    case NO_MEAN                : break;
    case POWERON_RESET          : return F("Vbat power on reset");
    case RTC_SW_SYS_RESET       : return F("Software reset digital core");
    case DEEPSLEEP_RESET        : isDEEPSLEEP_RESET = true; break;
    case TG0WDT_SYS_RESET       : return F("Timer Group0 Watch dog reset digital core");
    case TG1WDT_SYS_RESET       : return F("Timer Group1 Watch dog reset digital core");
    case RTCWDT_SYS_RESET       : return F("RTC Watch dog reset digital core");
    case INTRUSION_RESET        : return F("Instrusion tested to reset CPU");
    case TG0WDT_CPU_RESET       : return F("Time Group0 reset CPU");
    case RTC_SW_CPU_RESET       : return F("Software reset CPU");
    case RTCWDT_CPU_RESET       : return F("RTC Watch dog reset CPU");
    case RTCWDT_BROWN_OUT_RESET : return F("Reset when the vdd voltage is not stable");
    case RTCWDT_RTC_RESET       : return F("RTC Watch dog reset digital core and rtc module");
    case TG1WDT_CPU_RESET       : return F("Time Group1 reset CPU");
    case SUPER_WDT_RESET        : return F("super watchdog reset digital core and rtc module");
    case GLITCH_RTC_RESET       : return F("glitch reset digital core and rtc module");
    case EFUSE_RESET            : return F("efuse reset digital core");
  }

  #elif defined(ESP32S3)

  // See tools\sdk\esp32\include\esp_rom\include\esp32s3\rom\rtc.h
  switch (rtc_get_reset_reason(icore)) {
    case NO_MEAN                : break;
    case POWERON_RESET          : return F("Vbat power on reset");
    case RTC_SW_SYS_RESET       : return F("Software reset digital core");
    case DEEPSLEEP_RESET        : isDEEPSLEEP_RESET = true; break;
    case TG0WDT_SYS_RESET       : return F("Timer Group0 Watch dog reset digital core");
    case TG1WDT_SYS_RESET       : return F("Timer Group1 Watch dog reset digital core");
    case RTCWDT_SYS_RESET       : return F("RTC Watch dog reset digital core");
    case INTRUSION_RESET        : return F("Instrusion tested to reset CPU");
    case TG0WDT_CPU_RESET       : return F("Time Group0 reset CPU");
    case RTC_SW_CPU_RESET       : return F("Software reset CPU");
    case RTCWDT_CPU_RESET       : return F("RTC Watch dog reset CPU");
    case RTCWDT_BROWN_OUT_RESET : return F("Reset when the vdd voltage is not stable");
    case RTCWDT_RTC_RESET       : return F("RTC Watch dog reset digital core and rtc module");
    case TG1WDT_CPU_RESET       : return F("Time Group1 reset CPU");
    case SUPER_WDT_RESET        : return F("super watchdog reset digital core and rtc module");
    case GLITCH_RTC_RESET       : return F("glitch reset digital core and rtc module");
    case EFUSE_RESET            : return F("efuse reset digital core");
    case USB_UART_CHIP_RESET    : return F("usb uart reset digital core ");
    case USB_JTAG_CHIP_RESET    : return F("usb jtag reset digital core ");
    case POWER_GLITCH_RESET     : return F("power glitch reset digital core and rtc module");
  }

  #elif defined(ESP32C2)

  // See tools\sdk\esp32\include\esp_rom\include\esp32c2\rom\rtc.h
  switch (rtc_get_reset_reason(icore)) {
    case NO_MEAN                : break;
    case POWERON_RESET          : return F("Vbat power on reset");
    case RTC_SW_SYS_RESET       : return F("Software reset digital core");
    case DEEPSLEEP_RESET        : isDEEPSLEEP_RESET = true; break;
    // case DEEPSLEEP_RESET        : return F("Deep Sleep reset digital core");
    case TG0WDT_SYS_RESET       : return F("Timer Group0 Watch dog reset digital core");
    case RTCWDT_SYS_RESET       : return F("RTC Watch dog Reset digital core");
    case INTRUSION_RESET        : return F("Instrusion tested to reset CPU");
    case TG0WDT_CPU_RESET       : return F("Time Group0 reset CPU");
    case RTC_SW_CPU_RESET       : return F("Software reset CPU");
    case RTCWDT_CPU_RESET       : return F("RTC Watch dog Reset CPU");
    case RTCWDT_BROWN_OUT_RESET : return F("Reset when the vdd voltage is not stable");
    case RTCWDT_RTC_RESET       : return F("RTC Watch dog reset digital core and rtc module");
    case SUPER_WDT_RESET        : return F("super watchdog reset digital core and rtc module");
    case GLITCH_RTC_RESET       : return F("glitch reset digital core and rtc module");
    case EFUSE_RESET            : return F("efuse reset digital core");
    case JTAG_RESET             : return F("jtag reset CPU");
  }


  #elif defined(ESP32C3)

  // See tools\sdk\esp32\include\esp_rom\include\esp32c3\rom\rtc.h
  switch (rtc_get_reset_reason(icore)) {
    case NO_MEAN                : break;
    case POWERON_RESET          : return F("Vbat power on reset");
    case RTC_SW_SYS_RESET       : return F("Software reset digital core");
    case DEEPSLEEP_RESET        : isDEEPSLEEP_RESET = true; break;
    case TG0WDT_SYS_RESET       : return F("Timer Group0 Watch dog reset digital core");
    case TG1WDT_SYS_RESET       : return F("Timer Group1 Watch dog reset digital core");
    case RTCWDT_SYS_RESET       : return F("RTC Watch dog reset digital core");
    case INTRUSION_RESET        : return F("Instrusion tested to reset CPU");
    case TG0WDT_CPU_RESET       : return F("Time Group0 reset CPU");
    case RTC_SW_CPU_RESET       : return F("Software reset CPU");
    case RTCWDT_CPU_RESET       : return F("RTC Watch dog reset CPU");
    case RTCWDT_BROWN_OUT_RESET : return F("Reset when the vdd voltage is not stable");
    case RTCWDT_RTC_RESET       : return F("RTC Watch dog reset digital core and rtc module");
    case TG1WDT_CPU_RESET       : return F("Time Group1 reset CPU");
    case SUPER_WDT_RESET        : return F("super watchdog reset digital core and rtc module");
    case GLITCH_RTC_RESET       : return F("glitch reset digital core and rtc module");
    case EFUSE_RESET            : return F("efuse reset digital core");
    case USB_UART_CHIP_RESET    : return F("usb uart reset digital core ");
    case USB_JTAG_CHIP_RESET    : return F("usb jtag reset digital core ");
    case POWER_GLITCH_RESET     : return F("power glitch reset digital core and rtc module");
  }

  #elif defined(ESP32C6)

  // See tools\sdk\esp32\include\esp_rom\include\esp32c6\rom\rtc.h
  switch (rtc_get_reset_reason(icore)) {
    case NO_MEAN                : break;
    case POWERON_RESET          : return F("Vbat power on reset");
    case RTC_SW_SYS_RESET       : return F("Software reset digital core (hp system)");
    case DEEPSLEEP_RESET        : isDEEPSLEEP_RESET = true; break;
    //case DEEPSLEEP_RESET        : return F("Deep Sleep reset digital core (hp system)");
    case SDIO_RESET             : return F("Reset by SLC module, reset digital core (hp system)");
    case TG0WDT_SYS_RESET       : return F("Timer Group0 Watch dog reset digital core (hp system)");
    case TG1WDT_SYS_RESET       : return F("Timer Group1 Watch dog reset digital core (hp system)");
    case RTCWDT_SYS_RESET       : return F("RTC Watch dog Reset digital core (hp system)");
    case TG0WDT_CPU_RESET       : return F("Time Group0 reset CPU");
    case RTC_SW_CPU_RESET       : return F("Software reset CPU");
    case RTCWDT_CPU_RESET       : return F("RTC Watch dog Reset CPU");
    case RTCWDT_BROWN_OUT_RESET : return F("Reset when the vdd voltage is not stable");
    case RTCWDT_RTC_RESET       : return F("RTC Watch dog reset digital core and rtc module");
    case TG1WDT_CPU_RESET       : return F("Time Group1 reset CPU");
    case SUPER_WDT_RESET        : return F("super watchdog reset digital core and rtc module");
    case EFUSE_RESET            : return F("efuse reset digital core (hp system)");
    case USB_UART_CHIP_RESET    : return F("usb uart reset digital core (hp system)");
    case USB_JTAG_CHIP_RESET    : return F("usb jtag reset digital core (hp system)");
    case JTAG_RESET             : return F("jtag reset CPU");
  }


  # elif defined(ESP32_CLASSIC)

  // See https://github.com/espressif/esp-idf/blob/master/components/esp32/include/rom/rtc.h
  switch (rtc_get_reset_reason((RESET_REASON)icore)) {
    case NO_MEAN                : break;
    case POWERON_RESET          : return F("Vbat power on reset");
    case SW_RESET               : return F("Software reset digital core");
    case OWDT_RESET             : return F("Legacy watch dog reset digital core");
    case DEEPSLEEP_RESET        : isDEEPSLEEP_RESET = true; break;
    case SDIO_RESET             : return F("Reset by SLC module, reset digital core");
    case TG0WDT_SYS_RESET       : return F("Timer Group0 Watch dog reset digital core");
    case TG1WDT_SYS_RESET       : return F("Timer Group1 Watch dog reset digital core");
    case RTCWDT_SYS_RESET       : return F("RTC Watch dog reset digital core");
    case INTRUSION_RESET        : return F("Instrusion tested to reset CPU");
    case TGWDT_CPU_RESET        : return F("Time Group reset CPU");
    case SW_CPU_RESET           : return F("Software reset CPU");
    case RTCWDT_CPU_RESET       : return F("RTC Watch dog reset CPU");
    case EXT_CPU_RESET          : return F("for APP CPU, reset by PRO CPU");
    case RTCWDT_BROWN_OUT_RESET : return F("Reset when the vdd voltage is not stable");
    case RTCWDT_RTC_RESET       : return F("RTC Watch dog reset digital core and rtc module");
    default: break;
  }

  # else
    static_assert(false, "Implement processor architecture");

  #endif
  return F("");
}


String getResetReasonString(uint8_t icore) {
  bool isDEEPSLEEP_RESET(false);
  const String res = getResetReasonString_f(icore, isDEEPSLEEP_RESET);
  if (!res.isEmpty()) return res;

  if (isDEEPSLEEP_RESET) {
    String reason = F("Deep Sleep, Wakeup reason (");
    reason += rtc_get_wakeup_cause();
    reason += ')';

/*
  switch (reason) {
  #if CONFIG_IDF_TARGET_ESP32S2
    case POWERON_RESET:
    case RTC_SW_CPU_RESET:
    case DEEPSLEEP_RESET:
    case RTC_SW_SYS_RESET:
  #elif CONFIG_IDF_TARGET_ESP32
    case POWERON_RESET:
    case SW_CPU_RESET:
    case DEEPSLEEP_RESET:
    case SW_RESET:
  #endif
  }
*/
    return reason;
  }

  return F("Unknown");
}

#endif // ifdef ESP32

String getResetReasonString() {
  #ifdef ESP32
  String reason = F("CPU0: ");
  reason += getResetReasonString(0);
  if (getChipCores() > 1) { // Only report if we really have more than 1 core
    reason += F(" CPU1: ");
    reason += getResetReasonString(1);
  }
  return reason;
  #else // ifdef ESP32
  return ESP.getResetReason();
  #endif // ifdef ESP32
}

String getSystemBuildString() {
  return formatSystemBuildNr(get_build_nr());
}

String formatSystemBuildNr(uint16_t buildNr) {
  if (buildNr < 20200) return String(buildNr);

  // Build NR is used as a "revision" nr for settings
  // As of 2022-08-18, it is the nr of days since 2022-08-18 + 20200
  const uint32_t seconds_since_start = (buildNr - 20200) * 86400;
  const uint32_t unix_time_start = 1660780800; // Thu Aug 18 2022 00:00:00 GMT+0000
  struct tm build_time;
  breakTime(unix_time_start + seconds_since_start, build_time);

  String res = formatDateString(build_time, '\0');
  return res;
}

String getPluginDescriptionString() {
  String result = F("["
  #ifdef PLUGIN_BUILD_NORMAL
    "\"Normal\""
  #endif // ifdef PLUGIN_BUILD_NORMAL
  #ifdef PLUGIN_BUILD_COLLECTION
    "\"Collection\""
  #endif // ifdef PLUGIN_BUILD_COLLECTION
  #ifdef PLUGIN_BUILD_DEV
    "\"Development\""
  #endif // ifdef PLUGIN_BUILD_DEV
  #ifdef PLUGIN_DESCR
    "\"" PLUGIN_DESCR "\""
  #endif // ifdef PLUGIN_DESCR
  #ifdef BUILD_NO_DEBUG
    "\"No Debug Log\""
  #endif
  #if FEATURE_NON_STANDARD_24_TASKS && defined(ESP8266)
    "\"24tasks\""
  #endif // if FEATURE_NON_STANDARD_24_TASKS && defined(ESP8266)
  "]");
  result.replace("\"\"", "\",\"");
  return result;
}

String getSystemLibraryString() {
  String result;

  #if defined(ESP32)
  result += F("ESP32 SDK ");
  result += ESP.getSdkVersion();
  #else // if defined(ESP32)
  result += F("ESP82xx Core ");
  result += ESP.getCoreVersion();
  result += F(", NONOS SDK ");
  result += system_get_sdk_version();
  result += F(", LWIP: ");
  result += getLWIPversion();
  #endif // if defined(ESP32)

  if (puyaSupport()) {
    result += F(" PUYA support");
  }
  return result;
}

#ifdef ESP8266
String getLWIPversion() {
  String result;

  result += LWIP_VERSION_MAJOR;
  result += '.';
  result += LWIP_VERSION_MINOR;
  result += '.';
  result += LWIP_VERSION_REVISION;

  if (LWIP_VERSION_IS_RC) {
    result += F("-RC");
    result += LWIP_VERSION_RC;
  } else if (LWIP_VERSION_IS_DEVELOPMENT) {
    result += F("-dev");
  }
  return result;
}

#endif // ifdef ESP8266


#include "../Helpers/ESPEasy_time.h"

#include "../../ESPEasy_common.h"

#include "../../_Plugin_Helper.h"

#include "../CustomBuild/CompiletimeDefines.h"

#include "../DataStructs/NTP_packet.h"
#include "../DataStructs/TimingStats.h"

#include "../DataTypes/TimeSource.h"

#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../ESPEasyCore/ESPEasyNetwork.h"

#include "../Globals/EventQueue.h"
#include "../Globals/NetworkState.h"
#include "../Globals/Nodes.h"
#include "../Globals/RTC.h"
#include "../Globals/Settings.h"
#include "../Globals/TimeZone.h"

#ifdef USES_ESPEASY_NOW
# include "../Globals/ESPEasy_now_handler.h"
#endif // ifdef USES_ESPEASY_NOW


#include "../Helpers/Convert.h"
#include "../Helpers/Hardware.h"
#include "../Helpers/Hardware_I2C.h"
#include "../Helpers/Misc.h"
#include "../Helpers/Networking.h"
#include "../Helpers/Numerical.h"
#include "../Helpers/StringConverter.h"

#include "../Helpers/ESPEasy_time_calc.h"

#include <time.h>

#if FEATURE_EXT_RTC
# include <RTClib.h>
#endif // if FEATURE_EXT_RTC


ESPEasy_time::ESPEasy_time() {
  memset(&local_tm, 0, sizeof(tm));
  memset(&tsRise,   0, sizeof(tm));
  memset(&tsSet,    0, sizeof(tm));
  memset(&sunRise,  0, sizeof(tm));
  memset(&sunSet,   0, sizeof(tm));
}

struct tm ESPEasy_time::addSeconds(const struct tm& ts, int seconds, bool toLocalTime, bool fromLocalTime) const {
  unsigned long time = makeTime(ts);

  if (fromLocalTime) {
    time = time_zone.fromLocal(time);
  }

  time += seconds;

  if (toLocalTime) {
    time = time_zone.toLocal(time);
  }
  struct tm result;

  breakTime(time, result);
  return result;
}

void ESPEasy_time::restoreFromRTC()
{
  static bool firstCall = true;

#if FEATURE_EXT_RTC
  uint32_t unixtime = 0;

  if (ExtRTC_get(unixtime)) {
    setExternalTimeSource(unixtime, timeSource_t::External_RTC_time_source);
    firstCall = false;
    return;
  }
#endif // if FEATURE_EXT_RTC

  if (firstCall && (RTC.lastSysTime != 0) && (RTC.deepSleepState != 1)) {
    firstCall = false;

    // Check to see if we have some kind of believable timestamp
    // It should not be before the build time
    // Still it makes sense to restore RTC time to get some kind of continuous logging when no time source is available.
    // ToDo TD-er: Fix this when time travel appears to be possible
    setExternalTimeSource(RTC.lastSysTime + getUptime_in_sec(),
                          RTC.lastSysTime < get_build_unixtime()
        ? timeSource_t::No_time_source
        : timeSource_t::Restore_RTC_time_source);
    initTime();
  }
}

bool ESPEasy_time::setExternalTimeSource_withTimeWander(
  double       new_time,
  timeSource_t new_timeSource,
  int32_t      wander,
  uint8_t      unitnr)
{
  if ((lastSyncTime_ms != 0) && (new_timeSource == _timeSource)) {
    if (new_timeSource != timeSource_t::Manual_set) {
      // Update from the same type of time source, except when manually adjusting the time
      if (timePassedSince(lastSyncTime_ms) < EXT_TIME_SOURCE_MIN_UPDATE_INTERVAL_MSEC) {
        return false;
      }
    }
  }

  if ((_timeSource < new_timeSource) &&
      (new_timeSource != timeSource_t::No_time_source) &&
      (new_timeSource != timeSource_t::Manual_set))
  {
    if (wander < 0) {
      wander = computeExpectedWander(new_timeSource);
    }

    // New time source is potentially worse than the current one.
    if (computeExpectedWander(_timeSource, timePassedSince(lastSyncTime_ms)) <
        static_cast<uint32_t>(wander)) {
      return false;
    }
  }

  // FIXME TD-er: Why allowing (new_timeSource == timeSource_t::No_time_source) ???
  if ((new_timeSource == timeSource_t::No_time_source) ||
      (new_timeSource == timeSource_t::Manual_set) ||
      (new_time > get_build_unixtime())) {
    if (externalUnixTime_offset_usec == 0) {
      const int64_t cur_system_Unixtime_usec = getMicros64() + unixTime_usec_uptime_offset;
      const int64_t new_Unixtime_usec        = new_time * 1000000.0;
      externalUnixTime_offset_usec = new_Unixtime_usec - cur_system_Unixtime_usec;
    }
    extTimeSource       = new_timeSource;
    lastSyncTime_ms     = millis();
    timeSource_p2p_unit = unitnr;
#ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, strformat(
                   F("Time : Set Ext. Time Source: %s time: %.3f offset: %s"),
                   String(toString(new_timeSource)).c_str(),
                   new_time,
                   secondsToDayHourMinuteSecond_ms(externalUnixTime_offset_usec).c_str()));
    }
#endif // ifndef BUILD_NO_DEBUG

    initTime();
  }
  return true;
}

bool ESPEasy_time::setExternalTimeSource(double new_time, timeSource_t new_timeSource, uint8_t unitnr) {
  return setExternalTimeSource_withTimeWander(
    new_time,
    new_timeSource,
    computeExpectedWander(new_timeSource),
    unitnr);
}

uint32_t ESPEasy_time::getUptime_in_sec() const {
  return getMicros64() / 1000000ull;
}

uint32_t ESPEasy_time::getUnixTime() const
{
  const uint64_t unixtime_usec = getMicros64() + unixTime_usec_uptime_offset;

  return static_cast<uint32_t>(unixtime_usec / 1000000ull);
}

uint32_t ESPEasy_time::getUnixTime(uint32_t& unix_time_frac) const
{
  return systemMicros_to_Unixtime(getMicros64(), unix_time_frac);
}

int64_t ESPEasy_time::Unixtime_to_systemMicros(const uint32_t& unix_time_sec, uint32_t unix_time_frac) const
{
  const int64_t res =
    (static_cast<int64_t>(unix_time_sec) * 1000000ll) +
    unix_time_frac_to_micros(unix_time_frac);

  if (unixTime_usec_uptime_offset == 0) {
    // Time has not been set
    return res;
  }
  return res - unixTime_usec_uptime_offset;
}

uint32_t ESPEasy_time::systemMicros_to_Unixtime(const int64_t& systemMicros, uint32_t& unix_time_frac) const
{
  return micros_to_sec_time_frac(systemMicros + unixTime_usec_uptime_offset, unix_time_frac);
}

uint32_t ESPEasy_time::systemMicros_to_Localtime(const int64_t& systemMicros, uint32_t& unix_time_frac) const
{
  return time_zone.toLocal(systemMicros_to_Unixtime(systemMicros, unix_time_frac));
}

void ESPEasy_time::initTime()
{
  nextSyncTime = 0;
  now_();
}

unsigned long ESPEasy_time::getLocalUnixTime() const
{
  return time_zone.toLocal(getUnixTime());
}

unsigned long ESPEasy_time::getLocalUnixTime(uint32_t& unix_time_frac) const
{
  return time_zone.toLocal(getUnixTime(unix_time_frac));
}

unsigned long ESPEasy_time::now_() {
  bool timeSynced = false;

  if (nextSyncTime <= getUptime_in_sec()) {
    // nextSyncTime is in seconds
    double unixTime_d = -1.0;

    bool updatedTime = false;

    if ((externalUnixTime_offset_usec != 0) &&
        (extTimeSource != timeSource_t::No_time_source)) {
      unixTime_d = getMicros64() +
                   unixTime_usec_uptime_offset +
                   externalUnixTime_offset_usec;
      unixTime_d /= 1000000.0;

      syncInterval = EXT_TIME_SOURCE_MIN_UPDATE_INTERVAL_SEC;
      updatedTime  = true;
      _timeSource  = extTimeSource;
    } else {
      if (!isExternalTimeSource(_timeSource)
          || (timePassedSince(lastSyncTime_ms) > static_cast<long>(1000 * syncInterval)))
      {
        externalUnixTime_offset_usec = 0;

        // FIXME TD-er: calls to set external timesource should be done via the scheduler
        // Those should then also call setExternalTimeSource,
        // which determines whether the newly set time is an improvement
        if (getNtpTime(unixTime_d)) {
          updatedTime = true;
        } else {
        #if FEATURE_ESPEASY_P2P

          if (!updatedTime) {
            double  tmp_unixtime_d{};
            int32_t wander{};
            const timeSource_t tmp_timeSource = Nodes.getUnixTime(tmp_unixtime_d, wander, timeSource_p2p_unit);

            if (tmp_timeSource != timeSource_t::No_time_source) {
              // Nodes.getUnixTime does compensate for any delay since the timestamp was received from the p2p node
              // thus this can be used here without further compensation.

              // FIXME TD-er: Should check if this is a better time source compared to what we already have, using time wander

              unixTime_d   = tmp_unixtime_d;
              _timeSource  = tmp_timeSource;
              updatedTime  = true;
              syncInterval = EXT_TIME_SOURCE_MIN_UPDATE_INTERVAL_SEC;
            }
          }
        #endif // if FEATURE_ESPEASY_P2P

        #if FEATURE_EXT_RTC
          uint32_t tmp_unixtime = 0;

          if (!updatedTime &&
              (_timeSource > timeSource_t::External_RTC_time_source) && // No need to set from ext RTC more than once.
              ExtRTC_get(tmp_unixtime)) {
            unixTime_d   = tmp_unixtime;
            _timeSource  = timeSource_t::External_RTC_time_source;
            updatedTime  = true;
            syncInterval = 120; // Allow sync in 2 minutes to see if we get some better options from p2p nodes.
          }
        #endif // if FEATURE_EXT_RTC

          if (updatedTime && (externalUnixTime_offset_usec == 0)) {
            const int64_t cur_system_Unixtime_usec = getMicros64() + unixTime_usec_uptime_offset;
            const int64_t new_Unixtime_usec        = unixTime_d * 1000000.0;
            externalUnixTime_offset_usec = new_Unixtime_usec - cur_system_Unixtime_usec;
          }
        }
      }
    }

    // Clear the external time source so it has to be set again with updated values.
    extTimeSource = timeSource_t::No_time_source;

    if ((_timeSource != timeSource_t::ESPEASY_p2p_UDP) &&
        (_timeSource != timeSource_t::ESP_now_peer))
    {
      timeSource_p2p_unit = 0;
    }

    if (updatedTime) {
      START_TIMER;
      unixTime_usec_uptime_offset += externalUnixTime_offset_usec;

      constexpr int64_t ten_sec_in_usec = 10 * 1000000ll;

      if ((lastTimeWanderCalculation_ms != 0) &&
          statusNTPInitialized &&
          (std::abs(externalUnixTime_offset_usec) < ten_sec_in_usec)) {
        // Clock instability in ppm
        timeWander =
          static_cast<float>(externalUnixTime_offset_usec) /
          static_cast<float>(timePassedSince(lastTimeWanderCalculation_ms));
        timeWander *= 1000.0f;
      }

      lastTimeWanderCalculation_ms = millis();
      timeSynced                   = true;

      #if FEATURE_PLUGIN_STATS

      // GMT	Wed Jan 01 2020 00:00:00 GMT+0000
      constexpr int64_t unixTime_20200101_usec = 1577836800ll * 1000000ll;

      if (!statusNTPInitialized && (externalUnixTime_offset_usec > unixTime_20200101_usec)) {
        if (getUnixTime() > get_build_unixtime()) {
          // Update recorded plugin stats timestamps
          for (taskIndex_t taskIndex = 0; taskIndex < TASKS_MAX; taskIndex++)
          {
            PluginTaskData_base *taskData = getPluginTaskDataBaseClassOnly(taskIndex);

            if (taskData != nullptr) {
              taskData->processTimeSet(externalUnixTime_offset_usec / 1000000.0);
            }
          }
        }
      }

      #endif // if FEATURE_PLUGIN_STATS

      #if FEATURE_EXT_RTC

      // External RTC only stores with second resolution.
      // Thus to limit the error to +/- 500 ms, round the sysTime instead of just casting it.
      ExtRTC_set(static_cast<uint32_t>(unixTime_d + 0.5));
      #endif // if FEATURE_EXT_RTC
      {
        const unsigned long abs_time_offset_ms = std::abs(externalUnixTime_offset_usec) / 1000ll;

        if (_timeSource == timeSource_t::NTP_time_source) {
          // May need to lessen the load on the NTP servers, randomize the sync interval
          if (abs_time_offset_ms < 1000) {
            // offset is less than 1 second, so we consider it a regular time sync.
            if (abs_time_offset_ms < 100) {
              // Good clock stability, use 5 - 6 hour interval
              syncInterval = HwRandom(18000, 21600);
            } else {
              // Dynamic interval between 30 minutes ... 5 hours.
              syncInterval = 1800000 / abs_time_offset_ms;
            }
          } else {
            syncInterval = 3600;
          }

          if (syncInterval <= 3600) {
            syncInterval = HwRandom(3600, 4000);
          }
        } else if (_timeSource == timeSource_t::No_time_source) {
          syncInterval = 60;
        } else {
          syncInterval = 3600;
        }
      }

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        String log = F("Time set to ");
        #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
        log += doubleToString(unixTime_d, 3);
        #else // if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
        log += static_cast<uint32_t>(unixTime_d);
        #endif // if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE

        if (std::abs(externalUnixTime_offset_usec / 1000000ll) < 86400ll) {
          // Only useful to show adjustment if it is less than a day.
          log += strformat(
            F(" Time adjusted by %d msec. Wander: %.3f ppm Source: "),
            static_cast<int32_t>(externalUnixTime_offset_usec / 1000ll),
            timeWander);
          log += toString(_timeSource);
        }
        addLogMove(LOG_LEVEL_INFO, log);
      }

      time_zone.applyTimeZone(unixTime_d);
      lastSyncTime_ms = millis();
      nextSyncTime    = getUptime_in_sec() + syncInterval;

      if (isExternalTimeSource(_timeSource)) {
        #ifdef USES_ESPEASY_NOW
        ESPEasy_now_handler.sendNTPbroadcast();
        #endif // ifdef USES_ESPEASY_NOW
      }
      STOP_TIMER(SYSTIME_UPDATED);
      externalUnixTime_offset_usec = 0;
    }
  }
  RTC.lastSysTime = getUnixTime();
  uint32_t localSystime = time_zone.toLocal(RTC.lastSysTime);
  breakTime(localSystime, local_tm);

  calcSunRiseAndSet(timeSynced);

  if (timeSynced) {
    #ifndef BUILD_MINIMAL_OTA
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO, strformat(
               F("Local time: %s"),
               getDateTimeString('-', ':', ' ').c_str()));
    }
    #endif
    {
      // Notify plugins the time has been set.
      String dummy;
      PluginCall(PLUGIN_TIME_CHANGE, 0, dummy);
    }

    if (Settings.UseRules) {
      if (statusNTPInitialized) {
        eventQueue.add(F("Time#Set"));
      } else {
        eventQueue.add(F("Time#Initialized"));
      }
    }
    statusNTPInitialized = true; // @giig1967g: setting system variable %isntp%
  }
  return (unsigned long)localSystime;
}

bool ESPEasy_time::reportNewMinute()
{
  now_();

  int cur_min = local_tm.tm_min;

  if (!systemTimePresent()) {
    // Use millis() to compute some "minute"
    cur_min = (millis() / 60000) % 60;
  }

  if (cur_min == PrevMinutes)
  {
    return false;
  }
  PrevMinutes = cur_min;
  return true;
}

bool ESPEasy_time::systemTimePresent() const {
  switch (_timeSource) {
    case timeSource_t::No_time_source:
    case timeSource_t::Restore_RTC_time_source:
      break;
    case timeSource_t::External_RTC_time_source:
    case timeSource_t::GPS_time_source:
    case timeSource_t::GPS_PPS_time_source:
    case timeSource_t::GPS_time_source_no_fix:
    case timeSource_t::ESP_now_peer:
    case timeSource_t::ESPEASY_p2p_UDP:
    case timeSource_t::Manual_set:
      return true;
    case timeSource_t::NTP_time_source:
      break;
  }
  return getUnixTime() > get_build_unixtime();
}

bool ESPEasy_time::getNtpTime(double& unixTime_d)
{
  if (!Settings.UseNTP() || !NetworkConnected(10)) {
    return false;
  }

  if (lastNTPSyncTime_ms != 0) {
    if (timePassedSince(lastNTPSyncTime_ms) < static_cast<long>(1000 * syncInterval)) {
      // Make sure not to flood the NTP servers with requests.
      return false;
    }
  }
  START_TIMER;
  IPAddress timeServerIP;
  String    log = F("NTP  : NTP host ");

  bool useNTPpool = false;

  if (Settings.NTPHost[0] != 0) {
    resolveHostByName(Settings.NTPHost, timeServerIP);
    log += Settings.NTPHost;

    // When single set host fails, retry again in 20 seconds
    nextSyncTime = getUptime_in_sec() + HwRandom(20, 60);
  } else  {
    // Have to do a lookup each time, since the NTP pool always returns another IP
    const String ntpServerName = strformat(
      F("%d.pool.ntp.org"), HwRandom(0, 3));
    resolveHostByName(ntpServerName.c_str(), timeServerIP);
    log += ntpServerName;

    // When pool host fails, retry can be much sooner
    nextSyncTime = getUptime_in_sec() + HwRandom(5, 20);
    useNTPpool   = true;
  }

  log += F(" (");
  log += formatIP(timeServerIP);
  log += ')';

  if (!hostReachable(timeServerIP)) {
    log += F(" unreachable");
    addLogMove(LOG_LEVEL_INFO, log);
    STOP_TIMER(NTP_FAIL);
    return false;
  }

  WiFiUDP udp;

  if (!beginWiFiUDP_randomPort(udp)) {
    return false;
  }

  NTP_packet ntp_packet;

  log += F(" queried");
#ifndef BUILD_NO_DEBUG
  addLog(LOG_LEVEL_DEBUG_MORE, log);
#endif // ifndef BUILD_NO_DEBUG

  while (udp.parsePacket() > 0) { // discard any previously received packets
  }

  FeedSW_watchdog();

  if (udp.beginPacket(timeServerIP, 123) == 0) { // NTP requests are to port 123
    FeedSW_watchdog();
    udp.stop();
    STOP_TIMER(NTP_FAIL);
    return false;
  }
  constexpr int  NTP_packet_size = sizeof(NTP_packet);
  const uint64_t txMicros        = getMicros64() + unixTime_usec_uptime_offset;
  ntp_packet.setTxTimestamp(txMicros);
  udp.write(ntp_packet.data, NTP_packet_size);
  udp.endPacket();

  const uint32_t beginWait = millis();

#ifndef BUILD_NO_DEBUG
  addLog(LOG_LEVEL_DEBUG, concat(F("NTP  : before\n"), ntp_packet.toDebugString()));
#endif // ifndef BUILD_NO_DEBUG

  while (!timeOutReached(beginWait + 1000)) {
    const int size       = udp.parsePacket();
    const int remotePort = udp.remotePort();

    if (size >= NTP_packet_size) {
      if (remotePort != 123) {
#ifndef BUILD_NO_DEBUG
        addLog(LOG_LEVEL_DEBUG_MORE, concat(F("NTP  : Reply from wrong port: "), remotePort));
#endif // ifndef BUILD_NO_DEBUG
        udp.stop();
        STOP_TIMER(NTP_FAIL);
        return false;
      }
      udp.read(ntp_packet.data, NTP_packet_size); // read packet into the buffer
      const uint64_t receivedMicros = getMicros64();

      udp.stop();

#ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_DEBUG, concat(F("NTP  : after\n"), ntp_packet.toDebugString()));
#endif // ifndef BUILD_NO_DEBUG

      if (ntp_packet.isUnsynchronized()) {
        // Leap-Indicator: unknown (clock unsynchronized)
        // See: https://github.com/letscontrolit/ESPEasy/issues/2886#issuecomment-586656384
        if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
          addLog(LOG_LEVEL_ERROR, strformat(
                   F("NTP  : NTP host (%s) unsynchronized"),
                   formatIP(timeServerIP).c_str()));
        }

        if (!useNTPpool) {
          // Does not make sense to try it very often if a single host is used which is not synchronized.
          nextSyncTime = getUptime_in_sec() + 120;
        }
        STOP_TIMER(NTP_FAIL);
        return false;
      }

      // For more detailed info on improving accuracy, see:
      // https://github.com/lettier/ntpclient/issues/4#issuecomment-360703503

      int64_t offset_usec{};
      int64_t roundtripDelay_usec{};

      if (!ntp_packet.compute_usec(
            txMicros,
            receivedMicros + unixTime_usec_uptime_offset,
            offset_usec, roundtripDelay_usec))
      {
#ifndef BUILD_NO_DEBUG
        addLogMove(LOG_LEVEL_ERROR, strformat(
                     F("NTP  : NTP error: round-trip delay: %d [ms] offset: %s,\n  t0: %s,\n  t1: %s,\n  t2: %s,\n  t3: %s"),
                     static_cast<int32_t>(roundtripDelay_usec / 1000),
                     secondsToDayHourMinuteSecond_ms(offset_usec).c_str(),
                     doubleToString(ntp_packet.getReferenceTimestamp_usec() / 1000000.0, 3).c_str(),
                     doubleToString(ntp_packet.getOriginTimestamp_usec() / 1000000.0,    3).c_str(),
                     doubleToString(ntp_packet.getReceiveTimestamp_usec() / 1000000.0,   3).c_str(),
                     doubleToString(ntp_packet.getTransmitTimestamp_usec() / 1000000.0,  3).c_str()
                     ));
#else // ifndef BUILD_NO_DEBUG
        addLogMove(LOG_LEVEL_ERROR, strformat(
                     F("NTP  : NTP error: round-trip delay: %d [ms] offset: %s"),
                     static_cast<int32_t>(roundtripDelay_usec / 1000),
                     secondsToDayHourMinuteSecond_ms(offset_usec).c_str()
                     ));

#endif // ifndef BUILD_NO_DEBUG

        // Apparently this is not a valid packet
        // as the received timestamp is before the origin timestamp
        // or no valid timestamps from the NTP server.
        nextSyncTime = getUptime_in_sec() + 60;
        STOP_TIMER(NTP_FAIL);
        return false;
      }

      externalUnixTime_offset_usec = offset_usec;
      _timeSource                  = timeSource_t::NTP_time_source;
      lastSyncTime_ms              = millis();
      lastNTPSyncTime_ms           = lastSyncTime_ms;
      unixTime_d                   = getMicros64() +
                                     unixTime_usec_uptime_offset +
                                     externalUnixTime_offset_usec;
      unixTime_d /= 1000000.0;

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
#ifndef BUILD_NO_DEBUG
        addLogMove(LOG_LEVEL_INFO, strformat(
                     F("NTP  : NTP replied: delay %d ms round-trip delay: %u ms offset: %s,\n  t0: %s,\n  t1: %s,\n  t2: %s,\n  t3: %s"),
                     timePassedSince(beginWait),
                     static_cast<uint32_t>(roundtripDelay_usec / 1000),
                     secondsToDayHourMinuteSecond_ms(offset_usec).c_str(),
                     doubleToString(ntp_packet.getReferenceTimestamp_usec() / 1000000.0, 3).c_str(),
                     doubleToString(ntp_packet.getOriginTimestamp_usec() / 1000000.0,    3).c_str(),
                     doubleToString(ntp_packet.getReceiveTimestamp_usec() / 1000000.0,   3).c_str(),
                     doubleToString(ntp_packet.getTransmitTimestamp_usec() / 1000000.0,  3).c_str()
                     ));
#else // ifndef BUILD_NO_DEBUG
        addLogMove(LOG_LEVEL_INFO, strformat(
                     F("NTP  : NTP replied: delay %d ms round-trip delay: %u ms offset: %s"),
                     timePassedSince(beginWait),
                     static_cast<uint32_t>(roundtripDelay_usec / 1000),
                     secondsToDayHourMinuteSecond_ms(offset_usec).c_str()
                     ));
#endif // ifndef BUILD_NO_DEBUG
      }
      CheckRunningServices(); // FIXME TD-er: Sometimes services can only be started after NTP is successful
      STOP_TIMER(NTP_SUCCESS);
      return true;
    }
    delay(1);
  }

  // Timeout.
  if (!useNTPpool) {
    // Retry again in a minute.
    nextSyncTime = getUptime_in_sec() + 60;
  }

#ifndef BUILD_NO_DEBUG
  addLog(LOG_LEVEL_DEBUG_MORE, F("NTP  : No reply"));
#endif // ifndef BUILD_NO_DEBUG
  udp.stop();
  STOP_TIMER(NTP_FAIL);
  return false;
}

/**************************************************
* get the timezone-offset string in +/-0000 format
**************************************************/
String ESPEasy_time::getTimeZoneOffsetString() {
  int    dif            = static_cast<int>((static_cast<int64_t>(getLocalUnixTime()) - static_cast<int64_t>(getUnixTime())) / 60); // Minutes
  char   valueString[6] = { 0 };
  String tzoffset;

  // Formatting the timezone-offset string as [+|-]HHMM
  if (dif < 0) {
    tzoffset += '-';
  } else {
    tzoffset += '+';
  }

  dif = abs(dif);
  sprintf_P(valueString, PSTR("%02d%02d"), dif / 60, dif % 60);
  tzoffset += String(valueString);
  return tzoffset;
}

void ESPEasy_time::applyTimeZone()
{
  time_zone.applyTimeZone(getUnixTime());
}

/********************************************************************************************\
   Date/Time string formatters
 \*********************************************************************************************/
String ESPEasy_time::getDateString(char delimiter) const
{
  return formatDateString(local_tm, delimiter);
}

String ESPEasy_time::getTimeString(char delimiter, bool show_seconds /*=true*/, char hour_prefix /*='\0'*/) const
{
  return formatTimeString(local_tm, delimiter, false, show_seconds, hour_prefix);
}

String ESPEasy_time::getTimeString_ampm(char delimiter, bool show_seconds /*=true*/, char hour_prefix /*='\0'*/) const
{
  return formatTimeString(local_tm, delimiter, true, show_seconds, hour_prefix);
}

String ESPEasy_time::getDateTimeString(char dateDelimiter, char timeDelimiter,  char dateTimeDelimiter) const {
  return formatDateTimeString(local_tm, dateDelimiter, timeDelimiter, dateTimeDelimiter, false);
}

String ESPEasy_time::getDateTimeString_ampm(char dateDelimiter, char timeDelimiter,  char dateTimeDelimiter) const {
  return formatDateTimeString(local_tm, dateDelimiter, timeDelimiter, dateTimeDelimiter, true);
}

/********************************************************************************************\
   Get current time/date
 \*********************************************************************************************/
int ESPEasy_time::year(unsigned long t)
{
  struct tm tmp;

  breakTime(t, tmp);
  return 1900 + tmp.tm_year;
}

int ESPEasy_time::weekday(unsigned long t)
{
  struct tm tmp;

  breakTime(t, tmp);
  return tmp.tm_wday;
}

String ESPEasy_time::weekday_str(int wday)
{
  const String weekDays = F("SunMonTueWedThuFriSat");

  return weekDays.substring(wday * 3, wday * 3 + 3);
}

String ESPEasy_time::weekday_str() const
{
  return weekday_str(weekday() - 1);
}

String ESPEasy_time::month_str(int month)
{
  const String months = F("JanFebMarAprMayJunJulAugSepOctNovDec");

  return months.substring(month * 3, month * 3 + 3);
}

String ESPEasy_time::month_str() const
{
  return month_str(month() - 1);
}

/********************************************************************************************\
   Sunrise/Sunset calculations
 \*********************************************************************************************/
int ESPEasy_time::getSecOffset(const String& format) {
  int position_minus = format.indexOf('-');
  int position_plus  = format.indexOf('+');

  if ((position_minus == -1) && (position_plus == -1)) {
    return 0;
  }
  int sign_position    = _max(position_minus, position_plus);
  int position_percent = format.indexOf('%', sign_position);

  if (position_percent == -1) {
    return 0;
  }

  int32_t value;

  if (!validIntFromString(format.substring(sign_position, position_percent), value)) {
    return 0;
  }

  switch (format.charAt(position_percent - 1)) {
    case 'm':
    case 'M':
      return value * 60;
    case 'h':
    case 'H':
      return value * 3600;
  }
  return value;
}

String ESPEasy_time::getSunriseTimeString(char delimiter) const {
  return formatTimeString(sunRise, delimiter, false, false);
}

String ESPEasy_time::getSunsetTimeString(char delimiter) const {
  return formatTimeString(sunSet, delimiter, false, false);
}

String ESPEasy_time::getSunriseTimeString(char delimiter, int secOffset) const {
  if (secOffset == 0) {
    return getSunriseTimeString(delimiter);
  }
  return formatTimeString(getSunRise(secOffset), delimiter, false, false);
}

String ESPEasy_time::getSunsetTimeString(char delimiter, int secOffset) const {
  if (secOffset == 0) {
    return getSunsetTimeString(delimiter);
  }
  return formatTimeString(getSunSet(secOffset), delimiter, false, false);
}

float ESPEasy_time::sunDeclination(int doy) {
  // Declination of the sun in radians
  // Formula 2008 by Arnold(at)Barmettler.com, fit to 20 years of average declinations (2008-2027)
  return 0.409526325277017 * sin(0.0169060504029192 * (doy - 80.0856919827619));
}

float ESPEasy_time::diurnalArc(float dec, float lat) {
  // Duration of the half sun path in hours (time from sunrise to the highest level in the south)
  float rad    = 0.0174532925f; // = pi/180.0
  float height = -50.0f / 60.0f * rad;
  float latRad = lat * rad;

  return 12.0f * acos((sin(height) - sin(latRad) * sin(dec)) / (cos(latRad) * cos(dec))) / M_PI;
}

float ESPEasy_time::equationOfTime(int doy) {
  // Difference between apparent and mean solar time
  // Formula 2008 by Arnold(at)Barmettler.com, fit to 20 years of average equation of time (2008-2027)
  return -0.170869921174742 * sin(0.0336997028793971 * doy + 0.465419984181394) - 0.129890681040717 * sin(
    0.0178674832556871 * doy - 0.167936777524864);
}

int ESPEasy_time::dayOfYear(int year, int month, int day) {
  // Algorithm borrowed from DateToOrdinal by Ritchie Lawrence, www.commandline.co.uk
  int z = 14 - month;

  z /= 12;
  int y = year + 4800 - z;
  int m = month + 12 * z - 3;
  int j = 153 * m + 2;

  j = j / 5 + day + y * 365 + y / 4 - y / 100 + y / 400 - 32045;
  y = year + 4799;
  int k = y * 365 + y / 4 - y / 100 + y / 400 - 31738;

  return j - k + 1;
}

void ESPEasy_time::calcSunRiseAndSet(bool timeSynced) {
  if (!timeSynced &&
      (tsSet.tm_mday == local_tm.tm_mday)) {
    // No need to recalculate if already calculated for this day
    return;
  }

  const int   doy  = dayOfYear(local_tm.tm_year, local_tm.tm_mon + 1, local_tm.tm_mday);
  const float eqt  = equationOfTime(doy);
  const float dec  = sunDeclination(doy);
  const float da   = diurnalArc(dec, Settings.Latitude);
  const float rise = 12 - da - eqt;
  const float set  = 12 + da - eqt;

  tsRise.tm_hour = rise;
  tsRise.tm_min  = (rise - static_cast<int>(rise)) * 60.0f;
  tsSet.tm_hour  = set;
  tsSet.tm_min   = (set - static_cast<int>(set)) * 60.0f;
  tsRise.tm_mday = tsSet.tm_mday = local_tm.tm_mday;
  tsRise.tm_mon  = tsSet.tm_mon = local_tm.tm_mon;
  tsRise.tm_year = tsSet.tm_year = local_tm.tm_year;

  // Now apply the longitude
  const int secOffset_longitude = -1.0f * (Settings.Longitude / 15.0f) * 3600;

  tsSet  = addSeconds(tsSet, secOffset_longitude, false);
  tsRise = addSeconds(tsRise, secOffset_longitude, false);

  breakTime(time_zone.toLocal(makeTime(tsRise)), sunRise);
  breakTime(time_zone.toLocal(makeTime(tsSet)),  sunSet);
}

struct tm ESPEasy_time::getSunRise(int secOffset) const {
  return addSeconds(tsRise, secOffset, true);
}

struct tm ESPEasy_time::getSunSet(int secOffset) const {
  return addSeconds(tsSet, secOffset, true);
}

#if FEATURE_EXT_RTC
bool ESPEasy_time::ExtRTC_get(uint32_t& unixtime)
{
  unixtime = 0;
  #if FEATURE_I2C_MULTIPLE
  const uint8_t i2cBus = Settings.getI2CInterfaceRTC();
  #else
  const uint8_t i2cBus = 0;
  #endif // if FEATURE_I2C_MULTIPLE

  switch (Settings.ExtTimeSource()) {
    case ExtTimeSource_e::None:
      return false;
    case ExtTimeSource_e::DS1307:
    {
      I2CSelect_Max100kHz_ClockSpeed(i2cBus); // Only supports upto 100 kHz
      RTC_DS1307 rtc;

      if (!rtc.begin()) {
        // Not found
        break;
      }

      if (!rtc.isrunning()) {
        // not running
        break;
      }
      unixtime = rtc.now().unixtime();
      break;
    }
    case ExtTimeSource_e::DS3231:
    {
      I2CSelectHighClockSpeed(i2cBus);
      RTC_DS3231 rtc;

      if (!rtc.begin()) {
        // Not found
        break;
      }

      if (rtc.lostPower()) {
        // Cannot get the time from the module
        break;
      }
      unixtime = rtc.now().unixtime();
      break;
    }

    case ExtTimeSource_e::PCF8523:
    {
      I2CSelectHighClockSpeed(i2cBus);
      RTC_PCF8523 rtc;

      if (!rtc.begin()) {
        // Not found
        break;
      }

      if (rtc.lostPower() || !rtc.initialized() || !rtc.isrunning()) {
        // Cannot get the time from the module
        break;
      }
      unixtime = rtc.now().unixtime();
      break;
    }
    case ExtTimeSource_e::PCF8563:
    {
      I2CSelectHighClockSpeed(i2cBus);
      RTC_PCF8563 rtc;

      if (!rtc.begin()) {
        // Not found
        break;
      }

      if (rtc.lostPower() || !rtc.isrunning()) {
        // Cannot get the time from the module
        break;
      }
      unixtime = rtc.now().unixtime();
      break;
    }
  }

  if (unixtime != 0) {
    addLogMove(LOG_LEVEL_INFO, concat(
                 F("ExtRTC: Read external time source: "),
                 unixtime));
    return true;
  }
  addLog(LOG_LEVEL_ERROR, F("ExtRTC: Cannot get time from external time source"));
  return false;
}

#endif // if FEATURE_EXT_RTC

#if FEATURE_EXT_RTC
bool ESPEasy_time::ExtRTC_set(uint32_t unixtime)
{
  if (_timeSource >= timeSource_t::External_RTC_time_source) {
    // Do not adjust the external RTC time if we already used it as a time source.
    // or the new time source is worse than the external RTC time souce.
    return true;
  }
  bool timeAdjusted = false;
  #if FEATURE_I2C_MULTIPLE
  const uint8_t i2cBus = Settings.getI2CInterfaceRTC();
  #else
  const uint8_t i2cBus = 0;
  #endif // if FEATURE_I2C_MULTIPLE

  switch (Settings.ExtTimeSource()) {
    case ExtTimeSource_e::None:
      return false;
    case ExtTimeSource_e::DS1307:
    {
      I2CSelect_Max100kHz_ClockSpeed(i2cBus); // Only supports upto 100 kHz
      RTC_DS1307 rtc;

      if (rtc.begin()) {
        rtc.adjust(DateTime(unixtime));
        timeAdjusted = true;
      }
      break;
    }
    case ExtTimeSource_e::DS3231:
    {
      I2CSelectHighClockSpeed(i2cBus);
      RTC_DS3231 rtc;

      if (rtc.begin()) {
        rtc.adjust(DateTime(unixtime));
        timeAdjusted = true;
      }
      break;
    }

    case ExtTimeSource_e::PCF8523:
    {
      I2CSelectHighClockSpeed(i2cBus);
      RTC_PCF8523 rtc;

      if (rtc.begin()) {
        rtc.adjust(DateTime(unixtime));
        rtc.start();
        timeAdjusted = true;
      }
      break;
    }
    case ExtTimeSource_e::PCF8563:
    {
      I2CSelectHighClockSpeed(i2cBus);
      RTC_PCF8563 rtc;

      if (rtc.begin()) {
        rtc.adjust(DateTime(unixtime));
        rtc.start();
        timeAdjusted = true;
      }
      break;
    }
  }

  if (timeAdjusted) {
    addLogMove(LOG_LEVEL_INFO, concat(
                 F("ExtRTC: External time source set to: "),
                 unixtime));
    return true;
  }
  addLog(LOG_LEVEL_ERROR, F("ExtRTC: Cannot set time to external time source"));
  return false;
}

#endif // if FEATURE_EXT_RTC

#include "../Helpers/ESPEasy_TouchHandler.h"

#ifdef PLUGIN_USES_TOUCHHANDLER

# include "../Commands/ExecuteCommand.h"

tTouchObjects::tTouchObjects() :
  flags(0u),
  SurfaceAreas(0u),
  TouchTimers(0u),
#ifdef ESP32
  top_left({0u, 0u}),
  width_height({0u, 0u}),
#endif
  TouchStates(0u)
  # if TOUCH_FEATURE_EXTENDED_TOUCH
  , groupFlags           (0u)
  , colorOn              (0u)
  , colorOff             (0u)
  , colorCaption         (0u)
  , colorBorder          (0u)
  , colorDisabled        (0u)
  , colorDisabledCaption (0u)
  # endif // if TOUCH_FEATURE_EXTENDED_TOUCH
  {
    objectName.clear();
    captionOn.clear();
    captionOff.clear();
  }



/****************************************************************************
 * toString: Display-value for the touch action
 ***************************************************************************/
# if TOUCH_FEATURE_EXTENDED_TOUCH
const __FlashStringHelper* toString(Touch_action_e action) {
  switch (action) {
    case Touch_action_e::Default: return F("Default");
    case Touch_action_e::ActivateGroup: return F("Activate Group");
    case Touch_action_e::IncrementGroup: return F("Next Group");
    case Touch_action_e::DecrementGroup: return F("Previous Group");
    case Touch_action_e::IncrementPage: return F("Next Page (+10)");
    case Touch_action_e::DecrementPage: return F("Previous Page (-10)");
  }
  return F("Unsupported!");
}

# endif // if TOUCH_FEATURE_EXTENDED_TOUCH

/****************************************************************************
 * toString: Display-value for the swipe action
 ***************************************************************************/
# if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE
const __FlashStringHelper* toString(Swipe_action_e action) {
  switch (action) {
    case Swipe_action_e::Up: return F("Up");
    case Swipe_action_e::UpRight: return F("Up-Right");
    case Swipe_action_e::Right: return F("Right");
    case Swipe_action_e::RightDown: return F("Right-Down");
    case Swipe_action_e::Down: return F("Down");
    case Swipe_action_e::DownLeft: return F("Down-Left");
    case Swipe_action_e::Left: return F("Left");
    case Swipe_action_e::LeftUp: return F("Left-Up");
    case Swipe_action_e::None: return F("None");
    case Swipe_action_e::SwipeAction_MAX: break;
  }
  return F("Unknown");
}

# endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE

/**
 * Constructors
 */
ESPEasy_TouchHandler::ESPEasy_TouchHandler() {}

ESPEasy_TouchHandler::ESPEasy_TouchHandler(const taskIndex_t     & displayTask,
                                           const AdaGFXColorDepth& colorDepth)
  : _displayTask(displayTask), _colorDepth(colorDepth) {}

/**
 * Destructor
 */
ESPEasy_TouchHandler::~ESPEasy_TouchHandler() {}

/**
 * Load the touch objects from the settings, and initialize them properly where needed.
 */
void ESPEasy_TouchHandler::loadTouchObjects(struct EventStruct *event) {
  # ifdef TOUCH_DEBUG
  addLog(LOG_LEVEL_INFO, F("TOUCH DEBUG loadTouchObjects"));
  # endif // TOUCH_DEBUG
  LoadCustomTaskSettings(event->TaskIndex, settingsArray, TOUCH_ARRAY_SIZE, 0);

  lastObjectIndex = TOUCH_OBJECT_INDEX_START - 1; // START must be > 0!!!

  objectCount = 0;
  _buttonGroups.clear();                          // Clear groups
  _buttonGroups.insert(0u);                       // Always have group 0

  for (uint8_t i = TOUCH_OBJECT_INDEX_END; i >= TOUCH_OBJECT_INDEX_START; --i) {
    if (!settingsArray[i].isEmpty() && (lastObjectIndex < TOUCH_OBJECT_INDEX_START)) {
      lastObjectIndex = i;
      objectCount++; // Count actual number of objects
    }
  }

  // Get calibration and common settings
  Touch_Settings.calibrationEnabled = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START],
                                                       TOUCH_CALIBRATION_ENABLED, TOUCH_SETTINGS_SEPARATOR) == 1;
  Touch_Settings.logEnabled = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START],
                                               TOUCH_CALIBRATION_LOG_ENABLED, TOUCH_SETTINGS_SEPARATOR) == 1;
  int lSettings = 0;

  bitWrite(lSettings, TOUCH_FLAGS_SEND_XY,          TOUCH_TS_SEND_XY); // Defaults initialized
  bitWrite(lSettings, TOUCH_FLAGS_SEND_Z,           TOUCH_TS_SEND_Z);
  bitWrite(lSettings, TOUCH_FLAGS_SEND_OBJECTNAME,  TOUCH_TS_SEND_OBJECTNAME);
  bitWrite(lSettings, TOUCH_FLAGS_DEDUPLICATE,      TOUCH_TS_DEDUPLICATE);
  bitWrite(lSettings, TOUCH_FLAGS_INIT_OBJECTEVENT, TOUCH_TS_INIT_OBJECTEVENT);
  Touch_Settings.flags = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START],
                                          TOUCH_COMMON_FLAGS, TOUCH_SETTINGS_SEPARATOR, lSettings);
  Touch_Settings.top_left.x     = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START], TOUCH_CALIBRATION_TOP_X, TOUCH_SETTINGS_SEPARATOR);
  Touch_Settings.top_left.y     = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START], TOUCH_CALIBRATION_TOP_Y, TOUCH_SETTINGS_SEPARATOR);
  Touch_Settings.bottom_right.x = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START],
                                                   TOUCH_CALIBRATION_BOTTOM_X,
                                                   TOUCH_SETTINGS_SEPARATOR);
  Touch_Settings.bottom_right.y = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START],
                                                   TOUCH_CALIBRATION_BOTTOM_Y,
                                                   TOUCH_SETTINGS_SEPARATOR);
  Touch_Settings.debounceMs = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START], TOUCH_COMMON_DEBOUNCE_MS, TOUCH_SETTINGS_SEPARATOR,
                                               TOUCH_DEBOUNCE_MILLIS);
  # if TOUCH_FEATURE_EXTENDED_TOUCH
  Touch_Settings.colorOn = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START],
                                            TOUCH_COMMON_DEF_COLOR_ON, TOUCH_SETTINGS_SEPARATOR);
  Touch_Settings.colorOff = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START],
                                             TOUCH_COMMON_DEF_COLOR_OFF, TOUCH_SETTINGS_SEPARATOR);
  Touch_Settings.colorBorder = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START],
                                                TOUCH_COMMON_DEF_COLOR_BORDER, TOUCH_SETTINGS_SEPARATOR);
  Touch_Settings.colorCaption = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START],
                                                 TOUCH_COMMON_DEF_COLOR_CAPTION, TOUCH_SETTINGS_SEPARATOR);
  Touch_Settings.colorDisabled = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START],
                                                  TOUCH_COMMON_DEF_COLOR_DISABLED, TOUCH_SETTINGS_SEPARATOR);
  Touch_Settings.colorDisabledCaption = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START],
                                                         TOUCH_COMMON_DEF_COLOR_DISABCAPT, TOUCH_SETTINGS_SEPARATOR);

  if ((Touch_Settings.colorOn              == 0u) && // Validate and set defaults
      (Touch_Settings.colorOff             == 0u) &&
      (Touch_Settings.colorCaption         == 0u) &&
      (Touch_Settings.colorBorder          == 0u) &&
      (Touch_Settings.colorDisabled        == 0u) &&
      (Touch_Settings.colorDisabledCaption == 0u)) {
    Touch_Settings.colorOn              = TOUCH_DEFAULT_COLOR_ON;
    Touch_Settings.colorOff             = TOUCH_DEFAULT_COLOR_OFF;
    Touch_Settings.colorCaption         = TOUCH_DEFAULT_COLOR_CAPTION;
    Touch_Settings.colorBorder          = TOUCH_DEFAULT_COLOR_BORDER;
    Touch_Settings.colorDisabled        = TOUCH_DEFAULT_COLOR_DISABLED;
    Touch_Settings.colorDisabledCaption = TOUCH_DEFAULT_COLOR_DISABLED_CAPTION;
  }
  #  if TOUCH_FEATURE_SWIPE
  Touch_Settings.swipeMinimal = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START],
                                                 TOUCH_COMMON_SWIPE_MINIMAL, TOUCH_SETTINGS_SEPARATOR, TOUCH_DEF_SWIPE_MINIMAL);
  Touch_Settings.swipeMargin = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START],
                                                TOUCH_COMMON_SWIPE_MARGIN, TOUCH_SETTINGS_SEPARATOR, TOUCH_DEF_SWIPE_MARGIN);
  #  endif // if TOUCH_FEATURE_SWIPE
  # endif // if TOUCH_FEATURE_EXTENDED_TOUCH

  free_string(settingsArray[TOUCH_CALIBRATION_START]); // Free a little memory

  // Buffer some settings, mostly for readability, but also to be able to set from write command
  _flipped     = bitRead(Touch_Settings.flags, TOUCH_FLAGS_ROTATION_FLIPPED);
  _deduplicate = bitRead(Touch_Settings.flags, TOUCH_FLAGS_DEDUPLICATE);

  TouchObjects.clear();

  if (objectCount > 0) {
    TouchObjects.reserve(objectCount);
    uint8_t t = 0u;

    for (uint8_t i = TOUCH_OBJECT_INDEX_START; i <= lastObjectIndex; ++i) {
      if (!settingsArray[i].isEmpty()) {
        tTouchObjects touchObject{};
        touchObject.flags          = parseStringToInt(settingsArray[i], TOUCH_OBJECT_FLAGS, TOUCH_SETTINGS_SEPARATOR);
        String objectName     = parseStringKeepCase(settingsArray[i], TOUCH_OBJECT_NAME, TOUCH_SETTINGS_SEPARATOR);
        touchObject.objectName     = objectName;
        touchObject.top_left.x     = parseStringToInt(settingsArray[i], TOUCH_OBJECT_COORD_TOP_X, TOUCH_SETTINGS_SEPARATOR);
        touchObject.top_left.y     = parseStringToInt(settingsArray[i], TOUCH_OBJECT_COORD_TOP_Y, TOUCH_SETTINGS_SEPARATOR);
        touchObject.width_height.x = parseStringToInt(settingsArray[i], TOUCH_OBJECT_COORD_WIDTH, TOUCH_SETTINGS_SEPARATOR);
        touchObject.width_height.y = parseStringToInt(settingsArray[i], TOUCH_OBJECT_COORD_HEIGHT, TOUCH_SETTINGS_SEPARATOR);
        # if TOUCH_FEATURE_EXTENDED_TOUCH
        touchObject.colorOn              = parseStringToInt(settingsArray[i], TOUCH_OBJECT_COLOR_ON, TOUCH_SETTINGS_SEPARATOR);
        touchObject.colorOff             = parseStringToInt(settingsArray[i], TOUCH_OBJECT_COLOR_OFF, TOUCH_SETTINGS_SEPARATOR);
        touchObject.colorCaption         = parseStringToInt(settingsArray[i], TOUCH_OBJECT_COLOR_CAPTION, TOUCH_SETTINGS_SEPARATOR);
        String captionOn            = parseStringKeepCase(settingsArray[i], TOUCH_OBJECT_CAPTION_ON, TOUCH_SETTINGS_SEPARATOR);
        String captionOff           = parseStringKeepCase(settingsArray[i], TOUCH_OBJECT_CAPTION_OFF, TOUCH_SETTINGS_SEPARATOR);
        touchObject.captionOn            = captionOn;
        touchObject.captionOff           = captionOff;
        touchObject.colorBorder          = parseStringToInt(settingsArray[i], TOUCH_OBJECT_COLOR_BORDER, TOUCH_SETTINGS_SEPARATOR);
        touchObject.colorDisabled        = parseStringToInt(settingsArray[i], TOUCH_OBJECT_COLOR_DISABLED, TOUCH_SETTINGS_SEPARATOR);
        touchObject.colorDisabledCaption = parseStringToInt(settingsArray[i], TOUCH_OBJECT_COLOR_DISABCAPT, TOUCH_SETTINGS_SEPARATOR);
        touchObject.groupFlags           = parseStringToInt(settingsArray[i], TOUCH_OBJECT_GROUPFLAGS, TOUCH_SETTINGS_SEPARATOR);

        const uint8_t g = get8BitFromUL(touchObject.flags, TOUCH_OBJECT_FLAG_GROUP);

        if (!validButtonGroup(g)) {
          _buttonGroups.insert(g);
        }
        # endif // if TOUCH_FEATURE_EXTENDED_TOUCH

        touchObject.SurfaceAreas = 0u; // Reset runtime stuff
        touchObject.TouchTimers  = 0u;
        touchObject.TouchStates  = 0;

        # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE

        // Check if a slider/gauge with range not including 0 is used, then set starting value closest to 0
        if (bitRead(touchObject.flags, TOUCH_OBJECT_FLAG_SLIDER) && !touchObject.captionOff.isEmpty()) {
          int16_t _value    = 0;
          int16_t lowRange  = 0;
          int16_t highRange = 100;

          if (parseRangeToInt16(touchObject.captionOff, lowRange, highRange)) {
            if (lowRange > highRange) {
              if (_value < highRange) {
                _value = highRange;
              } else if (_value > lowRange) {
                _value = lowRange;
              }
            } else {
              if (_value < lowRange) {
                _value = lowRange;
              } else if (_value > highRange) {
                _value = highRange;
              }
            }
            touchObject.TouchStates = _value;
          }
        }
        # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE

        TouchObjects.push_back(std::move(touchObject));

        t++;

        free_string(settingsArray[i]); // Free a little memory
      }
    }
  }
}

/**
 * init
 */
void ESPEasy_TouchHandler::init(struct EventStruct *event) {
  if (!_settingsLoaded) {
    loadTouchObjects(event);
    _settingsLoaded = true;
  }

  # if TOUCH_FEATURE_EXTENDED_TOUCH
  _touchIgnored = bitRead(Touch_Settings.flags, TOUCH_FLAGS_IGNORE_TOUCH);

  if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_OBJECTNAME) &&
      bitRead(Touch_Settings.flags, TOUCH_FLAGS_INIT_OBJECTEVENT)) {
    if (_buttonGroups.size() > 1) {                // Multiple groups?
      displayButtonGroup(event, _buttonGroup, -3); // Clear all displayed groups
    }
    _buttonGroup = get8BitFromUL(Touch_Settings.flags, TOUCH_FLAGS_INITIAL_GROUP);
    #  ifdef TOUCH_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, strformat(F("TOUCH DEBUG group: %d, max group: %d"), _buttonGroup, *_buttonGroups.crbegin()));
    }
    #  endif // ifdef TOUCH_DEBUG

    displayButtonGroup(event, _buttonGroup); // Initialize selected group and group 0

    #  ifdef TOUCH_DEBUG
    addLog(LOG_LEVEL_INFO, F("TOUCH DEBUG group done."));
    #  endif // ifdef TOUCH_DEBUG
  }
  # endif // if TOUCH_FEATURE_EXTENDED_TOUCH
}

/**
 * helper function: use parseString() to read an argument, and convert that to an int value
 */
int ESPEasy_TouchHandler::parseStringToInt(const String & string,
                                           const uint8_t& indexFind,
                                           const char   & separator,
                                           const int    & defaultValue) {
  const String parsed = parseStringKeepCase(string, indexFind, separator);
  int32_t result      = defaultValue;

  validIntFromString(parsed, result);

  return result;
}

/**
 * Determine if calibration is enabled and usable.
 */
bool ESPEasy_TouchHandler::isCalibrationActive() {
  return _useCalibration
         && (Touch_Settings.top_left.x != 0 ||
             Touch_Settings.top_left.y != 0 ||
             Touch_Settings.bottom_right.x > Touch_Settings.top_left.x ||
             Touch_Settings.bottom_right.y > Touch_Settings.top_left.y); // Enabled and any value != 0 => Active
}

/**
 * Check within the list of defined objects if we touched one of them.
 * Must be in the current button group or in button group 0.
 * The smallest matching surface is selected if multiple objects overlap.
 * Returns state, sets selectedObjectName to the best matching object name
 * and selectedObjectIndex to the index into the TouchObjects vector.
 */
bool ESPEasy_TouchHandler::isValidAndTouchedTouchObject(const int16_t& x,
                                                        const int16_t& y,
                                                        String       & selectedObjectName,
                                                        int8_t       & selectedObjectIndex) {
  uint32_t lastObjectArea = 0u;
  bool     selected       = false;
  const uint16_t _x       = static_cast<uint16_t>(x);
  const uint16_t _y       = static_cast<uint16_t>(y);

  for (size_t objectNr = 0; objectNr < TouchObjects.size(); ++objectNr) {
    const uint8_t group = get8BitFromUL(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_GROUP);

    if (!TouchObjects[objectNr].objectName.isEmpty()
        && bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_ENABLED)
        && (TouchObjects[objectNr].width_height.x != 0)
        && (TouchObjects[objectNr].width_height.y != 0) // Not initial could be valid
        && ((group == 0) || (group == _buttonGroup))) { // Group 0 is always active
      if (TouchObjects[objectNr].SurfaceAreas == 0) {   // Need to calculate the surface area
        TouchObjects[objectNr].SurfaceAreas = TouchObjects[objectNr].width_height.x * TouchObjects[objectNr].width_height.y;
      }

      if ((TouchObjects[objectNr].top_left.x <= _x)
          && (TouchObjects[objectNr].top_left.y <= _y)
          && ((TouchObjects[objectNr].width_height.x + TouchObjects[objectNr].top_left.x) >= _x)
          && ((TouchObjects[objectNr].width_height.y + TouchObjects[objectNr].top_left.y) >= _y)
          && ((lastObjectArea == 0) ||
              (TouchObjects[objectNr].SurfaceAreas < lastObjectArea))) { // Select smallest area that fits the coordinates
        selectedObjectName  = TouchObjects[objectNr].objectName;
        selectedObjectIndex = objectNr;
        lastObjectArea      = TouchObjects[objectNr].SurfaceAreas;
        selected            = true;
      }
      # ifdef TOUCH_DEBUG

      if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
        addLog(LOG_LEVEL_DEBUG,
               strformat(F("TOUCH DEBUG Touched: obj: %s,%d,%d,%d,%d surface:%d x,y:%d,%d sel:%s/%d/%c"),
                         TouchObjects[objectNr].objectName.c_str(),
                         TouchObjects[objectNr].top_left.x,
                         TouchObjects[objectNr].top_left.y,
                         TouchObjects[objectNr].width_height.x,
                         TouchObjects[objectNr].width_height.y,
                         TouchObjects[objectNr].SurfaceAreas,
                         x,
                         y,
                         selectedObjectName.c_str(),
                         selectedObjectIndex,
                         selected ? 'T' : 'f'));
      }
      # endif // ifdef TOUCH_DEBUG
    }
  }
  return selected;
}

/**
 * Get either the int value or index of the objectName provided, optionally a button object
 */
int8_t ESPEasy_TouchHandler::getTouchObjectIndex(struct EventStruct *event,
                                                 const String      & touchObject,
                                                 const bool        & isButton) {
  if (touchObject.isEmpty()) { return -1; }

  int32_t index = -1;
  int32_t idx   = -1;

  if ((idx = touchObject.indexOf('.')) > -1) {
    String part = touchObject.substring(0, idx);

    # if TOUCH_FEATURE_EXTENDED_TOUCH

    int32_t grp = -1;

    if (validIntFromString(part, grp) && validButtonGroup(static_cast<int16_t>(grp), false)) {
      part = touchObject.substring(idx + 1);
      int32_t btn = -1;

      if (validIntFromString(part, btn)) {
        idx = 0;

        for (size_t objectNr = 0; objectNr < TouchObjects.size(); ++objectNr) {
          if (!TouchObjects[objectNr].objectName.isEmpty()
              && (get8BitFromUL(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_GROUP) == static_cast<uint8_t>(grp))
              && (!isButton || bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_BUTTON))) {
            idx++;

            if (idx == btn) {
              return static_cast<int8_t>(objectNr);
            }
          }
        }
      } else {
        return -1; // Invalid button name
      }
    } else {
      return -1;   // Invalid group number
    }
    # endif // if TOUCH_FEATURE_EXTENDED_TOUCH
  }

  // ATTENTION: Any externally provided objectNumber is 1-based, result is 0-based
  if (validIntFromString(touchObject, index) &&
      (index > 0) &&
      (index <= static_cast<int>(TouchObjects.size()))) {
    return static_cast<int8_t>(index - 1);
  }

  for (size_t objectNr = 0; objectNr < TouchObjects.size(); ++objectNr) {
    if (!TouchObjects[objectNr].objectName.isEmpty()
        && touchObject.equalsIgnoreCase(TouchObjects[objectNr].objectName)
        && (!isButton || bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_BUTTON))) {
      return static_cast<int8_t>(objectNr);
    }
  }
  return -1;
}

/**
 * Set the enabled/disabled state of an object. Will redraw if a button object.
 */
bool ESPEasy_TouchHandler::setTouchObjectState(struct EventStruct *event,
                                               const String      & touchObject,
                                               const bool        & state) {
  if (touchObject.isEmpty()) { return false; }
  bool success = false;

  const int8_t objectNr = getTouchObjectIndex(event, touchObject);

  if (objectNr > -1) {
    success = true;                                                             // Succes if matched object

    if (state != bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_ENABLED)) {
      bitWrite(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_ENABLED, state); // Store in settings, no save

      // Event when enabling/disabling
      if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_OBJECTNAME) &&
          bitRead(Touch_Settings.flags, TOUCH_FLAGS_INIT_OBJECTEVENT)) {
        generateObjectEvent(event, objectNr,
                            bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_SLIDER) ?
                            TouchObjects[objectNr].TouchStates :
                            (TouchObjects[objectNr].TouchStates > 0 ? 1 : 0),
                            state ? -1 : -2); // Redraw only, no activation
      }
    }
    # ifdef TOUCH_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      String log;
      log.reserve(72);
      log = strformat(F("TOUCH setTouchObjectState: obj: %s/%d"),
                      touchObject.c_str(), objectNr);

      if (success) {
        log += F(", new state: ");
        log += state ? F("en") : F("dis");
        log += F("abled.");
      } else {
        log += F(" failed!");
      }
      addLogMove(LOG_LEVEL_DEBUG, log);
    }
    # endif // ifdef TOUCH_DEBUG
  }

  return success;
}

/**
 * Get the enabled/disabled state of an object.
 */
int8_t ESPEasy_TouchHandler::getTouchObjectState(struct EventStruct *event,
                                                 const String      & touchObject) {
  if (touchObject.isEmpty()) { return false; }
  int8_t result         = -1;
  const int8_t objectNr = getTouchObjectIndex(event, touchObject);

  if (objectNr > -1) {
    result =  bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_ENABLED) ? 1 : 0;
  }
  return result;
}

/**
 * Set the on/off state of an enabled touch-button object. Will generate an event if so configured.
 */
bool ESPEasy_TouchHandler::setTouchButtonOnOff(struct EventStruct *event,
                                               const String      & touchObject,
                                               const bool        & state) {
  if (touchObject.isEmpty()) { return false; }
  bool success          = false;
  const int8_t objectNr = getTouchObjectIndex(event, touchObject, true);

  if ((objectNr > -1)
      && bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_ENABLED)
      && bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_BUTTON)) {
    success = true; // Always success if matched button

    if (state != TouchObjects[objectNr].TouchStates) {
      TouchObjects[objectNr].TouchStates = state;

      // Send event like it was pressed
      if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_OBJECTNAME) &&
          bitRead(Touch_Settings.flags, TOUCH_FLAGS_INIT_OBJECTEVENT)) {
        generateObjectEvent(event, objectNr, state ? 1 : 0);
      }
    }
    # ifdef TOUCH_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      String log;
      log.reserve(72);
      log = strformat(F("TOUCH setTouchButtonOnOff: obj: %s/%d, new state: "),
                      touchObject.c_str(), objectNr);
      log += state ? F("on") : F("off");
      addLogMove(LOG_LEVEL_DEBUG, log);
    }
    # endif // ifdef TOUCH_DEBUG
  }
  return success;
}

/**
 * Get the on/off state of an enabled touch-button object.
 */
int16_t ESPEasy_TouchHandler::getTouchObjectValue(struct EventStruct *event,
                                                  const String      & touchObject) {
  if (touchObject.isEmpty()) { return -1; }
  int16_t result        = -1; // invalid object
  const int8_t objectNr = getTouchObjectIndex(event, touchObject);

  if ((objectNr > -1)
      && bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_ENABLED)) {
    if (bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_BUTTON)) {
      result = TouchObjects[objectNr].TouchStates > 0 &&
               !bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_INVERTED) ? 1 : 0;
    } else {
      result = TouchObjects[objectNr].TouchStates;
    }
  }
  return result;
}

/**
 * Set the value of any enabled touch-object. Will generate an event if so configured.
 */
bool ESPEasy_TouchHandler::setTouchObjectValue(struct EventStruct *event,
                                               const String      & touchObject,
                                               const int16_t     & value) {
  if (touchObject.isEmpty()) { return false; }
  bool success          = false;
  const int8_t objectNr = getTouchObjectIndex(event, touchObject, false);
  int16_t _value        = value;

  if ((objectNr > -1)
      && bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_ENABLED)) {
    success = true; // Always success if matched object

    if (_value != TouchObjects[objectNr].TouchStates) {
      if (bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_SLIDER)) {
        int16_t lowRange  = 0;
        int16_t highRange = 100;

        if (!TouchObjects[objectNr].captionOff.isEmpty()) { // Off caption can hold range: <from>,<to>
          parseRangeToInt16(TouchObjects[objectNr].captionOff, lowRange, highRange);

          if (lowRange > highRange) {
            if (_value < highRange) {
              _value = highRange;
            } else if (_value > lowRange) {
              _value = lowRange;
            }
          } else {
            if (_value < lowRange) {
              _value = lowRange;
            } else if (_value > highRange) {
              _value = highRange;
            }
          }
        }
      }
      TouchObjects[objectNr].TouchStates = _value;

      // Send event like it was pressed
      if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_OBJECTNAME) &&
          bitRead(Touch_Settings.flags, TOUCH_FLAGS_INIT_OBJECTEVENT)) {
        generateObjectEvent(event, objectNr, _value);
      }
    }
    # ifdef TOUCH_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      addLogMove(LOG_LEVEL_DEBUG,
                 strformat(F("TOUCH setTouchObjectValue: obj: %s/%d, new value: %d"),
                           touchObject.c_str(), objectNr, _value));
    }
    # endif // ifdef TOUCH_DEBUG
  }
  return success;
}

/**
 * parseRangeToInt16: get the low and high values of a range and convert to int16_t
 */
bool ESPEasy_TouchHandler::parseRangeToInt16(const String& range,
                                             int16_t     & lowRange,
                                             int16_t     & highRange) {
  float  rangeFrom     = 0.0f;
  float  rangeTo       = 0.0f;
  String tmp           = parseString(range, 1);
  const bool validFrom = validFloatFromString(tmp, rangeFrom);

  tmp = parseString(range, 2);

  if (validFrom && validFloatFromString(tmp, rangeTo) &&
      !essentiallyZero(rangeFrom) && !essentiallyZero(rangeTo)) {
    lowRange  = static_cast<int16_t>(rangeFrom);
    highRange = static_cast<int16_t>(rangeTo);
    return true;
  }
  return false;
}

/**
 * mode: -2 = clear buttons in group, -3 = clear all buttongroups, -1 = draw buttons in group, 0 = initialize buttons
 */
# if TOUCH_FEATURE_EXTENDED_TOUCH
void ESPEasy_TouchHandler::displayButtonGroup(struct EventStruct *event,
                                              const int16_t     & buttonGroup,
                                              const int8_t      & mode) {
  for (int8_t objectNr = 0; objectNr < static_cast<int8_t>(TouchObjects.size()); ++objectNr) {
    displayButton(event, objectNr, buttonGroup, mode);

    delay(0);
  }

  if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_OBJECTNAME)) {
    // Send an event <taskname>#Group,<group>,<mode> with the selected group and the mode (-3..0)
    eventQueue.add(strformat(F("%s#Group=%d,%d"),
                             getTaskDeviceName(event->TaskIndex).c_str(),
                             buttonGroup,
                             mode));
  }

  delay(0);
}

/**
 * Display a single button, using mode from displayButtonGroup
 */
bool ESPEasy_TouchHandler::displayButton(struct EventStruct *event,
                                         const int8_t      & buttonNr,
                                         const int16_t     & buttonGroup,
                                         int8_t              mode) {
  if ((buttonNr < 0) || (buttonNr >= static_cast<int8_t>(TouchObjects.size()))) { return false; } // sanity check
  int8_t state        = 99;
  const int16_t group = get8BitFromUL(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_GROUP);

  #  if TOUCH_FEATURE_EXTENDED_TOUCH
  Touch_action_e action = static_cast<Touch_action_e>(get4BitFromUL(TouchObjects[buttonNr].groupFlags, TOUCH_OBJECT_GROUP_ACTION));
  #  endif // if TOUCH_FEATURE_EXTENDED_TOUCH
  bool isArrow = false;

  #  if TOUCH_FEATURE_EXTENDED_TOUCH

  if ((mode > -2) &&                                 // Not on clear (-2 and -3)
      bitRead(Touch_Settings.flags, TOUCH_FLAGS_AUTO_PAGE_ARROWS) &&
      ((action == Touch_action_e::DecrementGroup) || // Arrow buttons
       (action == Touch_action_e::IncrementGroup) ||
       (action == Touch_action_e::DecrementPage) ||
       (action == Touch_action_e::IncrementPage))) {
    isArrow = true;
  }
  #  endif // if TOUCH_FEATURE_EXTENDED_TOUCH

  if (!TouchObjects[buttonNr].objectName.isEmpty() &&
      ((bitRead(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_ENABLED) && (group == 0)) || (group > 0) || isArrow) &&
      (bitRead(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_BUTTON) ||
       bitRead(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_SLIDER)) &&
      (((group == buttonGroup) || (buttonGroup < 0)) ||
       ((mode != -2) && (group == 0)) ||
       (mode == -3))) {
    // Act like a button, 1 = On, 0 = Off, inversion is handled in generateObjectEvent()
    state = TouchObjects[buttonNr].TouchStates;

    #  if TOUCH_FEATURE_EXTENDED_TOUCH

    if (isArrow) {                                    // Auto-Enable/Disable the arrow buttons
      const bool pgupInvert = bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU);
      state = 1;                                      // always get ON state!

      if (action == Touch_action_e::DecrementGroup) { // Left arrow
        bitWrite(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_ENABLED, validButtonGroup(buttonGroup - (pgupInvert ? -1 : 1), true));
      } else
      if (action == Touch_action_e::IncrementGroup) { // Right arrow
        bitWrite(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_ENABLED, validButtonGroup(buttonGroup + (pgupInvert ? -1 : 1), true));
      } else
      if (action == Touch_action_e::DecrementPage) {  // Down arrow or Up arrow
        bitWrite(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_ENABLED, validButtonGroup(buttonGroup + (pgupInvert ? 10 : -10), true));
      } else
      if (action == Touch_action_e::IncrementPage) {  // Up arrow or Down arrow
        bitWrite(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_ENABLED, validButtonGroup(buttonGroup + (pgupInvert ? -10 : 10), true));
      }
    }
    #  endif // if TOUCH_FEATURE_EXTENDED_TOUCH

    if (bitRead(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_ENABLED)) {
      if (mode == 0) {
        mode = -1;
      }
    } else {
      state -= 2; // disabled
    }
    generateObjectEvent(event, buttonNr, state, mode, mode < 0, mode <= -2 ? -1 : 1);
  }
  #  ifdef TOUCH_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLog(LOG_LEVEL_DEBUG,
           strformat(F("TOUCH: button init, state: %d, group: %d, mode: %d, group: %d, en: %d, object: %d"),
                     state,
                     buttonGroup,
                     mode,
                     get8BitFromUL(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_GROUP),
                     bitRead(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_BUTTON),
                     buttonNr));
  }
  #  endif // ifdef TOUCH_DEBUG
  return true;
}

/**
 * Check if this is a valid button group, 2022-08-16: default changed to IGNORE group 0!
 * When ignoreZero = true will return false for group 0 if the number of groups > 1.
 * When ignoreZero = false will return true for group 0 also if the number of groups > 1.
 * NB: Group 0 is always available, even without button definitions!
 */
bool ESPEasy_TouchHandler::validButtonGroup(const int16_t& group,
                                            const bool   & ignoreZero /* = true*/) {
  return _buttonGroups.find(group) != _buttonGroups.end() &&
         (!ignoreZero || group > 0 || (group == 0 && _buttonGroups.size() == 1));
}

#  if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE

/**
 * set button group page via the Swipe event
 */
bool ESPEasy_TouchHandler::handleButtonSwipe(struct EventStruct *event,
                                             const int16_t     & swipeValue) {
  bool success               = false;
  const Swipe_action_e swipe = static_cast<Swipe_action_e>(swipeValue);
  const bool swapped         = bitRead(Touch_Settings.flags, TOUCH_FLAGS_SWAP_LEFT_RIGHT);

  if (swipe == Swipe_action_e::Up) {
    if (swapped) {
      prevButtonPage(event);
    } else {
      nextButtonPage(event);
    }
    success = true;
  } else if (swipe == Swipe_action_e::Right) {
    if (swapped) {
      prevButtonGroup(event);
    } else {
      nextButtonGroup(event);
    }
    success = true;
  } else if (swipe == Swipe_action_e::Down) {
    if (swapped) {
      nextButtonPage(event);
    } else {
      prevButtonPage(event);
    }
    success = true;
  } else if ((swipe == Swipe_action_e::Left)) {
    if (swapped) {
      nextButtonGroup(event);
    } else {
      prevButtonGroup(event);
    }
    success = true;
  }
  return success;
}

#  endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE

/**
 * Set the desired button group, must be a known group, previous group will be erased and new group drawn
 */
bool ESPEasy_TouchHandler::setButtonGroup(struct EventStruct *event,
                                          const int16_t     & buttonGroup) {
  if (validButtonGroup(buttonGroup, false)) { // We want to be able to select group 0
    if (buttonGroup != _buttonGroup) {
      displayButtonGroup(event, _buttonGroup, -2);
      _buttonGroup = buttonGroup;
      displayButtonGroup(event, _buttonGroup, -1);
    }
    return true;
  }
  return false;
}

/**
 * Increment button group if that group exists, if max. group > 0 then min. group = 1
 */
bool ESPEasy_TouchHandler::nextButtonGroup(struct EventStruct *event) {
  const bool pgupInvert = bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU);

  if (validButtonGroup(_buttonGroup + (pgupInvert ? -1 : 1))) {
    return setButtonGroup(event, _buttonGroup + (pgupInvert ? -1 : 1));
  }
  return false;
}

/**
 * Decrement button group if that group exists, if max. group > 0 then min. group = 1
 */
bool ESPEasy_TouchHandler::prevButtonGroup(struct EventStruct *event) {
  const bool pgupInvert = bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU);

  if (validButtonGroup(_buttonGroup - (pgupInvert ? -1 : 1))) {
    return setButtonGroup(event, _buttonGroup - (pgupInvert ? -1 : 1));
  }
  return false;
}

/**
 * Increment button group by page (+10), if max. group > 0 then min. group = 1
 */
bool ESPEasy_TouchHandler::nextButtonPage(struct EventStruct *event) {
  const bool pgupInvert = bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU);

  if (validButtonGroup(_buttonGroup + (pgupInvert ? -10 : 10))) {
    return setButtonGroup(event, _buttonGroup + (pgupInvert ? -10 : 10));
  }
  return false;
}

/**
 * Decrement button group by page (+10), if max. group > 0 then min. group = 1
 */
bool ESPEasy_TouchHandler::prevButtonPage(struct EventStruct *event) {
  const bool pgupInvert = bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU);

  if (validButtonGroup(_buttonGroup + (pgupInvert ? 10 : -10))) {
    return setButtonGroup(event, _buttonGroup + (pgupInvert ? 10 : -10));
  }
  return false;
}

# endif // if TOUCH_FEATURE_EXTENDED_TOUCH

/**
 * Get the PLUGIN_GET_DEVICEVTYPE, based on the user-selected setting for including Z-axis
 */
uint8_t ESPEasy_TouchHandler::get_device_valuecount(struct EventStruct *event) {
  if (!_settingsLoaded) {
    loadTouchObjects(event);
    _settingsLoaded = true;
  }

  return getValueCountFromSensorType(
    bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_Z)
    ? Sensor_VType::SENSOR_TYPE_TRIPLE
    : Sensor_VType::SENSOR_TYPE_DUAL);
}

/**
 * Load the settings onto the webpage
 */
bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) {
  if (!_settingsLoaded) {
    loadTouchObjects(event);
    _settingsLoaded = true;
  }

  addFormSubHeader(F("Touch configuration"));

  addFormCheckBox(F("Flip rotation 180&deg;"), F("rot_flip"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_ROTATION_FLIPPED));
  # ifndef LIMIT_BUILD_SIZE
  addFormNote(F("Some touchscreens are mounted 180&deg; rotated on the display."));
  # endif // ifndef LIMIT_BUILD_SIZE

  uint8_t choice3 = 0u;

  bitWrite(choice3, TOUCH_FLAGS_SEND_XY,         bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_XY));
  bitWrite(choice3, TOUCH_FLAGS_SEND_Z,          bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_Z));
  bitWrite(choice3, TOUCH_FLAGS_SEND_OBJECTNAME, bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_OBJECTNAME));
  {
    const __FlashStringHelper *options3[] =
    { F("None"),
      F("X and Y"),
      F("X, Y and Z"),
      # if TOUCH_FEATURE_EXTENDED_TOUCH
      F("Objectnames and Button groups"),
      F("Objectnames, Button groups, X and Y"),
      F("Objectnames, Button groups, X, Y and Z")
      # else // if TOUCH_FEATURE_EXTENDED_TOUCH
      F("Objectnames only"),
      F("Objectnames, X and Y"),
      F("Objectnames, X, Y and Z")
      # endif // if TOUCH_FEATURE_EXTENDED_TOUCH
    };
    const int optionValues3[] = { 0, 1, 3, 4, 5, 7 }; // Already used as a bitmap!
    const FormSelectorOptions selector(NR_ELEMENTS(optionValues3), options3, optionValues3);
    selector.addFormSelector(F("Events"), F("events"), choice3);

    addFormCheckBox(F("Draw buttons when started"), F("initobj"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_INIT_OBJECTEVENT));
    # ifndef LIMIT_BUILD_SIZE
    addFormNote(F("Needs Objectnames 'Events' to be enabled."));
    # endif // ifndef LIMIT_BUILD_SIZE
  }

  addFormCheckBox(F("Prevent duplicate events"), F("dedupe"),   bitRead(Touch_Settings.flags, TOUCH_FLAGS_DEDUPLICATE));

  # if TOUCH_FEATURE_EXTENDED_TOUCH
  addFormCheckBox(F("Ignore touch-screen"),      F("igntouch"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_IGNORE_TOUCH));
  #  ifndef LIMIT_BUILD_SIZE
  addFormNote(F("To enable the use of touch-object display-functions only."));
  #  endif // ifndef LIMIT_BUILD_SIZE
  # endif // if TOUCH_FEATURE_EXTENDED_TOUCH

  # ifndef LIMIT_BUILD_SIZE

  if (!Settings.UseRules) {
    addFormNote(F("Tools / Advanced / Rules must be enabled for events to be fired."));
  }
  # endif // ifndef LIMIT_BUILD_SIZE

  addFormSubHeader(F("Calibration"));

  addFormSelector_YesNo(F("Calibrate to screen resolution"),
                        F("usecalib"),
                        Touch_Settings.calibrationEnabled ? 1 : 0,
                        true); // reloadonchange

  if (Touch_Settings.calibrationEnabled) {
    addRowLabel(F("Calibration"));
    html_table(EMPTY_STRING, false); // Sub-table
    html_table_header(EMPTY_STRING);
    html_table_header(F("x"));
    html_table_header(F("y"));
    html_table_header(EMPTY_STRING);
    html_table_header(F("x"));
    html_table_header(F("y"));

    html_TR_TD();
    addHtml(F("Top-left"));
    html_TD();
    addNumericBox(F("cal_tl_x"),
                  Touch_Settings.top_left.x,
                  0,
                  65535);
    html_TD();
    addNumericBox(F("cal_tl_y"),
                  Touch_Settings.top_left.y,
                  0,
                  65535);
    html_TD();
    addHtml(F("Bottom-right"));
    html_TD();
    addNumericBox(F("cal_br_x"),
                  Touch_Settings.bottom_right.x,
                  0,
                  65535);
    html_TD();
    addNumericBox(F("cal_br_y"),
                  Touch_Settings.bottom_right.y,
                  0,
                  65535);

    html_end_table();
  }

  addFormCheckBox(F("Enable logging for calibration"), F("logcalib"),
                  Touch_Settings.logEnabled);

  addFormSubHeader(F("Object settings"));

  # if TOUCH_FEATURE_EXTENDED_TOUCH

  AdaGFXHtmlColorDepthDataList(F("adagfx65kcolors"), _colorDepth);

  {
    String parsed;
    addRowLabel(F("Default On/Off button colors"));
    html_table(F("sub"), false); // Sub-table
    html_table_header(F("ON color"));
    html_table_header(F("OFF color"));
    html_table_header(F("Border color"));
    html_table_header(F("Caption color"));
    html_table_header(F("Disabled color"));
    html_table_header(F("Disabled caption color"));

    html_TR_TD(); // ON color
    parsed = AdaGFXcolorToString(Touch_Settings.colorOn, _colorDepth, true);
    addTextBox(getPluginCustomArgName(3000), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false,
               EMPTY_STRING, F("widenumber")
               #  if TOUCH_FEATURE_TOOLTIPS
               , F("ON color")
               #  endif // if TOUCH_FEATURE_TOOLTIPS
               , F("adagfx65kcolors")
               );
    html_TD(); // OFF color
    parsed = AdaGFXcolorToString(Touch_Settings.colorOff, _colorDepth, true);
    addTextBox(getPluginCustomArgName(3001), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false,
               EMPTY_STRING, F("widenumber")
               #  if TOUCH_FEATURE_TOOLTIPS
               , F("OFF color")
               #  endif // if TOUCH_FEATURE_TOOLTIPS
               , F("adagfx65kcolors")
               );
    html_TD(); // Border color
    parsed = AdaGFXcolorToString(Touch_Settings.colorBorder, _colorDepth, true);
    addTextBox(getPluginCustomArgName(3002), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false,
               EMPTY_STRING, F("widenumber")
               #  if TOUCH_FEATURE_TOOLTIPS
               , F("Border color")
               #  endif // if TOUCH_FEATURE_TOOLTIPS
               , F("adagfx65kcolors")
               );
    html_TD(); // Caption color
    parsed = AdaGFXcolorToString(Touch_Settings.colorCaption, _colorDepth, true);
    addTextBox(getPluginCustomArgName(3003), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false,
               EMPTY_STRING, F("widenumber")
               #  if TOUCH_FEATURE_TOOLTIPS
               , F("Caption color")
               #  endif // if TOUCH_FEATURE_TOOLTIPS
               , F("adagfx65kcolors")
               );
    html_TD(); // Disabled color
    parsed = AdaGFXcolorToString(Touch_Settings.colorDisabled, _colorDepth, true);
    addTextBox(getPluginCustomArgName(3004), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false,
               EMPTY_STRING, F("widenumber")
               #  if TOUCH_FEATURE_TOOLTIPS
               , F("Disabled color")
               #  endif // if TOUCH_FEATURE_TOOLTIPS
               , F("adagfx65kcolors")
               );
    html_TD(); // Disabled caption color
    parsed = AdaGFXcolorToString(Touch_Settings.colorDisabledCaption, _colorDepth, true);
    addTextBox(getPluginCustomArgName(3005), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false,
               EMPTY_STRING, F("widenumber")
               #  if TOUCH_FEATURE_TOOLTIPS
               , F("Disabled caption color")
               #  endif // if TOUCH_FEATURE_TOOLTIPS
               , F("adagfx65kcolors")
               );
    html_end_table();
  }
  {
    addFormNumericBox(F("Initial button group"), F("initgrp"),
                      get8BitFromUL(Touch_Settings.flags, TOUCH_FLAGS_INITIAL_GROUP), 0, TOUCH_MAX_BUTTON_GROUPS
                      #  if TOUCH_FEATURE_TOOLTIPS
                      , F("Initial group")
                      #  endif // if TOUCH_FEATURE_TOOLTIPS
                      );
    addFormCheckBox(F("Draw buttons via Rules"), F("via_rules"),
                    bitRead(Touch_Settings.flags, TOUCH_FLAGS_DRAWBTN_VIA_RULES));
    addFormCheckBox(F("Enable/Disable page buttons"), F("pagebtns"),
                    bitRead(Touch_Settings.flags, TOUCH_FLAGS_AUTO_PAGE_ARROWS));
    addFormCheckBox(F("Navigation Left/Right/Up/ Down menu reversed"), F("pageblw"),
                    bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU));
    addFormCheckBox(F("Swipe Left/Right/Up/Down menu reversed"), F("swipeswap"),
                    bitRead(Touch_Settings.flags, TOUCH_FLAGS_SWAP_LEFT_RIGHT));
  }
  # endif // if TOUCH_FEATURE_EXTENDED_TOUCH
  {
    addFormNumericBox(F("Debounce delay for On/Off buttons"), F("debounce"),
                      Touch_Settings.debounceMs, 0, 255);
    addUnit(F("0..255 msec."));

    # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE
    addFormNumericBox(F("Minimal swipe movement"), F("swipemin"),
                      Touch_Settings.swipeMinimal, 1, 25);
    addUnit(F("1..25px"));

    addFormNumericBox(F("Maximum swipe margin"), F("swipemax"),
                      Touch_Settings.swipeMargin, 5, 250);
    addUnit(F("5..250px"));
    # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE
  }
  {
    addFormSubHeader(F("Touch objects"));

    {
      addRowLabel(F("Object"));

      html_table(F("multirow tworow"), false); // Sub-table with alternating highlight per 2 rows
      html_table_header(F("&nbsp;#&nbsp;"));
      html_table_header(F("On"));
      html_table_header(F("Objectname"));
      html_table_header(F("Top-left x"));
      html_table_header(F("Top-left y"));
      # if TOUCH_FEATURE_EXTENDED_TOUCH
      html_table_header(F("Button"));
      html_table_header(F("Layout"));
      html_table_header(F("ON color"));
      html_table_header(F("ON caption"));
      html_table_header(F("Border color"));
      html_table_header(F("Disab. cap. clr"));
      html_table_header(F("Touch action"));
      # else // if TOUCH_FEATURE_EXTENDED_TOUCH
      html_table_header(F("On/Off button"));
      # endif // if TOUCH_FEATURE_EXTENDED_TOUCH
      html_TR(); // New row
      html_table_header(EMPTY_STRING);
      html_table_header(EMPTY_STRING);
      # if TOUCH_FEATURE_EXTENDED_TOUCH
      html_table_header(F("Button-group"));
      # else // if TOUCH_FEATURE_EXTENDED_TOUCH
      html_table_header(EMPTY_STRING);
      # endif // if TOUCH_FEATURE_EXTENDED_TOUCH
      html_table_header(F("Width"));
      html_table_header(F("Height"));
      html_table_header(F("Inverted"));
      # if TOUCH_FEATURE_EXTENDED_TOUCH
      html_table_header(F("Font scale"));
      html_table_header(F("OFF color"));
      html_table_header(F("OFF caption"));
      html_table_header(F("Caption color"));
      html_table_header(F("Disabled clr"));
      html_table_header(F("Action group"));
      # endif // if TOUCH_FEATURE_EXTENDED_TOUCH
    }
    # if TOUCH_FEATURE_EXTENDED_TOUCH
    const __FlashStringHelper *buttonTypeOptions[] = {
      toString(Button_type_e::None),
      toString(Button_type_e::Square),
      toString(Button_type_e::Rounded),
      toString(Button_type_e::Circle),
      toString(Button_type_e::ArrowLeft),
      toString(Button_type_e::ArrowUp),
      toString(Button_type_e::ArrowRight),
      toString(Button_type_e::ArrowDown),
    };

    const int buttonTypeValues[] = {
      static_cast<int>(Button_type_e::None),
      static_cast<int>(Button_type_e::Square),
      static_cast<int>(Button_type_e::Rounded),
      static_cast<int>(Button_type_e::Circle),
      static_cast<int>(Button_type_e::ArrowLeft),
      static_cast<int>(Button_type_e::ArrowUp),
      static_cast<int>(Button_type_e::ArrowRight),
      static_cast<int>(Button_type_e::ArrowDown),
    };

    const __FlashStringHelper *buttonLayoutOptions[] = {
      toString(Button_layout_e::CenterAligned),
      toString(Button_layout_e::LeftAligned),
      toString(Button_layout_e::TopAligned),
      toString(Button_layout_e::RightAligned),
      toString(Button_layout_e::BottomAligned),
      toString(Button_layout_e::LeftTopAligned),
      toString(Button_layout_e::RightTopAligned),
      toString(Button_layout_e::LeftBottomAligned),
      toString(Button_layout_e::RightBottomAligned),
      toString(Button_layout_e::NoCaption),
      #  if ADAGFX_ENABLE_BMP_DISPLAY
      toString(Button_layout_e::Bitmap),
      #  endif // if ADAGFX_ENABLE_BMP_DISPLAY
      #  if ADAGFX_ENABLE_BUTTON_SLIDER
      toString(Button_layout_e::Slider),
      #  endif // if ADAGFX_ENABLE_BUTTON_SLIDER
    };

    const int buttonLayoutValues[] = {
      static_cast<int>(Button_layout_e::CenterAligned),
      static_cast<int>(Button_layout_e::LeftAligned),
      static_cast<int>(Button_layout_e::TopAligned),
      static_cast<int>(Button_layout_e::RightAligned),
      static_cast<int>(Button_layout_e::BottomAligned),
      static_cast<int>(Button_layout_e::LeftTopAligned),
      static_cast<int>(Button_layout_e::RightTopAligned),
      static_cast<int>(Button_layout_e::LeftBottomAligned),
      static_cast<int>(Button_layout_e::RightBottomAligned),
      static_cast<int>(Button_layout_e::NoCaption),
      #  if ADAGFX_ENABLE_BMP_DISPLAY
      static_cast<int>(Button_layout_e::Bitmap),
      #  endif // if ADAGFX_ENABLE_BMP_DISPLAY
      #  if ADAGFX_ENABLE_BUTTON_SLIDER
      static_cast<int>(Button_layout_e::Slider),
      #  endif // if ADAGFX_ENABLE_BUTTON_SLIDER
    };

    const __FlashStringHelper *touchActionOptions[] = {
      toString(Touch_action_e::Default),
      toString(Touch_action_e::ActivateGroup),
      toString(Touch_action_e::IncrementGroup),
      toString(Touch_action_e::DecrementGroup),
      toString(Touch_action_e::IncrementPage),
      toString(Touch_action_e::DecrementPage),
    };

    const int touchActionValues[] = {
      static_cast<int>(Touch_action_e::Default),
      static_cast<int>(Touch_action_e::ActivateGroup),
      static_cast<int>(Touch_action_e::IncrementGroup),
      static_cast<int>(Touch_action_e::DecrementGroup),
      static_cast<int>(Touch_action_e::IncrementPage),
      static_cast<int>(Touch_action_e::DecrementPage),
    };

    # endif // if TOUCH_FEATURE_EXTENDED_TOUCH

    const uint8_t maxIdx = std::min(static_cast<int>(TouchObjects.size() + TOUCH_EXTRA_OBJECT_COUNT), TOUCH_MAX_OBJECT_COUNT);
    String parsed;
    TouchObjects.resize(maxIdx, tTouchObjects());

    for (int8_t objectNr = 0; objectNr < maxIdx; ++objectNr) {
      html_TR_TD();
      addHtml(F("&nbsp;"));
      addHtmlInt(objectNr + 1); // Arrayindex to objectindex

      html_TD();

      // Enable new entries
      bool enabled = bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_ENABLED) || TouchObjects[objectNr].objectName.isEmpty();
      addCheckBox(getPluginCustomArgName(objectNr + 0),
                  enabled, false
                  # if TOUCH_FEATURE_TOOLTIPS
                  , F("Enabled")
                  # endif // if TOUCH_FEATURE_TOOLTIPS
                  );
      html_TD(); // Name
      addTextBox(getPluginCustomArgName(objectNr + 100),
                 TouchObjects[objectNr].objectName,
                 TOUCH_MaxObjectNameLength,
                 F("wide"));
      html_TD(); // top-x
      addNumericBox(getPluginCustomArgName(objectNr + 200),
                    TouchObjects[objectNr].top_left.x, 0, 65535
                    # if TOUCH_FEATURE_TOOLTIPS
                    , F("widenumber"), F("Top-left x")
                    # endif // if TOUCH_FEATURE_TOOLTIPS
                    );
      html_TD(); // top-y
      addNumericBox(getPluginCustomArgName(objectNr + 300),
                    TouchObjects[objectNr].top_left.y, 0, 65535
                    # if TOUCH_FEATURE_TOOLTIPS
                    , F("widenumber"), F("Top-left y")
                    # endif // if TOUCH_FEATURE_TOOLTIPS
                    );
      html_TD(); // (on/off) button (type)
      # if TOUCH_FEATURE_EXTENDED_TOUCH
      {
        FormSelectorOptions selector(
          NR_ELEMENTS(buttonTypeValues),
          buttonTypeOptions,
          buttonTypeValues);
        selector.classname = F("widenumber");
#  if TOUCH_FEATURE_TOOLTIPS
        selector.tooltip =  F("Button");
#  endif // if TOUCH_FEATURE_TOOLTIPS

        selector.addSelector(
          getPluginCustomArgName(objectNr + 800),
          get4BitFromUL(
            TouchObjects[objectNr].flags,
            TOUCH_OBJECT_FLAG_BUTTONTYPE));
      }
      {
        html_TD(); // button alignment
        FormSelectorOptions selector(
          NR_ELEMENTS(buttonLayoutOptions),
          buttonLayoutOptions,
          buttonLayoutValues);
        selector.classname = F("widenumber");
#  if TOUCH_FEATURE_TOOLTIPS
        selector.tooltip =  F("Layout");
#  endif // if TOUCH_FEATURE_TOOLTIPS

        selector.addSelector(
          getPluginCustomArgName(objectNr + 900),
          get4BitFromUL(
            TouchObjects[objectNr].flags,
            TOUCH_OBJECT_FLAG_BUTTONALIGN) << 4);
      }
      # else // if TOUCH_FEATURE_EXTENDED_TOUCH
      addCheckBox(getPluginCustomArgName(objectNr + 600),
                  bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_BUTTON), false
                  #  if TOUCH_FEATURE_TOOLTIPS
                  , F("On/Off button")
                  #  endif // if TOUCH_FEATURE_TOOLTIPS
                  );
      # endif // if TOUCH_FEATURE_EXTENDED_TOUCH
      # if TOUCH_FEATURE_EXTENDED_TOUCH
      html_TD(); // ON color
      parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorOn, _colorDepth, true);
      addTextBox(getPluginCustomArgName(objectNr + 1000), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false,
                 EMPTY_STRING, F("widenumber")
                 #  if TOUCH_FEATURE_TOOLTIPS
                 , F("ON color")
                 #  endif // if TOUCH_FEATURE_TOOLTIPS
                 , F("adagfx65kcolors")
                 );
      html_TD(); // ON Caption
      parsed = TouchObjects[objectNr].captionOn;
      parsed.replace('_', ' ');
      addTextBox(getPluginCustomArgName(objectNr + 1300),
                 parsed,
                 TOUCH_MaxCaptionNameLength,
                 false,
                 false,
                 EMPTY_STRING,
                 F("xwide")
                 #  if TOUCH_FEATURE_TOOLTIPS
                 , F("ON caption")
                 #  endif // if TOUCH_FEATURE_TOOLTIPS
                 );
      html_TD(); // Border color
      parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorBorder, _colorDepth, true);
      addTextBox(getPluginCustomArgName(objectNr + 1700), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false,
                 EMPTY_STRING, F("widenumber")
                 #  if TOUCH_FEATURE_TOOLTIPS
                 , F("Border color")
                 #  endif // if TOUCH_FEATURE_TOOLTIPS
                 , F("adagfx65kcolors")
                 );
      html_TD(); // Disabled caption color
      parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorDisabledCaption, _colorDepth, true);
      addTextBox(getPluginCustomArgName(objectNr + 1900), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false,
                 EMPTY_STRING, F("widenumber")
                 #  if TOUCH_FEATURE_TOOLTIPS
                 , F("Disabled caption color")
                 #  endif // if TOUCH_FEATURE_TOOLTIPS
                 , F("adagfx65kcolors")
                 );
      {
        html_TD(); // button action
        FormSelectorOptions selector(
          NR_ELEMENTS(touchActionOptions),
          touchActionOptions,
          touchActionValues);
        selector.classname = F("widenumber");
#  if TOUCH_FEATURE_TOOLTIPS
        selector.tooltip = F("Touch action");
#  endif // if TOUCH_FEATURE_TOOLTIPS

        selector.addSelector(
          getPluginCustomArgName(objectNr + 2000),
          get4BitFromUL(
            TouchObjects[objectNr].groupFlags,
            TOUCH_OBJECT_GROUP_ACTION));
      }
      # endif // if TOUCH_FEATURE_EXTENDED_TOUCH

      html_TR_TD(); // Start new row

      html_TD(2);   // Start with some blank columns
      # if TOUCH_FEATURE_EXTENDED_TOUCH
      {
        addNumericBox(getPluginCustomArgName(objectNr + 1600),
                      get8BitFromUL(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_GROUP), 0, TOUCH_MAX_BUTTON_GROUPS
                      #  if TOUCH_FEATURE_TOOLTIPS
                      , F("widenumber"), strformat(F("Button-group [0..%d]"), TOUCH_MAX_BUTTON_GROUPS)
                      #  endif // if TOUCH_FEATURE_TOOLTIPS
                      );
      }
      # endif // if TOUCH_FEATURE_EXTENDED_TOUCH
      html_TD(); // Width
      addNumericBox(getPluginCustomArgName(objectNr + 400),
                    TouchObjects[objectNr].width_height.x, 0, 65535
                    # if TOUCH_FEATURE_TOOLTIPS
                    , F("widenumber"), F("Width")
                    # endif // if TOUCH_FEATURE_TOOLTIPS
                    );
      html_TD(); // Height
      addNumericBox(getPluginCustomArgName(objectNr + 500),
                    TouchObjects[objectNr].width_height.y, 0, 65535
                    # if TOUCH_FEATURE_TOOLTIPS
                    , F("widenumber"), F("Height")
                    # endif // if TOUCH_FEATURE_TOOLTIPS
                    );
      html_TD(); // inverted
      addCheckBox(getPluginCustomArgName(objectNr + 700),
                  bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_INVERTED), false
                  # if TOUCH_FEATURE_TOOLTIPS
                  , F("Inverted")
                  # endif // if TOUCH_FEATURE_TOOLTIPS
                  );
      # if TOUCH_FEATURE_EXTENDED_TOUCH
      html_TD(); // font scale
      addNumericBox(getPluginCustomArgName(objectNr + 1200),
                    get4BitFromUL(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_FONTSCALE), 0, 10
                    #  if TOUCH_FEATURE_TOOLTIPS
                    , F("widenumber"), F("Font scaling [1x..10x]")
                    #  endif // if TOUCH_FEATURE_TOOLTIPS
                    );
      html_TD(); // OFF color
      parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorOff, _colorDepth, true);
      addTextBox(getPluginCustomArgName(objectNr + 1100), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false,
                 EMPTY_STRING, F("widenumber")
                 #  if TOUCH_FEATURE_TOOLTIPS
                 , F("OFF color")
                 #  endif // if TOUCH_FEATURE_TOOLTIPS
                 , F("adagfx65kcolors")
                 );
      html_TD(); // OFF Caption
      parsed = TouchObjects[objectNr].captionOff;
      parsed.replace('_', ' ');
      addTextBox(getPluginCustomArgName(objectNr + 1400),
                 parsed,
                 TOUCH_MaxCaptionNameLength,
                 false,
                 false,
                 EMPTY_STRING,
                 F("xwide")
                 #  if TOUCH_FEATURE_TOOLTIPS
                 , F("OFF caption")
                 #  endif // if TOUCH_FEATURE_TOOLTIPS
                 );
      html_TD(); // Caption color
      parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorCaption, _colorDepth, true);
      addTextBox(getPluginCustomArgName(objectNr + 1500), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false,
                 EMPTY_STRING, F("widenumber")
                 #  if TOUCH_FEATURE_TOOLTIPS
                 , F("Caption color")
                 #  endif // if TOUCH_FEATURE_TOOLTIPS
                 , F("adagfx65kcolors")
                 );
      html_TD(); // Disabled color
      parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorDisabled, _colorDepth, true);
      addTextBox(getPluginCustomArgName(objectNr + 1800), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false,
                 EMPTY_STRING, F("widenumber")
                 #  if TOUCH_FEATURE_TOOLTIPS
                 , F("Disabled color")
                 #  endif // if TOUCH_FEATURE_TOOLTIPS
                 , F("adagfx65kcolors")
                 );
      html_TD(); // Action Group
      addNumericBox(getPluginCustomArgName(objectNr + 2100),
                    get8BitFromUL(TouchObjects[objectNr].groupFlags, TOUCH_OBJECT_GROUP_ACTIONGROUP), 0, TOUCH_MAX_BUTTON_GROUPS
                    #  if TOUCH_FEATURE_TOOLTIPS
                    , F("widenumber"), F("Action group")
                    #  endif // if TOUCH_FEATURE_TOOLTIPS
                    );
      # endif // if TOUCH_FEATURE_EXTENDED_TOUCH
    }
    html_end_table();
  }
  return false;
}

/**
 * Save the settings from the web page to flash
 */
bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) {
  # ifdef TOUCH_DEBUG
  addLog(LOG_LEVEL_INFO, F("TOUCH DEBUG webform_save start."));
  # endif // ifdef TOUCH_DEBUG
  String config;

  uint16_t saveSize = 0;

  # if TOUCH_FEATURE_EXTENDED_TOUCH
  String colorInput;
  # endif // if TOUCH_FEATURE_EXTENDED_TOUCH
  config.reserve(80);

  uint32_t  lSettings   = 0u;
  const int eventsValue = getFormItemInt(F("events"));

  bitWrite(lSettings, TOUCH_FLAGS_SEND_XY,          bitRead(eventsValue, TOUCH_FLAGS_SEND_XY));
  bitWrite(lSettings, TOUCH_FLAGS_SEND_Z,           bitRead(eventsValue, TOUCH_FLAGS_SEND_Z));
  bitWrite(lSettings, TOUCH_FLAGS_SEND_OBJECTNAME,  bitRead(eventsValue, TOUCH_FLAGS_SEND_OBJECTNAME));
  bitWrite(lSettings, TOUCH_FLAGS_ROTATION_FLIPPED, isFormItemChecked(F("rot_flip")));
  bitWrite(lSettings, TOUCH_FLAGS_DEDUPLICATE,      isFormItemChecked(F("dedupe")));
  bitWrite(lSettings, TOUCH_FLAGS_INIT_OBJECTEVENT, isFormItemChecked(F("initobj")));
  # if TOUCH_FEATURE_EXTENDED_TOUCH
  set8BitToUL(lSettings, TOUCH_FLAGS_INITIAL_GROUP, getFormItemInt(F("initgrp"))); // Button group
  bitWrite(lSettings, TOUCH_FLAGS_DRAWBTN_VIA_RULES, isFormItemChecked(F("via_rules")));
  bitWrite(lSettings, TOUCH_FLAGS_AUTO_PAGE_ARROWS,  isFormItemChecked(F("pagebtns")));
  bitWrite(lSettings, TOUCH_FLAGS_PGUP_BELOW_MENU,   isFormItemChecked(F("pageblw")));
  bitWrite(lSettings, TOUCH_FLAGS_SWAP_LEFT_RIGHT,   isFormItemChecked(F("swipeswap")));
  bitWrite(lSettings, TOUCH_FLAGS_IGNORE_TOUCH,      isFormItemChecked(F("igntouch")));
  # endif // if TOUCH_FEATURE_EXTENDED_TOUCH

  config += getFormItemInt(F("usecalib")); // First value should NEVER be empty, or parseString() wil get confused
  config += TOUCH_SETTINGS_SEPARATOR;
  config += toStringNoZero(isFormItemChecked(F("logcalib")) ? 1 : 0);
  config += TOUCH_SETTINGS_SEPARATOR;
  config += toStringNoZero(getFormItemInt(F("cal_tl_x")));
  config += TOUCH_SETTINGS_SEPARATOR;
  config += toStringNoZero(getFormItemInt(F("cal_tl_y")));
  config += TOUCH_SETTINGS_SEPARATOR;
  config += toStringNoZero(getFormItemInt(F("cal_br_x")));
  config += TOUCH_SETTINGS_SEPARATOR;
  config += toStringNoZero(getFormItemInt(F("cal_br_y")));
  config += TOUCH_SETTINGS_SEPARATOR;
  config += toStringNoZero(getFormItemInt(F("debounce")));
  config += TOUCH_SETTINGS_SEPARATOR;
  config += ull2String(lSettings);
  # if TOUCH_FEATURE_EXTENDED_TOUCH
  config    += TOUCH_SETTINGS_SEPARATOR;
  colorInput = webArg(getPluginCustomArgName(3000)); // Default Color ON
  config    += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth));
  config    += TOUCH_SETTINGS_SEPARATOR;
  colorInput = webArg(getPluginCustomArgName(3001)); // Default Color OFF
  config    += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth, false));
  config    += TOUCH_SETTINGS_SEPARATOR;
  colorInput = webArg(getPluginCustomArgName(3002)); // Default Color Border
  config    += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth, false));
  config    += TOUCH_SETTINGS_SEPARATOR;
  colorInput = webArg(getPluginCustomArgName(3003)); // Default Color caption
  config    += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth, false));
  config    += TOUCH_SETTINGS_SEPARATOR;
  colorInput = webArg(getPluginCustomArgName(3004)); // Default Disabled Color
  config    += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth));
  config    += TOUCH_SETTINGS_SEPARATOR;
  colorInput = webArg(getPluginCustomArgName(3005)); // Default Disabled Caption Color
  config    += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth, false));
  #  if TOUCH_FEATURE_SWIPE
  config += TOUCH_SETTINGS_SEPARATOR;
  config += toStringNoZero(getFormItemInt(F("swipemin")));
  config += TOUCH_SETTINGS_SEPARATOR;
  config += toStringNoZero(getFormItemInt(F("swipemax")));
  #  endif // if TOUCH_FEATURE_SWIPE
  # endif // if TOUCH_FEATURE_EXTENDED_TOUCH

  settingsArray[TOUCH_CALIBRATION_START] = config;
  saveSize                              += config.length() + 1;

  # ifdef TOUCH_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    config.replace(TOUCH_SETTINGS_SEPARATOR, ',');
    addLogMove(LOG_LEVEL_INFO, concat(F("Save settings: "), config));
  }
  # endif // ifdef TOUCH_DEBUG

  String error;

  for (int8_t objectNr = 0; objectNr < TOUCH_MAX_OBJECT_COUNT; ++objectNr) {
    config.clear();
    config += webArg(getPluginCustomArgName(objectNr + 100)); // Name
    config.trim();                                            // Remove leading/trailing whitespace from name

    if (!config.isEmpty()) {                                  // Empty name => skip entry
      bool numStart = (config[0] >= '0' && config[0] <= '9'); // Numeric start?

      if (!ExtraTaskSettings.checkInvalidCharInNames(config.c_str()) ||
          numStart) {                                         // Check for invalid characters in objectname
        error += strformat(F("Invalid character in objectname #%d. "), objectNr + 1);
        error += numStart ? F("Should not start with a digit.\n") : F("Do not use ',-+/*=^%!#[]{}()' or space.\n");
      }
      config += TOUCH_SETTINGS_SEPARATOR;
      uint32_t flags = 0u;
      bitWrite(flags, TOUCH_OBJECT_FLAG_ENABLED,  isFormItemChecked(getPluginCustomArgName(objectNr + 0)));   // Enabled
      bitWrite(flags, TOUCH_OBJECT_FLAG_INVERTED, isFormItemChecked(getPluginCustomArgName(objectNr + 700))); // Inverted
      # if TOUCH_FEATURE_EXTENDED_TOUCH
      uint32_t groupFlags        = 0u;
      const uint8_t buttonType   = getFormItemIntCustomArgName(objectNr + 800);
      const uint8_t buttonLayout = getFormItemIntCustomArgName(objectNr + 900) >> 4;
      set4BitToUL(flags, TOUCH_OBJECT_FLAG_BUTTONTYPE,  buttonType);   // Buttontype
      set4BitToUL(flags, TOUCH_OBJECT_FLAG_BUTTONALIGN, buttonLayout); // Button layout
      #  if ADAGFX_ENABLE_BUTTON_SLIDER
      const bool isSlider = (static_cast<Button_layout_e>(buttonLayout << 4) == Button_layout_e::Slider);
      bitWrite(flags, TOUCH_OBJECT_FLAG_SLIDER, isSlider);             // Slider
      #  else // if ADAGFX_ENABLE_BUTTON_SLIDER
      const bool isSlider = false;
      #  endif // if ADAGFX_ENABLE_BUTTON_SLIDER
      const bool isButton = (static_cast<Button_type_e>(buttonType) != Button_type_e::None) && !isSlider;
      bitWrite(flags, TOUCH_OBJECT_FLAG_BUTTON, isButton);                                                   // On/Off button
      set4BitToUL(groupFlags, TOUCH_OBJECT_GROUP_ACTION, getFormItemIntCustomArgName(objectNr + 2000));      // ButtonAction
      set8BitToUL(groupFlags, TOUCH_OBJECT_GROUP_ACTIONGROUP, getFormItemIntCustomArgName(objectNr + 2100)); // ActionGroup
      set4BitToUL(flags, TOUCH_OBJECT_FLAG_FONTSCALE, getFormItemIntCustomArgName(objectNr + 1200));         // Font scaling
      set8BitToUL(flags, TOUCH_OBJECT_FLAG_GROUP, getFormItemIntCustomArgName(objectNr + 1600));             // Button group
      # else // if TOUCH_FEATURE_EXTENDED_TOUCH
      bitWrite(flags, TOUCH_OBJECT_FLAG_BUTTON, isFormItemChecked(getPluginCustomArgName(objectNr + 600)));  // On/Off button
      # endif // if TOUCH_FEATURE_EXTENDED_TOUCH

      // REMARK: Converting the code below to strformat() increases the build by 200 bytes!
      config += ull2String(flags);                                           // Flags
      config += TOUCH_SETTINGS_SEPARATOR;
      config += toStringNoZero(getFormItemIntCustomArgName(objectNr + 200)); // Top x
      config += TOUCH_SETTINGS_SEPARATOR;
      config += toStringNoZero(getFormItemIntCustomArgName(objectNr + 300)); // Top y
      config += TOUCH_SETTINGS_SEPARATOR;
      config += toStringNoZero(getFormItemIntCustomArgName(objectNr + 400)); // Bottom x
      config += TOUCH_SETTINGS_SEPARATOR;
      config += toStringNoZero(getFormItemIntCustomArgName(objectNr + 500)); // Bottom y

      # if TOUCH_FEATURE_EXTENDED_TOUCH
      config    += TOUCH_SETTINGS_SEPARATOR;
      colorInput = webArg(getPluginCustomArgName(objectNr + 1000)); // Color ON
      config    += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth, true));
      config    += TOUCH_SETTINGS_SEPARATOR;
      colorInput = webArg(getPluginCustomArgName(objectNr + 1100)); // Color OFF
      config    += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth, true));
      config    += TOUCH_SETTINGS_SEPARATOR;
      colorInput = webArg(getPluginCustomArgName(objectNr + 1500)); // Color caption
      config    += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth, true));
      config    += TOUCH_SETTINGS_SEPARATOR;                        // Caption ON
      colorInput = webArg(getPluginCustomArgName(objectNr + 1300));
      colorInput.replace(' ', '_');                                 // Replace spaces by '_', often cheaper than 2 quotes...
      config    += wrapWithQuotesIfContainsParameterSeparatorChar(colorInput);
      config    += TOUCH_SETTINGS_SEPARATOR;                        // Caption OFF
      colorInput = webArg(getPluginCustomArgName(objectNr + 1400));
      colorInput.replace(' ', '_');                                 // Replace spaces by '_', often cheaper than 2 quotes...
      config    += wrapWithQuotesIfContainsParameterSeparatorChar(colorInput);
      config    += TOUCH_SETTINGS_SEPARATOR;
      colorInput = webArg(getPluginCustomArgName(objectNr + 1700)); // Color Border
      config    += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth, true));
      config    += TOUCH_SETTINGS_SEPARATOR;
      colorInput = webArg(getPluginCustomArgName(objectNr + 1800)); // Disabled Color
      config    += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth, true));
      config    += TOUCH_SETTINGS_SEPARATOR;
      colorInput = webArg(getPluginCustomArgName(objectNr + 1900)); // Disabled Caption Color
      config    += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth, true));
      config    += TOUCH_SETTINGS_SEPARATOR;
      config    += ull2String(groupFlags);                          // Group Flags
      # endif // if TOUCH_FEATURE_EXTENDED_TOUCH
      config.trim();
    }

    String endZero; // Trim off <sep> and <sep>0 from the end
    endZero += TOUCH_SETTINGS_SEPARATOR;
    endZero += '0';
    const uint8_t endZeroLen = endZero.length();

    while (!config.isEmpty() && (config.endsWith(endZero) || config[config.length() - 1] == TOUCH_SETTINGS_SEPARATOR)) {
      if (config[config.length() - 1] == TOUCH_SETTINGS_SEPARATOR) {
        config.remove(config.length() - 1);
      } else {
        config.remove(config.length() - endZeroLen, endZeroLen);
      }
    }

    settingsArray[objectNr + TOUCH_OBJECT_INDEX_START] = config;
    saveSize                                          += config.length() + 1;

    # ifdef TOUCH_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO) &&
        !config.isEmpty()) {
      config.replace(TOUCH_SETTINGS_SEPARATOR, ',');
      addLogMove(LOG_LEVEL_INFO, strformat(F("Save touch object #%d settings: %s"), objectNr + 1, config.c_str()));
    }
    # endif // ifdef TOUCH_DEBUG
  }

  if (!error.isEmpty()) {
    addLog(LOG_LEVEL_ERROR, error);
    addHtmlError(error);
  }

  error = SaveCustomTaskSettings(event->TaskIndex, settingsArray, TOUCH_ARRAY_SIZE, 0);

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO, concat(F("TOUCH: Save settings, size: "), saveSize));
  }

  if (!error.isEmpty()) {
    addLog(LOG_LEVEL_ERROR, error);
    addHtmlError(error);
    return false;
  }
  return true;
}

/**
 * Every 20 milliseconds we check if the screen is touched,
 * handles button switching, swiping and slider-sliding
 */
bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event,
                                                   const int16_t     & x,
                                                   const int16_t     & y,
                                                   const int16_t     & ox,
                                                   const int16_t     & oy,
                                                   const int16_t     & rx,
                                                   const int16_t     & ry,
                                                   const int16_t     & z) {
  bool success = false;

  // Avoid event-storms by deduplicating coordinates, ignore z value when no z-event is generated
  if (!_deduplicate ||
      (_deduplicate && ((TOUCH_GET_VALUE_X != x) || (TOUCH_GET_VALUE_Y != y) ||
                        (bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_Z) && (TOUCH_GET_VALUE_Z != z))))) {
    success = true;
    TOUCH_SET_VALUE_X(x);
    TOUCH_SET_VALUE_Y(y);
    TOUCH_SET_VALUE_Z(z);
  }

  if (success &&
      Touch_Settings.logEnabled &&

      // This log is REQUIRED for calibration and setting up objects, so do not make this optional!
      loglevelActiveFor(LOG_LEVEL_INFO)) {
    // Space before the logged values for readability. Always log the z value even if not used.
    addLogMove(LOG_LEVEL_INFO, strformat(F("Touch calibration rx= %d, ry= %d; z= %d, x= %d, y= %d; ox= %d, oy= %d"),
                                         rx, ry, z, x, y, ox, oy));
  }

  // No events to handle if rules not enabled
  if (Settings.UseRules) {
    if (success && bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_XY)) { // Send events for each touch
      sendData(event);                                                   // Send X/Y(/Z) event
    }

    if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_OBJECTNAME)) {    // Send events for objectname if within reach, and swipes
      String selectedObjectName;
      int8_t selectedObjectIndex = -1;

      if (isValidAndTouchedTouchObject(x, y, selectedObjectName, selectedObjectIndex)) {
        # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE
        int16_t delta_x = x - _last_point.x;
        int16_t delta_y = y - _last_point.y;

        Swipe_action_e swipe = Swipe_action_e::None;

        if ((std::abs(delta_x) >= Touch_Settings.swipeMargin) || (std::abs(delta_x) <= Touch_Settings.swipeMinimal)) {
          delta_x = 0; // Ignore
        }

        if ((std::abs(delta_y) >= Touch_Settings.swipeMargin) || (std::abs(delta_y) <= Touch_Settings.swipeMinimal)) {
          delta_y = 0; // Ignore
        }

        if ((delta_x != 0) || (delta_y != 0)) {
          _lastObjectIndex = -2;

          // Swipe, determine direction (from 12 o'clock, clock-wise)
          if ((delta_x == 0) && (delta_y < 0)) {        // Up
            swipe = Swipe_action_e::Up;
          } else if ((delta_x > 0) && (delta_y < 0)) {  // Up-Right
            swipe = Swipe_action_e::UpRight;
          } else if ((delta_x > 0) && (delta_y == 0)) { // Right
            swipe = Swipe_action_e::Right;
          } else if ((delta_x > 0) && (delta_y > 0)) {  // Right-Down
            swipe = Swipe_action_e::RightDown;
          } else if ((delta_x == 0) && (delta_y > 0)) { // Down
            swipe = Swipe_action_e::Down;
          } else if ((delta_x < 0) && (delta_y > 0)) {  // Down-Left
            swipe = Swipe_action_e::DownLeft;
          } else if ((delta_x < 0) && (delta_y == 0)) { // Left
            swipe = Swipe_action_e::Left;
          } else if ((delta_x < 0) && (delta_y < 0)) {  // Left-Up
            swipe = Swipe_action_e::LeftUp;
          }

          #  ifdef TOUCH_DEBUG

          if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
            addLog(LOG_LEVEL_DEBUG,
                   strformat(F("Touch Swiped, direction: %s, dx: %d, dy: %d"),
                             String(toString(swipe)).c_str(), delta_x, delta_y));
          }
          #  endif // ifdef TOUCH_DEBUG
        }
        # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE

        // Not touched yet or too long ago
        if (
          # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE
          (swipe == Swipe_action_e::None) &&
          # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE
          ((TouchObjects[selectedObjectIndex].TouchTimers == 0) ||
           (TouchObjects[selectedObjectIndex].TouchTimers < (millis() - (1.5 * Touch_Settings.debounceMs)))
          )) {
          // From now wait the debounce time
          TouchObjects[selectedObjectIndex].TouchTimers = millis() + Touch_Settings.debounceMs;
        } else {
          // Debouncing time elapsed? Swiping/sliding passes through without debounce

          if (
            # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE
            (swipe != Swipe_action_e::None) ||
            # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE
            (TouchObjects[selectedObjectIndex].TouchTimers <= millis())) {
            TouchObjects[selectedObjectIndex].TouchTimers = 0;

            if (
              # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE
              (swipe == Swipe_action_e::None) &&
              # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE
              (selectedObjectIndex > -1) && bitRead(TouchObjects[selectedObjectIndex].flags, TOUCH_OBJECT_FLAG_BUTTON)) {
              // Button touched
              _lastObjectIndex = selectedObjectIndex; // Handle on release
            # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE
            } else if ((swipe != Swipe_action_e::None) &&
                       (selectedObjectIndex > -1) && bitRead(TouchObjects[selectedObjectIndex].flags, TOUCH_OBJECT_FLAG_SLIDER)) {
              // Handle slider immediately to move/set absolute position
              _lastObjectIndex = -1; // Handled
              const bool isVertical = TouchObjects[selectedObjectIndex].width_height.x < TouchObjects[selectedObjectIndex].width_height.y;
              int16_t    position   = 0;
              int16_t    lowRange   = 0;
              int16_t    highRange  = 100;
              bool useRange         = false;

              if (!TouchObjects[selectedObjectIndex].captionOff.isEmpty()) { // Off caption can hold range: <from>,<to>
                useRange = parseRangeToInt16(TouchObjects[selectedObjectIndex].captionOff, lowRange, highRange);
              }

              if (isVertical) {
                position = (TouchObjects[selectedObjectIndex].top_left.y + TouchObjects[selectedObjectIndex].width_height.y) - y;
                position = ceil(position / (TouchObjects[selectedObjectIndex].width_height.y / 100.0));
              } else {
                position = x - TouchObjects[selectedObjectIndex].top_left.x;
                position = ceil(position / (TouchObjects[selectedObjectIndex].width_height.x / 100.0));
              }

              if (useRange) { // Calculate range-boundaries
                position                                      = map(position, 0, 100, lowRange, highRange);
                TouchObjects[selectedObjectIndex].TouchStates = position;
              } else if (position < lowRange) {
                TouchObjects[selectedObjectIndex].TouchStates = lowRange;
              } else if (position > highRange) {
                TouchObjects[selectedObjectIndex].TouchStates = highRange;
              } else {
                TouchObjects[selectedObjectIndex].TouchStates = position;
              }

              // Reduce the number of events during sliding
              if ((TouchObjects[selectedObjectIndex].TouchTimers == 0) ||
                  (TouchObjects[selectedObjectIndex].TouchTimers < millis())) {
                generateObjectEvent(event, selectedObjectIndex, TouchObjects[selectedObjectIndex].TouchStates);
                TouchObjects[selectedObjectIndex].TouchTimers = millis() + (2 * Touch_Settings.debounceMs);
              } else {
                _lastObjectIndex = selectedObjectIndex; // Update on touch-release
              }
            # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE
            } else {                                    // Generic touch event
              _lastObjectIndex = -2;                    // Update on touch-release
              _lastObjectName  = selectedObjectName;

              # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE
              #  ifdef TOUCH_DEBUG
              addLogMove(LOG_LEVEL_INFO,
                         strformat(F("Swiped/touched, object: %s:%s"), _lastObjectName.c_str(),
                                   String(toString(swipe)).c_str()));
              #  endif // ifdef TOUCH_DEBUG

              if (swipe != Swipe_action_e::None) {
                _lastSwipe = swipe;
              }
              _last_delta_x = delta_x;
              _last_delta_y = delta_y;
              # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE
            }
          }
          _last_point.x   = x; // Save last touchpoint
          _last_point.y   = y;
          _last_point_z.x = z; // Don't want to extend the struct for 1 use
        }
      }
    }
  }

  return success;
}

/**
 * Release touch
 */
void ESPEasy_TouchHandler::releaseTouch(struct EventStruct *event) {
  if ((_lastObjectIndex > -1) && bitRead(TouchObjects[_lastObjectIndex].flags, TOUCH_OBJECT_FLAG_BUTTON)) {
    TouchObjects[_lastObjectIndex].TouchStates = (TouchObjects[_lastObjectIndex].TouchStates > 0 ? 0 : 1); // Flip state
    generateObjectEvent(event, _lastObjectIndex, TouchObjects[_lastObjectIndex].TouchStates > 0 ? 1 : 0);
    _lastObjectIndex = -1;                                                                                 // Handle only once
  } else if ((_lastObjectIndex > -1) && bitRead(TouchObjects[_lastObjectIndex].flags, TOUCH_OBJECT_FLAG_SLIDER)) {
    generateObjectEvent(event, _lastObjectIndex, TouchObjects[_lastObjectIndex].TouchStates);
    TouchObjects[_lastObjectIndex].TouchTimers = 0;
    _lastObjectIndex                           = -1; // Handle only once
  } else if (_lastObjectIndex != -1) {
    // Matching object is found, send <TaskDeviceName>#<ObjectName> event with x, y and z as %eventvalue1/2/3%
    String eventCommand;
    eventCommand.reserve(48);
    eventCommand  = getTaskDeviceName(event->TaskIndex);
    eventCommand += '#';

    # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE

    if (_lastSwipe == Swipe_action_e::None)
    # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE
    {
      eventCommand += strformat(F("%s=%d,%d,%d"),
                                _lastObjectName.c_str(),
                                _last_point.x,
                                _last_point.y,
                                _last_point_z.x);
    }
    # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE
    else {
      eventCommand += strformat(F("Swiped=%d,%d,%d"), // Add arguments
                                static_cast<int>(_lastSwipe),
                                _last_delta_x,
                                _last_delta_y);
      _lastSwipe = Swipe_action_e::None;
    }
    # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE
    eventQueue.addMove(std::move(eventCommand));
    _lastObjectIndex = -1; // Handle only once
  }
  _stillTouching = false;
}

/**
 * Parse and execute the plugin commands
 */
bool ESPEasy_TouchHandler::plugin_write(struct EventStruct *event,
                                        const String      & string) {
  bool    success = false;
  String  command;
  String  subcommand;
  String  arguments;
  uint8_t arg = 3;

  command = parseString(string, 1);

  if (equals(command, F("touch"))) {
    arguments.reserve(24);
    subcommand = parseString(string, 2);
    # ifdef TOUCH_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      addLog(LOG_LEVEL_DEBUG, strformat(F("TOUCH PLUGIN_WRITE arguments Par1: %d, 2: %d, 3: %d, 4: %d, string: %s"),
                                        event->Par1, event->Par2, event->Par3, event->Par4, string.c_str()));
    }
    # endif // ifdef TOUCH_DEBUG

    if (equals(subcommand, F("enable"))) { // touch,enable,<objectName>[,...] : Enable disabled objectname(s)
      arguments = parseString(string, arg);

      while (!arguments.isEmpty()) {
        success  |= setTouchObjectState(event, arguments, true);
        arguments = parseString(string, ++arg);
      }
    } else if (equals(subcommand, F("disable"))) { // touch,disable,<objectName>[,...] : Disable enabled objectname(s)
      arguments = parseString(string, arg);

      while (!arguments.isEmpty()) {
        success  |= setTouchObjectState(event, arguments, false);
        arguments = parseString(string, ++arg);
      }
    } else if (equals(subcommand, F("on"))) { // touch,on,<buttonObjectName>[,...] : Switch TouchButton(s) on
      arguments = parseString(string, arg);

      while (!arguments.isEmpty()) {
        success  |= setTouchButtonOnOff(event, arguments, true);
        arguments = parseString(string, ++arg);
      }
    } else if (equals(subcommand, F("off"))) { // touch,off,<buttonObjectName>[,...] : Switch TouchButton(s) off
      arguments = parseString(string, arg);

      while (!arguments.isEmpty()) {
        success  |= setTouchButtonOnOff(event, arguments, false);
        arguments = parseString(string, ++arg);
      }
    } else if (equals(subcommand, F("toggle"))) { // touch,toggle,<buttonObjectName>[,...] : Switch TouchButton(s) to the other state
      arguments = parseString(string, arg);

      while (!arguments.isEmpty()) {
        const int16_t state = getTouchObjectValue(event, arguments);

        if (state > -1) {
          success |= setTouchButtonOnOff(event, arguments, state == 0);
        }
        arguments = parseString(string, ++arg);
      }
    } else if (equals(subcommand, F("set"))) { // touch,set,<objectName>,<value> : Set TouchObject value
      arguments = parseString(string, arg);
      success   = setTouchObjectValue(event, arguments, event->Par3);
    # if TOUCH_FEATURE_EXTENDED_TOUCH
    #  if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE
    } else if (equals(subcommand, F("swipe"))) {        // touch,swipe,<swipeValue> : Switch button group via swipe value
      success = handleButtonSwipe(event, event->Par2);
    #  endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE
    } else if (equals(subcommand, F("setgrp"))) {       // touch,setgrp,<group> : Activate button group
      success = setButtonGroup(event, event->Par2);
    } else if (equals(subcommand, F("nextgrp"))) {      // touch,nextgrp : next group and Activate
      success = nextButtonGroup(event);
    } else if (equals(subcommand, F("prevgrp"))) {      // touch,prevgrp : previous group and Activate
      success = prevButtonGroup(event);
    } else if (equals(subcommand, F("nextpage"))) {     // touch,nextpage : next page and Activate
      success = nextButtonPage(event);
    } else if (equals(subcommand, F("prevpage"))) {     // touch,prevpage : previous page and Activate
      success = prevButtonPage(event);
    } else if (equals(subcommand, F("updatebutton"))) { // touch,updatebutton,<buttonnr>[,<group>[,<mode>]] : Update a button
      arguments = parseString(string, 3);

      // Check for a valid button name or number, returns a 0-based index
      const int8_t index = getTouchObjectIndex(event, arguments, true);

      if (index > -1) {
        const bool hasPar3 = !parseString(string, 4).isEmpty();
        const bool hasPar4 = !parseString(string, 5).isEmpty();

        if (hasPar4) {
          success = displayButton(event, index, event->Par3, event->Par4);
        } else if (hasPar3) {
          success = displayButton(event, index, event->Par3);
        } else {
          success = displayButton(event, index); // Use default argument values
        }
      }
    # endif // if TOUCH_FEATURE_EXTENDED_TOUCH
    }
  }
  return success;
}

/**
 * Handle getting config values from plugin/handler
 */
bool ESPEasy_TouchHandler::plugin_get_config_value(struct EventStruct *event,
                                                   String            & string) {
  bool success         = false;
  const String command = parseString(string, 1);

  if (equals(command, F("buttongroup"))) {
    string  = getButtonGroup();
    success = true;
  # if TOUCH_FEATURE_EXTENDED_TOUCH
  } else if (equals(command, F("hasgroup"))) {
    int32_t group; // We'll be ignoring group 0 if there are multiple button groups

    if (validIntFromString(parseString(string, 2), group)) {
      string  = validButtonGroup(group, true) ? 1 : 0;
      success = true;
    } else {
      string = '0'; // invalid number = false
    }
  # endif // if TOUCH_FEATURE_EXTENDED_TOUCH
  } else if (equals(command, F("enabled"))) {
    const int8_t enabled = getTouchObjectState(event, parseStringKeepCase(string, 2));

    if (enabled > -1) {
      string  = enabled;
      success = true;
    }
  } else if (equals(command, F("state"))) {
    const int16_t state = getTouchObjectValue(event, parseStringKeepCase(string, 2));

    string  = state;
    success = true;
  # if TOUCH_FEATURE_EXTENDED_TOUCH
  } else if (equals(command, F("pagemode"))) {
    string  = bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU);
    success = true;
  #  if TOUCH_FEATURE_SWIPE
  } else if (equals(command, F("swipedir"))) {
    int32_t state;

    if (validIntFromString(parseString(string, 2), state)) {
      string  = toString(static_cast<Swipe_action_e>(state));
      success = true;
    }
  #  endif // if TOUCH_FEATURE_SWIPE
  # endif // if TOUCH_FEATURE_EXTENDED_TOUCH
  }
  return success;
}

/**
 * generate an event for a touch object
 * When a display is configured add x,y coordinate, width,height of the object, objectIndex, and TaskIndex of display
 **************************************************************************/
void ESPEasy_TouchHandler::generateObjectEvent(struct EventStruct *event,
                                               const int8_t      & objectIndex,
                                               const int16_t     & onOffState,
                                               const int8_t      & mode,
                                               const bool        & groupSwitch,
                                               const int8_t      & factor) {
  if ((objectIndex < 0) || // Range check
      (objectIndex >= static_cast<int8_t>(TouchObjects.size()))) {
    return;
  }
  delay(0);
  String eventCommand;
  String extraCommand;

  eventCommand.reserve(120);
  extraCommand.reserve(48);

  // Task with added arguments: (%eventvalue#%)
  extraCommand += strformat(F("%s#%s="), getTaskDeviceName(event->TaskIndex).c_str(), TouchObjects[objectIndex].objectName.c_str());

  if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_DRAWBTN_VIA_RULES)) {
    eventCommand = extraCommand;
  } else {                                  // Handle via direct btn commands
    if (_displayTask != event->TaskIndex) { // Add arguments for display
      // Internal command trigger
      eventCommand += strformat(F("[%d].adagfx_trigger,btn,"), _displayTask + 1);
    } else {
      addLog(LOG_LEVEL_ERROR, F("TOUCH: No valid Display task selected."));
      return;
    }
  }

  if (bitRead(TouchObjects[objectIndex].flags, TOUCH_OBJECT_FLAG_SLIDER)) {
    eventCommand += onOffState;                      // Slider control: pass value as state (1 = state)
    extraCommand += onOffState;                      // duplicate
  } else {
    if (onOffState < 0) {                            // Negative value: pass on unaltered (1 = state)
      eventCommand += onOffState;
      extraCommand += onOffState;                    // duplicate
    } else {                                         // Check for inverted output (1 = state)
      if (bitRead(TouchObjects[objectIndex].flags, TOUCH_OBJECT_FLAG_INVERTED)) {
        eventCommand += onOffState == 1 ? '0' : '1'; // Act like an inverted button, 0 = On, 1 = Off
        extraCommand += onOffState == 1 ? '0' : '1'; // Act like an inverted button, 0 = On, 1 = Off // duplicate
      } else {
        eventCommand += onOffState == 1 ? '1' : '0'; // Act like a button, 1 = On, 0 = Off
        extraCommand += onOffState == 1 ? '1' : '0'; // Act like a button, 1 = On, 0 = Off // duplicate
      }
    }
  }
  eventCommand += ',';
  eventCommand += mode;                         // (2 = mode)
  extraCommand += ',';
  extraCommand += mode;                         // (2 = mode) // duplicate

  if (_displayTask != event->TaskIndex) {       // Add arguments for display
    eventCommand += strformat(
      F(",%d,%d,%d,%d,%d,%d"),
      TouchObjects[objectIndex].top_left.x,     // (3 = x)
      TouchObjects[objectIndex].top_left.y,     // (4 = y)
      TouchObjects[objectIndex].width_height.x, // (5 = width)
      TouchObjects[objectIndex].width_height.y, // (6 = height)
      objectIndex + 1,                          // Adjust to displayed index (7 = id)
                                                // (8 = type + layout, 4+4 bit, side by side)
      get8BitFromUL(TouchObjects[objectIndex].flags, TOUCH_OBJECT_FLAG_BUTTONTYPE) * factor);
    # if TOUCH_FEATURE_EXTENDED_TOUCH
    eventCommand += strformat(
      F(",%s,%s,%s,%d,"),
      AdaGFXcolorToString(TouchObjects[objectIndex].colorOn == 0
                          ? Touch_Settings.colorOn
                          : TouchObjects[objectIndex].colorOn, _colorDepth).c_str(),      // (9 = ON color)
      AdaGFXcolorToString(TouchObjects[objectIndex].colorOff == 0
                          ? Touch_Settings.colorOff
                          : TouchObjects[objectIndex].colorOff, _colorDepth).c_str(),     // (10 = OFF color)
      AdaGFXcolorToString(TouchObjects[objectIndex].colorCaption == 0
                          ? Touch_Settings.colorCaption
                          : TouchObjects[objectIndex].colorCaption, _colorDepth).c_str(), // (11 = Caption color)
      get4BitFromUL(TouchObjects[objectIndex].flags, TOUCH_OBJECT_FLAG_FONTSCALE));       // (12 = Font scaling)

    // (13 = ON caption, default=object name)
    String _capt;

    if (TouchObjects[objectIndex].captionOn.isEmpty()) {
      if (bitRead(TouchObjects[objectIndex].flags, TOUCH_OBJECT_FLAG_SLIDER)) {
        _capt = onOffState; // Override caption if not set
      } else {
        _capt = TouchObjects[objectIndex].objectName;
      }
    } else {
      _capt = TouchObjects[objectIndex].captionOn;
    }
    _capt.replace('_', ' '); // Replace all '_' by space
    eventCommand += wrapWithQuotesIfContainsParameterSeparatorChar(_capt);
    eventCommand += ',';     // (14 = OFF caption)

    if (TouchObjects[objectIndex].captionOff.isEmpty()) {
      if (bitRead(TouchObjects[objectIndex].flags, TOUCH_OBJECT_FLAG_SLIDER)) {
        _capt = onOffState; // override caption if not set
      } else {
        _capt = TouchObjects[objectIndex].objectName;
      }
    } else {
      _capt = TouchObjects[objectIndex].captionOff;
    }
    _capt.replace('_', ' '); // Replace all '_' by space
    eventCommand += strformat(
      F("%s,%s,%s,%s"),
      wrapWithQuotesIfContainsParameterSeparatorChar(_capt).c_str(),
      AdaGFXcolorToString(TouchObjects[objectIndex].colorBorder == 0
                          ? Touch_Settings.colorBorder
                          : TouchObjects[objectIndex].colorBorder, _colorDepth).c_str(),           // (15 = Border color)
      AdaGFXcolorToString(TouchObjects[objectIndex].colorDisabled == 0
                          ? Touch_Settings.colorDisabled
                          : TouchObjects[objectIndex].colorDisabled, _colorDepth).c_str(),         // (16 = Disabled color)
      AdaGFXcolorToString(TouchObjects[objectIndex].colorDisabledCaption == 0
                          ? Touch_Settings.colorDisabledCaption
                          : TouchObjects[objectIndex].colorDisabledCaption, _colorDepth).c_str()); // (17 = Disabled caption color)
    # endif // if TOUCH_FEATURE_EXTENDED_TOUCH
    eventCommand += strformat(
      F(",%d,%d"),
      _displayTask + 1,                                                         // What TaskIndex? (18) or (9)
      get8BitFromUL(TouchObjects[objectIndex].flags, TOUCH_OBJECT_FLAG_GROUP)); // Group (19) or (10)
    # if TOUCH_FEATURE_EXTENDED_TOUCH
    eventCommand += ',';                                                        // Group mode (20)
    const uint8_t action        = get4BitFromUL(TouchObjects[objectIndex].groupFlags, TOUCH_OBJECT_GROUP_ACTION);
    const Touch_action_e actGrp = static_cast<Touch_action_e>(action);

    if (!groupSwitch && (Touch_action_e::Default != actGrp)) {
      if (Touch_action_e::ActivateGroup == actGrp) {
        eventCommand += get8BitFromUL(TouchObjects[objectIndex].groupFlags, TOUCH_OBJECT_GROUP_ACTIONGROUP);
      } else {
        eventCommand += (action * -1); // Default is already ignored
      }
    } else {
      eventCommand += -1;              // No group to activate
    }
    # endif // if TOUCH_FEATURE_EXTENDED_TOUCH
  }

  # if TOUCH_FEATURE_EXTENDED_TOUCH

  if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_DRAWBTN_VIA_RULES)) {
    eventQueue.addMove(std::move(eventCommand));
  } else {
    eventCommand += ',';
    eventCommand += wrapWithQuotesIfContainsParameterSeparatorChar(TouchObjects[objectIndex].objectName);
    ExecuteCommand_all({ EventValueSource::Enum::VALUE_SOURCE_RULES, eventCommand }, true); // Simulate like from rules
    addLogMove(LOG_LEVEL_INFO, eventCommand);
    delay(0);

    // Handle group actions
    Touch_action_e action = static_cast<Touch_action_e>(get4BitFromUL(TouchObjects[objectIndex].groupFlags, TOUCH_OBJECT_GROUP_ACTION));

    if ((onOffState >= 0) && (mode >= 0)) {
      if (action == Touch_action_e::Default) {
        eventQueue.addMove(std::move(extraCommand)); // Issue the extra command for regular button presses
      } else {
        switch (action) {
          case Touch_action_e::ActivateGroup:
            setButtonGroup(event, get8BitFromUL(TouchObjects[objectIndex].groupFlags, TOUCH_OBJECT_GROUP_ACTIONGROUP));
            break;
          case Touch_action_e::IncrementGroup:
            nextButtonGroup(event);
            break;
          case Touch_action_e::DecrementGroup:
            prevButtonGroup(event);
            break;
          case Touch_action_e::IncrementPage:
            nextButtonPage(event);
            break;
          case Touch_action_e::DecrementPage:
            prevButtonPage(event);
            break;
          case Touch_action_e::Default:
            break;
        }
        addLogMove(LOG_LEVEL_INFO, concat(F("TOUCH event: "), toString(action)));
      }
    }
  }
  # endif // if TOUCH_FEATURE_EXTENDED_TOUCH
  delay(0);
}

#endif // ifdef PLUGIN_USES_TOUCHHANDLER

#include "../Helpers/Hardware_temperature_sensor.h"


#if FEATURE_INTERNAL_TEMPERATURE

# include "../Helpers/StringConverter.h"

/**
 * Code based on:
 * https://github.com/esphome/esphome/blob/518ecb4cc4489c8a76b899bfda7576b05d84c226/esphome/components/internal_temperature/internal_temperature.cpp#L40
 */

# ifdef ESP32
#  if defined(ESP32_CLASSIC)

// there is no official API available on the original ESP32
extern "C" {
uint8_t temprature_sens_read();
}
#  elif defined(ESP32C2) || defined(ESP32C3) || defined(ESP32C6) || defined(ESP32S2) || defined(ESP32S3)
#   if ESP_IDF_VERSION_MAJOR < 5
#    include <driver/temp_sensor.h>

// Work-around for bug in ESP-IDF < 5.0
#    if defined(ESP32S3) || defined(ESP32C3)
#     include <esp_efuse_rtc_calib.h>
#    elif defined(ESP32S2)
#     include <esp_efuse_rtc_table.h>
#    endif // if defined(ESP32S3) || defined(ESP32C3)
#   else // if ESP_IDF_VERSION_MAJOR < 5
#    include <driver/temperature_sensor.h>
#   endif  // if ESP_IDF_VERSION_MAJOR < 5
#  endif   // ESP32_CLASSIC
# endif    // ESP32


# ifdef ESP32
#  if defined(ESP32_CLASSIC)


esp_err_t do_read_internal_temperature(float& celsius) {
  esp_err_t result  = ESP_FAIL;
  uint8_t   raw     = 128u;
  int8_t    retries = 2;

  while ((128u == raw) && (0 != retries)) {
    delay(0);
    raw = temprature_sens_read(); // Each reading takes about 112 microseconds
    --retries;
  }
#   ifndef BUILD_NO_DEBUG

  //  addLog(LOG_LEVEL_DEBUG, concat(F("ESP32: Raw temperature value: "), raw));
#   endif // ifndef BUILD_NO_DEBUG

  if (raw != 128) {
    result = ESP_OK;

    // Raw value is in Fahrenheit
    celsius = (raw - 32) / 1.8f;
  }
  return result;
}

#  elif defined(ESP32C2) || defined(ESP32C3) || defined(ESP32C6) || defined(ESP32S2) || defined(ESP32S3)

esp_err_t do_read_internal_temperature(float& celsius) {
  esp_err_t result = ESP_FAIL;

  celsius = 0.0f; // Make sure it is initialized and within the default range.

#   if ESP_IDF_VERSION_MAJOR < 5

  temp_sensor_config_t tsens = TSENS_CONFIG_DEFAULT();

  temp_sensor_set_config(tsens);
  temp_sensor_start();

  // Work-around for bug in ESP-IDF < 5.0
  // Seems to be fixed in ESP_IDF5.1
  // temp_sensor_get_config always returns ESP_OK
  // Thus dac_offset can be just about anything
  // dac_offset is used as index in an array without bounds checking
  {
#    if defined(ESP32S3) || defined(ESP32C2) || defined(ESP32C3) || defined(ESP32C6)
    static float s_deltaT = (esp_efuse_rtc_calib_get_ver() == 1) ?
                            (esp_efuse_rtc_calib_get_cal_temp(1) / 10.0f) :
                            0.0f;
#    elif defined(ESP32S2)
    static uint32_t version  = esp_efuse_rtc_table_read_calib_version();
    static float    s_deltaT = (version == 1 || version == 2) ?
                               (esp_efuse_rtc_table_get_parsed_efuse_value(RTCCALIB_IDX_TMPSENSOR, false) / 10.0f) :
                               0.0f;
#    endif // if defined(ESP32S3) || defined(ESP32C3)


    /*
        if (isnan(s_deltaT)) { //suggests that the value is not initialized
          uint32_t version = esp_efuse_rtc_calib_get_ver();
          if (version == 1) {
              // fetch calibration value for temp sensor from eFuse
              s_deltaT = esp_efuse_rtc_calib_get_cal_temp(version);
          } else {
              // no value to fetch, use 0.
              s_deltaT = 0;
          }
        }
     */
#    ifndef TSENS_ADC_FACTOR
#     define TSENS_ADC_FACTOR  (0.4386)
#    endif // ifndef TSENS_ADC_FACTOR
#    ifndef TSENS_DAC_FACTOR
#     define TSENS_DAC_FACTOR  (27.88)
#    endif // ifndef TSENS_DAC_FACTOR
#    ifndef TSENS_SYS_OFFSET
#     define TSENS_SYS_OFFSET  (20.52)
#    endif // ifndef TSENS_SYS_OFFSET
    uint32_t tsens_raw{};
    temp_sensor_read_raw(&tsens_raw);
    celsius = (TSENS_ADC_FACTOR * tsens_raw) - s_deltaT - TSENS_SYS_OFFSET;
    result  = ESP_OK;
  }

#   else // if ESP_IDF_VERSION_MAJOR < 5

  //  result = temp_sensor_read_celsius(&celsius);


  static temperature_sensor_handle_t temp_sensor = nullptr;

  // Use range which seems to have the smallest error
  // See: https://docs.espressif.com/projects/esp-idf/en/stable/esp32c3/api-reference/peripherals/temp_sensor.html
  static int range_min = -10;
  static int range_max = 80;

  bool must_reinstall = false;

  if (temp_sensor == nullptr) {
    temperature_sensor_config_t temp_sensor_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(range_min, range_max);
    result = temperature_sensor_install(&temp_sensor_config, &temp_sensor);
  } else {
    result = ESP_OK;
  }

  if (ESP_OK == result) {
    result = temperature_sensor_enable(temp_sensor);

    if (result == ESP_ERR_INVALID_STATE) {
      // Sensor reports to be not enabled
      must_reinstall = true;
    }
  }

  if (ESP_OK == result) {
    result = temperature_sensor_get_celsius(temp_sensor, &celsius);

    if (result == ESP_FAIL) {
      must_reinstall = true;
    }

    // FIXME TD-er: What to do when result == ESP_FAIL (can be indication of out-of-range)
    if (celsius < (range_min + 10)) {
      range_min -= 10;

      if (range_min > celsius) {
        range_min = celsius - 10;
      }
      must_reinstall = true;
    }

    if (celsius > (range_max - 10)) {
      range_max += 10;

      if (range_max < celsius) {
        range_max = celsius + 10;
      }
      must_reinstall = true;
    }
  }

  temperature_sensor_disable(temp_sensor);


  if (must_reinstall) {
    temperature_sensor_uninstall(temp_sensor);
    temp_sensor = nullptr;
  }


#   endif // if ESP_IDF_VERSION_MAJOR < 5

  return result;
}

#  endif // if defined(ESP32_CLASSIC)
# endif  // ifdef ESP32


bool getInternalTemperature(float& temperatureCelsius) {
  static float temperature_filtered = NAN; // Improbable value
  float celsius{};
  esp_err_t result = do_read_internal_temperature(celsius);

  if (ESP_OK == result) {
    if (isnanf(temperature_filtered)) {
      temperature_filtered = celsius;
    } else {
      constexpr float IIR_FACTOR  = 5.0f;
      constexpr float IIR_DIVIDER = IIR_FACTOR + 1.0f;
      temperature_filtered = ((IIR_FACTOR * temperature_filtered) + celsius) / IIR_DIVIDER;
    }
  }
  temperatureCelsius = temperature_filtered;
  return ESP_OK == result;
}

float getInternalTemperature() {
  float temperatureCelsius{};

  getInternalTemperature(temperatureCelsius);
  return temperatureCelsius;
}

#endif // if FEATURE_INTERNAL_TEMPERATURE

#include "../Helpers/Hardware_device_info.h"

#ifdef ESP32C61


const __FlashStringHelper* getChipModel(uint32_t chip_model, uint32_t chip_revision, uint32_t pkg_version, bool single_core)
{
  if (20 == chip_model) { // ESP32-C61
    return F("ESP32-C61");
  }

  return F("Unknown");
}

#endif // ifdef ESP32C61

#include "../Helpers/Audio.h"

#include "../DataStructs/TimingStats.h"
#include "../ESPEasyCore/ESPEasyGPIO.h"
#include "../Globals/RamTracker.h"
#include "../Helpers/Hardware_GPIO.h"
#include "../Helpers/Hardware_PWM.h"
#include "../Helpers/StringConverter.h"


/********************************************************************************************\
   Generate a tone of specified frequency on pin
 \*********************************************************************************************/
bool tone_espEasy(int8_t _pin, unsigned int frequency, unsigned long duration) {
  if (!validGpio(_pin)) { return false; }

  // Duty cycle can be used as some kind of volume.
  if (!set_Gpio_PWM_pct(_pin, 50, frequency)) { return false; }

  if (duration > 0) {
    delay(duration);
    return set_Gpio_PWM(_pin, 0, frequency);
  }
  return true;
}

/********************************************************************************************\
   Play RTTTL string on specified pin
 \*********************************************************************************************/
#if FEATURE_RTTTL
# if FEATURE_ANYRTTTL_LIB
#  include <anyrtttl.h>
#  include <pitches.h>
#  if FEATURE_RTTTL_EVENTS
#   include "../Globals/EventQueue.h"
#   include "../Globals/Settings.h"
static bool rtttlPlaying = false;
#  endif // if FEATURE_RTTTL_EVENTS
#  if FEATURE_ANYRTTTL_ASYNC
static String rtttlMelody;

void clear_rtttl_melody() {
  // The non-blocking play will read from a char pointer.
  // So we must stop the playing before changing the string as it could otherwise lead to a crash.
  if (anyrtttl::nonblocking::isPlaying()) { // If currently playing, cancel that
    addLog(LOG_LEVEL_INFO, F("RTTTL: Cancelling running song..."));
    anyrtttl::nonblocking::stop();
    #   if FEATURE_RTTTL_EVENTS

    if (Settings.UseRules) {
      eventQueue.add(F("RTTTL#Cancelled"));
    }
    rtttlPlaying = false;
    #   endif // if FEATURE_RTTTL_EVENTS
  }

  free_string(rtttlMelody);
}

void set_rtttl_melody(String& melody) {
  clear_rtttl_melody();
  rtttlMelody = melody;
}

#  endif // if FEATURE_ANYRTTTL_ASYNC

#  ifdef ESP32

void esp32NoTone(uint8_t pin) {
  ledcWrite(pin, 0);
}

void esp32Tone(uint8_t pin, unsigned int frq, unsigned long duration) {
  ledcWriteTone(pin, frq); // pin, freq
}

void esp32ToneSetup(uint8_t pin) {
  const uint8_t channel = attachLedChannel(pin); // Use defaults

  setToneChannel(channel);                       // Claim channel for Tone use
}

static uint8_t rtttlPin = 255;
#  endif // ifdef ESP32

bool play_rtttl(int8_t _pin, const char *p) {
  if (!validGpio(_pin)) { return false; }

  // addLog(LOG_LEVEL_INFO, F("RTTTL: Using AnyRtttl"));

  #  ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("play_rtttl"));
  #  endif // ifndef BUILD_NO_RAM_TRACKER

  #  ifdef ESP32

  esp32ToneSetup(_pin);
  rtttlPin = _pin;

  anyrtttl::setToneFunction(&esp32Tone);
  anyrtttl::setNoToneFunction(&esp32NoTone);
  #  endif // ifdef ESP32
  #  ifdef ESP8266
  anyrtttl::setNoToneFunction(&setInternalGPIOPullupMode);
  #  endif // ifdef ESP8266
  #  if FEATURE_ANYRTTTL_ASYNC

  if (!rtttlMelody.isEmpty()) {
    anyrtttl::nonblocking::begin(_pin, rtttlMelody.c_str());
  } else {
    anyrtttl::nonblocking::begin(_pin, p);
  }
  anyrtttl::nonblocking::play();
  #   if FEATURE_RTTTL_EVENTS

  if (Settings.UseRules) {
    eventQueue.add(F("RTTTL#Started"));
  }
  rtttlPlaying = true;
  #   endif // if FEATURE_RTTTL_EVENTS
  #  else // if FEATURE_ANYRTTTL_ASYNC
  anyrtttl::blocking::play(_pin, p);
  #   ifdef ESP32
  esp32NoTone(_pin);
  detachLedChannel(_pin);
  setInternalGPIOPullupMode(_pin);
  #   endif // ifdef ESP32
  #  endif // if FEATURE_ANYRTTTL_ASYNC
  #  ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("play_rtttl2"));
  #  endif // ifndef BUILD_NO_RAM_TRACKER
  return true;
}

#  if FEATURE_ANYRTTTL_ASYNC
void update_rtttl() {
  START_TIMER

  if (anyrtttl::nonblocking::isPlaying()) {
    anyrtttl::nonblocking::play();
  } else {
    #   ifdef ESP32
    esp32NoTone(rtttlPin);
    detachLedChannel(rtttlPin);
    setInternalGPIOPullupMode(rtttlPin);
    #   endif // ifdef ESP32
    #   if FEATURE_RTTTL_EVENTS

    if (rtttlPlaying) {
      if (Settings.UseRules) {
        eventQueue.add(F("RTTTL#Finished"));
      }
      rtttlPlaying = false;
    }
    #   endif // if FEATURE_RTTTL_EVENTS
    clear_rtttl_melody(); // Release memory
  }
  STOP_TIMER(UPDATE_RTTTL);
}

#  endif // if FEATURE_ANYRTTTL_ASYNC

# else // if FEATURE_ANYRTTTL_LIB
bool play_rtttl(int8_t _pin, const char *p)
{
  if (!validGpio(_pin)) { return false; }

  #  ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("play_rtttl"));
  #  endif // ifndef BUILD_NO_RAM_TRACKER
  #  define OCTAVE_OFFSET 0

  // FIXME: Absolutely no error checking in here

  const int notes[] = { 0,
                        262, 277,   294,   311,   330,   349,   370,   392,   415,   440,   466,   494,
                        523, 554,   587,   622,   659,   698,   740,   784,   831,   880,   932,   988,
                        1047,1109,  1175,  1245,  1319,  1397,  1480,  1568,  1661,  1760,  1865,  1976,
                        2093,2217,  2349,  2489,  2637,  2794,  2960,  3136,  3322,  3520,  3729,  3951
  };


  uint8_t default_dur = 4;
  uint8_t default_oct = 6;
  int     bpm         = 63;
  int     num;
  long    wholenote;
  long    duration;
  uint8_t note;
  uint8_t scale;

  // format: d=N,o=N,b=NNN:
  // find the start (skip name, etc)

  while (*p != ':') {
    p++; // ignore name

    if (*p == 0) { return false; }
  }
  p++; // skip ':'

  // get default duration
  if (*p == 'd')
  {
    p++; p++; // skip "d="
    num = 0;

    while (isdigit(*p))
    {
      num = (num * 10) + (*p++ - '0');
    }

    if (num > 0) { default_dur = num; }
    p++; // skip comma
  }

  // get default octave
  if (*p == 'o')
  {
    p++; p++; // skip "o="
    num = *p++ - '0';

    if ((num >= 3) && (num <= 7)) { default_oct = num; }
    p++; // skip comma
  }

  // get BPM
  if (*p == 'b')
  {
    p++; p++; // skip "b="
    num = 0;

    while (isdigit(*p))
    {
      num = (num * 10) + (*p++ - '0');
    }
    bpm = num;
    p++; // skip colon
  }

  // BPM usually expresses the number of quarter notes per minute
  wholenote = (60 * 1000L / bpm) * 4; // this is the time for whole note (in milliseconds)

  // now begin note loop
  while (*p)
  {
    // first, get note duration, if available
    num = 0;

    while (isdigit(*p))
    {
      num = (num * 10) + (*p++ - '0');
    }

    if (num) { duration = wholenote / num; }
    else { duration = wholenote / default_dur; // we will need to check if we are a dotted note after
    }

    // now get the note
    switch (*p)
    {
      case 'c':
        note = 1;
        break;
      case 'd':
        note = 3;
        break;
      case 'e':
        note = 5;
        break;
      case 'f':
        note = 6;
        break;
      case 'g':
        note = 8;
        break;
      case 'a':
        note = 10;
        break;
      case 'b':
        note = 12;
        break;
      case 'p':
      default:
        note = 0;
    }
    p++;

    // now, get optional '#' sharp
    if (*p == '#')
    {
      note++;
      p++;
    }

    // now, get optional '.' dotted note
    if (*p == '.')
    {
      duration += duration / 2;
      p++;
    }

    // now, get scale
    if (isdigit(*p))
    {
      scale = *p - '0';
      p++;
    }
    else
    {
      scale = default_oct;
    }

    scale += OCTAVE_OFFSET;

    if (*p == ',') {
      p++; // skip comma for next note (or we may be at the end)
    }

    // now play the note
    if (note)
    {
      if (!tone_espEasy(_pin, notes[(scale - 4) * 12 + note], duration)) {
        return false;
      }
    }
    else
    {
      delay(duration / 10);
    }
  }
  setInternalGPIOPullupMode(_pin); // Turn off sound, Arduino _noTone() doesn't do that reliably
  #  ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("play_rtttl2"));
  #  endif // ifndef BUILD_NO_RAM_TRACKER
  return true;
}

# endif // if FEATURE_ANYRTTTL_LIB
#endif // if FEATURE_RTTTL

#include "../Helpers/CRC_functions.h"


int calc_CRC16(const String& text) {
  return calc_CRC16(text.c_str(), text.length());
}

int calc_CRC16(const char *ptr, int count)
{
  int crc;

  crc = 0;

  if (ptr == nullptr) {
    return crc;
  }

  while (--count >= 0)
  {
    crc = crc ^ static_cast<int>(*ptr++) << 8;
    char i = 8;

    do
    {
      if (crc & 0x8000) {
        crc = crc << 1 ^ 0x1021;
      }
      else {
        crc = crc << 1;
      }
    } while (--i);
  }
  return crc;
}

uint32_t calc_CRC32(const uint8_t *data, size_t length) {
  uint32_t crc = 0xffffffff;

  if (data != nullptr) {
    while (length--) {
      uint8_t c = *data++;

      for (uint32_t i = 0x80; i > 0; i >>= 1) {
        bool bit = crc & 0x80000000;

        if (c & i) {
          bit = !bit;
        }
        crc <<= 1;

        if (bit) {
          crc ^= 0x04c11db7;
        }
      }
    }
  }
  return crc;
}

uint8_t calc_CRC8(const uint8_t *data, size_t length)
{
  /*
   *	Name           : CRC-8
   * Polynomial     : 0x31 (x8 + x5 + x4 + 1)
   * Initialization : 0xFF
   * Reflect input  : False
   * Reflect output : False
   * Final          : XOR 0x00
   *	Example        : calc_CRC8( *(0xBE, 0xEF)) == 0x92 should be true
   */
  uint8_t crc = 0xFF;

  // for (uint8_t bytenr = 0; bytenr < 2; ++bytenr) {
  if (data != nullptr) {
    while (length--) {
      uint8_t c = *data++;
      crc ^= c;

      for (uint8_t i = 0; i < 8; ++i) {
        crc = crc & 0x80 ? (crc << 1) ^ 0x31 : crc << 1;
      }
    }
  }
  return crc;
}

bool calc_CRC8(uint8_t MSB, uint8_t LSB, uint8_t CRC)
{
  /*
   *	Name           : CRC-8
   * Polynomial     : 0x31 (x8 + x5 + x4 + 1)
   * Initialization : 0xFF
   * Reflect input  : False
   * Reflect output : False
   * Final          : XOR 0x00
   *	Example        : CRC8( 0xBE, 0xEF, 0x92) should be true
   */
  uint8_t crc = 0xFF;

  for (uint8_t bytenr = 0; bytenr < 2; ++bytenr) {
    crc ^= (bytenr == 0) ? MSB : LSB;

    for (uint8_t i = 0; i < 8; ++i) {
      crc = crc & 0x80 ? (crc << 1) ^ 0x31 : crc << 1;
    }
  }
  return crc == CRC;
}

#include "../Helpers/ESPEasy_FactoryDefault.h"

#include "../../ESPEasy_common.h"
#include "../../_Plugin_Helper.h"

#include "../CustomBuild/CompiletimeDefines.h"
#include "../CustomBuild/StorageLayout.h"

#include "../DataStructs/ControllerSettingsStruct.h"
#include "../DataStructs/FactoryDefaultPref.h"
#include "../DataStructs/GpioFactorySettingsStruct.h"

#include "../ESPEasyCore/ESPEasy_backgroundtasks.h"
#include "../ESPEasyCore/ESPEasyWifi.h"
#include "../ESPEasyCore/Serial.h"

#include "../Globals/ESPEasyWiFiEvent.h"
#include "../Globals/RTC.h"
#include "../Globals/ResetFactoryDefaultPref.h"
#include "../Globals/SecuritySettings.h"

#include "../Helpers/_CPlugin_Helper.h"
#include "../Helpers/ESPEasyRTC.h"
#include "../Helpers/FS_Helper.h"
#include "../Helpers/Misc.h"

#ifdef ESP32

// Store in NVS partition
# include "../Helpers/ESPEasy_NVS_Helper.h"


// Max. 15 char namespace for ESPEasy Factory Default settings
# define FACTORY_DEFAULT_NVS_NAMESPACE      "ESPEasyFacDef"

# include "../Helpers/StringConverter.h"
# include "../DataStructs/FactoryDefaultPref.h"
# include "../DataStructs/FactoryDefault_UnitName_NVS.h"
# include "../DataStructs/FactoryDefault_WiFi_NVS.h"
# include "../DataStructs/FactoryDefault_Network_NVS.h"
# include "../DataStructs/FactoryDefault_LogConsoleSettings_NVS.h"
# if FEATURE_ALTERNATIVE_CDN_URL
#  include "../DataStructs/FactoryDefault_CDN_customurl_NVS.h"
# endif // if FEATURE_ALTERNATIVE_CDN_URL


#endif // ifdef ESP32


/********************************************************************************************\
   Reset all settings to factory defaults
 \*********************************************************************************************/
void ResetFactory(bool formatFS)
{
  bool mustApplySafebootDefaults = false;

  #ifdef ESP32
  ESPEasy_NVS_Helper preferences;

  if (!ResetFactoryDefaultPreference.init(preferences)) {
    mustApplySafebootDefaults = true;
  }
  #endif // ifdef ESP32
  #ifdef ESP8266 // Apply defaults on clean setup
  mustApplySafebootDefaults = 0 == Settings.ResetFactoryDefaultPreference;
  #endif // ifdef ESP8266

  if (ResetFactoryDefaultPreference.getPreference() == 0)
  {
#if FEATURE_CUSTOM_PROVISIONING
    ResetFactoryDefaultPreference.setDeviceModel(static_cast<DeviceModel>(DEFAULT_FACTORY_DEFAULT_DEVICE_MODEL));
    #if DEFAULT_PROVISIONING_FETCH_RULES1
    ResetFactoryDefaultPreference.fetchRulesTXT(0, true);
    #endif
    #if DEFAULT_PROVISIONING_FETCH_RULES2
    ResetFactoryDefaultPreference.fetchRulesTXT(1, true);
    #endif
    #if DEFAULT_PROVISIONING_FETCH_RULES3
    ResetFactoryDefaultPreference.fetchRulesTXT(2, true);
    #endif
    #if DEFAULT_PROVISIONING_FETCH_RULES4
    ResetFactoryDefaultPreference.fetchRulesTXT(3, true);
    #endif
    #if DEFAULT_PROVISIONING_FETCH_NOTIFICATIONS
    ResetFactoryDefaultPreference.fetchNotificationDat(true);
    #endif
    #if DEFAULT_PROVISIONING_FETCH_SECURITY
    ResetFactoryDefaultPreference.fetchSecurityDat(true);
    #endif
    #if DEFAULT_PROVISIONING_FETCH_CONFIG
    ResetFactoryDefaultPreference.fetchConfigDat(true);
    #endif
    #if DEFAULT_PROVISIONING_FETCH_PROVISIONING
    ResetFactoryDefaultPreference.fetchProvisioningDat(true);
    #endif
    #if DEFAULT_PROVISIONING_SAVE_URL
    ResetFactoryDefaultPreference.saveURL(true);
    #endif
    #if DEFAULT_PROVISIONING_SAVE_CREDENTIALS
    ResetFactoryDefaultPreference.storeCredentials(true);
    #endif
  #endif
    #if DEFAULT_FACTORY_RESET_KEEP_UNIT_NAME
    ResetFactoryDefaultPreference.keepUnitName(true);
    #endif
    #if DEFAULT_FACTORY_RESET_KEEP_WIFI
    ResetFactoryDefaultPreference.keepWiFi(true);
    #endif
    #if DEFAULT_FACTORY_RESET_KEEP_NETWORK
    ResetFactoryDefaultPreference.keepNetwork(true);
    #endif
    #if DEFAULT_FACTORY_RESET_KEEP_NTP_DST
    ResetFactoryDefaultPreference.keepNTP(true);
    #endif
    #if DEFAULT_FACTORY_RESET_KEEP_CONSOLE_LOG
    ResetFactoryDefaultPreference.keepLogConsoleSettings(true);
    #endif
#ifdef PLUGIN_BUILD_SAFEBOOT
    mustApplySafebootDefaults = true;
    ResetFactoryDefaultPreference.keepCustomCdnUrl(true);

    Settings.UseLastWiFiFromRTC(true);
#endif // ifdef PLUGIN_BUILD_SAFEBOOT
  }


  const GpioFactorySettingsStruct gpio_settings(ResetFactoryDefaultPreference.getDeviceModel());
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("ResetFactory"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  // Direct Serial is allowed here, since this is only an emergency task.
  serialPrint(F("RESET: Resetting factory defaults... using "));
  serialPrint(getDeviceModelString(ResetFactoryDefaultPreference.getDeviceModel()));
  serialPrintln(F(" settings"));
  process_serialWriteBuffer();
  delay(1000);

  if (readFromRTC())
  {
    serialPrint(F("RESET: Warm boot, reset count: "));
    serialPrintln(String(RTC.factoryResetCounter));

    if (RTC.factoryResetCounter >= 3)
    {
      serialPrintln(F("RESET: Too many resets, protecting your flash memory (powercycle to solve this)"));
      return;
    }
  }
  else
  {
    serialPrintln(F("RESET: Cold boot"));
    initRTC();

    // TODO TD-er: Store set device model in RTC.
  }

  RTC.flashCounter = 0; // reset flashcounter, since we're already counting the number of factory-resets. we dont want to hit a flash-count
                        // limit during reset.
  RTC.factoryResetCounter++;
  saveToRTC();

  if (formatFS) {
    // always format on factory reset, in case of corrupt FS
    ESPEASY_FS.end();
    serialPrintln(F("RESET: formatting..."));
    FS_format();
    serialPrintln(F("RESET: formatting done..."));
    process_serialWriteBuffer();

    if (!ESPEASY_FS.begin())
    {
      serialPrintln(F("RESET: FORMAT FS FAILED!"));
      return;
    }
  }

#if FEATURE_CUSTOM_PROVISIONING
  {
    MakeProvisioningSettings(ProvisioningSettings);

    if (ProvisioningSettings.get()) {
      ProvisioningSettings->setUser(F(DEFAULT_PROVISIONING_USER));
      ProvisioningSettings->setPass(F(DEFAULT_PROVISIONING_PASS));
      ProvisioningSettings->setUrl(F(DEFAULT_PROVISIONING_URL));
      ProvisioningSettings->ResetFactoryDefaultPreference = ResetFactoryDefaultPreference.getPreference();
      saveProvisioningSettings(*ProvisioningSettings);
    }
  }
#endif // if FEATURE_CUSTOM_PROVISIONING

  // pad files with extra zeros for future extensions
  InitFile(SettingsType::SettingsFileEnum::FILE_CONFIG_type);
  InitFile(SettingsType::SettingsFileEnum::FILE_SECURITY_type);
  #if FEATURE_NOTIFIER
  InitFile(SettingsType::SettingsFileEnum::FILE_NOTIFICATION_type);
  #endif // if FEATURE_NOTIFIER

  InitFile(getRulesFileName(0), 0);

  Settings.clearMisc();

  if (!ResetFactoryDefaultPreference.keepNTP() || mustApplySafebootDefaults) {
    Settings.clearTimeSettings();
    Settings.UseNTP(DEFAULT_USE_NTP);
    strcpy_P(Settings.NTPHost, PSTR(DEFAULT_NTP_HOST));
    Settings.TimeZone = DEFAULT_TIME_ZONE;
    Settings.DST      = DEFAULT_USE_DST;
  }

  if (!ResetFactoryDefaultPreference.keepNetwork() || mustApplySafebootDefaults) {
    Settings.clearNetworkSettings();

    // TD-er Reset access control
    str2ip(F(DEFAULT_IPRANGE_LOW),  SecuritySettings.AllowedIPrangeLow);
    str2ip(F(DEFAULT_IPRANGE_HIGH), SecuritySettings.AllowedIPrangeHigh);
    SecuritySettings.IPblockLevel = DEFAULT_IP_BLOCK_LEVEL;

    #if DEFAULT_USE_STATIC_IP
    str2ip((char *)DEFAULT_IP,     Settings.IP);
    str2ip((char *)DEFAULT_DNS,    Settings.DNS);
    str2ip((char *)DEFAULT_GW,     Settings.Gateway);
    str2ip((char *)DEFAULT_SUBNET, Settings.Subnet);
    #endif // if DEFAULT_USE_STATIC_IP
    Settings.IncludeHiddenSSID(DEFAULT_WIFI_INCLUDE_HIDDEN_SSID);
  }

  Settings.clearNotifications();
  Settings.clearControllers();
  Settings.clearTasks();

  if (!ResetFactoryDefaultPreference.keepLogConsoleSettings() || mustApplySafebootDefaults) {
    Settings.clearLogSettings();
    str2ip((char *)DEFAULT_SYSLOG_IP, Settings.Syslog_IP);

    setLogLevelFor(LOG_TO_SYSLOG, DEFAULT_SYSLOG_LEVEL);
    setLogLevelFor(LOG_TO_SERIAL, DEFAULT_SERIAL_LOG_LEVEL);
    setLogLevelFor(LOG_TO_WEBLOG, DEFAULT_WEB_LOG_LEVEL);
    setLogLevelFor(LOG_TO_SDCARD, DEFAULT_SD_LOG_LEVEL);
    Settings.SyslogFacility = DEFAULT_SYSLOG_FACILITY;
    Settings.SyslogPort     = DEFAULT_SYSLOG_PORT;
    Settings.UseValueLogger = DEFAULT_USE_SD_LOG;

    // FIXME TD-er: Must also keep console settings.
    Settings.console_serial_port      = DEFAULT_CONSOLE_PORT;
    Settings.console_serial_rxpin     = DEFAULT_CONSOLE_PORT_RXPIN;
    Settings.console_serial_txpin     = DEFAULT_CONSOLE_PORT_TXPIN;
    Settings.console_serial0_fallback = DEFAULT_CONSOLE_SER0_FALLBACK;
    Settings.UseSerial                = DEFAULT_USE_SERIAL;
    Settings.BaudRate                 = DEFAULT_SERIAL_BAUD;
  }

  if (!ResetFactoryDefaultPreference.keepUnitName() || mustApplySafebootDefaults) {
    Settings.clearUnitNameSettings();
    Settings.Unit = UNIT;
    strcpy_P(Settings.Name, PSTR(DEFAULT_NAME));
    Settings.UDPPort = DEFAULT_SYNC_UDP_PORT;
  }

  if (!ResetFactoryDefaultPreference.keepWiFi() || mustApplySafebootDefaults) {
    strcpy_P(SecuritySettings.WifiSSID,  PSTR(DEFAULT_SSID));
    strcpy_P(SecuritySettings.WifiKey,   PSTR(DEFAULT_KEY));
    strcpy_P(SecuritySettings.WifiSSID2, PSTR(DEFAULT_SSID2));
    strcpy_P(SecuritySettings.WifiKey2,  PSTR(DEFAULT_KEY2));
    strcpy_P(SecuritySettings.WifiAPKey, PSTR(DEFAULT_AP_KEY));
  }
  strcpy_P(SecuritySettings.Password, PSTR(DEFAULT_ADMIN_PASS));

  Settings.ResetFactoryDefaultPreference = ResetFactoryDefaultPreference.getPreference();

  // now we set all parameters that need to be non-zero as default value


  Settings.PID     = ESP_PROJECT_PID;
  Settings.Version = VERSION;
  Settings.Build   = get_build_nr();

  //  Settings.IP_Octet				 = DEFAULT_IP_OCTET;
  //  Settings.Delay                   = DEFAULT_DELAY;
  Settings.Pin_i2c_sda    = gpio_settings.i2c_sda;
  Settings.Pin_i2c_scl    = gpio_settings.i2c_scl;
  Settings.Pin_status_led = gpio_settings.status_led;

  //  Settings.Pin_status_led_Inversed = DEFAULT_PIN_STATUS_LED_INVERSED;
  Settings.Pin_sd_cs   = -1;
  Settings.Pin_Reset   = DEFAULT_PIN_RESET_BUTTON;
  Settings.Protocol[0] = DEFAULT_PROTOCOL;

  //  Settings.deepSleep_wakeTime      = 0; // Sleep disabled
  //  Settings.CustomCSS               = false;
  //  Settings.InitSPI                 = DEFAULT_SPI;

  // advanced Settings
  //  Settings.UseRules                         = DEFAULT_USE_RULES;
  Settings.ControllerEnabled[0]             = DEFAULT_CONTROLLER_ENABLED;
  Settings.MQTTRetainFlag_unused            = DEFAULT_MQTT_RETAIN;
  Settings.MessageDelay_unused              = DEFAULT_MQTT_DELAY;
  Settings.MQTTUseUnitNameAsClientId_unused = DEFAULT_MQTT_USE_UNITNAME_AS_CLIENTID;

  // allow to set default latitude and longitude
  #ifdef DEFAULT_LATITUDE
  Settings.Latitude = DEFAULT_LATITUDE;
  #endif // ifdef DEFAULT_LATITUDE
  #ifdef DEFAULT_LONGITUDE
  Settings.Longitude = DEFAULT_LONGITUDE;
  #endif // ifdef DEFAULT_LONGITUDE

#ifdef ESP32

  // Ethernet related settings are never used on ESP8266
  Settings.ETH_Phy_Addr   = gpio_settings.eth_phyaddr;
  Settings.ETH_Pin_mdc_cs    = gpio_settings.eth_mdc;
  Settings.ETH_Pin_mdio_irq   = gpio_settings.eth_mdio;
  Settings.ETH_Pin_power_rst  = gpio_settings.eth_power;
  Settings.ETH_Phy_Type   = gpio_settings.eth_phytype;
  Settings.ETH_Clock_Mode = gpio_settings.eth_clock_mode;
#endif // ifdef ESP32
  Settings.NetworkMedium = gpio_settings.network_medium;

  /*
          Settings.GlobalSync						= DEFAULT_USE_GLOBAL_SYNC;

          Settings.IP_Octet						= DEFAULT_IP_OCTET;
          Settings.WDI2CAddress					= DEFAULT_WD_IC2_ADDRESS;
          Settings.UseSSDP						= DEFAULT_USE_SSDP;
          Settings.ConnectionFailuresThreshold	= DEFAULT_CON_FAIL_THRES;
          Settings.WireClockStretchLimit			= DEFAULT_I2C_CLOCK_LIMIT;
   */

  //  Settings.I2C_clockSpeed = DEFAULT_I2C_CLOCK_SPEED;

  Settings.JSONBoolWithoutQuotes(DEFAULT_JSON_BOOL_WITHOUT_QUOTES);
  Settings.EnableTimingStats(DEFAULT_ENABLE_TIMING_STATS);

#ifdef PLUGIN_DESCR
  strcpy_P(Settings.Name, PSTR(PLUGIN_DESCR));
#endif // ifdef PLUGIN_DESCR

#ifndef LIMIT_BUILD_SIZE
  addPredefinedPlugins(gpio_settings);
  addPredefinedRules(gpio_settings);
#endif // ifndef LIMIT_BUILD_SIZE

#if DEFAULT_CONTROLLER
  {
    // Place in a scope to have its memory freed ASAP
    MakeControllerSettings(ControllerSettings); // -V522

    if (AllocatedControllerSettings()) {
      safe_strncpy(ControllerSettings->Subscribe,            F(DEFAULT_SUB),            sizeof(ControllerSettings->Subscribe));
      safe_strncpy(ControllerSettings->Publish,              F(DEFAULT_PUB),            sizeof(ControllerSettings->Publish));
      safe_strncpy(ControllerSettings->MQTTLwtTopic,         F(DEFAULT_MQTT_LWT_TOPIC), sizeof(ControllerSettings->MQTTLwtTopic));
      safe_strncpy(ControllerSettings->LWTMessageConnect,    F(DEFAULT_MQTT_LWT_CONNECT_MESSAGE),
                   sizeof(ControllerSettings->LWTMessageConnect));
      safe_strncpy(ControllerSettings->LWTMessageDisconnect, F(DEFAULT_MQTT_LWT_DISCONNECT_MESSAGE),
                   sizeof(ControllerSettings->LWTMessageDisconnect));
      str2ip((char *)DEFAULT_SERVER, ControllerSettings->IP);
      ControllerSettings->setHostname(F(DEFAULT_SERVER_HOST));
      ControllerSettings->UseDNS = DEFAULT_SERVER_USEDNS;
      ControllerSettings->useExtendedCredentials(DEFAULT_USE_EXTD_CONTROLLER_CREDENTIALS);
      ControllerSettings->Port          = DEFAULT_PORT;
      ControllerSettings->ClientTimeout = DEFAULT_CONTROLLER_TIMEOUT;
      setControllerUser(0, *ControllerSettings, F(DEFAULT_CONTROLLER_USER));
      setControllerPass(0, *ControllerSettings, F(DEFAULT_CONTROLLER_PASS));

      SaveControllerSettings(0, *ControllerSettings);
    }
  }
#endif // if DEFAULT_CONTROLLER

#ifdef ESP32
  if (!mustApplySafebootDefaults)
  {
    Settings.ResetFactoryDefaultPreference = ResetFactoryDefaultPreference.getPreference();

    if (ResetFactoryDefaultPreference.keepUnitName())
    {
      FactoryDefault_UnitName_NVS unitNameNVS{};
      unitNameNVS.applyToSettings_from_NVS(preferences);
    }

    if (ResetFactoryDefaultPreference.keepWiFi())
    {
      FactoryDefault_WiFi_NVS wifiNVS{};
      wifiNVS.applyToSettings_from_NVS(preferences);
    }

    if (ResetFactoryDefaultPreference.keepNetwork())
    {
      // Restore Network IP settings
      FactoryDefault_Network_NVS network_nvs;
      network_nvs.applyToSettings_from_NVS(preferences);
    }

    if (ResetFactoryDefaultPreference.keepLogConsoleSettings())
    {
      // Restore Log and Console settings
      FactoryDefault_LogConsoleSettings_NVS log_console_nvs;
      log_console_nvs.applyToSettings_from_NVS(preferences);
    }

# if FEATURE_ALTERNATIVE_CDN_URL

    if (ResetFactoryDefaultPreference.keepCustomCdnUrl()) {
      FactoryDefault_CDN_customurl_NVS::applyToSettings_from_NVS(preferences);
    }
# endif // if FEATURE_ALTERNATIVE_CDN_URL
    preferences.end();
  }
#endif // ifdef ESP32

  const bool forFactoryReset = true;
  SaveSettings(forFactoryReset);
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("ResetFactory2"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  serialPrintln(F("RESET: Successful, rebooting. (you might need to press the reset button if you've just flashed the firmware)"));

  // NOTE: this is a known ESP8266 bug, not our fault. :)
  delay(1000);
  WiFi.persistent(true);  // use SDK storage of SSID/WPA parameters
  WiFiEventData.intent_to_reboot = true;
  WifiDisconnect();       // this will store empty ssid/wpa into sdk storage
  WiFi.persistent(false); // Do not use SDK storage of SSID/WPA parameters
  reboot(IntendedRebootReason_e::ResetFactory);
}

/*********************************************************************************************\
   Collect the stored preference for factory default
\*********************************************************************************************/
void applyFactoryDefaultPref() {
  // TODO TD-er: Store it in more places to make it more persistent
  Settings.ResetFactoryDefaultPreference = ResetFactoryDefaultPreference.getPreference();

#ifdef ESP32
  ESPEasy_NVS_Helper preferences;
  preferences.begin(F(FACTORY_DEFAULT_NVS_NAMESPACE));
  ResetFactoryDefaultPreference.to_NVS(preferences);
  {
    FactoryDefault_UnitName_NVS unitNameNVS{};

    if (ResetFactoryDefaultPreference.keepUnitName())
    {
      // Store Unit nr and hostname
      unitNameNVS.fromSettings_to_NVS(preferences);
    } else {
      unitNameNVS.clear_from_NVS(preferences);
    }
  }
  {
    FactoryDefault_WiFi_NVS wifiNVS{};

    if (ResetFactoryDefaultPreference.keepWiFi())
    {
      // Store WiFi credentials
      wifiNVS.fromSettings_to_NVS(preferences);
    } else {
      wifiNVS.clear_from_NVS(preferences);
    }
  }
  {
    FactoryDefault_Network_NVS network_nvs{};

    if (ResetFactoryDefaultPreference.keepNetwork())
    {
      // Store Network IP settings
      network_nvs.fromSettings_to_NVS(preferences);
    } else {
      network_nvs.clear_from_NVS(preferences);
    }
  }
  {
    FactoryDefault_LogConsoleSettings_NVS log_console_nvs{};

    if (ResetFactoryDefaultPreference.keepLogConsoleSettings())
    {
      // Store Log and Console settings
      log_console_nvs.fromSettings_to_NVS(preferences);
    } else {
      log_console_nvs.clear_from_NVS(preferences);
    }
  }
# if FEATURE_ALTERNATIVE_CDN_URL
  {
    if (ResetFactoryDefaultPreference.keepCustomCdnUrl())
    {
      // Store custom CDN
      FactoryDefault_CDN_customurl_NVS::fromSettings_to_NVS(preferences);
    } else {
      FactoryDefault_CDN_customurl_NVS::clear_from_NVS(preferences);
    }
  }
# endif // if FEATURE_ALTERNATIVE_CDN_URL


  preferences.end();
#endif // ifdef ESP32
}

#include "../Helpers/Memory.h"


#ifdef ESP8266
extern "C" {
# include <user_interface.h>
}
#endif // ifdef ESP8266

#include "../../ESPEasy_common.h"


#ifdef ESP32
# if ESP_IDF_VERSION_MAJOR < 5
#  include <soc/cpu.h>
# endif // if ESP_IDF_VERSION_MAJOR < 5
#endif  // ifdef ESP32

#include "../Helpers/Hardware_device_info.h"

/*********************************************************************************************\
   Memory management
\*********************************************************************************************/


// For keeping track of 'cont' stack
// See: https://github.com/esp8266/Arduino/issues/2557
//      https://github.com/esp8266/Arduino/issues/5148#issuecomment-424329183
//      https://github.com/letscontrolit/ESPEasy/issues/1824
#ifdef ESP32

// FIXME TD-er: For ESP32 you need to provide the task number, or nullptr to get from the calling task.
uint32_t getCurrentFreeStack() {
  return ((uint8_t *)esp_cpu_get_sp()) - pxTaskGetStackStart(nullptr);
}

uint32_t getFreeStackWatermark() {
  return uxTaskGetStackHighWaterMark(nullptr);
}

#else // ifdef ESP32

uint32_t getCurrentFreeStack() {
  // https://github.com/esp8266/Arduino/issues/2557
  register uint32_t *sp asm ("a1");

  return 4 * (sp - g_pcont->stack);
}

uint32_t getFreeStackWatermark() {
  return cont_get_free_stack(g_pcont);
}

bool allocatedOnStack(const void *address) {
  register uint32_t *sp asm ("a1");

  if (sp < address) { return false; }
  return g_pcont->stack < address;
}

#endif // ESP32


/********************************************************************************************\
   Get free system mem
 \*********************************************************************************************/
unsigned long FreeMem()
{
  #if defined(ESP8266)
  return system_get_free_heap_size();
  #endif // if defined(ESP8266)
  #if defined(ESP32)
  return ESP.getFreeHeap();
  #endif // if defined(ESP32)
}

#ifdef USE_SECOND_HEAP
unsigned long FreeMem2ndHeap()
{
  HeapSelectIram ephemeral;

  return ESP.getFreeHeap();
}

#endif // ifdef USE_SECOND_HEAP


unsigned long getMaxFreeBlock()
{
  const unsigned long freemem = FreeMem();

  // computing max free block is a rather extensive operation, so only perform when free memory is already low.
  if (freemem < 6144) {
  #if  defined(ESP32)
    return ESP.getMaxAllocHeap();
  #endif // if  defined(ESP32)
  #ifdef CORE_POST_2_5_0
    return ESP.getMaxFreeBlockSize();
  #endif // ifdef CORE_POST_2_5_0
  }
  return freemem;
}

/********************************************************************************************\
   Special alloc functions to allocate in PSRAM if available
   See: https://github.com/espressif/esp-idf/blob/master/components/heap/port/esp32s3/memory_layout.c
 \*********************************************************************************************/
void* special_malloc(uint32_t size) {
  void *res = nullptr;

#ifdef ESP32

  if (UsePSRAM()) {
    res = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
  }
#else // ifdef ESP32
  {
# ifdef USE_SECOND_HEAP

    // Try allocating on ESP8266 2nd heap
    HeapSelectIram ephemeral;
# endif // ifdef USE_SECOND_HEAP
    res = malloc(size);
  }
#endif  // ifdef ESP32

  if (res == nullptr) {
#ifdef USE_SECOND_HEAP

    // Not successful, try allocating on (ESP8266) main heap
    HeapSelectDram ephemeral;
#endif // ifdef USE_SECOND_HEAP
    res = malloc(size);
  }

  return res;
}

void* special_realloc(void *ptr, size_t size) {
  void *res = nullptr;

#ifdef ESP32

  if (UsePSRAM()) {
    res = heap_caps_realloc(ptr, size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
  }
#else // ifdef ESP32
  {
# ifdef USE_SECOND_HEAP

    // Try allocating on ESP8266 2nd heap
    HeapSelectIram ephemeral;
# endif // ifdef USE_SECOND_HEAP
    res = realloc(ptr, size);
  }
#endif  // ifdef ESP32

  if (res == nullptr) {
#ifdef USE_SECOND_HEAP

    // Not successful, try allocating on (ESP8266) main heap
    HeapSelectDram ephemeral;
#endif // ifdef USE_SECOND_HEAP
    res = realloc(ptr, size);
  }

  return res;
}

void* special_calloc(size_t num, size_t size) {
  void *res = nullptr;

#ifdef ESP32

  if (UsePSRAM()) {
    res = heap_caps_calloc(num, size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
  }
#else // ifdef ESP32
# ifdef USE_SECOND_HEAP
  if (size > 64 || FreeMem() < 5000) {

    // Try allocating on ESP8266 2nd heap, only when sufficiently large data is needed
    HeapSelectIram ephemeral;
    res = calloc(num, size);
  }
# endif // ifdef USE_SECOND_HEAP
#endif  // ifdef ESP32

  if (res == nullptr) {
#ifdef USE_SECOND_HEAP

    // Not successful, try allocating on (ESP8266) main heap
    HeapSelectDram ephemeral;
#endif // ifdef USE_SECOND_HEAP
    res = calloc(num, size);
  }
#if defined(ESP8266) && defined(USE_SECOND_HEAP)
  if (res == nullptr) {
    // Not successful, try allocating on (ESP8266) 2nd heap
    HeapSelectIram ephemeral;
    res = calloc(num, size);
  }
#endif  
  return res;
}


#ifdef ESP8266
bool String_reserve_special(String& str, size_t size) {
  if (str.length() >= size) {
    // Nothing needs to be done
    return true;
  }
  #ifdef USE_SECOND_HEAP
  if (size >= 48 || FreeMem() < 5000) {
    // Only try to store larger strings here as those tend to be kept for a longer period.
    HeapSelectIram ephemeral;
    // String does round up to nearest multiple of 16 bytes, so no need to round up to multiples of 32 bit here
    if (str.reserve(size)) {
      return true;
    }
  }
  #endif
  return str.reserve(size);
}
#endif

#ifdef ESP32

// Special class to get access to the protected String functions
// This class only has a constructor which will perform 
// the requested allocation in PSRAM when possible
class PSRAM_String : public String {
  public:
  PSRAM_String(size_t size) : String() {
    sso.isSSO = 0;      // setSSO(false);
    ptr.buff = nullptr; // setBuffer(nullptr);
    ptr.cap = 0;        // setCapacity(0);
    ptr.len = 0;        // setLen(0);

    if (size != 0 && size > capacity() && UsePSRAM()) {
      size_t newSize = (size + 16) & (~0xf);
      void *ptr = special_calloc(1, newSize);
      if (ptr != nullptr) {
        setSSO(false);
        setBuffer((char *)ptr);
        setCapacity(newSize - 1);
        setLen(newSize - 1); // TD-er: Not sure if needed?
      }
    }
  }
};


bool String_reserve_special(String& str, size_t size) {
  if (size == 0) {
    return true;
  }
  if (!UsePSRAM()) {
    return str.reserve(size);
  }
  if (str.length() <= size) {
    // As we like to move this to PSRAM, it also makes sense 
    // to do this when the length equals size
    PSRAM_String psram_str(size);

    if (psram_str.length() == 0) {
      return str.reserve(size);
    }

    // Copy any existing content
    String tmp(std::move(str));

    // Move the newly allocated buffer to a tmp String object.
    // Needs to be empty, so the buffer is moved.
    // N.B. String::clear() = String::setlen(0))
    str = std::move(psram_str);
    str.clear();  

    if (tmp.length()) {
      str = tmp;
    }
  }
  return true;
}
#endif

#include "../Helpers/SerialWriteBuffer.h"

#include "../Helpers/Memory.h"

void SerialWriteBuffer_t::add(const String& line)
{
  // When the buffer is too full, try to dump at least the size of what we try to add.
  const bool mustPop = _buffer.size() > _maxSize;
  {
    #ifdef USE_SECOND_HEAP

    // Do not store in 2nd heap, std::dequeue cannot handle 2nd heap well
    HeapSelectIram ephemeral;
    #endif // ifdef USE_SECOND_HEAP
    int roomLeft = getRoomLeft();

    auto it = line.begin();

    while (roomLeft > 0 && it != line.end()) {
      if (mustPop) {
        _buffer.pop_front();
      }
      _buffer.push_back(*it);
      --roomLeft;
      ++it;
    }
  }
}

void SerialWriteBuffer_t::add(const __FlashStringHelper *line)
{
  add(String(line));
}

void SerialWriteBuffer_t::add(char c)
{
  #ifdef USE_SECOND_HEAP

  // Do not store in 2nd heap, std::dequeue cannot handle 2nd heap well
  HeapSelectIram ephemeral;
  #endif // ifdef USE_SECOND_HEAP

  if (_buffer.size() > _maxSize) {
    _buffer.pop_front();
  }
  _buffer.push_back(c);
}

void SerialWriteBuffer_t::addNewline()
{
  add('\r');
  add('\n');
}

void SerialWriteBuffer_t::clear()
{
  _buffer.clear();
}

size_t SerialWriteBuffer_t::write(Stream& stream, size_t nrBytesToWrite)
{
  size_t bytesWritten     = 0;
  const size_t bufferSize = _buffer.size();

  if (bufferSize == 0) {
    return bytesWritten;
  }

  if (nrBytesToWrite > 0) {
    if (nrBytesToWrite > bufferSize) {
      nrBytesToWrite = bufferSize;
    }

    while (nrBytesToWrite > 0 && !_buffer.empty()) {
      uint8_t tmpBuffer[16]{};

      size_t tmpBufferUsed = 0;

      auto it = _buffer.begin();

      bool done = false;

      for (; tmpBufferUsed < sizeof(tmpBuffer) &&
           !done &&
           it != _buffer.end();) {
        tmpBuffer[tmpBufferUsed] = (uint8_t)(*it);

        if ((*it == '\n') ||
            (tmpBufferUsed >= nrBytesToWrite)) {
          done = true;
        }
        ++tmpBufferUsed;
        ++it;
      }

      //      done = false;
      const size_t written = (tmpBufferUsed == 0) ? 0 : stream.write(tmpBuffer, tmpBufferUsed);

      if (written < tmpBufferUsed) {
        done = true;
      }

      for (size_t i = 0; i < written; ++i) {
        _buffer.pop_front();
        --nrBytesToWrite;
        ++bytesWritten;
      }

      if (done) {
        return bytesWritten;
      }
    }
  }
  return bytesWritten;
}

int SerialWriteBuffer_t::getRoomLeft() const {
  #ifdef USE_SECOND_HEAP

  // Do not store in 2nd heap, std::dequeue cannot handle 2nd heap well
  HeapSelectIram ephemeral;
  #endif // ifdef USE_SECOND_HEAP

  int roomLeft = getMaxFreeBlock();

  if (roomLeft < 1000) {
    roomLeft = 0;                    // Do not append to buffer.
  } else if (roomLeft < 4000) {
    roomLeft = 128 - _buffer.size(); // 1 buffer.
  } else {
    roomLeft -= 4000;                // leave some free for normal use.
  }
  return roomLeft;
}

#include "../Helpers/Convert.h"

#include "../Helpers/StringConverter.h"
#include "../Helpers/ESPEasy_time_calc.h"

/*********************************************************************************************\
   Convert bearing in degree to bearing string
\*********************************************************************************************/
const __FlashStringHelper * getBearing(int degrees)
{
  const __FlashStringHelper* directions[] {
    F("N"),
    F("NNE"),
    F("NE"),
    F("ENE"),
    F("E"),
    F("ESE"),
    F("SE"),
    F("SSE"),
    F("S"),
    F("SSW"),
    F("SW"),
    F("WSW"),
    F("W"),
    F("WNW"),
    F("NW"),
    F("NNW")
  };
  constexpr size_t nrDirections = NR_ELEMENTS(directions);
  const float stepsize          = (360.0f / nrDirections);

  if (degrees < 0) { degrees += 360; } // Allow for bearing -360 .. 359
  const size_t bearing_idx = int((degrees + (stepsize / 2.0f)) / stepsize) % nrDirections;

  if (bearing_idx < nrDirections) {
    return directions[bearing_idx];
  }
  return F("");
}

float CelsiusToFahrenheit(float celsius) {
  constexpr float ratio = 9.0f / 5.0f;
  return celsius * ratio + 32;
}

int m_secToBeaufort(float m_per_sec) {
  // Use ints wit 0.1 m/sec resolution to reduce size.
  const uint16_t dm_per_sec = 10 * m_per_sec;
  const uint16_t speeds[]{3, 16, 34, 55, 80, 108, 139, 172, 208, 245, 285, 326};  
  constexpr int nrElements = NR_ELEMENTS(speeds);
  
  for (int bft = 0; bft < nrElements; ++bft) {
    if (dm_per_sec < speeds[bft]) return bft;
  }
  return nrElements;  
}

String centimeterToImperialLength(float cm) {
  return millimeterToImperialLength(cm * 10.0f);
}

String millimeterToImperialLength(float mm) {
  float inches = mm / 25.4f;
  int   feet   = inches / 12.0f;

  inches = inches - (feet * 12);
  String result;
  result.reserve(10);

  if (feet != 0) {
    result += feet;
    result += '\'';
  }
  result += toString(inches, 1);
  result += '"';
  return result;
}

float minutesToDay(int minutes) {
  return minutes / 1440.0f;
}

String minutesToDayHour(int minutes) {
  const int  days  = minutes / 1440;
  const int  hours = (minutes % 1440) / 60;
  return strformat(F("%dd%02dh"), days, hours);
}

String minutesToDayHourMinute(int minutes) {
  const int  days  = minutes / 1440;
  const int  hours = (minutes % 1440) / 60;
  const int  mins  = (minutes % 1440) % 60;
  if (days == 0) {
    return strformat(F("%02dh%02dm"), hours, mins);
  }
  return strformat(F("%dd%02dh%02dm"), days, hours, mins);
}

String minutesToHourColonMinute(int minutes) {
  const int  hours = (minutes % 1440) / 60;
  const int  mins  = (minutes % 1440) % 60;

  return strformat(F("%02d:%02d"), hours, mins);
}

String secondsToDayHourMinuteSecond(int seconds) {
  const int  sec     = seconds % 60;
  const int  minutes = seconds / 60;
  const int  days    = minutes / 1440;
  const int  min_day = (minutes % 1440);
  const int  hours   = min_day / 60;
  const int  mins    = min_day % 60;
  if (days == 0) {
    return strformat(F("%02d:%02d:%02d"), hours, mins, sec);
  } 
  return strformat(F("%dT%02d:%02d:%02d"), days, hours, mins, sec);
}

String secondsToDayHourMinuteSecond_ms(int64_t systemMicros) 
{
  if (systemMicros < 0ll) {
    return concat('-', secondsToDayHourMinuteSecond_ms(-1ll*systemMicros));
  }

  uint32_t usec{};
  const uint32_t seconds = micros_to_sec_usec(systemMicros, usec);
  return strformat(
    F("%s.%03u"),
    secondsToDayHourMinuteSecond(seconds).c_str(),
    usec / 1000ul);
}

String format_msec_duration(int64_t duration) {
  if (duration < 0ll) {
    return concat('-', format_msec_duration(-1ll*duration));
  }
  const uint32_t duration_s = duration / 1000ll;
  const int32_t duration_ms = duration % 1000ll;

  if (duration_s < 60) {
    return strformat(
      F("%02d.%03d"),
      duration_s,
      duration_ms);
  }
  return strformat(
    F("%s.%03d"),
    secondsToDayHourMinuteSecond(duration_s).c_str(),
    duration_ms);
}


// Compute the dew point temperature, given temperature and humidity (temp in Celsius)
// Formula: http://www.ajdesigner.com/phphumidity/dewpoint_equation_dewpoint_temperature.php
// Td = (f/100)^(1/8) * (112 + 0.9*T) + 0.1*T - 112
float compute_dew_point_temp(float temperature, float humidity_percentage) {
  return powf(humidity_percentage / 100.0f, 0.125f) *
         (112.0f + 0.9f*temperature) + 0.1f*temperature - 112.0f;
}

// Compute the humidity given temperature and dew point temperature (temp in Celsius)
// Formula: http://www.ajdesigner.com/phphumidity/dewpoint_equation_relative_humidity.php
// f = 100 * ((112 - 0.1*T + Td) / (112 + 0.9 * T))^8
float compute_humidity_from_dewpoint(float temperature, float dew_temperature) {
  return 100.0f * powf((112.0f - 0.1f * temperature + dew_temperature) /
                     (112.0f + 0.9f * temperature), 8);
}



/********************************************************************************************\
   Compensate air pressure for given altitude (in meters)
 \*********************************************************************************************/
float pressureElevation(float atmospheric, float altitude) {
  // Equation taken from BMP180 datasheet (page 16):
  //  http://www.adafruit.com/datasheets/BST-BMP180-DS000-09.pdf

  // Note that using the equation from wikipedia can give bad results
  // at high altitude.  See this thread for more information:
  //  http://forums.adafruit.com/viewtopic.php?f=22&t=58064
  return atmospheric / powf(1.0f - (altitude / 44330.0f), 5.255f);
}

float altitudeFromPressure(float atmospheric, float seaLevel)
{
  // Equation taken from BMP180 datasheet (page 16):
  //  http://www.adafruit.com/datasheets/BST-BMP180-DS000-09.pdf

  // Note that using the equation from wikipedia can give bad results
  // at high altitude.  See this thread for more information:
  //  http://forums.adafruit.com/viewtopic.php?f=22&t=58064
  return 44330.0f * (1.0f - powf(atmospheric / seaLevel, 0.1903f));
}




/********************************************************************************************\
   In memory convert float to long
 \*********************************************************************************************/
unsigned long float2ul(float f)
{
  unsigned long ul;

  memcpy(&ul, &f, 4);
  return ul;
}

/********************************************************************************************\
   In memory convert long to float
 \*********************************************************************************************/
float ul2float(unsigned long ul)
{
  float f;

  memcpy(&f, &ul, 4);
  return f;
}



#include "../Helpers/Rules_calculate.h"

#include "../DataStructs/TimingStats.h"
#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../Globals/RamTracker.h"
#include "../Helpers/ESPEasy_math.h"
#include "../Helpers/Hardware.h"
#include "../Helpers/Numerical.h"
#include "../Helpers/StringConverter.h"


RulesCalculate_t::RulesCalculate_t() {
  for (int i = 0; i < STACK_SIZE; ++i) {
    globalstack[i] = 0.0;
  }
}

/********************************************************************************************\
   Calculate function for simple expressions
 \*********************************************************************************************/
bool isError(CalculateReturnCode returnCode) {
  return returnCode != CalculateReturnCode::OK;
}

bool RulesCalculate_t::is_number(char oc, char c, char pc)
{
  // Check if it matches part of a number (identifier)
  return
    (c == '.')   ||                                // A decimal point of a floating point number.
    ((oc == '0') && ((c == 'x') || (c == 'b'))) || // HEX (0x) or BIN (0b) prefixes.
    isxdigit(c)  ||                                // HEX digit also includes normal decimal numbers
    ((is_operator(oc) || ('\0' == oc)) && (c == '-') 
        && (isdigit(pc) || ('\0' == pc))) // Beginning of a negative number after an operator or 'separator' and before a digit or end-of-digit.
  ;
}

bool RulesCalculate_t::is_operator(char c)
{
  return c == '+' || c == '-' || c == '*' || c == '/' || c == '^' || c == '%';
}

bool RulesCalculate_t::is_unary_operator(char c)
{
  const UnaryOperator op = static_cast<UnaryOperator>(c);
  return (op == UnaryOperator::Not || (
          c >= static_cast<char>(UnaryOperator::Log) &&
          c <= static_cast<char>(UnaryOperator::ArcTan_d)));
/*
  switch (op) {
    case UnaryOperator::Not:
    case UnaryOperator::Log:
    case UnaryOperator::Ln:
    case UnaryOperator::Abs:
    case UnaryOperator::Exp:
    case UnaryOperator::Sqrt:
    case UnaryOperator::Sq:
    case UnaryOperator::Round:
    case UnaryOperator::Sin:
    case UnaryOperator::Cos:
    case UnaryOperator::Tan:
    case UnaryOperator::ArcSin:
    case UnaryOperator::ArcCos:
    case UnaryOperator::ArcTan:
    case UnaryOperator::Sin_d:
    case UnaryOperator::Cos_d:
    case UnaryOperator::Tan_d:
    case UnaryOperator::ArcSin_d:
    case UnaryOperator::ArcCos_d:
    case UnaryOperator::ArcTan_d:
      return true;
  }
  return false;
  */
}

#if !defined(LIMIT_BUILD_SIZE) && FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES
// binary = 2-argument functions
bool RulesCalculate_t::is_binary_operator(char c)
{
  return (c >= static_cast<char>(BinaryOperator::ArcTan2) &&
          c <= static_cast<char>(BinaryOperator::FMod));
}
#endif // if !defined(LIMIT_BUILD_SIZE) && FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES

// quinary = 5-argument functions
bool RulesCalculate_t::is_quinary_operator(char c)
{
  const UnaryOperator op = static_cast<UnaryOperator>(c);

  return op == UnaryOperator::Map || op == UnaryOperator::MapC;
}

CalculateReturnCode RulesCalculate_t::push(ESPEASY_RULES_FLOAT_TYPE value)
{
  if (sp != sp_max) // Full
  {
    *(++sp) = value;
    return CalculateReturnCode::OK;
  }
  return CalculateReturnCode::ERROR_STACK_OVERFLOW;
}

ESPEASY_RULES_FLOAT_TYPE RulesCalculate_t::pop()
{
  if (sp != (globalstack - 1)) { // empty
    return *(sp--);
  }
  else {
    return 0.0;
  }
}

ESPEASY_RULES_FLOAT_TYPE RulesCalculate_t::apply_operator(char op, ESPEASY_RULES_FLOAT_TYPE first, ESPEASY_RULES_FLOAT_TYPE second)
{
  switch (op)
  {
    case '+':
      return first + second;
    case '-':
      return first - second;
    case '*':
      return first * second;
    case '/':
      return first / second;
    case '%':
    #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
      return static_cast<int>(round(first)) % static_cast<int>(round(second));
    #else
      return static_cast<int>(roundf(first)) % static_cast<int>(roundf(second));
    #endif
    case '^':
    #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
      return pow(first, second);
    #else
      return powf(first, second);
    #endif
    default:
      return 0;
  }
}

ESPEASY_RULES_FLOAT_TYPE RulesCalculate_t::apply_unary_operator(char op, ESPEASY_RULES_FLOAT_TYPE first)
{
  ESPEASY_RULES_FLOAT_TYPE ret{};
  const UnaryOperator un_op = static_cast<UnaryOperator>(op);

  switch (un_op) {
    case UnaryOperator::Not:
    #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
      return essentiallyZero(round(first)) ? 1 : 0;
    #else
      return essentiallyZero(roundf(first)) ? 1 : 0;
    #endif
    case UnaryOperator::Log:
    #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
      return log10(first);
    #else
      return log10f(first);
    #endif
    case UnaryOperator::Ln:
    #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
      return log(first);
    #else
      return logf(first);
    #endif
    case UnaryOperator::Abs:
    #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
      return fabs(first);
    #else
      return fabsf(first);
    #endif
    case UnaryOperator::Exp:
    #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
      return exp(first);
    #else
      return expf(first);
    #endif
    case UnaryOperator::Sqrt:
    #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
      return sqrt(first);
    #else
      return sqrtf(first);
    #endif
    case UnaryOperator::Sq:
      return first * first;
    case UnaryOperator::Round:
    #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
      return round(first);
    #else
      return roundf(first);
    #endif
    default:
      break;
  }

#if FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES
  const bool useDegree = angleDegree(un_op);

  // First the trigonometric functions with angle as output
  switch (un_op) {
    case UnaryOperator::ArcSin:
    case UnaryOperator::ArcSin_d:
    #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
      ret = asin(first);
    #else
      ret = asinf(first);
    #endif
      return useDegree ? degrees(ret) : ret;
    case UnaryOperator::ArcCos:
    case UnaryOperator::ArcCos_d:
    #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
      ret = acos(first);
    #else
      ret = acosf(first);
    #endif
      return useDegree ? degrees(ret) : ret;
    case UnaryOperator::ArcTan:
    case UnaryOperator::ArcTan_d:
    #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
      ret = atan(first);
    #else
      ret = atanf(first);
    #endif
      return useDegree ? degrees(ret) : ret;
    default:
      break;
  }

  // Now the trigonometric functions with angle as input
  if (useDegree) {
    first = radians(first);
  }

  switch (un_op) {
    case UnaryOperator::Sin:
    case UnaryOperator::Sin_d:
    #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
      return sin(first);
    #else
      return sinf(first);
    #endif
    case UnaryOperator::Cos:
    case UnaryOperator::Cos_d:
    #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
      return cos(first);
    #else
      return cosf(first);
    #endif
    case UnaryOperator::Tan:
    case UnaryOperator::Tan_d:
    #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
      return tan(first);
    #else
      return tanf(first);
    #endif
    default:
      break;
  }
#else // if FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES

  switch (un_op) {
    case UnaryOperator::Sin:
    case UnaryOperator::Sin_d:
    case UnaryOperator::Cos:
    case UnaryOperator::Cos_d:
    case UnaryOperator::Tan:
    case UnaryOperator::Tan_d:
    case UnaryOperator::ArcSin:
    case UnaryOperator::ArcSin_d:
    case UnaryOperator::ArcCos:
    case UnaryOperator::ArcCos_d:
    case UnaryOperator::ArcTan:
    case UnaryOperator::ArcTan_d:
      addLog(LOG_LEVEL_ERROR, F("FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES not defined in build"));
      break;
    default:
      break;
  }
#endif // if FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES
  return ret;
}

#if !defined(LIMIT_BUILD_SIZE) && FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES
ESPEASY_RULES_FLOAT_TYPE RulesCalculate_t::apply_binary_operator(char op, ESPEASY_RULES_FLOAT_TYPE first, ESPEASY_RULES_FLOAT_TYPE second)
{
  ESPEASY_RULES_FLOAT_TYPE ret{};
  const BinaryOperator bin_op = static_cast<BinaryOperator>(op);
  
  const bool useDegree = angleDegree(bin_op);
  switch (bin_op) {
    case BinaryOperator::ArcTan2:
    case BinaryOperator::ArcTan2_d:
      #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
      ret = atan2(first, second);
      #else
      ret = atan2f(first, second);
      #endif
      return useDegree ? degrees(ret) : ret;
    case BinaryOperator::FMod:
      #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
      return fmod(first, second);
      #else // if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
      return fmodf(first, second);
      #endif // if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
  }      
  return ret;
}
#endif // if !defined(LIMIT_BUILD_SIZE) && FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES

ESPEASY_RULES_FLOAT_TYPE RulesCalculate_t::apply_quinary_operator(char op, 
                                                                  ESPEASY_RULES_FLOAT_TYPE first,
                                                                  ESPEASY_RULES_FLOAT_TYPE second,
                                                                  ESPEASY_RULES_FLOAT_TYPE third,
                                                                  ESPEASY_RULES_FLOAT_TYPE fourth,
                                                                  ESPEASY_RULES_FLOAT_TYPE fifth)
{
  ESPEASY_RULES_FLOAT_TYPE ret{};
  const UnaryOperator qu_op = static_cast<UnaryOperator>(op);

  if (UnaryOperator::Map == qu_op || UnaryOperator::MapC == qu_op) {

    // Clamp the result if the operator is MapC
    if (qu_op == UnaryOperator::MapC) {
      first = constrain(first, min(second, third), max(second, third));;
    }
    ret = mapADCtoFloat(first, second, third, fourth, fifth);
  }
  return ret;
}

/*
   char * RulesCalculate_t::next_token(char *linep)
   {
   while (isspace(*(linep++))) {}

   while (*linep && !isspace(*(linep++))) {}
   return linep;
   }
 */
CalculateReturnCode RulesCalculate_t::RPNCalculate(char *token)
{
  CalculateReturnCode ret = CalculateReturnCode::OK;

  if (token[0] == 0) {
    return ret; // Don't bother for an empty string
  }

  if (is_operator(token[0]) && (token[1] == 0))
  {
    ESPEASY_RULES_FLOAT_TYPE second = pop();
    ESPEASY_RULES_FLOAT_TYPE first  = pop();

    ret = push(apply_operator(token[0], first, second));
    // addLog(LOG_LEVEL_INFO, strformat(F("RPNCalculate operator %c: 1: %.4f 2: %.4f"), token[0], first, second));

// FIXME TD-er: Regardless whether it is an error, all code paths return ret;
//    if (isError(ret)) { return ret; }
  } else if (is_unary_operator(token[0]) && (token[1] == 0))
  {
    ESPEASY_RULES_FLOAT_TYPE first = pop();

    ret = push(apply_unary_operator(token[0], first));
    // addLog(LOG_LEVEL_INFO, strformat(F("RPNCalculate unary %d: 1: %.4f"), token[0], first));

// FIXME TD-er: Regardless whether it is an error, all code paths return ret;
//    if (isError(ret)) { return ret; }
  #if !defined(LIMIT_BUILD_SIZE) && FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES
  } else if (is_binary_operator(token[0]) && (token[1] == 0))
  {
    const ESPEASY_RULES_FLOAT_TYPE second = pop();
    const ESPEASY_RULES_FLOAT_TYPE first  = pop();
    
    ret = push(apply_binary_operator(token[0], first, second));
    // addLog(LOG_LEVEL_INFO, strformat(F("RPNCalculate binary %d: 1: %.4f 2: %.4f"), token[0], first, second));
  
  #endif // if !defined(LIMIT_BUILD_SIZE) && FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES
  } else if (is_quinary_operator(token[0]) && (token[1] == 0))
  {
    ESPEASY_RULES_FLOAT_TYPE fifth  = pop();
    ESPEASY_RULES_FLOAT_TYPE fourth = pop();
    ESPEASY_RULES_FLOAT_TYPE third  = pop();
    ESPEASY_RULES_FLOAT_TYPE second = pop();
    ESPEASY_RULES_FLOAT_TYPE first  = pop();

    ret = push(apply_quinary_operator(token[0], first, second, third, fourth, fifth));
    // addLog(LOG_LEVEL_INFO, strformat(F("RPNCalculate quinary %d: 1: %.4f 2: %.4f 3: %.4f 4: %.4f 5: %.4f"), token[0], first, second, third, fourth, fifth));

  } else {
    // Fetch next if there is any
    ESPEASY_RULES_FLOAT_TYPE value{};
    if (validDoubleFromString(token, value)) {

    //   addLog(LOG_LEVEL_INFO, strformat(F("RPNCalculate push value: %.4f token: %s"), value, token));
    // } else {
    //   addLog(LOG_LEVEL_INFO, strformat(F("RPNCalculate unknown token: %s"), token));
    }
    ret = push(value); // If it is a value, push to the stack

// FIXME TD-er: Regardless whether it is an error, all code paths return ret;
//    if (isError(ret)) { return ret; }
  }

  return ret;
}

// operators
// precedence   operators         associativity
// 4            !                 right to left
// 3            ^                 left to right
// 2            * / %             left to right
// 1            + -               left to right
int RulesCalculate_t::op_preced(const char c)
{
  if (is_unary_operator(c)) { return 4; // right to left
  }

  switch (c)
  {
    case '^':
      return 3;
    case '*':
    case '/':
    case '%':
      return 2;
    case '+':
    case '-':
      return 1;
  }
  return 0;
}

bool RulesCalculate_t::op_left_assoc(const char c)
{
  if (is_operator(c)) { return true;        // left to right
  }
/*
  // FIXME TD-er: Disabled the check as the return value is false anyway.
  if (is_unary_operator(c)) { return false; // right to left
  }
  */
  return false;
}

/* unused:
unsigned int RulesCalculate_t::op_arg_count(const char c)
{
  if (is_unary_operator(c)) { return 1; }

  if (is_operator(c)) { return 2; }

  if (is_quinary_operator(c)) { return 5; }
  return 0;
}
*/

CalculateReturnCode RulesCalculate_t::doCalculate(const char *input, ESPEASY_RULES_FLOAT_TYPE *result)
{

  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("Calculate"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  const char *strpos = input, *strend = input + strlen(input);
  char token[TOKEN_LENGTH]{};
  char c, oc, pc, *TokenPos = token;
  const char *pcpos;
  char stack[OPERATOR_STACK_SIZE]{}; // operator stack
  unsigned int sl = 0;               // stack length
  char sc;                           // used for record stack element
  CalculateReturnCode error = CalculateReturnCode::OK;

  // *sp=0; // bug, it stops calculating after 50 times
  sp = globalstack - 1;
  oc = c = 0;

  if (input[0] == '=') {
    ++strpos;

    if (strpos < strend) {
      c = *strpos;
    }
  }

  while (strpos < strend)
  {
    if ((TokenPos - &token[0]) >= (TOKEN_LENGTH - 1)) { return CalculateReturnCode::ERROR_TOKEN_LENGTH_EXCEEDED; }

    // read one token from the input stream
    oc = c;
    c  = *strpos;

    if (c != ' ')
    {
      // Get peek-ahead char
      pcpos = strpos + 1;
      pc = *pcpos;
      while ((pcpos < strend) && (' ' == pc)) {
        ++pcpos;
        pc = *pcpos;
      }
      // addLog(LOG_LEVEL_INFO, strformat(F("doCalculate is_number: oc: %d c: %d/%c pc: %d token: %s input: %s sl: %u"), oc, c, c, pc, token, input, sl));
      if ((pcpos >= strend)) {
        pc = '\0';
      }
      // If the token is a number (identifier), then add it to the token queue.
      if (is_number(oc, c, pc))
      {
        *TokenPos = c;
        ++TokenPos;
        *(TokenPos) = 0; // Mark current end of token string
      }

      // If the token is any operator, op1, then:
      else if (is_operator(c) || is_unary_operator(c) ||
               #if !defined(LIMIT_BUILD_SIZE) && FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES
               is_binary_operator(c) ||
               #endif // if !defined(LIMIT_BUILD_SIZE) && FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES
               is_quinary_operator(c))
      {
        *(TokenPos) = 0; // Mark end of token string
        error       = RPNCalculate(token);
        TokenPos    = token;

        if (isError(error)) { return error; }

        while (sl > 0 && sl < (OPERATOR_STACK_SIZE - 1))
        {
          sc = stack[sl - 1];

          // While there is an operator token, op2, at the top of the stack
          // op1 is left-associative and its precedence is less than or equal to that of op2,
          // or op1 has precedence less than that of op2,
          // The differing operator priority decides pop / push
          // If 2 operators have equal priority then associativity decides.
          if (is_operator(sc) &&
              (
                (op_left_assoc(c) && (op_preced(c) <= op_preced(sc))) ||
                (op_preced(c) < op_preced(sc))
              )
              )
          {
            // Pop op2 off the stack, onto the token queue;
            *TokenPos = sc;
            ++TokenPos;
            *(TokenPos) = 0; // Mark end of token string
            error       = RPNCalculate(token);
            TokenPos    = token;

            if (isError(error)) { return error; }
            sl--;
          }
          else {
            break;
          }
        }

        // push op1 onto the stack.
        stack[sl] = c;
        ++sl;
      }

      // Process the token at a colon (separator)
      else if (c == ':')
      {
        *(TokenPos) = 0; // Mark end of token string
        error       = RPNCalculate(token);
        TokenPos    = token;
        c           = 0; // reset
      }

      // If the token is a left parenthesis, then push it onto the stack.
      else if (c == '(')
      {
        if (sl >= OPERATOR_STACK_SIZE) { return CalculateReturnCode::ERROR_STACK_OVERFLOW; }
        stack[sl] = c;
        ++sl;
        c = 0; // reset
      }

      // If the token is a right parenthesis:
      else if (c == ')')
      {
        bool pe = false;
        sc = '\0';
        // addLog(LOG_LEVEL_INFO, strformat(F("doCalculate stack content 0x%s"), formatToHex_array(reinterpret_cast<const uint8_t*>(stack), sl + 1).c_str()));

        // Until the token at the top of the stack is a left parenthesis,
        // pop operators off the stack onto the token queue
        while (sl < OPERATOR_STACK_SIZE)
        {
          *(TokenPos) = 0; // Mark end of token string
          // addLog(LOG_LEVEL_INFO, strformat(F("doCalculate popping stack sl: %u token: %s sc: %d"), sl, token, sc));
          if (sc == '(') {
            // const auto first = pop(); // Get last value from stack
            // push(first); // push back
            // addLog(LOG_LEVEL_INFO, strformat(F("doCalculate at ( last value: %.4f sl: %u"), first, sl));
          } else {
            error = RPNCalculate(token);
            // const auto first = pop(); // Get last value from stack
            // push(first); // push back
            // addLog(LOG_LEVEL_INFO, strformat(F("doCalculate last value on stack: %.4f sl: %u"), first, sl));
          }
          TokenPos = token;

          if (isError(error)) { return error; }

          // we're not growing the stack: if (sl > OPERATOR_STACK_SIZE) { return CalculateReturnCode::ERROR_STACK_OVERFLOW; }
          if (sl > 0) {
            sc = stack[sl - 1];
          }
          if (pe || (sl == 0)) {
            break;
          }

          if (sc == '(')
          {
            pe = true;
            if (sl > 1) {
              sc = stack[sl - 2];
              if (is_operator(sc)) { // Not a function call
                // Don't touch
              } else if (is_unary_operator(sc) ||
                         #if !defined(LIMIT_BUILD_SIZE) && FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES
                         is_binary_operator(sc) ||
                         #endif // if !defined(LIMIT_BUILD_SIZE) && FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES
                         is_quinary_operator(sc)) { // Function call, so process the function too
                *TokenPos = sc;
                ++TokenPos;
                stack[sl - 2] = '\0'; // Don't process again on stack wind-down
              } else {
                sc = '\0'; // Reset
              }
            }
          }
          else
          {
            *TokenPos = sc;
            ++TokenPos;
            if (sl > 0) {
              sl--;
            }
            // addLog(LOG_LEVEL_INFO, strformat(F("doCalculate next token: %s sl: %u sc: %d"), token, sl, sc));
          }
        }

        // If the stack runs out without finding a left parenthesis, then there are mismatched parentheses.
        if (!pe) {
          return CalculateReturnCode::ERROR_PARENTHESES_MISMATCHED;
        }

        // Pop the left parenthesis from the stack, but not onto the token queue.
        if (sl > 0) {
          sl--;
        }

        // If the token at the top of the stack is a function token, pop it onto the token queue.
        // FIXME TD-er: This sc value is never used, it is re-assigned a new value before it is being checked.
        // if ((sl > 0) && (sl < OPERATOR_STACK_SIZE)) {
        //   sc = stack[sl - 1];
        // }
      }
      else {
        return CalculateReturnCode::ERROR_UNKNOWN_TOKEN;
      }
    }
    ++strpos;
  }

  // addLog(LOG_LEVEL_INFO, strformat(F("doCalculate final stack content 0x%s"), formatToHex_array(reinterpret_cast<const uint8_t*>(stack), sl + 1).c_str()));
  // When there are no more tokens to read:
  // While there are still operator tokens in the stack:
  while (sl > 0)
  {
    sc = stack[sl - 1];

    if ((sc == '(') || (sc == ')')) {
      return CalculateReturnCode::ERROR_PARENTHESES_MISMATCHED;
    }

    *(TokenPos) = 0; // Mark end of token string
    // addLog(LOG_LEVEL_INFO, strformat(F("doCalculate closing up stack sl: %u token: %s sc: %d"), sl, token, sc));
    error       = RPNCalculate(token);
    TokenPos    = token;
    // const auto first = pop(); // Get last value from stack
    // push(first); // push back
    // addLog(LOG_LEVEL_INFO, strformat(F("doCalculate closing, last value on stack: %.4f sl: %u"), first, sl));

    if (isError(error)) { return error; }
    *TokenPos = sc;
    ++TokenPos;
    --sl;
  }

  *(TokenPos) = 0; // Mark end of token string
  error       = RPNCalculate(token);
  TokenPos    = token;
  // const auto first = pop(); // Get last value from stack
  // push(first); // push back
  // addLog(LOG_LEVEL_INFO, strformat(F("doCalculate final value on stack: %.4f sl: %u"), first, sl));

  if (isError(error))
  {
    *result = 0;
    return error;
  }
  *result = *sp;
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("Calculate2"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  return CalculateReturnCode::OK;
}

void preProcessReplace(String& input, UnaryOperator op) {
  String find = toString(op);

  if (find.isEmpty()) { return; }
  find += '('; // Add opening parenthesis.

  const String replace = String(static_cast<char>(op)) + '(';

  input.replace(find, replace);
}

#if !defined(LIMIT_BUILD_SIZE) && FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES
void preProcessReplace(String& input, BinaryOperator op) {
  String find = toString(op);

  if (find.isEmpty()) { return; }
  find += '('; // Add opening parenthesis.

  const String replace = String(static_cast<char>(op)) + '(';

  input.replace(find, replace);
}
#endif // if !defined(LIMIT_BUILD_SIZE) && FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES

bool angleDegree(UnaryOperator op)
{
  switch (op) {
    case UnaryOperator::Sin_d:
    case UnaryOperator::Cos_d:
    case UnaryOperator::Tan_d:
    case UnaryOperator::ArcSin_d:
    case UnaryOperator::ArcCos_d:
    case UnaryOperator::ArcTan_d:
      return true;
    default:
      break;
  }
  return false;
}

#if !defined(LIMIT_BUILD_SIZE) && FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES
bool angleDegree(BinaryOperator op)
{
  return BinaryOperator::ArcTan2_d == op;
  // switch (op) // Future extension for 4 or more BinaryOperator options and multiple _d options
  // {
  // case BinaryOperator::ArcTan2_d:
  //   return true;
  // case BinaryOperator::ArcTan2:
  // case BinaryOperator::FMod:
  //   return false;
  // }
  // return false;
}
#endif // if !defined(LIMIT_BUILD_SIZE) && FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES

const __FlashStringHelper* toString(UnaryOperator op)
{
  switch (op) {
    case UnaryOperator::Not:
      break; // No need to replace
    case UnaryOperator::Log:
      return F("log");
    case UnaryOperator::Ln:
      return F("ln");
    case UnaryOperator::Abs:
      return F("abs");
    case UnaryOperator::Exp:
      return F("exp");
    case UnaryOperator::Sqrt:
      return F("sqrt");
    case UnaryOperator::Sq:
      return F("sq");
    case UnaryOperator::Round:
      return F("round");
    case UnaryOperator::Sin:
      return F("sin");
    case UnaryOperator::Sin_d:
      return F("sin_d");
    case UnaryOperator::Cos:
      return F("cos");
    case UnaryOperator::Cos_d:
      return F("cos_d");
    case UnaryOperator::Tan:
      return F("tan");
    case UnaryOperator::Tan_d:
      return F("tan_d");
    case UnaryOperator::ArcSin:
      return F("asin");
    case UnaryOperator::ArcSin_d:
      return F("asin_d");
    case UnaryOperator::ArcCos:
      return F("acos");
    case UnaryOperator::ArcCos_d:
      return F("acos_d");
    case UnaryOperator::ArcTan:
      return F("atan");
    case UnaryOperator::ArcTan_d:
      return F("atan_d");
    case UnaryOperator::Map:
      return F("map");
    case UnaryOperator::MapC:
      return F("mapc");
  }
  return F("");
}

#if !defined(LIMIT_BUILD_SIZE) && FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES
const __FlashStringHelper* toString(BinaryOperator op)
{
  switch (op) {
    case BinaryOperator::ArcTan2:
      return F("atan2");
    case BinaryOperator::ArcTan2_d:
      return F("atan2_d");
    case BinaryOperator::FMod:
      return F("fmod");
  }
  return F("");
}
#endif // if !defined(LIMIT_BUILD_SIZE) && FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES

String RulesCalculate_t::preProces(const String& input)
{
  String preprocessed = input;

  const UnaryOperator operators[] = {
    UnaryOperator::Not
    ,UnaryOperator::Log
    ,UnaryOperator::Ln
    ,UnaryOperator::Abs
    ,UnaryOperator::Exp
    ,UnaryOperator::Sqrt
    ,UnaryOperator::Sq
    ,UnaryOperator::Round
    #if FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES

    // Try the "arc" functions first or else "sin" is already replaced when "asin" is tried.
    ,UnaryOperator::ArcSin
    ,UnaryOperator::ArcSin_d
    ,UnaryOperator::Sin
    ,UnaryOperator::Sin_d

    ,UnaryOperator::ArcCos
    ,UnaryOperator::ArcCos_d
    ,UnaryOperator::Cos
    ,UnaryOperator::Cos_d

    ,UnaryOperator::ArcTan
    ,UnaryOperator::ArcTan_d
    ,UnaryOperator::Tan
    ,UnaryOperator::Tan_d
    #endif // if FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES
    ,UnaryOperator::Map // FIXME ?Move to new QuintaryOperator enum?
    ,UnaryOperator::MapC

  };

  constexpr size_t nrOperators = NR_ELEMENTS(operators);

  for (size_t i = 0; i < nrOperators; ++i) {
    const UnaryOperator op = operators[i];
#if FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES
    if (op == UnaryOperator::ArcSin && preprocessed.indexOf(F("sin")) == -1) i += 3;
    else if (op == UnaryOperator::ArcCos && preprocessed.indexOf(F("cos")) == -1) i += 3;
    else if (op == UnaryOperator::ArcTan && preprocessed.indexOf(F("tan")) == -1) i += 3;
    else 
#endif
    {
      preProcessReplace(preprocessed, op);
    }
  }

  #if !defined(LIMIT_BUILD_SIZE) && FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES

  const BinaryOperator operators2[] = {
    BinaryOperator::ArcTan2,
    BinaryOperator::ArcTan2_d,
    BinaryOperator::FMod,
  };

  constexpr size_t nrOperators2 = NR_ELEMENTS(operators2);
  for (size_t i = 0; i < nrOperators2; ++i) {
    const BinaryOperator op = operators2[i];
    preProcessReplace(preprocessed, op);
  }
  #endif // if !defined(LIMIT_BUILD_SIZE) && FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES

  return preprocessed;
}


#include "../Helpers/HTTPResponseParser.h"

#if RESPONSE_PARSER_SUPPORT
# include "../Helpers/Networking.h"
# include "../../ESPEasy_common.h"
# include "../../_Plugin_Helper.h"
# include "../Globals/EventQueue.h"
# include "../Helpers/StringConverter.h"
# include "../Helpers/StringProvider.h"

# if FEATURE_JSON_EVENT
#  include "../Helpers/ESPEasy_Storage.h"
#  include "../WebServer/LoadFromFS.h"

#  include <ArduinoJson.h>

# endif // if FEATURE_JSON_EVENT


void eventFromResponse(const String& host, const int& httpCode, const String& uri, HTTPClient& http) {
  if ((httpCode == 200)) {
    // -------------------------------------------------------------------------------------------Thingspeak
  # if FEATURE_THINGSPEAK_EVENT

    // Generate event with the response of a
    // thingspeak request (https://de.mathworks.com/help/thingspeak/readlastfieldentry.html &
    // https://de.mathworks.com/help/thingspeak/readdata.html)
    // e.g. command for a specific field: "sendToHTTP,api.thingspeak.com,80,/channels/1637928/fields/5/last.csv"
    // command for all fields: "sendToHTTP,api.thingspeak.com,80,/channels/1637928/feeds/last.csv"
    // where first eventvalue is the channel number and the second to the nineth event values
    // are the field values
    // Example of the event: "EVENT: ThingspeakReply=1637928,5,24.2,12,900,..."
    //                                                  ^    ^ └------┬------┘
    //                                   channel number ┘    |        └ received values
    //                                                   field number (only available for a "single-value-event")
    // In rules you can grep the reply by "On ThingspeakReply Do ..."
    // -----------------------------------------------------------------------------------------------------------------------------
    // 2024-02-05 - Added the option to get a single value of a field or all values of a channel at a certain time (not only the last entry)
    // Examples:
    // Single channel: "sendtohttp,api.thingspeak.com,80,channels/1637928/fields/1.csv?end=2024-01-01%2023:59:00&results=1"
    // => gets the value of field 1 at (or the last entry before) 23:59:00 of the channel 1637928
    // All channels: "sendtohttp,api.thingspeak.com,80,channels/1637928/feeds.csv?end=2024-01-01%2023:59:00&results=1"
    // => gets the value of each field of the channel 1637928 at (or the last entry before) 23:59:00
    // -----------------------------------------------------------------------------------------------------------------------------

    if (equals(host, F("api.thingspeak.com")) &&
        (uri.endsWith(F("/last.csv")) || ((uri.indexOf(F("results=1")) >= 0) && (uri.indexOf(F(".csv")) >= 0)))) {
      String result = http.getString();

      result.replace(' ', '_'); // if using a single field with a certain time, the result contains a space and would break the code
      const int posTimestamp = result.lastIndexOf(':');

      if (posTimestamp >= 0) {
        result = parseStringToEndKeepCase(result.substring(posTimestamp), 3);

        if (uri.indexOf(F("fields")) >= 0) {                                        // when there is a single field call add the field
                                                                                    // number
                                                                                    // before the value
          result = parseStringKeepCase(uri, 4, '/').substring(0, 1) + "," + result; // since the field number is always the fourth part of
                                                                                    // the
                                                                                    // url and is always a single digit, we can use this to
                                                                                    // extract the fieldnumber
        }
        eventQueue.addMove(strformat(
                             F("ThingspeakReply=%s,%s"),
                             parseStringKeepCase(uri, 2, '/').c_str(),
                             result.c_str()));
      }
    }
  # endif // if FEATURE_THINGSPEAK_EVENT

    // ------------------------------------------------------------------------------------------- OpenMeteo
  # if FEATURE_OPENMETEO_EVENT

    // Generate an event with the response of an open-meteo request.
    // Example command:
    // sendtohttp,api.open-meteo.com,80,"/v1/forecast?latitude=52.52&longitude=13.41¤t=temperature_2m,weather_code,wind_speed_10m&daily=temperature_2m_max,temperature_2m_min&forecast_days=1"
    // No need for an api key and it is free (daily requests are limited to 10,000 in the free version)
    // Visit the URL (https://open-meteo.com/en/docs) and build your personal URL by selecting the location and values you want to receive.
    // Supported variable kinds are current, hourly, daily!
    // In rules you can grep the reply by the kind of weather variables with "On Openmeteo#<type> Do ..."
    // e.g. "On Openmeteo#current Do ..."
    // Note: hourly and daily results are arrays which can become very long.
    // Best to make seperate calls. Especially for hourly results.

    // define limits
  #  define WEATHER_KEYS_MAX 10

    if (equals(host, F("api.open-meteo.com"))) {
      const String str = http.getString();

      if (str.length() > RESPONSE_MAX_LENGTH) {
        if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
          addLog(LOG_LEVEL_ERROR,
                 strformat(F("Response exceeds %d characters which could cause instabilities or crashes!"), RESPONSE_MAX_LENGTH));
        }
      }

      auto processAndQueueParams = [](const String& url, const String& str, const String& eventName) {
                                     // Extract the parameters from the URL
                                     int start = url.indexOf(eventName + '=');

                                     if (start == -1) {
                                       return; // No parameters found for the given eventName
                                     }
                                     start += eventName.length() + 1;
                                     const int end       = url.indexOf('&', start);
                                     const String params = (end == -1) ? url.substring(start) : url.substring(start, end);

                                     if (!params.isEmpty()) {
                                       String keys[WEATHER_KEYS_MAX];
                                       int    keyCount   = 0;
                                       int    startIndex = 0;
                                       int    commaIndex = params.indexOf(',');

                                       // Split and add keys to the array
                                       while (commaIndex != -1) {
                                         if (keyCount >= WEATHER_KEYS_MAX) {
                                           // Stop adding keys if array is full
                                           if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
                                             addLog(LOG_LEVEL_ERROR,
                                                    strformat(F(
                                                                "More than %d keys in the URL, this could cause instabilities or crashes! Try to split up the calls.."),
                                                              WEATHER_KEYS_MAX));
                                           }
                                           break;
                                         }
                                         String key = params.substring(startIndex, commaIndex);
                                         keys[keyCount++] = key;
                                         startIndex       = commaIndex + 1;
                                         commaIndex       = params.indexOf(',', startIndex);
                                       }

                                       // Add the last key
                                       if (keyCount < WEATHER_KEYS_MAX) {
                                         const String lastKey = params.substring(startIndex);
                                         keys[keyCount++] = lastKey;
                                       }

                                       String csv;
                                       const int startStringIndex = str.indexOf(strformat(F("\"%s\":"),
                                                                                          eventName.c_str())) +
                                                                    eventName.length() + 4;

                                       // example( },"current":{"time":... ) we want to start after current":{

                                       for (int i = 0; i < keyCount; i++) // Use keyCount to limit the iteration
                                       {
                                         String key = keys[i];
                                         String value;
                                         int    startIndex = str.indexOf(strformat(F("%s\":"), key.c_str()), startStringIndex);

                                         if (startIndex == -1) {
                                           // Handle case where key is not found
                                           value = F("-256"); // Placeholder value
                                         } else {
                                           int endIndex = 0;

                                           if (!equals(eventName, F("current"))) {
                                             // In daily and hourly, the values are stored in an array
                                             startIndex += key.length() + 3; // Move index past the key
                                             endIndex    = str.indexOf(']', startIndex);
                                           } else {
                                             startIndex += key.length() + 2; // Move index past the key
                                             endIndex    = str.indexOf(',', startIndex);
                                           }

                                           // Find the index of the next comma
                                           if ((endIndex == -1) || (endIndex > str.indexOf("}", startIndex))) {
                                             endIndex = str.indexOf('}', startIndex); // If no comma is found or comma comes after },
                                                                                      // take the rest of the string
                                           }

                                           value = str.substring(startIndex, endIndex);
                                           value.trim(); // Remove any surrounding whitespace
                                         }

                                         if (!csv.isEmpty()) {
                                           csv += ',';
                                         }
                                         csv += value;
                                       }
                                       eventQueue.addMove(strformat(F("OpenMeteo#%s=%s"), eventName.c_str(), csv.c_str()));
                                     }
                                   };

      processAndQueueParams(uri, str, F("current"));
      processAndQueueParams(uri, str, F("hourly"));
      processAndQueueParams(uri, str, F("daily"));
    }

  # endif // if FEATURE_OMETEO_EVENT

    // ------------------------------------------------------------------------------------------- JSONevent
  # if FEATURE_JSON_EVENT

    if ((uri.indexOf(F("#json")) != -1) || (uri.indexOf(F("?json")) != -1)) {
      int numJson = 0; // Default value

      int i = uri.length() - 1;

      // Check if the URL ends with a number
      if ((i > -1) && isDigit(uri.charAt(i))) {
        // Find the position of the last non-digit character
        while (i >= 0 && isDigit(uri.charAt(i))) {
          i--;
        }

        // Extract the number from the string
        numJson = uri.substring(i + 1).toInt();
      }

      const String message = http.getString();

      if (message.length() > RESPONSE_MAX_LENGTH) {
        if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
          addLog(LOG_LEVEL_ERROR,
                 strformat(F("Response exceeds %d characters which could cause instabilities or crashes!"), RESPONSE_MAX_LENGTH));
        }
      }

      DynamicJsonDocument*root       = nullptr;
      uint16_t lastJsonMessageLength = 512;

      // Cleanup lambda to deallocate resources
      auto cleanupJSON = [&root]() {
                           if (root != nullptr) {
                             root->clear();
                             delete root;
                             root = nullptr;
                           }
                         };

      // Check if root needs resizing or cleanup
      if ((root != nullptr) && (message.length() * 2.5 > lastJsonMessageLength)) {
        cleanupJSON();
      }

      // Resize lastJsonMessageLength if needed
      if (message.length() * 2 > lastJsonMessageLength) {
        lastJsonMessageLength = message.length() * 2;
      }

      // Allocate memory for root if needed
      if (root == nullptr) {
  #  ifdef USE_SECOND_HEAP
        HeapSelectIram ephemeral;
  #  endif // ifdef USE_SECOND_HEAP
        root = new (std::nothrow) DynamicJsonDocument(lastJsonMessageLength);
      }

      if (root != nullptr) {
        // Parse the JSON
        DeserializationError error = deserializeJson(*root, message);

        if (!error) {
          // Process the keys from the file
          readAndProcessJsonKeys(root, numJson);
        } else {
          if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
            addLog(LOG_LEVEL_ERROR, strformat(F("Parsing JSON failed: %s"), error.c_str()));
          }
        }

        // Cleanup JSON resources
        cleanupJSON();
      }
    }
    # endif // if FEATURE_JSON_EVENT
  }
}

// ------------------------------------------------------------------------------------------- JSONevent Key processing
   # if FEATURE_JSON_EVENT

void readAndProcessJsonKeys(DynamicJsonDocument *root, int numJson) {
#  if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
  int nr_decimals = ESPEASY_DOUBLE_NR_DECIMALS;
#  else // ifdef FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
  int nr_decimals = ESPEASY_FLOAT_NR_DECIMALS;
#  endif // ifdef FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE

  // Open the `json.keys` file
#  ifdef ESP8266
  const String fileName = F("json.keys");
#  else // ifdef ESP8266
  const String fileName = F("/json.keys");
#  endif // ifdef ESP8266

  if (!fileExists(fileName)) {
    if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
      addLogMove(LOG_LEVEL_ERROR, strformat(F("%s does not exist!"), fileName.c_str()));
    }
    return;
  }
  File keyFile = ESPEASY_FS.open(fileName, "r");

  int keyCount                   = 0;
  int successfullyProcessedCount = 0; // Counter for successfully processed keys
  String csvOutput;                   // Collect values as a CSV string

  while (keyFile.available()) {
    String key = keyFile.readStringUntil('\n');
    key.trim(); // Remove extra spaces or newlines

    if (key.isEmpty()) {
      continue;
    }
    int keyNumber = 0;

    // Extract the number prefix (before the first dot)
    if (key.indexOf(':') != -1) {
      keyNumber = key.substring(0, key.indexOf(':')).toInt();
      key       = key.substring(key.indexOf(':') + 1);
    }

    // Only process keys that match the filterNumber (or process all if no match)
    if ((numJson > 0) && (keyNumber != numJson)) {
      continue; // Skip this key if it doesn't match the filter number
    }
    keyCount++;

    if (keyCount > MAX_KEYS) {
      if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
        addLogMove(LOG_LEVEL_ERROR, strformat(F("Warning: More than %d keys in %s"), MAX_KEYS, fileName.c_str()));
      }
    }

    // Process the key and navigate the JSON
    JsonVariant value = *root;
    size_t start = 0, end;

    while ((end = key.indexOf('.', start)) != (unsigned int)-1) {
      const String part = key.substring(start, end);
      value = value[part];

      if (value.isNull()) {
        break; // Key path is invalid
      }
      start = end + 1;
    }

    // Handle the last part of the key
    if (!value.isNull()) {
      const String lastPart = key.substring(start);
      value = value[lastPart];
      successfullyProcessedCount++;
    }

    // Append the value to the CSV string if it exists
    if (!value.isNull()) {
      if (value.is<int>()) {
        csvOutput += String(value.as<int>());
      } else if (value.is<float>()) {
        csvOutput += doubleToString(value.as<double>(), nr_decimals, true);
      } else if (value.is<const char *>()) {
        csvOutput += String(value.as<const char *>());
      } else if (value.is<JsonArray>()) {
        // If the value is an array, iterate over its elements
        JsonArray array        = value.as<JsonArray>();
        size_t    arraySize    = array.size(); // Get the total number of elements in the array
        size_t    currentIndex = 0;            // Track the current index

        for (JsonVariant element : array) {
          if (element.is<int>()) {
            csvOutput += String(element.as<int>());
          } else if (element.is<float>()) {
            csvOutput += doubleToString(element.as<double>(), nr_decimals, true);
          } else if (element.is<const char *>()) {
            csvOutput += String(element.as<const char *>());
          } else {
            csvOutput += F("unknown");
          }

          // Add a comma unless it's the last element
          currentIndex++;

          if (currentIndex < arraySize) {
            csvOutput += ',';
          }
        }
      } else {
        csvOutput += F("unknown");
      }
    } else {
      csvOutput += F("null"); // Indicate missing value
    }
    csvOutput += ',';
  }

  keyFile.close();

  if (!csvOutput.isEmpty()) {                   // If there is nothing to show don't create an event
    if (csvOutput.charAt(csvOutput.length() - 1) == ',') {
      // Check if the last character is a comma
      csvOutput.remove(csvOutput.length() - 1); // Remove the last character
    }

    // Log the results
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO, strformat(F("Successfully processed %d out of %d keys"), successfullyProcessedCount, keyCount));
      eventQueue.addMove(strformat(F("JsonReply%s%s=%s"),
                                   numJson != 0 ? "#" : "",
                                   toStringNoZero(numJson).c_str(),
                                   csvOutput.c_str()));
    }
  }
}

  # endif // if FEATURE_JSON_EVENT
#endif // if RESPONSE_PARSER_SUPPORT

#include "../Helpers/Hardware_GPIO.h"

#ifdef ESP32S2
# include <driver/gpio.h>

# include "../Helpers/Hardware_device_info.h"

// ********************************************************************************
// Get info of a specific GPIO pin
// ********************************************************************************

bool getGpioInfo(int gpio, int& pinnr, bool& input, bool& output, bool& warning) {
  pinnr = -1; // ESP32 does not label the pins, they just use the GPIO number.

  input   = GPIO_IS_VALID_GPIO(gpio);
  output  = GPIO_IS_VALID_OUTPUT_GPIO(gpio);
  warning = isBootStrapPin(gpio);

  if (!(GPIO_IS_VALID_GPIO(gpio))) { return false; }

  // Input GPIOs:  0-21, 26, 33-46
  // Output GPIOs: 0-21, 26, 33-45
  input  = gpio <= 46;
  output = gpio <= 45;

  if ((gpio < 0) || ((gpio > 21) && (gpio < 33) && (gpio != 26))) {
    input  = false;
    output = false;
  }

  if (FoundPSRAM() && (gpio == 26)) {
    // Pin shared with the flash memory and/or PSRAM.
    // Cannot be used as regular GPIO
    input   = false;
    output  = false;
    warning = true;
  }

  if ((gpio > 26) && (gpio < 33)) {
    // SPIHD, SPIWP, SPICS0, SPICLK, SPIQ, SPID pins of ESP32-S2FH2 and ESP32-S2FH4
    // are connected to embedded flash and not recommended for other uses.
    warning = true;
  }

  return (input || output);
}

bool isBootStrapPin(int gpio)
{
  if (gpio == 45) {
    // VDD_SPI can work as the power supply for the external device at either
    // 1.8 V (when GPIO45 is 1 during boot), or
    // 3.3 V (when GPIO45 is 0 and at default state during boot).
    return true;
  }

  // GPIO 0  State during boot determines boot mode.
  if (gpio == 0) { return true; }

  if (gpio == 46) {
    // Strapping pin which must be low during flashing
    return true;
  }

  return false;
}

bool getGpioPullResistor(int gpio, bool& hasPullUp, bool& hasPullDown) {
  hasPullDown = false;
  hasPullUp   = false;

  if (!validGpio(gpio)) {
    return false;
  }

  // GPI: GPIO46 is fixed to pull-down and is input only.
  if (gpio <= 45) {
    hasPullUp   = true;
    hasPullDown = true;
  }
  return true;
}

bool validGpio(int gpio) {
  if (!GPIO_IS_VALID_GPIO(gpio)) { return false; }

  int  pinnr;
  bool input;
  bool output;
  bool warning;

  return getGpioInfo(gpio, pinnr, input, output, warning);
}

// Get ADC related info for a given GPIO pin
// @param gpio_pin   GPIO pin number
// @param adc        Number of ADC unit (0 == Hall effect)
// @param ch         Channel number on ADC unit
// @param t          index of touch pad ID
bool getADC_gpio_info(int gpio_pin, int& adc, int& ch, int& t)
{
  adc = -1;
  ch  = -1;
  t   = -1;

  switch (gpio_pin) {
    case 1: adc  = 1; ch = 0; t = 1; break;
    case 2: adc  = 1; ch = 1; t = 2; break;
    case 3: adc  = 1; ch = 2; t = 3; break;
    case 4: adc  = 1; ch = 3; t = 4; break;
    case 5: adc  = 1; ch = 4; t = 5; break;
    case 6: adc  = 1; ch = 5; t = 6; break;
    case 7: adc  = 1; ch = 6; t = 7; break;
    case 8: adc  = 1; ch = 7; t = 8; break;
    case 9: adc  = 1; ch = 8; t = 9; break;
    case 10: adc = 1; ch = 9; t = 10; break;
    case 11: adc = 2; ch = 0; t = 11; break;
    case 12: adc = 2; ch = 1; t = 12; break;
    case 13: adc = 2; ch = 2; t = 13; break;
    case 14: adc = 2; ch = 3; t = 14; break;
    case 15: adc = 2; ch = 4;  break;
    case 16: adc = 2; ch = 5;  break;
    case 17: adc = 2; ch = 6;  break;
    case 18: adc = 2; ch = 7;  break;
    case 19: adc = 2; ch = 8;  break;
    case 20: adc = 2; ch = 9;  break;
    default:
      return false;
  }

  return true;
}

#if SOC_TOUCH_SENSOR_SUPPORTED
int touchPinToGpio(int touch_pin)
{
  switch (touch_pin) {
    case 1: return T1;
    case 2: return T2;
    case 3: return T3;
    case 4: return T4;
    case 5: return T5;
    case 6: return T6;
    case 7: return T7;
    case 8: return T8;
    case 9: return T9;
    case 10: return T10;
    case 11: return T11;
    case 12: return T12;
    case 13: return T13;
    case 14: return T14;
    default:
      break;
  }

  return -1;
}
#endif

#if SOC_DAC_SUPPORTED
// Get DAC related info for a given GPIO pin
// @param gpio_pin   GPIO pin number
// @param dac        Number of DAC unit
bool getDAC_gpio_info(int gpio_pin, int& dac)
{
  switch (gpio_pin) {
    case 17: dac = 1; break;
    case 18: dac = 2; break;
    default:
      return false;
  }
  return true;
}
#endif

#endif // ifdef ESP32S2

#include "../Helpers/Scheduler.h"

#include "../DataTypes/SchedulerTimerType.h"

#ifndef BUILD_NO_DEBUG
# include "../DataStructs/Scheduler_ConstIntervalTimerID.h"
# include "../DataStructs/Scheduler_GPIOTimerID.h"
# include "../DataStructs/Scheduler_IntendedRebootTimerID.h"
# include "../DataStructs/Scheduler_PluginDeviceTimerID.h"
# include "../DataStructs/Scheduler_PluginTaskTimerID.h"
# include "../DataStructs/Scheduler_RulesTimerID.h"
# include "../DataStructs/Scheduler_SystemEventQueueTimerID.h"
# include "../DataStructs/Scheduler_TaskDeviceTimerID.h"
#endif // ifndef BUILD_NO_DEBUG

String ESPEasy_Scheduler::decodeSchedulerId(SchedulerTimerID timerID) {
  if (timerID.mixed_id == 0) {
    return F("Background Task");
  }
  String result = toString(timerID.getTimerType());

  result += F(": ");

#ifndef BUILD_NO_DEBUG

  String decoded;

  // Must call decode on specific class instance.
  switch (timerID.getTimerType()) {
    case SchedulerTimerType_e::ConstIntervalTimer:
      decoded = reinterpret_cast<const ConstIntervalTimerID *>(&timerID)->decode();
      break;
    case SchedulerTimerType_e::GPIO_timer:
      decoded = reinterpret_cast<const GPIOTimerID *>(&timerID)->decode();
      break;
    case SchedulerTimerType_e::IntendedReboot:
      decoded = reinterpret_cast<const IntendedRebootTimerID *>(&timerID)->decode();
      break;
    case SchedulerTimerType_e::PLUGIN_DEVICETIMER_IN_e:
      decoded = reinterpret_cast<const PluginDeviceTimerID *>(&timerID)->decode();
      break;
    case SchedulerTimerType_e::PLUGIN_TASKTIMER_IN_e:
      decoded = reinterpret_cast<const PluginTaskTimerID *>(&timerID)->decode();
      break;
    case SchedulerTimerType_e::RulesTimer:
      decoded = reinterpret_cast<const RulesTimerID *>(&timerID)->decode();
      break;
    case SchedulerTimerType_e::SystemEventQueue:
      decoded = reinterpret_cast<const SystemEventQueueTimerID *>(&timerID)->decode();
      break;
    case SchedulerTimerType_e::TaskDeviceTimer:
      decoded = reinterpret_cast<const TaskDeviceTimerID *>(&timerID)->decode();
      break;

      // TD-er: Do not add a default: case here, so the compiler will warn us when we're missing one later.
  }

  if (!decoded.isEmpty()) {
    result += decoded;
    return result;
  }

#endif // ifndef BUILD_NO_DEBUG
  result += F(" timer, id: ");
  result += timerID.getId();
  return result;
}

#include "../Helpers/Misc.h"

#include "../../ESPEasy-Globals.h"
#include "../../ESPEasy_common.h"
#include "../../_Plugin_Helper.h"
#include "../ESPEasyCore/ESPEasy_backgroundtasks.h"
#include "../ESPEasyCore/Serial.h"
#include "../Globals/ESPEasy_time.h"
#include "../Globals/Statistics.h"
#include "../Helpers/_CPlugin_init.h"
#include "../Helpers/ESPEasy_FactoryDefault.h"
#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/Numerical.h"
#include "../Helpers/PeriodicalActions.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringParser.h"

#if FEATURE_SD
# include <SD.h>
#endif // if FEATURE_SD


bool remoteConfig(struct EventStruct *event, const String& string)
{
  // FIXME TD-er: Why have an event here as argument? It is not used.
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("remoteConfig"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  bool   success = false;
  String command = parseString(string, 1);

  if (equals(command, F("config")))
  {
    // Command: "config,task,<taskname>,<actual Set Config command>"
    if (equals(parseString(string, 2), F("task")))
    {
      String configTaskName = parseStringKeepCase(string, 3);

      // FIXME TD-er: This command is not using the tolerance setting
      // tolerantParseStringKeepCase(Line, 4);
      String configCommand = parseStringToEndKeepCase(string, 4);

      if ((configTaskName.isEmpty()) || (configCommand.isEmpty())) {
        return success;
      }
      taskIndex_t index = findTaskIndexByName(configTaskName);

      if (validTaskIndex(index))
      {
        event->setTaskIndex(index);
        success = PluginCall(PLUGIN_SET_CONFIG, event, configCommand);
      }
    } else {
      addLog(LOG_LEVEL_ERROR, F("Expected syntax: config,task,<taskname>,<config command>"));
    }
  }
  return success;
}

/********************************************************************************************\
   delay in milliseconds with background processing
 \*********************************************************************************************/
void delayBackground(unsigned long dsdelay)
{
  unsigned long timer = millis() + dsdelay;

  while (!timeOutReached(timer)) {
    backgroundtasks();
  }
}

/********************************************************************************************\
   Toggle controller enabled state
 \*********************************************************************************************/
bool setControllerEnableStatus(controllerIndex_t controllerIndex, bool enabled)
{
  if (!validControllerIndex(controllerIndex)) { return false; }
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("setControllerEnableStatus"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  // Only enable controller if it has a protocol configured
  if ((Settings.Protocol[controllerIndex] != 0) || !enabled) {
    Settings.ControllerEnabled[controllerIndex] = enabled;
    const protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(controllerIndex);

    if (validProtocolIndex(ProtocolIndex)) {
      struct EventStruct TempEvent;
      TempEvent.ControllerIndex = controllerIndex;
      String dummy;
      const CPlugin::Function cfunction =
        enabled ? CPlugin::Function::CPLUGIN_INIT : CPlugin::Function::CPLUGIN_EXIT;
      return CPluginCall(ProtocolIndex, cfunction, &TempEvent, dummy) || CPlugin::Function::CPLUGIN_EXIT == cfunction;
    }
  }
  return false;
}

/********************************************************************************************\
   Toggle task enabled state
 \*********************************************************************************************/
bool setTaskEnableStatus(struct EventStruct *event, bool enabled)
{
  if (!validTaskIndex(event->TaskIndex)) { return false; }
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("setTaskEnableStatus"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  // Only enable task if it has a Plugin configured
  if (validPluginID(Settings.getPluginID_for_task(event->TaskIndex)) || !enabled) {
    if (enabled != Settings.TaskDeviceEnabled[event->TaskIndex])
    {
      String dummy;

      if (!enabled) {
        PluginCall(PLUGIN_EXIT, event, dummy);
      }

      // Toggle enable/disable state via command
      // FIXME TD-er: Should this be a 'runtime' change, or actually change the intended state?
      // Settings.TaskDeviceEnabled[event->TaskIndex].enabled = enabled;
      Settings.TaskDeviceEnabled[event->TaskIndex] = enabled;

      if (enabled) {
        // Schedule the plugin to be read.
        // Do this before actual init, to allow the plugin to schedule a specific first read.
        Scheduler.schedule_task_device_timer(event->TaskIndex, millis() + 10);

        if (!PluginCall(PLUGIN_INIT, event, dummy)) {
          return false;
        }
      }
    }
    return true;
  }
  return false;
}

/********************************************************************************************\
   Clear task settings for given task
 \*********************************************************************************************/
void taskClear(taskIndex_t taskIndex, bool save)
{
  if (!validTaskIndex(taskIndex)) { return; }
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("taskClear"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  if (Settings.TaskDeviceEnabled[taskIndex]) {
    struct EventStruct TempEvent(taskIndex);
    String dummy;
    PluginCall(PLUGIN_EXIT, &TempEvent, dummy);
  }
  Settings.clearTask(taskIndex);
  clearTaskCache(taskIndex); // Invalidate any cached values.
  ExtraTaskSettings.clear();
  ExtraTaskSettings.TaskIndex = taskIndex;

  if (save) {
    #ifndef BUILD_MINIMAL_OTA
    addLog(LOG_LEVEL_INFO, F("taskClear() save settings"));
    #endif // ifndef BUILD_MINIMAL_OTA
    SaveTaskSettings(taskIndex);
    SaveSettings();
  }
}

/********************************************************************************************\
   check the program memory hash
   The const MD5_MD5_MD5_MD5_BoundariesOfTheSegmentsGoHere... needs to remain unchanged as it will be replaced by
   - 16 bytes md5 hash, followed by
   - 4 * uint32_t start of memory segment 1-4
   - 4 * uint32_t end of memory segment 1-4
   currently there are only two segemts included in the hash. Unused segments have start adress 0.
   Execution time 520kb @80Mhz: 236ms
   Returns: 0 if hash compare fails, number of checked bytes otherwise.
   The reference hash is calculated by a .py file and injected into the binary.
   Caution: currently the hash sits in an unchecked segment. If it ever moves to a checked segment, make sure
   it is excluded from the calculation !
 \*********************************************************************************************/
#if defined(ARDUINO_ESP8266_RELEASE_2_3_0)
void dump(uint32_t addr) { // Seems already included in core 2.4 ...
  serialPrint(String(addr, HEX));
  serialPrint(": ");

  for (uint32_t a = addr; a < addr + 16; a++)
  {
    serialPrint(String(pgm_read_byte(a), HEX));
    serialPrint(" ");
  }
  serialPrintln();
}

#endif // if defined(ARDUINO_ESP8266_RELEASE_2_3_0)

/*
   uint32_t progMemMD5check(){
    checkRAM(F("progMemMD5check"));
 #define BufSize 10
    uint32_t calcBuffer[BufSize];
    CRCValues.numberOfCRCBytes = 0;
    memcpy (calcBuffer,CRCValues.compileTimeMD5,16);                                                  // is there still the dummy in memory
       ? - the dummy needs to be replaced by the real md5 after linking.
    if( memcmp (calcBuffer, "MD5_MD5_MD5_",12)==0){                                                   // do not memcmp with CRCdummy
       directly or it will get optimized away.
        addLog(LOG_LEVEL_INFO, F("CRC  : No program memory checksum found. Check output of crc2.py"));
        return 0;
    }
    MD5Builder md5;
    md5.begin();
    for (int l = 0; l<4; l++){                                                                            // check max segments,  if the
       pointer is not 0
        uint32_t *ptrStart = (uint32_t *)&CRCValues.compileTimeMD5[16+l*4];
        uint32_t *ptrEnd =   (uint32_t *)&CRCValues.compileTimeMD5[16+4*4+l*4];
        if ((*ptrStart) == 0) break;                                                                      // segment not used.
        for (uint32_t i = *ptrStart; i< (*ptrEnd) ; i=i+sizeof(calcBuffer)){                              // "<" includes last byte
             for (int buf = 0; buf < BufSize; buf ++){
                calcBuffer[buf] = pgm_read_dword((uint32_t*)i+buf);                                       // read 4 bytes
                CRCValues.numberOfCRCBytes+=sizeof(calcBuffer[0]);
             }
             md5.add(reinterpret_cast<const uint8_t *>(&calcBuffer[0]),(*ptrEnd-i)<sizeof(calcBuffer) ? (*ptrEnd-i):sizeof(calcBuffer) );
                    // add buffer to md5.
                At the end not the whole buffer. md5 ptr to data in ram.
        }
   }
   md5.calculate();
   md5.getBytes(CRCValues.runTimeMD5);
   if ( CRCValues.checkPassed())  {
      addLog(LOG_LEVEL_INFO, F("CRC  : program checksum       ...OK"));
      return CRCValues.numberOfCRCBytes;
   }
   addLog(LOG_LEVEL_INFO, F("CRC  : program checksum       ...FAIL"));
   return 0;
   }
 */

/********************************************************************************************\
   Handler for keeping ExtraTaskSettings up to date using cache
 \*********************************************************************************************/
String getTaskDeviceName(taskIndex_t TaskIndex) {
  return Cache.getTaskDeviceName(TaskIndex);
}

/********************************************************************************************\
   Handler for getting Value Names from TaskIndex

   - value names can be accessed with task variable index
   - maximum number of variables <= defined number of variables in plugin
 \*********************************************************************************************/
String getTaskValueName(taskIndex_t TaskIndex, uint8_t TaskValueIndex) {
  const int valueCount = getValueCountForTask(TaskIndex);

  if (TaskValueIndex < valueCount) {
    return Cache.getTaskDeviceValueName(TaskIndex, TaskValueIndex);
  }
  return EMPTY_STRING;
}

/********************************************************************************************\
   If RX and TX tied together, perform emergency reset to get the system out of boot loops
 \*********************************************************************************************/
void emergencyReset()
{
  // Direct Serial is allowed here, since this is only an emergency task.
  ESPEASY_SERIAL_0.begin(115200);
  ESPEASY_SERIAL_0.write(0xAA);
  ESPEASY_SERIAL_0.write(0x55);
  delay(1);

  if (ESPEASY_SERIAL_0.available() == 2) {
    if ((ESPEASY_SERIAL_0.read() == 0xAA) && (ESPEASY_SERIAL_0.read() == 0x55))
    {
      serialPrintln(F("\n\n\rSystem will reset to factory defaults in 10 seconds..."));
      delay(10000);
      ResetFactory();
    }
  }
}

/********************************************************************************************\
   Delayed reboot, in case of issues, do not reboot with high frequency as it might not help...
 \*********************************************************************************************/
void delayedReboot(int rebootDelay, IntendedRebootReason_e reason)
{
  // Direct Serial is allowed here, since this is only an emergency task.
  while (rebootDelay != 0)
  {
    serialPrint(F("Delayed Reset "));
    serialPrintln(String(rebootDelay));
    rebootDelay--;
    delay(1000);
  }
  reboot(reason);
}

void reboot(IntendedRebootReason_e reason) {
  prepareShutdown(reason);
  #if defined(ESP32)
  ESP.restart();
  #else // if defined(ESP32)
  ESP.reset();
  #endif // if defined(ESP32)
}

void FeedSW_watchdog()
{
  #ifdef ESP8266
  ESP.wdtFeed();
  #endif // ifdef ESP8266
}

void SendValueLogger(taskIndex_t TaskIndex)
{
#if !defined(BUILD_NO_DEBUG) || FEATURE_SD
  bool   featureSD = false;
  String logger;
  # if FEATURE_SD
  featureSD = true;
  # endif // if FEATURE_SD

  if (featureSD
      # ifndef BUILD_NO_DEBUG
      || loglevelActiveFor(LOG_LEVEL_DEBUG)
      # endif // ifndef BUILD_NO_DEBUG
      ) {
    const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(TaskIndex);

    if (validDeviceIndex(DeviceIndex)) {
      const uint8_t valueCount = getValueCountForTask(TaskIndex);
      String taskName          = getTaskDeviceName(TaskIndex);

      const String logline_prefix =
        strformat(F("%s %s,%d,%s")
                  , node_time.getDateString('-').c_str()
                  , node_time.getTimeString(':').c_str()
                  , Settings.Unit
                  , taskName.c_str()
                  );

      for (uint8_t varNr = 0; varNr < valueCount; varNr++)
      {
        logger += strformat(F("%s,%s,%s\r\n")
                            , logline_prefix.c_str()
                            , Cache.getTaskDeviceValueName(TaskIndex, varNr).c_str()
                            , formatUserVarNoCheck(TaskIndex, varNr).c_str()
                            );
      }
      # if FEATURE_STRING_VARIABLES

      if (Settings.EventAndLogDerivedTaskValues(TaskIndex)) {
        taskName.toLowerCase();
        String postfix;
        const String search = getDerivedValueSearchAndPostfix(taskName, postfix);

        auto it = customStringVar.begin();

        while (it != customStringVar.end()) {
          if (it->first.startsWith(search) && it->first.endsWith(postfix)) {
            String valueName    = it->first.substring(search.length(), it->first.indexOf('-'));
            const String vname2 = getDerivedValueName(taskName, valueName);

            if (!vname2.isEmpty()) {
              valueName = vname2;
            }

            if (!it->second.isEmpty()) {
              String value(it->second);
              value   = parseTemplateAndCalculate(value);
              logger += strformat(F("%s,%s,%s\r\n")
                                  , logline_prefix.c_str()
                                  , valueName.c_str()
                                  , value.c_str()
                                  );
            }
          }
          else if (it->first.substring(0, search.length()).compareTo(search) > 0) {
            break;
          }
          ++it;
        }
      }
      # endif // if FEATURE_STRING_VARIABLES
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_DEBUG, logger);
      # endif // ifndef BUILD_NO_DEBUG
    }
  }
#endif // if !defined(BUILD_NO_DEBUG) || FEATURE_SD

#if FEATURE_SD

  if (!logger.isEmpty()) {
    String   filename = patch_fname(F("VALUES.CSV"));
    fs::File logFile  = SD.open(filename, "a+");

    if (logFile) {
      logFile.print(logger);
    }
    logFile.close();
  }
#endif // if FEATURE_SD
}

// #######################################################################################################
// ############################ quite acurate but slow color converter####################################
// #######################################################################################################
// uses H 0..360 S 1..100 I/V 1..100 (according to homie convention)
// Source https://blog.saikoled.com/post/44677718712/how-to-convert-from-hsi-to-rgb-white

void HSV2RGB(float H, float S, float I, int rgb[3]) {
  // FIXME TD-er:   Why not just call HSV2RGBW and leave out the W part?

  int rgbw[4]{};

  HSV2RGBW(H, S, I, rgbw);
  memcpy(rgb, rgbw, 3 * sizeof(int));

  /*

     int r, g, b;

     H = fmod(H, 360);                           // cycle H around to 0-360 degrees
     constexpr float deg2rad = 3.14159f / 180.0f;
     H *= deg2rad;                               // Convert to radians.
     S = S / 100;
     S = S > 0 ? (S < 1 ? S : 1) : 0;            // clamp S and I to interval [0,1]
     I = I / 100;
     I = I > 0 ? (I < 1 ? I : 1) : 0;

     // Math! Thanks in part to Kyle Miller.
     if (H < 2.09439f) {
     r = 255 * I / 3 * (1 + S * cosf(H) / cosf(1.047196667f - H));
     g = 255 * I / 3 * (1 + S * (1 - cosf(H) / cosf(1.047196667f - H)));
     b = 255 * I / 3 * (1 - S);
     } else if (H < 4.188787f) {
     H = H - 2.09439f;
     g = 255 * I / 3 * (1 + S * cosf(H) / cosf(1.047196667f - H));
     b = 255 * I / 3 * (1 + S * (1 - cosf(H) / cosf(1.047196667f - H)));
     r = 255 * I / 3 * (1 - S);
     } else {
     H = H - 4.188787f;
     b = 255 * I / 3 * (1 + S * cosf(H) / cosf(1.047196667f - H));
     r = 255 * I / 3 * (1 + S * (1 - cosf(H) / cosf(1.047196667f - H)));
     g = 255 * I / 3 * (1 - S);
     }
     rgb[0] = r;
     rgb[1] = g;
     rgb[2] = b;
   */
}

// uses H 0..360 S 1..100 I/V 1..100 (according to homie convention)
// Source https://blog.saikoled.com/post/44677718712/how-to-convert-from-hsi-to-rgb-white
void HSV2RGBW(float H, float S, float I, int rgbw[4]) {
  H = fmod(H, 360);                 // cycle H around to 0-360 degrees
  constexpr float deg2rad = 3.14159f / 180.0f;
  H *= deg2rad;                     // Convert to radians.
  S  = S / 100;
  S  = S > 0 ? (S < 1 ? S : 1) : 0; // clamp S and I to interval [0,1]
  I  = I / 100;
  I  = I > 0 ? (I < 1 ? I : 1) : 0;

  #define RGB_ORDER 0
  #define BRG_ORDER 1
  #define GBR_ORDER 2

  int order = RGB_ORDER;

  constexpr float ANGLE_120_DEG = 120.0f * deg2rad;
  constexpr float ANGLE_240_DEG = 240.0f * deg2rad;
  constexpr float ANGLE_60_DEG  =  60.0f * deg2rad;

  if (H < ANGLE_120_DEG) {
    order = RGB_ORDER;
  } else if (H < ANGLE_240_DEG) {
    H     = H - ANGLE_120_DEG;
    order = BRG_ORDER;
  } else {
    H     = H - ANGLE_240_DEG;
    order = GBR_ORDER;
  }
  const float cos_h      = cosf(H);
  const float cos_1047_h = cosf(ANGLE_60_DEG - H);

  const int r = S * 255 * I / 3 * (1 + cos_h / cos_1047_h);
  const int g = S * 255 * I / 3 * (1 + (1 - cos_h / cos_1047_h));
  const int b = 0;
  rgbw[3] = 255 * (1 - S) * I;

  if (RGB_ORDER == order) {
    rgbw[0] = r;
    rgbw[1] = g;
    rgbw[2] = b;
  } else if (BRG_ORDER == order) {
    rgbw[0] = b;
    rgbw[1] = r;
    rgbw[2] = g;
  } else if (GBR_ORDER == order) {
    rgbw[0] = g;
    rgbw[1] = b;
    rgbw[2] = r;
  }
}

// Convert RGB Color to HSV Color
void RGB2HSV(uint8_t r, uint8_t g, uint8_t b, float hsv[3]) {
  const float rf = static_cast<float>(r) / 255.0f;
  const float gf = static_cast<float>(g) / 255.0f;
  const float bf = static_cast<float>(b) / 255.0f;
  float maxval   = rf;

  if (gf > maxval) { maxval = gf; }

  if (bf > maxval) { maxval = bf; }
  float minval = rf;

  if (gf < minval) { minval = gf; }

  if (bf < minval) { minval = bf; }
  float h = 0.0f, s, v = maxval;
  float f = maxval - minval;

  s = maxval == 0.0f ? 0.0f : f / maxval;

  if (maxval == minval) {
    h = 0.0f; // achromatic
  } else {
    if (maxval == rf) {
      h = (gf - bf) / f + (gf < bf ? 6.0f : 0.0f);
    } else if (maxval == gf) {
      h = (bf - rf) / f + 2.0f;
    } else if (maxval == bf) {
      h = (rf - gf) / f + 4.0f;
    }
    h /= 6.0f;
  }

  hsv[0] = h * 360.0f;
  hsv[1] = s * 255.0f;
  hsv[2] = v * 255.0f;
}

float getCPUload() {
  return 100.0f - Scheduler.getIdleTimePct();
}

int getLoopCountPerSec() {
  return loopCounterLast / 30;
}

int getUptimeMinutes() {
  return wdcounter / 2;
}

/******************************************************************************
 * scan an int array of specified size for a value
 *****************************************************************************/
bool intArrayContains(const int arraySize, const int array[], const int& value) {
  for (int i = 0; i < arraySize; i++) {
    if (array[i] == value) { return true; }
  }
  return false;
}

bool intArrayContains(const int arraySize, const uint8_t array[], const uint8_t& value) {
  for (int i = 0; i < arraySize; i++) {
    if (array[i] == value) { return true; }
  }
  return false;
}

#ifndef BUILD_NO_RAM_TRACKER
void logMemUsageAfter(const __FlashStringHelper *function, int value) {
  // Store free memory in an int, as subtracting may sometimes result in negative value.
  // The recorded used memory is not an exact value, as background (or interrupt) tasks may also allocate or free heap memory.
  static int last_freemem = ESP.getFreeHeap();
  const int  freemem_end  = ESP.getFreeHeap();

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    String log;

    if (reserve_special(log, 128)) {
      log  = F("After ");
      log += function;

      if (value >= 0) {
        log += value;
      }

      while (log.length() < 30) { log += ' '; }
      log += F("Free mem after: ");
      log += freemem_end;

      while (log.length() < 55) { log += ' '; }
      log += F("diff: ");
      log += last_freemem - freemem_end;
      addLogMove(LOG_LEVEL_DEBUG, log);
    }
  }

  last_freemem = freemem_end;
}

#endif // ifndef BUILD_NO_RAM_TRACKER

#include "../Helpers/Hardware_GPIO.h"

#ifdef ESP32_CLASSIC
# include <driver/gpio.h>

# include "../Globals/Settings.h"
# include "../Helpers/Hardware_device_info.h"

// ********************************************************************************
// Get info of a specific GPIO pin
// ********************************************************************************

bool getGpioInfo(int gpio, int& pinnr, bool& input, bool& output, bool& warning) {
  pinnr = -1; // ESP32 does not label the pins, they just use the GPIO number.

  input   = GPIO_IS_VALID_GPIO(gpio);
  output  = GPIO_IS_VALID_OUTPUT_GPIO(gpio);
  warning = isBootStrapPin(gpio);

  if ((gpio < 0) || !(GPIO_IS_VALID_GPIO(gpio))) { return false; }


  // ESP32 classic

  // Input GPIOs:  0-19, 21-23, 25-27, 32-39
  // Output GPIOs: 0-19, 21-23, 25-27, 32-33
  input  = gpio <= 39;
  output = gpio <= 33;

  if ((gpio < 0) || (gpio == 20) || (gpio == 24) || ((gpio > 27) && (gpio < 32))) {
    input  = false;
    output = false;
  }

  if ((gpio == 37) || (gpio == 38)) {
    // Pins are not present on the ESP32
    input  = true;
    output = false;
  }

  if (isFlashInterfacePin_ESPEasy(gpio)) {
    // Connected to the integrated SPI flash.
    input   = false;
    output  = false;
    warning = true;
  }

# if FEATURE_ETHERNET

  // Check pins used for RMII Ethernet PHY
  if (NetworkMedium_t::Ethernet == Settings.NetworkMedium) {
    switch (gpio) {
      case 0:
      case 21:
      case 19:
      case 22:
      case 25:
      case 26:
      case 27:
        warning = true;
        break;
    }


    // FIXME TD-er: Must we also check for pins used for MDC/MDIO and Eth PHY power?
  }
# endif // if FEATURE_ETHERNET

  if (FoundPSRAM()) {
    // ESP32 PSRAM can use GPIO 16 and 17
    // There will be a high frequency signal on those pins (flash frequency)
    // which makes them unusable for other purposes.
    // WROVER does not even have these pins made available on the outside.
    switch (gpio) {
      case 16:
      case 17:
        warning = true;
        break;
    }
  }

  return (input || output);
}

bool isBootStrapPin(int gpio)
{
  // GPIO 0 & 2 can't be used as an input. State during boot is dependent on boot mode.
  if ((gpio == 0) || (gpio == 2)) {
    return true;
  }

  if (gpio == 12) {
    // If driven High, flash voltage (VDD_SDIO) is 1.8V not default 3.3V.
    // Has internal pull-down, so unconnected = Low = 3.3V.
    // May prevent flashing and/or booting if 3.3V flash is used and this pin is
    // pulled high, causing the flash to brownout.
    // See the ESP32 datasheet for more details.
    return true;
  }

  if (gpio == 15) {
    // If driven Low, silences boot messages printed by the ROM bootloader.
    // Has an internal pull-up, so unconnected = High = normal output.
    return true;
  }


  return false;
}

bool getGpioPullResistor(int gpio, bool& hasPullUp, bool& hasPullDown) {
  hasPullDown = false;
  hasPullUp   = false;

  int  pinnr;
  bool input;
  bool output;
  bool warning;

  if (!getGpioInfo(gpio, pinnr, input, output, warning)) {
    return false;
  }

  // ESP32 classic
  if (gpio >= 34) {
    // For GPIO 34 .. 39, no pull-up nor pull-down.
  } else if (gpio == 12) {
    // No Pull-up on GPIO12
    // compatible with the SDIO protocol.
    // Just connect GPIO12 to VDD via a 10 kOhm resistor.
  } else {
    hasPullUp   = true;
    hasPullDown = true;
  }

  return true;
}

bool validGpio(int gpio) {
  if (gpio < 0) { return false; }

  if (!GPIO_IS_VALID_GPIO(gpio)) { return false; }

  int  pinnr;
  bool input;
  bool output;
  bool warning;

  return getGpioInfo(gpio, pinnr, input, output, warning);
}

// Get ADC related info for a given GPIO pin
// @param gpio_pin   GPIO pin number
// @param adc        Number of ADC unit (0 == Hall effect)
// @param ch         Channel number on ADC unit
// @param t          index of touch pad ID
bool getADC_gpio_info(int gpio_pin, int& adc, int& ch, int& t)
{
  adc = -1;
  ch  = -1;
  t   = -1;

  // Classic ESP32
  switch (gpio_pin) {
    case -1: adc = 0; break; // Hall effect Sensor
    case 36: adc = 1; ch = 0; break;
    case 37: adc = 1; ch = 1; break;
    case 38: adc = 1; ch = 2; break;
    case 39: adc = 1; ch = 3; break;
    case 32: adc = 1; ch = 4; t = 9; break;
    case 33: adc = 1; ch = 5; t = 8; break;
    case 34: adc = 1; ch = 6; break;
    case 35: adc = 1; ch = 7; break;
    case 4:  adc = 2; ch = 0; t = 0; break;
    case 0:  adc = 2; ch = 1; t = 1; break;
    case 2:  adc = 2; ch = 2; t = 2; break;
    case 15: adc = 2; ch = 3; t = 3; break;
    case 13: adc = 2; ch = 4; t = 4; break;
    case 12: adc = 2; ch = 5; t = 5; break;
    case 14: adc = 2; ch = 6; t = 6; break;
    case 27: adc = 2; ch = 7; t = 7; break;
    case 25: adc = 2; ch = 8; break;
    case 26: adc = 2; ch = 9; break;
    default:
      return false;
  }

  return true;
}

#if SOC_TOUCH_SENSOR_SUPPORTED
int touchPinToGpio(int touch_pin)
{
  // ESP32 classic
  switch (touch_pin) {
    case 0: return T0;
    case 1: return T1;
    case 2: return T2;
    case 3: return T3;
    case 4: return T4;
    case 5: return T5;
    case 6: return T6;
    case 7: return T7;
    case 8: return T8;
    case 9: return T9;
    default:
      break;
  }
  return -1;
}
#endif

#if SOC_DAC_SUPPORTED
// Get DAC related info for a given GPIO pin
// @param gpio_pin   GPIO pin number
// @param dac        Number of DAC unit
bool getDAC_gpio_info(int gpio_pin, int& dac)
{
  switch (gpio_pin) {
    case 25: dac = 1; break;
    case 26: dac = 2; break;
    default:
      return false;
  }
  return true;
}
#endif

#endif // ifdef ESP32_CLASSIC

#include "../Helpers/StringGenerator_WiFi.h"

#include "../Helpers/StringConverter.h"

#include "../Globals/ESPEasyWiFiEvent.h"

#ifdef ESP8266
#include <ESP8266WiFi.h>
#endif

const __FlashStringHelper * WiFi_encryptionType(uint8_t encryptionType) {
switch (encryptionType) {
  #ifdef ESP32
    case WIFI_AUTH_OPEN:             return F("open"); 
    case WIFI_AUTH_WEP:              return F("WEP"); 
    case WIFI_AUTH_WPA_PSK:          return F("WPA/PSK"); 
    case WIFI_AUTH_WPA2_PSK:         return F("WPA2/PSK"); 
    case WIFI_AUTH_WPA_WPA2_PSK:     return F("WPA/WPA2/PSK"); 
    case WIFI_AUTH_WPA2_ENTERPRISE:  return F("WPA2 Enterprise"); 
  #else // ifdef ESP32
    case ENC_TYPE_WEP:   return F("WEP"); 
    case ENC_TYPE_TKIP:  return F("WPA/PSK"); 
    case ENC_TYPE_CCMP:  return F("WPA2/PSK"); 
    case ENC_TYPE_NONE:  return F("open"); 
    case ENC_TYPE_AUTO:  return F("WPA/WPA2/PSK"); 
  #endif // ifdef ESP32
    default:
	break;
      
  }
  return F("-");
}


#ifdef ESP8266
#ifdef LIMIT_BUILD_SIZE
String SDKwifiStatusToString(uint8_t sdk_wifistatus)
{
  return String(sdk_wifistatus);
}
#else
const __FlashStringHelper * SDKwifiStatusToString(uint8_t sdk_wifistatus)
{
  switch (sdk_wifistatus) {
    case STATION_IDLE:           return F("STATION_IDLE");
    case STATION_CONNECTING:     return F("STATION_CONNECTING");
    case STATION_WRONG_PASSWORD: return F("STATION_WRONG_PASSWORD");
    case STATION_NO_AP_FOUND:    return F("STATION_NO_AP_FOUND");
    case STATION_CONNECT_FAIL:   return F("STATION_CONNECT_FAIL");
    case STATION_GOT_IP:         return F("STATION_GOT_IP");
  }
  return F("Unknown");
}
#endif
#endif

const __FlashStringHelper * ArduinoWifiStatusToFlashString(uint8_t arduino_corelib_wifistatus) {
  switch (arduino_corelib_wifistatus) {
    case WL_NO_SHIELD:       return F("WL_NO_SHIELD");
#ifdef ESP32
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 2)
    case WL_STOPPED:         return F("WL_STOPPED");
#endif
#endif
    case WL_IDLE_STATUS:     return F("WL_IDLE_STATUS");
    case WL_NO_SSID_AVAIL:   return F("WL_NO_SSID_AVAIL");
    case WL_SCAN_COMPLETED:  return F("WL_SCAN_COMPLETED");
    case WL_CONNECTED:       return F("WL_CONNECTED");
    case WL_CONNECT_FAILED:  return F("WL_CONNECT_FAILED");
    case WL_CONNECTION_LOST: return F("WL_CONNECTION_LOST");
    case WL_DISCONNECTED:    return F("WL_DISCONNECTED");
  }
  return F("-");
}

String ArduinoWifiStatusToString(uint8_t arduino_corelib_wifistatus) {
  #ifdef LIMIT_BUILD_SIZE
  return String(arduino_corelib_wifistatus);
  #else
  String log = ArduinoWifiStatusToFlashString(arduino_corelib_wifistatus);
  log += ' ';
  log += arduino_corelib_wifistatus;
  return log;
  #endif
}


const __FlashStringHelper * getLastDisconnectReason(WiFiDisconnectReason reason) {
  switch (reason) {
    case WIFI_DISCONNECT_REASON_UNSPECIFIED:                return F("Unspecified");              
    case WIFI_DISCONNECT_REASON_AUTH_EXPIRE:                return F("Auth expire");              
    case WIFI_DISCONNECT_REASON_AUTH_LEAVE:                 return F("Auth leave");               
    case WIFI_DISCONNECT_REASON_ASSOC_EXPIRE:               return F("Assoc expire");             
    case WIFI_DISCONNECT_REASON_ASSOC_TOOMANY:              return F("Assoc toomany");            
    case WIFI_DISCONNECT_REASON_NOT_AUTHED:                 return F("Not authed");               
    case WIFI_DISCONNECT_REASON_NOT_ASSOCED:                return F("Not assoced");              
    case WIFI_DISCONNECT_REASON_ASSOC_LEAVE:                return F("Assoc leave");              
    case WIFI_DISCONNECT_REASON_ASSOC_NOT_AUTHED:           return F("Assoc not authed");         
    case WIFI_DISCONNECT_REASON_DISASSOC_PWRCAP_BAD:        return F("Disassoc pwrcap bad");      
    case WIFI_DISCONNECT_REASON_DISASSOC_SUPCHAN_BAD:       return F("Disassoc supchan bad");     
    case WIFI_DISCONNECT_REASON_IE_INVALID:                 return F("IE invalid");               
    case WIFI_DISCONNECT_REASON_MIC_FAILURE:                return F("Mic failure");              
    case WIFI_DISCONNECT_REASON_4WAY_HANDSHAKE_TIMEOUT:     return F("4way handshake timeout");   
    case WIFI_DISCONNECT_REASON_GROUP_KEY_UPDATE_TIMEOUT:   return F("Group key update timeout"); 
    case WIFI_DISCONNECT_REASON_IE_IN_4WAY_DIFFERS:         return F("IE in 4way differs");       
    case WIFI_DISCONNECT_REASON_GROUP_CIPHER_INVALID:       return F("Group cipher invalid");     
    case WIFI_DISCONNECT_REASON_PAIRWISE_CIPHER_INVALID:    return F("Pairwise cipher invalid");  
    case WIFI_DISCONNECT_REASON_AKMP_INVALID:               return F("AKMP invalid");             
    case WIFI_DISCONNECT_REASON_UNSUPP_RSN_IE_VERSION:      return F("Unsupp RSN IE version");    
    case WIFI_DISCONNECT_REASON_INVALID_RSN_IE_CAP:         return F("Invalid RSN IE cap");       
    case WIFI_DISCONNECT_REASON_802_1X_AUTH_FAILED:         return F("802 1X auth failed");       
    case WIFI_DISCONNECT_REASON_CIPHER_SUITE_REJECTED:      return F("Cipher suite rejected");    
    case WIFI_DISCONNECT_REASON_BEACON_TIMEOUT:             return F("Beacon timeout");           
    case WIFI_DISCONNECT_REASON_NO_AP_FOUND:                return F("No AP found");              
    case WIFI_DISCONNECT_REASON_AUTH_FAIL:                  return F("Auth fail");                
    case WIFI_DISCONNECT_REASON_ASSOC_FAIL:                 return F("Assoc fail");               
    case WIFI_DISCONNECT_REASON_HANDSHAKE_TIMEOUT:          return F("Handshake timeout");
    default:  return F("Unknown");
  }
}

String getLastDisconnectReason() {
  #ifndef LIMIT_BUILD_SIZE
  String reason = wrap_braces(String(WiFiEventData.lastDisconnectReason));
  reason += ' ';
  reason += getLastDisconnectReason(WiFiEventData.lastDisconnectReason);
  return reason;
  #else
  return wrap_braces(String(WiFiEventData.lastDisconnectReason));
  #endif
}

#include "../Helpers/Hardware_device_info.h"


#ifdef ESP8266



bool isESP8285(uint32_t& pkg_version, bool& high_temp_version)
{
  // Original code from Tasmota:
  // https://github.com/arendst/Tasmota/blob/62675a37a0e7b46283e2fdfe459bb8fd29d1cc2a/tasmota/tasmota_support/support_esp.ino#L151

  /*
     ESP8266 SoCs
     - 32-bit MCU & 2.4 GHz Wi-Fi
     - High-performance 160 MHz single-core CPU
     - +19.5 dBm output power ensures a good physical range
     - Sleep current is less than 20 μA, making it suitable for battery-powered and wearable-electronics applications
     - Peripherals include UART, GPIO, I2C, I2S, SDIO, PWM, ADC and SPI
   */

  // esptool.py get_efuses
  uint32_t efuse0 = *(uint32_t *)(0x3FF00050);

  //  uint32_t efuse1 = *(uint32_t*)(0x3FF00054);
  uint32_t efuse2 = *(uint32_t *)(0x3FF00058);
  uint32_t efuse3 = *(uint32_t *)(0x3FF0005C);

  bool r0_4  = efuse0 & (1 << 4);    // ESP8285
  bool r2_16 = efuse2 & (1 << 16);   // ESP8285

  if (r0_4 || r2_16) {               // ESP8285
    //                                                              1M 2M 2M 4M flash size
    //   r0_4                                                       1  1  0  0
    bool r3_25 = efuse3 & (1 << 25); // flash matrix  0  0  1  1
    bool r3_26 = efuse3 & (1 << 26); // flash matrix  0  1  0  1
    bool r3_27 = efuse3 & (1 << 27); // flash matrix  0  0  0  0
    pkg_version = 0;

    if (!r3_27) {
      if (r0_4 && !r3_25) {
        pkg_version = (r3_26) ? 2 : 1;
      }
      else if (!r0_4 && r3_25) {
        pkg_version = (r3_26) ? 4 : 2;
      }
    }
    high_temp_version = efuse0 & (1 << 5); // Max flash temperature (0 = 85C, 1 = 105C)
    return true;
  }
  return false;
}

bool isESP8285() {
  uint32_t pkg_version{};
  bool     high_temp_version{};
  return isESP8285(pkg_version, high_temp_version);
}

bool isFlashInterfacePin_ESPEasy(int gpio) {
  if (isESP8285()) {
    return (gpio) == 6 || (gpio) == 7 || (gpio) == 8 || (gpio) == 11;
  }
  return (gpio) >= 6 && (gpio) <= 11;
}

const __FlashStringHelper* getChipModel() {
  uint32_t pkg_version{};
  bool     high_temp_version{};

  if (isESP8285(pkg_version, high_temp_version)) {
    switch (pkg_version) {
      case 1:
        return (high_temp_version)
          ? F("ESP8285H08") // 1M flash
          : F("ESP8285N08");
      case 2:
        return (high_temp_version)
          ? F("ESP8285H16") // 2M flash
          : F("ESP8285N16");
      case 4:
        return (high_temp_version)
          ? F("ESP8285H32") // 4M flash
          : F("ESP8285N32");
    }
    return F("ESP8285");
  }
  return F("ESP8266EX");
}

#endif
#include "../Helpers/_CPlugin_init.h"

#include "../DataStructs/ESPEasy_EventStruct.h"
#include "../DataStructs/TimingStats.h"

#include "../DataTypes/ESPEasy_plugin_functions.h"

#include "../Globals/CPlugins.h"
#include "../Globals/Settings.h"

#include "../Helpers/Misc.h"

// ********************************************************************************
// Initialize all Controller CPlugins that where defined earlier
// and initialize the function call pointer into the CCPlugin array
// ********************************************************************************

constexpr cpluginID_t ProtocolIndex_to_CPlugin_id[] PROGMEM =
{
#ifdef USES_C001
  1,
#endif // ifdef USES_C001

#ifdef USES_C002
  2,
#endif // ifdef USES_C002

#ifdef USES_C003
  3,
#endif // ifdef USES_C003

#ifdef USES_C004
  4,
#endif // ifdef USES_C004

#ifdef USES_C005
  5,
#endif // ifdef USES_C005

#ifdef USES_C006
  6,
#endif // ifdef USES_C006

#ifdef USES_C007
  7,
#endif // ifdef USES_C007

#ifdef USES_C008
  8,
#endif // ifdef USES_C008

#ifdef USES_C009
  9,
#endif // ifdef USES_C009

#ifdef USES_C010
  10,
#endif // ifdef USES_C010

#ifdef USES_C011
  11,
#endif // ifdef USES_C011

#ifdef USES_C012
  12,
#endif // ifdef USES_C012

#ifdef USES_C013
  13,
#endif // ifdef USES_C013

#ifdef USES_C014
  14,
#endif // ifdef USES_C014

#ifdef USES_C015
  15,
#endif // ifdef USES_C015

#ifdef USES_C016
  16,
#endif // ifdef USES_C016

#ifdef USES_C017
  17,
#endif // ifdef USES_C017

#ifdef USES_C018
  18,
#endif // ifdef USES_C018

#ifdef USES_C019
  19,
#endif // ifdef USES_C019

#ifdef USES_C020
  20,
#endif // ifdef USES_C020

#ifdef USES_C021
  21,
#endif // ifdef USES_C021

#ifdef USES_C022
  22,
#endif // ifdef USES_C022

#ifdef USES_C023
  23,
#endif // ifdef USES_C023

#ifdef USES_C024
  24,
#endif // ifdef USES_C024

#ifdef USES_C025
  25,
#endif // ifdef USES_C025

#ifdef USES_C026
  26,
#endif // ifdef USES_C026

#ifdef USES_C027
  27,
#endif // ifdef USES_C027

#ifdef USES_C028
  28,
#endif // ifdef USES_C028

#ifdef USES_C029
  29,
#endif // ifdef USES_C029

#ifdef USES_C030
  30,
#endif // ifdef USES_C030

#ifdef USES_C031
  31,
#endif // ifdef USES_C031

#ifdef USES_C032
  32,
#endif // ifdef USES_C032

#ifdef USES_C033
  33,
#endif // ifdef USES_C033

#ifdef USES_C034
  34,
#endif // ifdef USES_C034

#ifdef USES_C035
  35,
#endif // ifdef USES_C035

#ifdef USES_C036
  36,
#endif // ifdef USES_C036

#ifdef USES_C037
  37,
#endif // ifdef USES_C037

#ifdef USES_C038
  38,
#endif // ifdef USES_C038

#ifdef USES_C039
  39,
#endif // ifdef USES_C039

#ifdef USES_C040
  40,
#endif // ifdef USES_C040

#ifdef USES_C041
  41,
#endif // ifdef USES_C041

#ifdef USES_C042
  42,
#endif // ifdef USES_C042

#ifdef USES_C043
  43,
#endif // ifdef USES_C043

#ifdef USES_C044
  44,
#endif // ifdef USES_C044

#ifdef USES_C045
  45,
#endif // ifdef USES_C045

#ifdef USES_C046
  46,
#endif // ifdef USES_C046

#ifdef USES_C047
  47,
#endif // ifdef USES_C047

#ifdef USES_C048
  48,
#endif // ifdef USES_C048

#ifdef USES_C049
  49,
#endif // ifdef USES_C049

#ifdef USES_C050
  50,
#endif // ifdef USES_C050

#ifdef USES_C051
  51,
#endif // ifdef USES_C051

#ifdef USES_C052
  52,
#endif // ifdef USES_C052

#ifdef USES_C053
  53,
#endif // ifdef USES_C053

#ifdef USES_C054
  54,
#endif // ifdef USES_C054

#ifdef USES_C055
  55,
#endif // ifdef USES_C055

#ifdef USES_C056
  56,
#endif // ifdef USES_C056

#ifdef USES_C057
  57,
#endif // ifdef USES_C057

#ifdef USES_C058
  58,
#endif // ifdef USES_C058

#ifdef USES_C059
  59,
#endif // ifdef USES_C059

#ifdef USES_C060
  60,
#endif // ifdef USES_C060

#ifdef USES_C061
  61,
#endif // ifdef USES_C061

#ifdef USES_C062
  62,
#endif // ifdef USES_C062

#ifdef USES_C063
  63,
#endif // ifdef USES_C063

#ifdef USES_C064
  64,
#endif // ifdef USES_C064

#ifdef USES_C065
  65,
#endif // ifdef USES_C065

#ifdef USES_C066
  66,
#endif // ifdef USES_C066

#ifdef USES_C067
  67,
#endif // ifdef USES_C067

#ifdef USES_C068
  68,
#endif // ifdef USES_C068

#ifdef USES_C069
  69,
#endif // ifdef USES_C069

#ifdef USES_C070
  70,
#endif // ifdef USES_C070

#ifdef USES_C071
  71,
#endif // ifdef USES_C071

#ifdef USES_C072
  72,
#endif // ifdef USES_C072

#ifdef USES_C073
  73,
#endif // ifdef USES_C073

#ifdef USES_C074
  74,
#endif // ifdef USES_C074

#ifdef USES_C075
  75,
#endif // ifdef USES_C075

#ifdef USES_C076
  76,
#endif // ifdef USES_C076

#ifdef USES_C077
  77,
#endif // ifdef USES_C077

#ifdef USES_C078
  78,
#endif // ifdef USES_C078

#ifdef USES_C079
  79,
#endif // ifdef USES_C079

#ifdef USES_C080
  80,
#endif // ifdef USES_C080

#ifdef USES_C081
  81,
#endif // ifdef USES_C081

#ifdef USES_C082
  82,
#endif // ifdef USES_C082

#ifdef USES_C083
  83,
#endif // ifdef USES_C083

#ifdef USES_C084
  84,
#endif // ifdef USES_C084

#ifdef USES_C085
  85,
#endif // ifdef USES_C085

#ifdef USES_C086
  86,
#endif // ifdef USES_C086

#ifdef USES_C087
  87,
#endif // ifdef USES_C087

#ifdef USES_C088
  88,
#endif // ifdef USES_C088

#ifdef USES_C089
  89,
#endif // ifdef USES_C089

#ifdef USES_C090
  90,
#endif // ifdef USES_C090

#ifdef USES_C091
  91,
#endif // ifdef USES_C091

#ifdef USES_C092
  92,
#endif // ifdef USES_C092

#ifdef USES_C093
  93,
#endif // ifdef USES_C093

#ifdef USES_C094
  94,
#endif // ifdef USES_C094

#ifdef USES_C095
  95,
#endif // ifdef USES_C095

#ifdef USES_C096
  96,
#endif // ifdef USES_C096

#ifdef USES_C097
  97,
#endif // ifdef USES_C097

#ifdef USES_C098
  98,
#endif // ifdef USES_C098

#ifdef USES_C099
  99,
#endif // ifdef USES_C099

#ifdef USES_C100
  100,
#endif // ifdef USES_C100

#ifdef USES_C101
  101,
#endif // ifdef USES_C101

#ifdef USES_C102
  102,
#endif // ifdef USES_C102

#ifdef USES_C103
  103,
#endif // ifdef USES_C103

#ifdef USES_C104
  104,
#endif // ifdef USES_C104

#ifdef USES_C105
  105,
#endif // ifdef USES_C105

#ifdef USES_C106
  106,
#endif // ifdef USES_C106

#ifdef USES_C107
  107,
#endif // ifdef USES_C107

#ifdef USES_C108
  108,
#endif // ifdef USES_C108

#ifdef USES_C109
  109,
#endif // ifdef USES_C109

#ifdef USES_C110
  110,
#endif // ifdef USES_C110

#ifdef USES_C111
  111,
#endif // ifdef USES_C111

#ifdef USES_C112
  112,
#endif // ifdef USES_C112

#ifdef USES_C113
  113,
#endif // ifdef USES_C113

#ifdef USES_C114
  114,
#endif // ifdef USES_C114

#ifdef USES_C115
  115,
#endif // ifdef USES_C115

#ifdef USES_C116
  116,
#endif // ifdef USES_C116

#ifdef USES_C117
  117,
#endif // ifdef USES_C117

#ifdef USES_C118
  118,
#endif // ifdef USES_C118

#ifdef USES_C119
  119,
#endif // ifdef USES_C119

#ifdef USES_C120
  120,
#endif // ifdef USES_C120

#ifdef USES_C121
  121,
#endif // ifdef USES_C121

#ifdef USES_C122
  122,
#endif // ifdef USES_C122

#ifdef USES_C123
  123,
#endif // ifdef USES_C123

#ifdef USES_C124
  124,
#endif // ifdef USES_C124

#ifdef USES_C125
  125,
#endif // ifdef USES_C125

#ifdef USES_C126
  126,
#endif // ifdef USES_C126

#ifdef USES_C127
  127,
#endif // ifdef USES_C127

#ifdef USES_C128
  128,
#endif // ifdef USES_C128

#ifdef USES_C129
  129,
#endif // ifdef USES_C129

#ifdef USES_C130
  130,
#endif // ifdef USES_C130

#ifdef USES_C131
  131,
#endif // ifdef USES_C131

#ifdef USES_C132
  132,
#endif // ifdef USES_C132

#ifdef USES_C133
  133,
#endif // ifdef USES_C133

#ifdef USES_C134
  134,
#endif // ifdef USES_C134

#ifdef USES_C135
  135,
#endif // ifdef USES_C135

#ifdef USES_C136
  136,
#endif // ifdef USES_C136

#ifdef USES_C137
  137,
#endif // ifdef USES_C137

#ifdef USES_C138
  138,
#endif // ifdef USES_C138

#ifdef USES_C139
  139,
#endif // ifdef USES_C139

#ifdef USES_C140
  140,
#endif // ifdef USES_C140

#ifdef USES_C141
  141,
#endif // ifdef USES_C141

#ifdef USES_C142
  142,
#endif // ifdef USES_C142

#ifdef USES_C143
  143,
#endif // ifdef USES_C143

#ifdef USES_C144
  144,
#endif // ifdef USES_C144

#ifdef USES_C145
  145,
#endif // ifdef USES_C145

#ifdef USES_C146
  146,
#endif // ifdef USES_C146

#ifdef USES_C147
  147,
#endif // ifdef USES_C147

#ifdef USES_C148
  148,
#endif // ifdef USES_C148

#ifdef USES_C149
  149,
#endif // ifdef USES_C149

#ifdef USES_C150
  150,
#endif // ifdef USES_C150

#ifdef USES_C151
  151,
#endif // ifdef USES_C151

#ifdef USES_C152
  152,
#endif // ifdef USES_C152

#ifdef USES_C153
  153,
#endif // ifdef USES_C153

#ifdef USES_C154
  154,
#endif // ifdef USES_C154

#ifdef USES_C155
  155,
#endif // ifdef USES_C155

#ifdef USES_C156
  156,
#endif // ifdef USES_C156

#ifdef USES_C157
  157,
#endif // ifdef USES_C157

#ifdef USES_C158
  158,
#endif // ifdef USES_C158

#ifdef USES_C159
  159,
#endif // ifdef USES_C159

#ifdef USES_C160
  160,
#endif // ifdef USES_C160

#ifdef USES_C161
  161,
#endif // ifdef USES_C161

#ifdef USES_C162
  162,
#endif // ifdef USES_C162

#ifdef USES_C163
  163,
#endif // ifdef USES_C163

#ifdef USES_C164
  164,
#endif // ifdef USES_C164

#ifdef USES_C165
  165,
#endif // ifdef USES_C165

#ifdef USES_C166
  166,
#endif // ifdef USES_C166

#ifdef USES_C167
  167,
#endif // ifdef USES_C167

#ifdef USES_C168
  168,
#endif // ifdef USES_C168

#ifdef USES_C169
  169,
#endif // ifdef USES_C169

#ifdef USES_C170
  170,
#endif // ifdef USES_C170

#ifdef USES_C171
  171,
#endif // ifdef USES_C171

#ifdef USES_C172
  172,
#endif // ifdef USES_C172

#ifdef USES_C173
  173,
#endif // ifdef USES_C173

#ifdef USES_C174
  174,
#endif // ifdef USES_C174

#ifdef USES_C175
  175,
#endif // ifdef USES_C175

#ifdef USES_C176
  176,
#endif // ifdef USES_C176

#ifdef USES_C177
  177,
#endif // ifdef USES_C177

#ifdef USES_C178
  178,
#endif // ifdef USES_C178

#ifdef USES_C179
  179,
#endif // ifdef USES_C179

#ifdef USES_C180
  180,
#endif // ifdef USES_C180

#ifdef USES_C181
  181,
#endif // ifdef USES_C181

#ifdef USES_C182
  182,
#endif // ifdef USES_C182

#ifdef USES_C183
  183,
#endif // ifdef USES_C183

#ifdef USES_C184
  184,
#endif // ifdef USES_C184

#ifdef USES_C185
  185,
#endif // ifdef USES_C185

#ifdef USES_C186
  186,
#endif // ifdef USES_C186

#ifdef USES_C187
  187,
#endif // ifdef USES_C187

#ifdef USES_C188
  188,
#endif // ifdef USES_C188

#ifdef USES_C189
  189,
#endif // ifdef USES_C189

#ifdef USES_C190
  190,
#endif // ifdef USES_C190

#ifdef USES_C191
  191,
#endif // ifdef USES_C191

#ifdef USES_C192
  192,
#endif // ifdef USES_C192

#ifdef USES_C193
  193,
#endif // ifdef USES_C193

#ifdef USES_C194
  194,
#endif // ifdef USES_C194

#ifdef USES_C195
  195,
#endif // ifdef USES_C195

#ifdef USES_C196
  196,
#endif // ifdef USES_C196

#ifdef USES_C197
  197,
#endif // ifdef USES_C197

#ifdef USES_C198
  198,
#endif // ifdef USES_C198

#ifdef USES_C199
  199,
#endif // ifdef USES_C199

#ifdef USES_C200
  200,
#endif // ifdef USES_C200

#ifdef USES_C201
  201,
#endif // ifdef USES_C201

#ifdef USES_C202
  202,
#endif // ifdef USES_C202

#ifdef USES_C203
  203,
#endif // ifdef USES_C203

#ifdef USES_C204
  204,
#endif // ifdef USES_C204

#ifdef USES_C205
  205,
#endif // ifdef USES_C205

#ifdef USES_C206
  206,
#endif // ifdef USES_C206

#ifdef USES_C207
  207,
#endif // ifdef USES_C207

#ifdef USES_C208
  208,
#endif // ifdef USES_C208

#ifdef USES_C209
  209,
#endif // ifdef USES_C209

#ifdef USES_C210
  210,
#endif // ifdef USES_C210

#ifdef USES_C211
  211,
#endif // ifdef USES_C211

#ifdef USES_C212
  212,
#endif // ifdef USES_C212

#ifdef USES_C213
  213,
#endif // ifdef USES_C213

#ifdef USES_C214
  214,
#endif // ifdef USES_C214

#ifdef USES_C215
  215,
#endif // ifdef USES_C215

#ifdef USES_C216
  216,
#endif // ifdef USES_C216

#ifdef USES_C217
  217,
#endif // ifdef USES_C217

#ifdef USES_C218
  218,
#endif // ifdef USES_C218

#ifdef USES_C219
  219,
#endif // ifdef USES_C219

#ifdef USES_C220
  220,
#endif // ifdef USES_C220

#ifdef USES_C221
  221,
#endif // ifdef USES_C221

#ifdef USES_C222
  222,
#endif // ifdef USES_C222

#ifdef USES_C223
  223,
#endif // ifdef USES_C223

#ifdef USES_C224
  224,
#endif // ifdef USES_C224

#ifdef USES_C225
  225,
#endif // ifdef USES_C225

#ifdef USES_C226
  226,
#endif // ifdef USES_C226

#ifdef USES_C227
  227,
#endif // ifdef USES_C227

#ifdef USES_C228
  228,
#endif // ifdef USES_C228

#ifdef USES_C229
  229,
#endif // ifdef USES_C229

#ifdef USES_C230
  230,
#endif // ifdef USES_C230

#ifdef USES_C231
  231,
#endif // ifdef USES_C231

#ifdef USES_C232
  232,
#endif // ifdef USES_C232

#ifdef USES_C233
  233,
#endif // ifdef USES_C233

#ifdef USES_C234
  234,
#endif // ifdef USES_C234

#ifdef USES_C235
  235,
#endif // ifdef USES_C235

#ifdef USES_C236
  236,
#endif // ifdef USES_C236

#ifdef USES_C237
  237,
#endif // ifdef USES_C237

#ifdef USES_C238
  238,
#endif // ifdef USES_C238

#ifdef USES_C239
  239,
#endif // ifdef USES_C239

#ifdef USES_C240
  240,
#endif // ifdef USES_C240

#ifdef USES_C241
  241,
#endif // ifdef USES_C241

#ifdef USES_C242
  242,
#endif // ifdef USES_C242

#ifdef USES_C243
  243,
#endif // ifdef USES_C243

#ifdef USES_C244
  244,
#endif // ifdef USES_C244

#ifdef USES_C245
  245,
#endif // ifdef USES_C245

#ifdef USES_C246
  246,
#endif // ifdef USES_C246

#ifdef USES_C247
  247,
#endif // ifdef USES_C247

#ifdef USES_C248
  248,
#endif // ifdef USES_C248

#ifdef USES_C249
  249,
#endif // ifdef USES_C249

#ifdef USES_C250
  250,
#endif // ifdef USES_C250

#ifdef USES_C251
  251,
#endif // ifdef USES_C251

#ifdef USES_C252
  252,
#endif // ifdef USES_C252

#ifdef USES_C253
  253,
#endif // ifdef USES_C253

#ifdef USES_C254
  254,
#endif // ifdef USES_C254

#ifdef USES_C255
  255,
#endif // ifdef USES_C255
};


typedef bool (*CPlugin_ptr_t)(CPlugin::Function,
                      struct EventStruct *,
                      String&);

const CPlugin_ptr_t PROGMEM CPlugin_ptr[] =
{
#ifdef USES_C001
  &CPlugin_001,
#endif // ifdef USES_C001

#ifdef USES_C002
  &CPlugin_002,
#endif // ifdef USES_C002

#ifdef USES_C003
  &CPlugin_003,
#endif // ifdef USES_C003

#ifdef USES_C004
  &CPlugin_004,
#endif // ifdef USES_C004

#ifdef USES_C005
  &CPlugin_005,
#endif // ifdef USES_C005

#ifdef USES_C006
  &CPlugin_006,
#endif // ifdef USES_C006

#ifdef USES_C007
  &CPlugin_007,
#endif // ifdef USES_C007

#ifdef USES_C008
  &CPlugin_008,
#endif // ifdef USES_C008

#ifdef USES_C009
  &CPlugin_009,
#endif // ifdef USES_C009

#ifdef USES_C010
  &CPlugin_010,
#endif // ifdef USES_C010

#ifdef USES_C011
  &CPlugin_011,
#endif // ifdef USES_C011

#ifdef USES_C012
  &CPlugin_012,
#endif // ifdef USES_C012

#ifdef USES_C013
  &CPlugin_013,
#endif // ifdef USES_C013

#ifdef USES_C014
  &CPlugin_014,
#endif // ifdef USES_C014

#ifdef USES_C015
  &CPlugin_015,
#endif // ifdef USES_C015

#ifdef USES_C016
  &CPlugin_016,
#endif // ifdef USES_C016

#ifdef USES_C017
  &CPlugin_017,
#endif // ifdef USES_C017

#ifdef USES_C018
  &CPlugin_018,
#endif // ifdef USES_C018

#ifdef USES_C019
  &CPlugin_019,
#endif // ifdef USES_C019

#ifdef USES_C020
  &CPlugin_020,
#endif // ifdef USES_C020

#ifdef USES_C021
  &CPlugin_021,
#endif // ifdef USES_C021

#ifdef USES_C022
  &CPlugin_022,
#endif // ifdef USES_C022

#ifdef USES_C023
  &CPlugin_023,
#endif // ifdef USES_C023

#ifdef USES_C024
  &CPlugin_024,
#endif // ifdef USES_C024

#ifdef USES_C025
  &CPlugin_025,
#endif // ifdef USES_C025

#ifdef USES_C026
  &CPlugin_026,
#endif // ifdef USES_C026

#ifdef USES_C027
  &CPlugin_027,
#endif // ifdef USES_C027

#ifdef USES_C028
  &CPlugin_028,
#endif // ifdef USES_C028

#ifdef USES_C029
  &CPlugin_029,
#endif // ifdef USES_C029

#ifdef USES_C030
  &CPlugin_030,
#endif // ifdef USES_C030

#ifdef USES_C031
  &CPlugin_031,
#endif // ifdef USES_C031

#ifdef USES_C032
  &CPlugin_032,
#endif // ifdef USES_C032

#ifdef USES_C033
  &CPlugin_033,
#endif // ifdef USES_C033

#ifdef USES_C034
  &CPlugin_034,
#endif // ifdef USES_C034

#ifdef USES_C035
  &CPlugin_035,
#endif // ifdef USES_C035

#ifdef USES_C036
  &CPlugin_036,
#endif // ifdef USES_C036

#ifdef USES_C037
  &CPlugin_037,
#endif // ifdef USES_C037

#ifdef USES_C038
  &CPlugin_038,
#endif // ifdef USES_C038

#ifdef USES_C039
  &CPlugin_039,
#endif // ifdef USES_C039

#ifdef USES_C040
  &CPlugin_040,
#endif // ifdef USES_C040

#ifdef USES_C041
  &CPlugin_041,
#endif // ifdef USES_C041

#ifdef USES_C042
  &CPlugin_042,
#endif // ifdef USES_C042

#ifdef USES_C043
  &CPlugin_043,
#endif // ifdef USES_C043

#ifdef USES_C044
  &CPlugin_044,
#endif // ifdef USES_C044

#ifdef USES_C045
  &CPlugin_045,
#endif // ifdef USES_C045

#ifdef USES_C046
  &CPlugin_046,
#endif // ifdef USES_C046

#ifdef USES_C047
  &CPlugin_047,
#endif // ifdef USES_C047

#ifdef USES_C048
  &CPlugin_048,
#endif // ifdef USES_C048

#ifdef USES_C049
  &CPlugin_049,
#endif // ifdef USES_C049

#ifdef USES_C050
  &CPlugin_050,
#endif // ifdef USES_C050

#ifdef USES_C051
  &CPlugin_051,
#endif // ifdef USES_C051

#ifdef USES_C052
  &CPlugin_052,
#endif // ifdef USES_C052

#ifdef USES_C053
  &CPlugin_053,
#endif // ifdef USES_C053

#ifdef USES_C054
  &CPlugin_054,
#endif // ifdef USES_C054

#ifdef USES_C055
  &CPlugin_055,
#endif // ifdef USES_C055

#ifdef USES_C056
  &CPlugin_056,
#endif // ifdef USES_C056

#ifdef USES_C057
  &CPlugin_057,
#endif // ifdef USES_C057

#ifdef USES_C058
  &CPlugin_058,
#endif // ifdef USES_C058

#ifdef USES_C059
  &CPlugin_059,
#endif // ifdef USES_C059

#ifdef USES_C060
  &CPlugin_060,
#endif // ifdef USES_C060

#ifdef USES_C061
  &CPlugin_061,
#endif // ifdef USES_C061

#ifdef USES_C062
  &CPlugin_062,
#endif // ifdef USES_C062

#ifdef USES_C063
  &CPlugin_063,
#endif // ifdef USES_C063

#ifdef USES_C064
  &CPlugin_064,
#endif // ifdef USES_C064

#ifdef USES_C065
  &CPlugin_065,
#endif // ifdef USES_C065

#ifdef USES_C066
  &CPlugin_066,
#endif // ifdef USES_C066

#ifdef USES_C067
  &CPlugin_067,
#endif // ifdef USES_C067

#ifdef USES_C068
  &CPlugin_068,
#endif // ifdef USES_C068

#ifdef USES_C069
  &CPlugin_069,
#endif // ifdef USES_C069

#ifdef USES_C070
  &CPlugin_070,
#endif // ifdef USES_C070

#ifdef USES_C071
  &CPlugin_071,
#endif // ifdef USES_C071

#ifdef USES_C072
  &CPlugin_072,
#endif // ifdef USES_C072

#ifdef USES_C073
  &CPlugin_073,
#endif // ifdef USES_C073

#ifdef USES_C074
  &CPlugin_074,
#endif // ifdef USES_C074

#ifdef USES_C075
  &CPlugin_075,
#endif // ifdef USES_C075

#ifdef USES_C076
  &CPlugin_076,
#endif // ifdef USES_C076

#ifdef USES_C077
  &CPlugin_077,
#endif // ifdef USES_C077

#ifdef USES_C078
  &CPlugin_078,
#endif // ifdef USES_C078

#ifdef USES_C079
  &CPlugin_079,
#endif // ifdef USES_C079

#ifdef USES_C080
  &CPlugin_080,
#endif // ifdef USES_C080

#ifdef USES_C081
  &CPlugin_081,
#endif // ifdef USES_C081

#ifdef USES_C082
  &CPlugin_082,
#endif // ifdef USES_C082

#ifdef USES_C083
  &CPlugin_083,
#endif // ifdef USES_C083

#ifdef USES_C084
  &CPlugin_084,
#endif // ifdef USES_C084

#ifdef USES_C085
  &CPlugin_085,
#endif // ifdef USES_C085

#ifdef USES_C086
  &CPlugin_086,
#endif // ifdef USES_C086

#ifdef USES_C087
  &CPlugin_087,
#endif // ifdef USES_C087

#ifdef USES_C088
  &CPlugin_088,
#endif // ifdef USES_C088

#ifdef USES_C089
  &CPlugin_089,
#endif // ifdef USES_C089

#ifdef USES_C090
  &CPlugin_090,
#endif // ifdef USES_C090

#ifdef USES_C091
  &CPlugin_091,
#endif // ifdef USES_C091

#ifdef USES_C092
  &CPlugin_092,
#endif // ifdef USES_C092

#ifdef USES_C093
  &CPlugin_093,
#endif // ifdef USES_C093

#ifdef USES_C094
  &CPlugin_094,
#endif // ifdef USES_C094

#ifdef USES_C095
  &CPlugin_095,
#endif // ifdef USES_C095

#ifdef USES_C096
  &CPlugin_096,
#endif // ifdef USES_C096

#ifdef USES_C097
  &CPlugin_097,
#endif // ifdef USES_C097

#ifdef USES_C098
  &CPlugin_098,
#endif // ifdef USES_C098

#ifdef USES_C099
  &CPlugin_099,
#endif // ifdef USES_C099

#ifdef USES_C100
  &CPlugin_100,
#endif // ifdef USES_C100

#ifdef USES_C101
  &CPlugin_101,
#endif // ifdef USES_C101

#ifdef USES_C102
  &CPlugin_102,
#endif // ifdef USES_C102

#ifdef USES_C103
  &CPlugin_103,
#endif // ifdef USES_C103

#ifdef USES_C104
  &CPlugin_104,
#endif // ifdef USES_C104

#ifdef USES_C105
  &CPlugin_105,
#endif // ifdef USES_C105

#ifdef USES_C106
  &CPlugin_106,
#endif // ifdef USES_C106

#ifdef USES_C107
  &CPlugin_107,
#endif // ifdef USES_C107

#ifdef USES_C108
  &CPlugin_108,
#endif // ifdef USES_C108

#ifdef USES_C109
  &CPlugin_109,
#endif // ifdef USES_C109

#ifdef USES_C110
  &CPlugin_110,
#endif // ifdef USES_C110

#ifdef USES_C111
  &CPlugin_111,
#endif // ifdef USES_C111

#ifdef USES_C112
  &CPlugin_112,
#endif // ifdef USES_C112

#ifdef USES_C113
  &CPlugin_113,
#endif // ifdef USES_C113

#ifdef USES_C114
  &CPlugin_114,
#endif // ifdef USES_C114

#ifdef USES_C115
  &CPlugin_115,
#endif // ifdef USES_C115

#ifdef USES_C116
  &CPlugin_116,
#endif // ifdef USES_C116

#ifdef USES_C117
  &CPlugin_117,
#endif // ifdef USES_C117

#ifdef USES_C118
  &CPlugin_118,
#endif // ifdef USES_C118

#ifdef USES_C119
  &CPlugin_119,
#endif // ifdef USES_C119

#ifdef USES_C120
  &CPlugin_120,
#endif // ifdef USES_C120

#ifdef USES_C121
  &CPlugin_121,
#endif // ifdef USES_C121

#ifdef USES_C122
  &CPlugin_122,
#endif // ifdef USES_C122

#ifdef USES_C123
  &CPlugin_123,
#endif // ifdef USES_C123

#ifdef USES_C124
  &CPlugin_124,
#endif // ifdef USES_C124

#ifdef USES_C125
  &CPlugin_125,
#endif // ifdef USES_C125

#ifdef USES_C126
  &CPlugin_126,
#endif // ifdef USES_C126

#ifdef USES_C127
  &CPlugin_127,
#endif // ifdef USES_C127

#ifdef USES_C128
  &CPlugin_128,
#endif // ifdef USES_C128

#ifdef USES_C129
  &CPlugin_129,
#endif // ifdef USES_C129

#ifdef USES_C130
  &CPlugin_130,
#endif // ifdef USES_C130

#ifdef USES_C131
  &CPlugin_131,
#endif // ifdef USES_C131

#ifdef USES_C132
  &CPlugin_132,
#endif // ifdef USES_C132

#ifdef USES_C133
  &CPlugin_133,
#endif // ifdef USES_C133

#ifdef USES_C134
  &CPlugin_134,
#endif // ifdef USES_C134

#ifdef USES_C135
  &CPlugin_135,
#endif // ifdef USES_C135

#ifdef USES_C136
  &CPlugin_136,
#endif // ifdef USES_C136

#ifdef USES_C137
  &CPlugin_137,
#endif // ifdef USES_C137

#ifdef USES_C138
  &CPlugin_138,
#endif // ifdef USES_C138

#ifdef USES_C139
  &CPlugin_139,
#endif // ifdef USES_C139

#ifdef USES_C140
  &CPlugin_140,
#endif // ifdef USES_C140

#ifdef USES_C141
  &CPlugin_141,
#endif // ifdef USES_C141

#ifdef USES_C142
  &CPlugin_142,
#endif // ifdef USES_C142

#ifdef USES_C143
  &CPlugin_143,
#endif // ifdef USES_C143

#ifdef USES_C144
  &CPlugin_144,
#endif // ifdef USES_C144

#ifdef USES_C145
  &CPlugin_145,
#endif // ifdef USES_C145

#ifdef USES_C146
  &CPlugin_146,
#endif // ifdef USES_C146

#ifdef USES_C147
  &CPlugin_147,
#endif // ifdef USES_C147

#ifdef USES_C148
  &CPlugin_148,
#endif // ifdef USES_C148

#ifdef USES_C149
  &CPlugin_149,
#endif // ifdef USES_C149

#ifdef USES_C150
  &CPlugin_150,
#endif // ifdef USES_C150

#ifdef USES_C151
  &CPlugin_151,
#endif // ifdef USES_C151

#ifdef USES_C152
  &CPlugin_152,
#endif // ifdef USES_C152

#ifdef USES_C153
  &CPlugin_153,
#endif // ifdef USES_C153

#ifdef USES_C154
  &CPlugin_154,
#endif // ifdef USES_C154

#ifdef USES_C155
  &CPlugin_155,
#endif // ifdef USES_C155

#ifdef USES_C156
  &CPlugin_156,
#endif // ifdef USES_C156

#ifdef USES_C157
  &CPlugin_157,
#endif // ifdef USES_C157

#ifdef USES_C158
  &CPlugin_158,
#endif // ifdef USES_C158

#ifdef USES_C159
  &CPlugin_159,
#endif // ifdef USES_C159

#ifdef USES_C160
  &CPlugin_160,
#endif // ifdef USES_C160

#ifdef USES_C161
  &CPlugin_161,
#endif // ifdef USES_C161

#ifdef USES_C162
  &CPlugin_162,
#endif // ifdef USES_C162

#ifdef USES_C163
  &CPlugin_163,
#endif // ifdef USES_C163

#ifdef USES_C164
  &CPlugin_164,
#endif // ifdef USES_C164

#ifdef USES_C165
  &CPlugin_165,
#endif // ifdef USES_C165

#ifdef USES_C166
  &CPlugin_166,
#endif // ifdef USES_C166

#ifdef USES_C167
  &CPlugin_167,
#endif // ifdef USES_C167

#ifdef USES_C168
  &CPlugin_168,
#endif // ifdef USES_C168

#ifdef USES_C169
  &CPlugin_169,
#endif // ifdef USES_C169

#ifdef USES_C170
  &CPlugin_170,
#endif // ifdef USES_C170

#ifdef USES_C171
  &CPlugin_171,
#endif // ifdef USES_C171

#ifdef USES_C172
  &CPlugin_172,
#endif // ifdef USES_C172

#ifdef USES_C173
  &CPlugin_173,
#endif // ifdef USES_C173

#ifdef USES_C174
  &CPlugin_174,
#endif // ifdef USES_C174

#ifdef USES_C175
  &CPlugin_175,
#endif // ifdef USES_C175

#ifdef USES_C176
  &CPlugin_176,
#endif // ifdef USES_C176

#ifdef USES_C177
  &CPlugin_177,
#endif // ifdef USES_C177

#ifdef USES_C178
  &CPlugin_178,
#endif // ifdef USES_C178

#ifdef USES_C179
  &CPlugin_179,
#endif // ifdef USES_C179

#ifdef USES_C180
  &CPlugin_180,
#endif // ifdef USES_C180

#ifdef USES_C181
  &CPlugin_181,
#endif // ifdef USES_C181

#ifdef USES_C182
  &CPlugin_182,
#endif // ifdef USES_C182

#ifdef USES_C183
  &CPlugin_183,
#endif // ifdef USES_C183

#ifdef USES_C184
  &CPlugin_184,
#endif // ifdef USES_C184

#ifdef USES_C185
  &CPlugin_185,
#endif // ifdef USES_C185

#ifdef USES_C186
  &CPlugin_186,
#endif // ifdef USES_C186

#ifdef USES_C187
  &CPlugin_187,
#endif // ifdef USES_C187

#ifdef USES_C188
  &CPlugin_188,
#endif // ifdef USES_C188

#ifdef USES_C189
  &CPlugin_189,
#endif // ifdef USES_C189

#ifdef USES_C190
  &CPlugin_190,
#endif // ifdef USES_C190

#ifdef USES_C191
  &CPlugin_191,
#endif // ifdef USES_C191

#ifdef USES_C192
  &CPlugin_192,
#endif // ifdef USES_C192

#ifdef USES_C193
  &CPlugin_193,
#endif // ifdef USES_C193

#ifdef USES_C194
  &CPlugin_194,
#endif // ifdef USES_C194

#ifdef USES_C195
  &CPlugin_195,
#endif // ifdef USES_C195

#ifdef USES_C196
  &CPlugin_196,
#endif // ifdef USES_C196

#ifdef USES_C197
  &CPlugin_197,
#endif // ifdef USES_C197

#ifdef USES_C198
  &CPlugin_198,
#endif // ifdef USES_C198

#ifdef USES_C199
  &CPlugin_199,
#endif // ifdef USES_C199

#ifdef USES_C200
  &CPlugin_200,
#endif // ifdef USES_C200

#ifdef USES_C201
  &CPlugin_201,
#endif // ifdef USES_C201

#ifdef USES_C202
  &CPlugin_202,
#endif // ifdef USES_C202

#ifdef USES_C203
  &CPlugin_203,
#endif // ifdef USES_C203

#ifdef USES_C204
  &CPlugin_204,
#endif // ifdef USES_C204

#ifdef USES_C205
  &CPlugin_205,
#endif // ifdef USES_C205

#ifdef USES_C206
  &CPlugin_206,
#endif // ifdef USES_C206

#ifdef USES_C207
  &CPlugin_207,
#endif // ifdef USES_C207

#ifdef USES_C208
  &CPlugin_208,
#endif // ifdef USES_C208

#ifdef USES_C209
  &CPlugin_209,
#endif // ifdef USES_C209

#ifdef USES_C210
  &CPlugin_210,
#endif // ifdef USES_C210

#ifdef USES_C211
  &CPlugin_211,
#endif // ifdef USES_C211

#ifdef USES_C212
  &CPlugin_212,
#endif // ifdef USES_C212

#ifdef USES_C213
  &CPlugin_213,
#endif // ifdef USES_C213

#ifdef USES_C214
  &CPlugin_214,
#endif // ifdef USES_C214

#ifdef USES_C215
  &CPlugin_215,
#endif // ifdef USES_C215

#ifdef USES_C216
  &CPlugin_216,
#endif // ifdef USES_C216

#ifdef USES_C217
  &CPlugin_217,
#endif // ifdef USES_C217

#ifdef USES_C218
  &CPlugin_218,
#endif // ifdef USES_C218

#ifdef USES_C219
  &CPlugin_219,
#endif // ifdef USES_C219

#ifdef USES_C220
  &CPlugin_220,
#endif // ifdef USES_C220

#ifdef USES_C221
  &CPlugin_221,
#endif // ifdef USES_C221

#ifdef USES_C222
  &CPlugin_222,
#endif // ifdef USES_C222

#ifdef USES_C223
  &CPlugin_223,
#endif // ifdef USES_C223

#ifdef USES_C224
  &CPlugin_224,
#endif // ifdef USES_C224

#ifdef USES_C225
  &CPlugin_225,
#endif // ifdef USES_C225

#ifdef USES_C226
  &CPlugin_226,
#endif // ifdef USES_C226

#ifdef USES_C227
  &CPlugin_227,
#endif // ifdef USES_C227

#ifdef USES_C228
  &CPlugin_228,
#endif // ifdef USES_C228

#ifdef USES_C229
  &CPlugin_229,
#endif // ifdef USES_C229

#ifdef USES_C230
  &CPlugin_230,
#endif // ifdef USES_C230

#ifdef USES_C231
  &CPlugin_231,
#endif // ifdef USES_C231

#ifdef USES_C232
  &CPlugin_232,
#endif // ifdef USES_C232

#ifdef USES_C233
  &CPlugin_233,
#endif // ifdef USES_C233

#ifdef USES_C234
  &CPlugin_234,
#endif // ifdef USES_C234

#ifdef USES_C235
  &CPlugin_235,
#endif // ifdef USES_C235

#ifdef USES_C236
  &CPlugin_236,
#endif // ifdef USES_C236

#ifdef USES_C237
  &CPlugin_237,
#endif // ifdef USES_C237

#ifdef USES_C238
  &CPlugin_238,
#endif // ifdef USES_C238

#ifdef USES_C239
  &CPlugin_239,
#endif // ifdef USES_C239

#ifdef USES_C240
  &CPlugin_240,
#endif // ifdef USES_C240

#ifdef USES_C241
  &CPlugin_241,
#endif // ifdef USES_C241

#ifdef USES_C242
  &CPlugin_242,
#endif // ifdef USES_C242

#ifdef USES_C243
  &CPlugin_243,
#endif // ifdef USES_C243

#ifdef USES_C244
  &CPlugin_244,
#endif // ifdef USES_C244

#ifdef USES_C245
  &CPlugin_245,
#endif // ifdef USES_C245

#ifdef USES_C246
  &CPlugin_246,
#endif // ifdef USES_C246

#ifdef USES_C247
  &CPlugin_247,
#endif // ifdef USES_C247

#ifdef USES_C248
  &CPlugin_248,
#endif // ifdef USES_C248

#ifdef USES_C249
  &CPlugin_249,
#endif // ifdef USES_C249

#ifdef USES_C250
  &CPlugin_250,
#endif // ifdef USES_C250

#ifdef USES_C251
  &CPlugin_251,
#endif // ifdef USES_C251

#ifdef USES_C252
  &CPlugin_252,
#endif // ifdef USES_C252

#ifdef USES_C253
  &CPlugin_253,
#endif // ifdef USES_C253

#ifdef USES_C254
  &CPlugin_254,
#endif // ifdef USES_C254

#ifdef USES_C255
  &CPlugin_255,
#endif // ifdef USES_C255
};


constexpr size_t ProtocolIndex_to_CPlugin_id_size = sizeof(ProtocolIndex_to_CPlugin_id);

// Highest CPlugin ID included in the build
constexpr size_t Highest_CPlugin_id = ProtocolIndex_to_CPlugin_id_size == 0 ? 0 : ProtocolIndex_to_CPlugin_id[ProtocolIndex_to_CPlugin_id_size - 1];

constexpr size_t CPlugin_id_to_ProtocolIndex_size = Highest_CPlugin_id + 1;

// Array filled during init.
// Valid index: 1 ... Highest_CPlugin_id
// Returns index to the ProtocolIndex_to_CPlugin_id array
protocolIndex_t CPlugin_id_to_ProtocolIndex[CPlugin_id_to_ProtocolIndex_size]{};


ProtocolStruct ProtocolArray[ProtocolIndex_to_CPlugin_id_size + 1]{};

ProtocolStruct& getProtocolStruct(protocolIndex_t protocolIndex)
{
  if (protocolIndex >= ProtocolIndex_to_CPlugin_id_size) {
    protocolIndex = ProtocolIndex_to_CPlugin_id_size;
  }
  return ProtocolArray[protocolIndex];
}

protocolIndex_t getProtocolIndex_from_CPluginID_(cpluginID_t cpluginID)
{
  if (cpluginID < CPlugin_id_to_ProtocolIndex_size)
  {
    return static_cast<protocolIndex_t>(CPlugin_id_to_ProtocolIndex[cpluginID]);
  }
  return INVALID_PROTOCOL_INDEX;
}

cpluginID_t getCPluginID_from_ProtocolIndex_(protocolIndex_t protocolIndex)
{
  if (protocolIndex < ProtocolIndex_to_CPlugin_id_size)
  {
    //    return static_cast<cpluginID_t>(ProtocolIndex_to_CPlugin_id[protocolIndex]);
    return static_cast<cpluginID_t>(pgm_read_byte(ProtocolIndex_to_CPlugin_id + protocolIndex));
  }
  return INVALID_C_PLUGIN_ID;
}

bool validProtocolIndex_init(protocolIndex_t protocolIndex)
{
  return protocolIndex < ProtocolIndex_to_CPlugin_id_size;
}

cpluginID_t getHighestIncludedCPluginID()
{ 
  return Highest_CPlugin_id;
}


bool CPluginCall(protocolIndex_t protocolIndex, CPlugin::Function Function, struct EventStruct *event, String& string)
{
  if (protocolIndex < ProtocolIndex_to_CPlugin_id_size)
  {
    START_TIMER;
    CPlugin_ptr_t cplugin_call = (CPlugin_ptr_t)pgm_read_ptr(CPlugin_ptr + protocolIndex);
    const bool res = cplugin_call(Function, event, string);
    STOP_TIMER_CONTROLLER(protocolIndex, Function);
    return res;
  }
  return false;
}

void CPluginSetup()
{
  static bool setupDone = false;

  if (setupDone) { return; }

  for (size_t id = 0; id < CPlugin_id_to_ProtocolIndex_size; ++id)
  {
    CPlugin_id_to_ProtocolIndex[id] = INVALID_PROTOCOL_INDEX;
  }

  for (protocolIndex_t protocolIndex = 0; protocolIndex < ProtocolIndex_to_CPlugin_id_size; ++protocolIndex)
  {
    const cpluginID_t cpluginID = getCPluginID_from_ProtocolIndex_(protocolIndex);

    if (INVALID_C_PLUGIN_ID != cpluginID) {
      CPlugin_id_to_ProtocolIndex[cpluginID] = protocolIndex;
      struct EventStruct TempEvent;
      TempEvent.idx = protocolIndex;
      String dummy;
      CPluginCall(protocolIndex, CPlugin::Function::CPLUGIN_PROTOCOL_ADD, &TempEvent, dummy);
    }
  }
  setupDone = true;
}

void CPluginInit()
{
  // Set all not supported cplugins to disabled.
  for (controllerIndex_t controller = 0; controller < CONTROLLER_MAX; ++controller) {
    if (!supportedCPluginID(Settings.Protocol[controller])) {
      Settings.ControllerEnabled[controller] = false;
    }
  }
  CPluginCall(CPlugin::Function::CPLUGIN_INIT_ALL, 0);
}

#include "../Helpers/Hardware_device_info.h"

#ifdef ESP32_CLASSIC

# include <soc/soc.h>
# include <soc/efuse_reg.h>
# include <soc/spi_reg.h>
# include <soc/rtc.h>
# include <esp_chip_info.h>
# include <bootloader_common.h>

bool isFlashInterfacePin_ESPEasy(int gpio) {
  if (getChipFeatures().embeddedFlash ||
      getChipFeatures().embeddedPSRAM) {
    // See page 20: https://www.espressif.com/sites/default/files/documentation/esp32_datasheet_en.pdf
    if (getChipFeatures().embeddedFlash) {
      //  ESP32-U4WDH In-Package Flash (4 MB)
      //  SD_DATA_1   IO0/DI      (GPIO-8)
      //  GPIO17      IO1/DO      (GPIO-17)
      //  SD_DATA_0   IO2/WP#     (GPIO-7)
      //  SD_CMD      IO3/HOLD#   (GPIO-11)
      //  SD_CLK      CLK         (GPIO-6)
      //  GPIO16      CS#         (GPIO-16)
      //  GND         VSS
      //  VDD_SDIO1   VDD

      if ((gpio >= 6) && (gpio <= 8)) { return true; }

      if ((gpio == 17) || (gpio == 11) || (gpio == 16)) { return true; }
    }

    if (getChipFeatures().embeddedPSRAM) {
      //  ESP32-D0WDR2-V3 In-Package PSRAM (2 MB)
      //  SD_DATA_1       SIO0/SI (GPIO-8)
      //  SD_DATA_0       SIO1/SO (GPIO-7)
      //  SD_DATA_3       SIO2    (GPIO-10)
      //  SD_DATA_2       SIO3    (GPIO-9)
      //  SD_CLK          SCLK    (GPIO-6)
      //  GPIO16          CE#     (GPIO-16)
      //  GND             VSS
      //  VDD_SDIO1       VDD
      if ((gpio >= 6) && (gpio <= 10)) { return true; }

      if (gpio == 16) { return true; }
    }
    return false;
  }


  // GPIO-6 ... 11: SPI flash and PSRAM
  // GPIO-16 & 17: CS for PSRAM, thus only unuable when PSRAM is present
  return (gpio) >= 6 && (gpio) <= 11;
}

bool flashVddPinCanBeUsedAsGPIO()
{
  return false;
}

int32_t getEmbeddedFlashSize()
{
  return 0;
}

int32_t getEmbeddedPSRAMSize()
{
  // FIXME TD-er: Need to implement
  return 0;
}

# ifndef isPSRAMInterfacePin
bool isPSRAMInterfacePin(int gpio) {
  // GPIO-6 ... 11: SPI flash and PSRAM
  // GPIO-16 & 17: CS for PSRAM, thus only unuable when PSRAM is present
  return FoundPSRAM() ? ((gpio) == 16 || (gpio) == 17) : false;
}

# endif // ifndef isPSRAMInterfacePin


const __FlashStringHelper* getChipModel(uint32_t chip_model, uint32_t chip_revision, uint32_t pkg_version, bool single_core)
{
  //  uint32_t chip_revision = ESP.getChipRevision();
  const bool rev3 = (3 == chip_revision);

  if (chip_model < 2) { // ESP32
    /* esptool:
       def get_pkg_version(self):
       word3 = self.read_efuse(3)
       pkg_version = (word3 >> 9) & 0x07
       pkg_version += ((word3 >> 2) & 0x1) << 3
       return pkg_version
     */
# if ESP_IDF_VERSION_MAJOR < 5
    uint32_t chip_ver = REG_GET_FIELD(EFUSE_BLK0_RDATA3_REG, EFUSE_RD_CHIP_VER_PKG);
# else // if ESP_IDF_VERSION_MAJOR < 5
    uint32_t chip_ver = REG_GET_FIELD(EFUSE_BLK0_RDATA3_REG, EFUSE_RD_CHIP_PACKAGE);
# endif // if ESP_IDF_VERSION_MAJOR < 5
    pkg_version = chip_ver & 0x7;


    //    AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("HDW: ESP32 Model %d, Revision %d, Core %d, Package %d"), chip_info.model, chip_revision,
    // chip_info.cores, chip_ver);

    switch (pkg_version) {
      case EFUSE_RD_CHIP_VER_PKG_ESP32D0WDQ6:

        if (single_core) { return F("ESP32-S0WDQ6"); }    // Max 240MHz, Single core, QFN 6*6
        else if (rev3)   { return F("ESP32-D0WDQ6-V3"); } // Max 240MHz, Dual core, QFN 6*6
        else {             return F("ESP32-D0WDQ6"); }    // Max 240MHz, Dual core, QFN 6*6
      case EFUSE_RD_CHIP_VER_PKG_ESP32D0WDQ5:

        if (single_core) { return F("ESP32-S0WD"); }      // Max 160MHz, Single core, QFN 5*5, ESP32-SOLO-1, ESP32-DevKitC
        else if (rev3)   { return F("ESP32-D0WDQ5-V3"); } // Max 240MHz, Dual core, QFN 5*5, ESP32-WROOM-32E, ESP32_WROVER-E, ESP32-DevKitC
        else {             return F("ESP32-D0WDQ5"); }    // Max 240MHz, Dual core, QFN 5*5, ESP32-WROOM-32D, ESP32_WROVER-B, ESP32-DevKitC
      case EFUSE_RD_CHIP_VER_PKG_ESP32D2WDQ5:
        return F("ESP32-D2WDQ5"); // Max 160MHz, Dual core, QFN 5*5, 2MB embedded flash
      case 3:

        if (single_core) { return F("ESP32-S0WD-OEM"); } // Max 160MHz, Single core, QFN 5*5, Xiaomi Yeelight
        else {             return F("ESP32-D0WD-OEM"); } // Max 240MHz, Dual core, QFN 5*5
      case EFUSE_RD_CHIP_VER_PKG_ESP32U4WDH:
        return F("ESP32-U4WDH");                         // Max 160MHz, Single core, QFN 5*5, 4MB embedded flash, ESP32-MINI-1,
      // ESP32-DevKitM-1
      case EFUSE_RD_CHIP_VER_PKG_ESP32PICOD4:

        if (rev3)        { return F("ESP32-PICO-V3"); } // Max 240MHz, Dual core, LGA 7*7, ESP32-PICO-V3-ZERO, ESP32-PICO-V3-ZERO-DevKit
        else {             return F("ESP32-PICO-D4"); } // Max 240MHz, Dual core, LGA 7*7, 4MB embedded flash, ESP32-PICO-KIT
      case EFUSE_RD_CHIP_VER_PKG_ESP32PICOV302:
        return F("ESP32-PICO-V3-02");                   // Max 240MHz, Dual core, LGA 7*7, 8MB embedded flash, 2MB embedded PSRAM,
      // ESP32-PICO-MINI-02, ESP32-PICO-DevKitM-2
      case EFUSE_RD_CHIP_VER_PKG_ESP32D0WDR2V3:
        return F("ESP32-D0WDR2-V3"); // Max 240MHz, Dual core, QFN 5*5, ESP32-WROOM-32E, ESP32_WROVER-E, ESP32-DevKitC
    }
    return F("ESP32");
  }

  return F("Unknown");
}

#endif // ifdef ESP32_CLASSIC

#include "../Helpers/Hardware_device_info.h"

#include "../Helpers/Hardware_defines.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/FS_Helper.h"

#ifdef ESP32
  # include <soc/soc.h>
  # include <soc/efuse_reg.h>
  # include <soc/spi_reg.h>
  # include <soc/rtc.h>

  # if ESP_IDF_VERSION_MAJOR == 4
    #  if CONFIG_IDF_TARGET_ESP32S3 // ESP32-S3
      #   include <esp32s3/rom/spi_flash.h>
      #   include <esp32s3/spiram.h>
      #   include <esp32s3/rom/rtc.h>
    #  elif CONFIG_IDF_TARGET_ESP32S2 // ESP32-S2
      #   include <esp32s2/rom/spi_flash.h>
      #   include <esp32s2/spiram.h>
      #   include <esp32s2/rom/rtc.h>
    #  elif CONFIG_IDF_TARGET_ESP32C3 // ESP32-C3
      #   include <esp32c3/rom/spi_flash.h>
      #   include <esp32c3/rom/rtc.h>
    #  elif CONFIG_IDF_TARGET_ESP32   // ESP32/PICO-D4
      #   include <esp32/rom/spi_flash.h>
      #   include <esp32/rom/rtc.h>
      #   include <esp32/spiram.h>
    #  else // if CONFIG_IDF_TARGET_ESP32S3
      #   error Target CONFIG_IDF_TARGET is not supported
    #  endif // if CONFIG_IDF_TARGET_ESP32S3
  # else    // ESP32 IDF 5.x and later
    #  include <rom/spi_flash.h>
    #  include <rom/rtc.h>
    #  include <bootloader_common.h>
  # endif // if ESP_IDF_VERSION_MAJOR == 4


# if CONFIG_IDF_TARGET_ESP32 // ESP32/PICO-D4
  #  if ESP_IDF_VERSION_MAJOR < 5
  #   define HAS_HALL_EFFECT_SENSOR  1
  #  else // if ESP_IDF_VERSION_MAJOR < 5

// Support for Hall Effect sensor was removed in ESP_IDF 5.x
  #   define HAS_HALL_EFFECT_SENSOR  0
  #  endif // if ESP_IDF_VERSION_MAJOR < 5
# else // if CONFIG_IDF_TARGET_ESP32
  #  define HAS_HALL_EFFECT_SENSOR  0
# endif // if CONFIG_IDF_TARGET_ESP32

# if ESP_IDF_VERSION_MAJOR >= 5

#  include <esp_chip_info.h>
#  include <soc/soc.h>
#  include <driver/ledc.h>
#  include <esp_psram.h>

// #include <hal/ledc_hal.h>

# endif // if ESP_IDF_VERSION_MAJOR >= 5
#endif  // ifdef ESP32

/********************************************************************************************\
   Hardware information
 \*********************************************************************************************/
#ifdef ESP8266
int32_t getPartitionInfo(ESP8266_partition_type ptype, uint32_t& address, int32_t& size)
{
  address = 0;
  size    = -1;
  const uint32_t addr_offset = 0x40200000;
  const uint32_t realSize    = getFlashRealSizeInBytes();

  switch (ptype) {
    case ESP8266_partition_type::sketch:
      address = 0;
      size    = getSketchSize();
      break;
    case ESP8266_partition_type::ota:
      address = getSketchSize();
      size    = getFreeSketchSpace();
      break;
    case ESP8266_partition_type::fs:
      address = ((uint32_t)&_FS_start - addr_offset);
      size    = ((uint32_t)((uint32_t)&_FS_end - (uint32_t)&_FS_start));
      break;
    case ESP8266_partition_type::eeprom:
      address = ((uint32_t)&_EEPROM_start - addr_offset);
      size    = realSize - address - 16384;
      break;
    case ESP8266_partition_type::rf_cal:
      address = realSize - 16384;
      size    = 4096;
      break;
    case ESP8266_partition_type::wifi:
      address = realSize - 12288;
      size    = 12288;
      break;
  }

  if (size > 0) {
    return address / SPI_FLASH_SEC_SIZE;
  }
  return -1;
}

#endif // ifdef ESP8266


uint32_t getFlashChipId() {
  // Cache since size does not change
  static uint32_t flashChipId = 0;

  if (flashChipId == 0) {
  #ifdef ESP32
    uint32_t tmp = g_rom_flashchip.device_id;

    for (int i = 0; i < 3; ++i) {
      flashChipId  = flashChipId << 8;
      flashChipId |= (tmp & 0xFF);
      tmp          = tmp >> 8;
    }

    //    esp_flash_read_id(nullptr, &flashChipId);
  #elif defined(ESP8266)
    flashChipId = ESP.getFlashChipId();
  #endif // ifdef ESP32
  }
  return flashChipId;
}

uint32_t getFlashRealSizeInBytes() {
  // Cache since size does not change
  static uint32_t res = 0;

  if (res == 0) {
    #if defined(ESP32)
    res = (1 << ((getFlashChipId() >> 16) & 0xFF));
    #else // if defined(ESP32)
    res = ESP.getFlashChipRealSize(); // ESP.getFlashChipSize();
    #endif // if defined(ESP32)
  }
  return res;
}

#ifdef ESP32
uint32_t getXtalFrequencyMHz() {
  return rtc_clk_xtal_freq_get();
}

esp32_chip_features getChipFeatures() {
  static esp32_chip_features res;
  static bool loaded = false;

  if (!loaded) {
    esp_chip_info_t chip_info;
    esp_chip_info(&chip_info);

    res.embeddedFlash     = chip_info.features & CHIP_FEATURE_EMB_FLASH;
    res.wifi_bgn          = chip_info.features & CHIP_FEATURE_WIFI_BGN;
    res.bluetooth_ble     = chip_info.features & CHIP_FEATURE_BLE;
    res.bluetooth_classic = chip_info.features & CHIP_FEATURE_BT;
    res.ieee_802_15_4     = chip_info.features & CHIP_FEATURE_IEEE802154;
    res.embeddedPSRAM     = chip_info.features & CHIP_FEATURE_EMB_PSRAM;

    if (!res.embeddedFlash) {
      const int32_t flash_cap = getEmbeddedFlashSize();

      if (flash_cap > 0) {
        res.embeddedFlash = true;
      }
    }

    loaded = true;
  }
  return res;
}

String getChipFeaturesString() {
  String features;

  if (getChipFeatures().wifi_bgn) { features += F("Wi-Fi bgn / "); }

  if (getChipFeatures().bluetooth_ble) { features += F("BLE / "); }

  if (getChipFeatures().ieee_802_15_4) { features += F("IEEE 802.15.4 / "); }

  const int32_t flash_cap = getEmbeddedFlashSize();

  if (getChipFeatures().embeddedFlash || (flash_cap != 0)) {
    if (flash_cap > 0) {
      features += strformat(F("%dMB "), flash_cap);
    } else if (flash_cap < 0) {
      features += strformat(F("(%d) "), flash_cap);
    }
    features += F("Emb. Flash");
    features += F(" / ");
  }

  const int32_t psram_cap = getEmbeddedPSRAMSize();

  if (getChipFeatures().embeddedPSRAM || (psram_cap != 0)) {
    if (psram_cap > 0) {
      features += strformat(F("%dMB "), psram_cap);
    } else if (psram_cap < 0) {
      features += strformat(F("(%d) "), psram_cap);
    }
    features += F("Emb. PSRAM");
  }
  features.trim();

  if (features.endsWith(F("/"))) { features = features.substring(0, features.length() - 1); }
  return features;
}

bool getFlashChipOPI_wired() {
  # if defined(ESP32_CLASSIC) || defined(ESP32C2)
  return false;

  # else // ifdef ESP32_CLASSIC

  // Source: https://github.com/espressif/esptool/commit/b25606b95920bd06df87aff9202c7a15377d4a30
  const uint32_t data = REG_GET_BIT(EFUSE_RD_REPEAT_DATA3_REG, BIT(9));
  return data != 0;
  # endif // ifdef ESP32_CLASSIC
}

#endif    // ifdef ESP32


uint32_t getFlashChipSpeed() {
  #ifdef ESP8266
  return ESP.getFlashChipSpeed();
  #else // ifdef ESP8266
# if ESP_IDF_STILL_NEEDS_SPI_REGISTERS_FIXED

  // ESP IDF still needs to patch those SPI registers
  // for which patches have been submitted and somehow they managed to merge it completely wrong.
  return ESP.getFlashChipSpeed();
# else // if ESP_IDF_STILL_NEEDS_SPI_REGISTERS_FIXED

  // All ESP32-variants have the SPI flash wired to SPI peripheral 1
  const uint32_t spi_clock = REG_READ(SPI_CLOCK_REG(1));

  /*
     addLog(LOG_LEVEL_INFO,   strformat(
      F("SPI_clock: %x  FSPI: %d SPI_CLOCK_REG(1): %x"),
      spi_clock, FSPI, SPI_CLOCK_REG(1)));
   */

  if (spi_clock & BIT(31)) {
    // spi_clk is equal to system clock
    return getApbFrequency();
  }
  return spiClockDivToFrequency(spi_clock);
# endif // if ESP_IDF_STILL_NEEDS_SPI_REGISTERS_FIXED
  #endif // ifdef ESP8266
}

const __FlashStringHelper* getFlashChipMode() {
  #ifdef ESP32

  if (getFlashChipOPI_wired()) {
    switch (ESP.getFlashChipMode()) {
      case FM_QIO:     return F("QIO (OPI Wired)");
      case FM_QOUT:    return F("QOUT (OPI Wired)");
      case FM_DIO:     return F("DIO (OPI Wired)");
      case FM_DOUT:    return F("DOUT (OPI Wired)");
  # ifdef ESP32
      case FM_FAST_READ: return F("Fast (OPI Wired)");
      case FM_SLOW_READ: return F("Slow (OPI Wired)");
  # endif // ifdef ESP32
      case FM_UNKNOWN: break;
    }
  }

  #endif // ifdef ESP32

  switch (ESP.getFlashChipMode()) {
    case FM_QIO:     return F("QIO");
    case FM_QOUT:    return F("QOUT");
    case FM_DIO:     return F("DIO");
    case FM_DOUT:    return F("DOUT");
#ifdef ESP32
    case FM_FAST_READ: return F("Fast");
    case FM_SLOW_READ: return F("Slow");
#endif // ifdef ESP32
    case FM_UNKNOWN: break;
  }
  return F("Unknown");
}

bool puyaSupport() {
  bool supported = false;

#ifdef PUYA_SUPPORT

  // New support starting core 2.5.0
  if (PUYA_SUPPORT) { supported = true; }
#endif // ifdef PUYA_SUPPORT
#ifdef PUYASUPPORT

  // Old patch
  supported = true;
#endif // ifdef PUYASUPPORT
  return supported;
}

uint8_t getFlashChipVendorId() {
#ifdef PUYA_SUPPORT
  return ESP.getFlashChipVendorId();
#else // ifdef PUYA_SUPPORT
  # if defined(ESP8266)

  // Cache since size does not change
  static uint32_t flashChipId = ESP.getFlashChipId();
  return flashChipId & 0x000000ff;
  # elif defined(ESP32)
  return 0xFF; // Not an existing function for ESP32
  # endif // if defined(ESP8266)
#endif // ifdef PUYA_SUPPORT
}

bool flashChipVendorPuya() {
  const uint8_t vendorId = getFlashChipVendorId();

  return vendorId == 0x85; // 0x146085 PUYA
}

uint32_t getChipId() {
  uint32_t chipId = 0;

#ifdef ESP8266
  chipId = ESP.getChipId();
#endif // ifdef ESP8266
#ifdef ESP32

  for (int i = 0; i < 17; i = i + 8) {
    chipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i;
  }
#endif // ifdef ESP32

  return chipId;
}

uint8_t getChipCores() {
  #ifdef ESP8266
  return 1;
  #else // ifdef ESP8266
  static uint8_t cores = 0;

  if (cores == 0) {
    esp_chip_info_t chip_info;
    esp_chip_info(&chip_info);
    cores = chip_info.cores;
  }
  return cores;
  #endif // ifdef ESP8266
}

#ifdef ESP32
bool isESP8285() {
  return false;
}

#endif // ifdef ESP32


String getChipRevision() {
  static uint16_t rev = 0;

  #ifdef ESP32

  // See: https://github.com/espressif/esp-idf/blob/master/examples/get-started/hello_world/main/hello_world_main.c
  if (rev == 0) {
    esp_chip_info_t chip_info;
    esp_chip_info(&chip_info);
    # if ESP_IDF_VERSION_MAJOR < 5
    rev = chip_info.full_revision;
    # else // if ESP_IDF_VERSION_MAJOR < 5
    rev = chip_info.revision;
    # endif // if ESP_IDF_VERSION_MAJOR < 5
  }
  #endif // ifdef ESP32
  return strformat(F("%d.%02d"), rev / 100, rev % 100);
}

uint32_t getSketchSize() {
  // Cache the value as this never changes during run time.
  static uint32_t res = ESP.getSketchSize();

  return res;
}

uint32_t getFreeSketchSpace() {
  // Cache the value as this never changes during run time.
  static uint32_t res = ESP.getFreeSketchSpace();

  return res;
}


/********************************************************************************************\
   PSRAM support
 \*********************************************************************************************/

#ifdef ESP32

// this function is a replacement for `psramFound()`.
// `psramFound()` can return true even if no PSRAM is actually installed
// This new version also checks `esp_spiram_is_initialized` to know if the PSRAM is initialized
// Original Tasmota:
// https://github.com/arendst/Tasmota/blob/1e6b78a957be538cf494f0e2dc49060d1cb0fe8b/tasmota/support_esp.ino#L470
bool FoundPSRAM() {
  # if ESP_IDF_VERSION_MAJOR >= 5
  return psramFound();
  # else // if ESP_IDF_VERSION_MAJOR >= 5
#  if CONFIG_IDF_TARGET_ESP32C3
  return psramFound();
#  else  // if CONFIG_IDF_TARGET_ESP32C3
  return psramFound() && esp_spiram_is_initialized();
#  endif // if CONFIG_IDF_TARGET_ESP32C3
  # endif // if ESP_IDF_VERSION_MAJOR >= 5
}

// new function to check whether PSRAM is present and supported (i.e. required pacthes are present)
bool UsePSRAM() {
  static bool can_use_psram = CanUsePSRAM();

  return FoundPSRAM() && can_use_psram;
}

/*
 * ESP32 v1 and v2 needs some special patches to use PSRAM.
 * Original function used from Tasmota:
 * https://github.com/arendst/Tasmota/blob/1e6b78a957be538cf494f0e2dc49060d1cb0fe8b/tasmota/support_esp.ino#L762
 *
 * If using ESP32 v1, please add: `-mfix-esp32-psram-cache-issue -lc-psram-workaround -lm-psram-workaround`
 *
 * This function returns true if the chip supports PSRAM natively (v3) or if the
 * patches are present.
 */
bool CanUsePSRAM() {
  if (!FoundPSRAM()) { return false; }
# ifdef HAS_PSRAM_FIX
  return true;
# endif // ifdef HAS_PSRAM_FIX
# if CONFIG_IDF_TARGET_ESP32
  esp_chip_info_t chip_info;
  esp_chip_info(&chip_info);

  if ((CHIP_ESP32 == chip_info.model) &&
#  if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
      (chip_info.revision < 300)
#  else // if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
      (chip_info.revision < 3)
#  endif // if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
      ) {
    return false;
  }
#  if ESP_IDF_VERSION_MAJOR < 4
  uint32_t chip_ver    = REG_GET_FIELD(EFUSE_BLK0_RDATA3_REG, EFUSE_RD_CHIP_VER_PKG);
  uint32_t pkg_version = chip_ver & 0x7;

  if ((CHIP_ESP32 == chip_info.model) && (pkg_version >= 6)) {
    return false; // support for embedded PSRAM of ESP32-PICO-V3-02 requires esp-idf 4.4
  }
#  endif // ESP_IDF_VERSION_MAJOR < 4

# endif // if CONFIG_IDF_TARGET_ESP32
  return true;
}

# ifdef ESP32
const __FlashStringHelper* getChipModel()
{
  // https://www.espressif.com/en/products/socs
  // https://github.com/arendst/Tasmota/blob/1e6b78a957be538cf494f0e2dc49060d1cb0fe8b/tasmota/support_esp.ino#L579

  /*
     Source: esp_chip_info.h & esptool.py  ('IMAGE_CHIP_ID') https://github.com/espressif/esptool/search?q=IMAGE_CHIP_ID

     typedef enum {
     CHIP_ESP32      = 1,  //!< ESP32
     CHIP_ESP32S2    = 2,  //!< ESP32-S2
     CHIP_ESP32S3_b  = 4,  //!< ESP32-S3(beta2)
     CHIP_ESP32S3    = 9,  //!< ESP32-S3
     CHIP_ESP32C3    = 5,  //!< ESP32-C3
     CHIP_ESP32C2    = 12, //!< ESP32-C2
     CHIP_ESP32C5    = 17, //!< ESP32-C5 beta3 (MPW)
     CHIP_ESP32C5    = 23, //!< ESP32-C5 MP
     CHIP_ESP32C6_b  = 7,  //!< ESP32-C6(beta)
     CHIP_ESP32C6    = 13, //!< ESP32-C6
     CHIP_ESP32C61   = 20, //!< ESP32-C61
     CHIP_ESP32H2_b1 = 10, //!< ESP32-H2(beta1)
     CHIP_ESP32H2_b2 = 14, //!< ESP32-H2(beta2)
     CHIP_ESP32H2    = 16, //!< ESP32-H2
     CHIP_ESP32P4    = 18, //!< ESP32-P4
     } esp_chip_model_t;

     // Chip feature flags, used in esp_chip_info_t
   #define CHIP_FEATURE_EMB_FLASH      BIT(0)      //!< Chip has embedded flash memory
   #define CHIP_FEATURE_WIFI_BGN       BIT(1)      //!< Chip has 2.4GHz WiFi
   #define CHIP_FEATURE_BLE            BIT(4)      //!< Chip has Bluetooth LE
   #define CHIP_FEATURE_BT             BIT(5)      //!< Chip has Bluetooth Classic
   #define CHIP_FEATURE_IEEE802154     BIT(6)      //!< Chip has IEEE 802.15.4
   #define CHIP_FEATURE_EMB_PSRAM      BIT(7)      //!< Chip has embedded psram

     // The structure represents information about the chip
     typedef struct {
     esp_chip_model_t model;  //!< chip model, one of esp_chip_model_t
     uint32_t features;       //!< bit mask of CHIP_FEATURE_x feature flags
     uint8_t cores;           //!< number of CPU cores
     uint8_t revision;        //!< chip revision number
     } esp_chip_info_t;
   */

  esp_chip_info_t chip_info;

  esp_chip_info(&chip_info);

  uint32_t chip_model          = chip_info.model;
  const uint32_t chip_revision =
#  if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
    chip_info.revision / 100;
#  else // if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
    chip_info.revision;
#  endif // if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)

  uint32_t pkg_version = 0;
#  if (ESP_IDF_VERSION_MAJOR >= 5)
  pkg_version = bootloader_common_get_chip_ver_pkg();
#  endif // if (ESP_IDF_VERSION_MAJOR >= 5)

  //  bool single_core = (1 == ESP.getChipCores());
  bool single_core = (1 == chip_info.cores);


  return getChipModel(chip_model, chip_revision, pkg_version, single_core);
}

# endif // ifdef ESP32


#endif  // ESP32

#include "../Helpers/Hardware_GPIO.h"


#include "../Globals/Settings.h"
#include "../Helpers/Hardware_defines.h"
#include "../Helpers/Hardware_device_info.h"

// ********************************************************************************
// Get info of a specific GPIO pin
// ********************************************************************************


bool isSerialConsolePin(int gpio) {
  if (!Settings.UseSerial) { return false; }

#if defined(SOC_RX0) && defined(SOC_TX0)
  return (gpio == SOC_TX0 || gpio == SOC_RX0)
         #if USES_ESPEASY_CONSOLE_FALLBACK_PORT
         && Settings.console_serial0_fallback
         #endif // if USES_ESPEASY_CONSOLE_FALLBACK_PORT
  ;
#else
  static_assert(false, "Implement processor architecture");
  return false;
#endif
}

#include "../Helpers/PeriodicalActions.h"


#include "../../ESPEasy-Globals.h"

#include "../ControllerQueue/DelayQueueElements.h"
#include "../ControllerQueue/MQTT_queue_element.h"
#include "../DataStructs/TimingStats.h"
#include "../DataTypes/ESPEasy_plugin_functions.h"
#include "../ESPEasyCore/Controller.h"
#include "../ESPEasyCore/ESPEasyGPIO.h"
#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../ESPEasyCore/ESPEasyNetwork.h"
#include "../ESPEasyCore/ESPEasyWifi.h"
#include "../ESPEasyCore/ESPEasyRules.h"
#include "../ESPEasyCore/Serial.h"
#include "../Globals/ESPEasyWiFiEvent.h"
#if FEATURE_ETHERNET
#include "../Globals/ESPEasyEthEvent.h"
#endif
#include "../Globals/ESPEasy_Scheduler.h"
#include "../Globals/ESPEasy_time.h"
#include "../Globals/EventQueue.h"
#include "../Globals/MainLoopCommand.h"
#include "../Globals/MQTT.h"
#include "../Globals/NetworkState.h"
#include "../Globals/RTC.h"
#include "../Globals/Services.h"
#include "../Globals/Settings.h"
#include "../Globals/Statistics.h"
#include "../Globals/WiFi_AP_Candidates.h"
#include "../Helpers/ESPEasyRTC.h"
#include "../Helpers/FS_Helper.h"
#include "../Helpers/Hardware_temperature_sensor.h"
#include "../Helpers/Memory.h"
#include "../Helpers/Misc.h"
#include "../Helpers/Networking.h"
#include "../Helpers/StringGenerator_System.h"
#include "../Helpers/StringGenerator_WiFi.h"
#include "../Helpers/StringProvider.h"

#ifdef USES_C015
#include "../../ESPEasy_fdwdecl.h"
#endif



#define PLUGIN_ID_MQTT_IMPORT         37


/*********************************************************************************************\
 * Tasks that run 50 times per second
\*********************************************************************************************/

void run50TimesPerSecond() {
  String dummy;
  {
    START_TIMER;
    PluginCall(PLUGIN_FIFTY_PER_SECOND, 0, dummy);
    STOP_TIMER(PLUGIN_CALL_50PS);
  }
  {
    START_TIMER;
    CPluginCall(CPlugin::Function::CPLUGIN_FIFTY_PER_SECOND, 0, dummy);
    STOP_TIMER(CPLUGIN_CALL_50PS);
  }
  processNextEvent();
}

/*********************************************************************************************\
 * Tasks that run 10 times per second
\*********************************************************************************************/
void run10TimesPerSecond() {
  String dummy;
  //@giig19767g: WARNING: Monitor10xSec must run before PLUGIN_TEN_PER_SECOND
  {
    START_TIMER;
    GPIO_Monitor10xSec();
    STOP_TIMER(PLUGIN_CALL_10PSU);
  }
  {
    START_TIMER;
    PluginCall(PLUGIN_TEN_PER_SECOND, 0, dummy);
    STOP_TIMER(PLUGIN_CALL_10PS);
  }
  {
    START_TIMER;
//    PluginCall(PLUGIN_UNCONDITIONAL_POLL, 0, dummyString);
    PluginCall(PLUGIN_MONITOR, 0, dummy);
    STOP_TIMER(PLUGIN_CALL_10PSU);
  }
  {
    START_TIMER;
    CPluginCall(CPlugin::Function::CPLUGIN_TEN_PER_SECOND, 0, dummy);
    STOP_TIMER(CPLUGIN_CALL_10PS);
  }
  
  #ifdef USES_C015
  if (NetworkConnected()) {
    Blynk_Run_c015();
  }
  #endif
  if (!UseRTOSMultitasking) {
    START_TIMER
    web_server.handleClient();
    STOP_TIMER(WEBSERVER_HANDLE_CLIENT);
  }
}


/*********************************************************************************************\
 * Tasks each second
\*********************************************************************************************/
void runOncePerSecond()
{
  START_TIMER;
  updateLogLevelCache();
  dailyResetCounter++;
  if (dailyResetCounter > 86400) // 1 day elapsed... //86400
  {
    RTC.flashDayCounter=0;
    saveToRTC();
    dailyResetCounter=0;
    #ifndef BUILD_MINIMAL_OTA
    addLog(LOG_LEVEL_INFO, F("SYS  : Reset 24h counters"));
    #endif
  }

  if (Settings.ConnectionFailuresThreshold)
    if (WiFiEventData.connectionFailures > Settings.ConnectionFailuresThreshold)
      delayedReboot(60, IntendedRebootReason_e::DelayedReboot);

  if (cmd_within_mainloop != 0)
  {
    switch (cmd_within_mainloop)
    {
      case CMD_WIFI_DISCONNECT:
        {
          WifiDisconnect();
          break;
        }
      case CMD_REBOOT:
        {
          reboot(IntendedRebootReason_e::CommandReboot);
          break;
        }
    }
    cmd_within_mainloop = 0;
  }
  // clock events
  if (node_time.reportNewMinute()) {
    String dummy;
    PluginCall(PLUGIN_CLOCK_IN, 0, dummy);
    if (Settings.UseRules)
    {
      // FIXME TD-er: What to do when the system time is not (yet) present?
      if (node_time.systemTimePresent()) {
        // TD-er: Do not add to the eventQueue, but execute right now.
        const String event = strformat(
          F("Clock#Time=%s,%s"), 
          node_time.weekday_str().c_str(),
          node_time.getTimeString(':', false).c_str());
        rulesProcessing(event);
      }
    }
  }

//  unsigned long start = micros();
  String dummy;
  PluginCall(PLUGIN_ONCE_A_SECOND, 0, dummy);
//  unsigned long elapsed = micros() - start;


  // I2C Watchdog feed
  if (Settings.WDI2CAddress != 0)
  {
    #if FEATURE_I2C_MULTIPLE
    I2CSelectHighClockSpeed(Settings.getI2CInterfaceWDT()); // Select bus
    #endif // if FEATURE_I2C_MULTIPLE
    I2C_write8(Settings.WDI2CAddress, 0xA5);
  }

  #if FEATURE_MDNS
  #ifdef ESP8266
  // Allow MDNS processing
  if (NetworkConnected()) {
    MDNS.announce();
  }
  #endif
  #endif // if FEATURE_MDNS

  #if FEATURE_INTERNAL_TEMPERATURE && defined(ESP32_CLASSIC)
  getInternalTemperature(); // Just read the value every second to hopefully get a valid next reading on original ESP32
  #endif // if FEATURE_INTERNAL_TEMPERATURE && defined(ESP32_CLASSIC)

  checkResetFactoryPin();
  STOP_TIMER(PLUGIN_CALL_1PS);
}

/*********************************************************************************************\
 * Tasks each 30 seconds
\*********************************************************************************************/
void runEach30Seconds()
{
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAMtoLog();
  #endif
  wdcounter++;
  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    String log = strformat(
      F("WD   : Uptime %d  ConnectFailures %u FreeMem %u"),
      getUptimeMinutes(),
      WiFiEventData.connectionFailures,
      FreeMem());
    bool logWiFiStatus = true;
    #if FEATURE_ETHERNET
    if(active_network_medium == NetworkMedium_t::Ethernet) {
      logWiFiStatus = false;
      log += F( " EthSpeedState ");
      log += getValue(LabelType::ETH_SPEED_STATE);
      log += F(" ETH status: ");
      log += EthEventData.ESPEasyEthStatusToString();
    }
    #endif // if FEATURE_ETHERNET
    if (logWiFiStatus) {
      log += strformat(
        F(" WiFiStatus: %s ESPeasy internal wifi status: %s"),
        ArduinoWifiStatusToString(WiFi.status()).c_str(),
        WiFiEventData.ESPeasyWifiStatusToString().c_str());
    }
//    log += F(" ListenInterval ");
//    log += WiFi.getListenInterval();
    addLogMove(LOG_LEVEL_INFO, log);
#if FEATURE_DEFINE_SERIAL_CONSOLE_PORT
//    addLogMove(LOG_LEVEL_INFO,  ESPEASY_SERIAL_CONSOLE_PORT.getLogString());
#endif
  }
  WiFi_AP_Candidates.purge_expired();
  #if FEATURE_ESPEASY_P2P
  sendSysInfoUDP(1);
  refreshNodeList();
  #endif

  // sending $stats to homie controller
  CPluginCall(CPlugin::Function::CPLUGIN_INTERVAL, 0);

  #if defined(ESP8266)
  #if FEATURE_SSDP
  if (Settings.UseSSDP)
    SSDP_update();

  #endif // if FEATURE_SSDP
  #endif
#if FEATURE_ADC_VCC
  if (!WiFiEventData.wifiConnectInProgress) {
    vcc = ESP.getVcc() / 1000.0f;
  }
#endif

  #if FEATURE_REPORTING
  ReportStatus();
  #endif // if FEATURE_REPORTING

}

#if FEATURE_MQTT


void scheduleNextMQTTdelayQueue() {
  if (MQTTDelayHandler != nullptr) {
    Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_MQTT_DELAY_QUEUE, MQTTDelayHandler->getNextScheduleTime());
  }
}

void schedule_all_MQTTimport_tasks() {
  constexpr pluginID_t PLUGIN_MQTT_IMPORT(PLUGIN_ID_MQTT_IMPORT);

  deviceIndex_t DeviceIndex = getDeviceIndex(PLUGIN_MQTT_IMPORT); // Check if P037_MQTTimport is present in the build
  if (validDeviceIndex(DeviceIndex)) {
    for (taskIndex_t task = 0; task < TASKS_MAX; task++) {
      if ((Settings.getPluginID_for_task(task) == PLUGIN_MQTT_IMPORT) &&
          (Settings.TaskDeviceEnabled[task])) {
        // Schedule a call to each enabled MQTT import plugin to notify the broker connection state
        EventStruct event(task);
        event.Par1 = MQTTclient_connected ? 1 : 0;
        Scheduler.schedule_plugin_task_event_timer(DeviceIndex, PLUGIN_MQTT_CONNECTION_STATE, std::move(event));
      }
    }
  }
}

void processMQTTdelayQueue() {
  if (MQTTDelayHandler == nullptr) {
    return;
  }
  runPeriodicalMQTT(); // Update MQTT connected state.
  if (!MQTTclient_connected) {
    scheduleNextMQTTdelayQueue();
    return;
  }

  START_TIMER;
  MQTT_queue_element *element(static_cast<MQTT_queue_element *>(MQTTDelayHandler->getNext()));

  if (element == nullptr) { return; }

  bool handled = false;

  if (element->_call_PLUGIN_PROCESS_CONTROLLER_DATA) {
    struct EventStruct TempEvent(element->_taskIndex);
    String dummy;

    // FIXME TD-er: Do we need anything from the element in the event?
//    TempEvent.String1 = element->_topic;
//    TempEvent.String2 = element->_payload;
    if (PluginCall(PLUGIN_PROCESS_CONTROLLER_DATA, &TempEvent, dummy)) {
      handled = true;
      MQTTDelayHandler->markProcessed(true);
    } else {
      MQTTDelayHandler->markProcessed(false);
    }
  } else
  if (!handled) {
    if (MQTTclient.publish(element->_topic.c_str(), element->_payload.c_str(), element->_retained)) {
      if (WiFiEventData.connectionFailures > 0) {
        --WiFiEventData.connectionFailures;
      }
      MQTTDelayHandler->markProcessed(true);
    } else {
      MQTTDelayHandler->markProcessed(false);
#ifndef BUILD_NO_DEBUG

      if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
        String log = F("MQTT : process MQTT queue not published, ");
        log += MQTTDelayHandler->sendQueue.size();
        log += F(" items left in queue");
        addLogMove(LOG_LEVEL_DEBUG, log);
      }
#endif // ifndef BUILD_NO_DEBUG
    }
  }
  Scheduler.setIntervalTimerOverride(SchedulerIntervalTimer_e::TIMER_MQTT, 10); // Make sure the MQTT is being processed as soon as possible.
  scheduleNextMQTTdelayQueue();
  STOP_TIMER(MQTT_DELAY_QUEUE);
}

void updateMQTTclient_connected() {
  if (MQTTclient_connected != MQTTclient.connected()) {
    MQTTclient_connected = !MQTTclient_connected;
    if (!MQTTclient_connected) {
      if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
        String connectionError = F("MQTT : Connection lost, state: ");
        connectionError += getMQTT_state();
        addLogMove(LOG_LEVEL_ERROR, connectionError);
      }
      MQTTclient_must_send_LWT_connected = false;
    }
    if (Settings.UseRules) {
      if (MQTTclient_connected) {
        eventQueue.add(F("MQTT#Connected"));
      } else {
        eventQueue.add(F("MQTT#Disconnected"));
      }
    }
    // Now schedule all tasks using the MQTT Import plugin.
    schedule_all_MQTTimport_tasks();
  }
  if (!MQTTclient_connected) {
    // As suggested here: https://github.com/letscontrolit/ESPEasy/issues/1356
    if (timermqtt_interval < 30000) {
      timermqtt_interval += 500;
    }
  } else {
    timermqtt_interval = 100;
  }
  Scheduler.setIntervalTimer(SchedulerIntervalTimer_e::TIMER_MQTT);
  scheduleNextMQTTdelayQueue();
  #if FEATURE_MQTT_CONNECT_BACKGROUND
  MQTTConnectInBackground(CONTROLLER_MAX, true); // Report state
  #endif // if FEATURE_MQTT_CONNECT_BACKGROUND
}

void runPeriodicalMQTT() {
  // MQTT_KEEPALIVE = 15 seconds.
  if (!NetworkConnected(10)) {
    updateMQTTclient_connected();
    return;
  }
  //dont do this in backgroundtasks(), otherwise causes crashes. (https://github.com/letscontrolit/ESPEasy/issues/683)
  controllerIndex_t enabledMqttController = firstEnabledMQTT_ControllerIndex();
  if (validControllerIndex(enabledMqttController)) {
    if (!MQTTclient.loop()) {
      updateMQTTclient_connected();
      if (MQTTCheck(enabledMqttController)) {
        updateMQTTclient_connected();
      }
    }
  } else {
    if (MQTTclient.connected()) {
      #if FEATURE_MQTT_CONNECT_BACKGROUND
      if (MQTT_task_data.taskHandle) {
        vTaskDelete(MQTT_task_data.taskHandle);
        MQTT_task_data.taskHandle = NULL;
      }
      MQTT_task_data.status = MQTT_connect_status_e::Disconnected;
      #endif // if FEATURE_MQTT_CONNECT_BACKGROUND
      MQTTclient.disconnect();
      updateMQTTclient_connected();
    }
  }
}


#endif //if FEATURE_MQTT



void logTimerStatistics() {
# ifndef BUILD_NO_DEBUG
  const uint8_t loglevel = LOG_LEVEL_DEBUG;
#else
  const uint8_t loglevel = LOG_LEVEL_NONE;
#endif
  updateLoopStats_30sec(loglevel);
#ifndef BUILD_NO_DEBUG
//  logStatistics(loglevel, true);
  if (loglevelActiveFor(loglevel)) {
    String queueLog = F("Scheduler stats: (called/tasks/max_length/idle%) ");
    queueLog += Scheduler.getQueueStats();
    addLogMove(loglevel, queueLog);
  }
#endif
}

void updateLoopStats_30sec(uint8_t loglevel) {
  loopCounterLast = loopCounter;
  loopCounter = 0;
  if (loopCounterLast > loopCounterMax)
    loopCounterMax = loopCounterLast;

  Scheduler.updateIdleTimeStats();

#ifndef BUILD_NO_DEBUG
  if (loglevelActiveFor(loglevel)) {
    String log = F("LoopStats: shortestLoop: ");
    log += shortestLoop;
    log += F(" longestLoop: ");
    log += longestLoop;
    log += F(" avgLoopDuration: ");
    log += loop_usec_duration_total / loopCounter_full;
    log += F(" loopCounterMax: ");
    log += loopCounterMax;
    log += F(" loopCounterLast: ");
    log += loopCounterLast;
    addLogMove(loglevel, log);
  }
#endif
  loop_usec_duration_total = 0;
  loopCounter_full = 1;
}


/********************************************************************************************\
   Clean up all before going to sleep or reboot.
 \*********************************************************************************************/
void flushAndDisconnectAllClients() {
  if (anyControllerEnabled()) {
#if FEATURE_MQTT
    bool mqttControllerEnabled = validControllerIndex(firstEnabledMQTT_ControllerIndex());
#endif //if FEATURE_MQTT
    unsigned long timer = millis() + 1000;
    while (!timeOutReached(timer)) {
      // call to all controllers (delay queue) to flush all data.
      CPluginCall(CPlugin::Function::CPLUGIN_FLUSH, 0);
#if FEATURE_MQTT      
      if (mqttControllerEnabled && MQTTclient.connected()) {
        MQTTclient.loop();
      }
#endif //if FEATURE_MQTT
    }
#if FEATURE_MQTT
    if (mqttControllerEnabled && MQTTclient.connected()) {
      #if FEATURE_MQTT_CONNECT_BACKGROUND
      if (MQTT_task_data.taskHandle) {
        vTaskDelete(MQTT_task_data.taskHandle);
        MQTT_task_data.taskHandle = NULL;
      }
      MQTT_task_data.status = MQTT_connect_status_e::Disconnected;
      #endif // if FEATURE_MQTT_CONNECT_BACKGROUND
      MQTTclient.disconnect();
      updateMQTTclient_connected();
    }
#endif //if FEATURE_MQTT
    saveToRTC();
    delay(100); // Flush anything in the network buffers.
  }
  process_serialWriteBuffer();
}


void prepareShutdown(IntendedRebootReason_e reason)
{
  WiFiEventData.intent_to_reboot = true;
#if FEATURE_MQTT
  runPeriodicalMQTT(); // Flush outstanding MQTT messages
#endif // if FEATURE_MQTT
  process_serialWriteBuffer();
  flushAndDisconnectAllClients();
  saveUserVarToRTC();
  setWifiMode(WIFI_OFF);
  ESPEASY_FS.end();
  process_serialWriteBuffer();
  delay(100); // give the node time to flush all before reboot or sleep
  node_time.now_();
  Scheduler.markIntendedReboot(reason);
  saveToRTC();
}



#include "../Helpers/_Plugin_SensorTypeHelper.h"

#include "../../_Plugin_Helper.h"
#include "../DataStructs/DeviceStruct.h"
#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../Globals/Device.h"
#include "../Globals/Plugins.h"

#include "../WebServer/Markup.h"


void sensorTypeHelper_webformLoad_allTypes(struct EventStruct *event, int pconfigIndex)
{
  const uint8_t optionValues[] {
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_SINGLE),
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_DUAL),
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_TRIPLE),
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_QUAD),
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_TEMP_HUM),
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_TEMP_BARO),
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_TEMP_HUM_BARO),
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_SWITCH),
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_DIMMER),
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_WIND),
#if FEATURE_EXTENDED_TASK_VALUE_TYPES
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_INT32_SINGLE),
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_INT32_DUAL),
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_INT32_TRIPLE),
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_INT32_QUAD),
#endif // if FEATURE_EXTENDED_TASK_VALUE_TYPES
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_ULONG),
#if FEATURE_EXTENDED_TASK_VALUE_TYPES
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_UINT32_DUAL),
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_UINT32_TRIPLE),
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_UINT32_QUAD),
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_INT64_SINGLE),
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_INT64_DUAL),
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_UINT64_SINGLE),
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_UINT64_DUAL),
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_DOUBLE_SINGLE),
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_DOUBLE_DUAL),
#endif // if FEATURE_EXTENDED_TASK_VALUE_TYPES
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_STRING)
  };
  constexpr int optionCount = NR_ELEMENTS(optionValues);

  sensorTypeHelper_webformLoad(event, pconfigIndex, optionCount, optionValues);
}

void sensorTypeHelper_webformLoad_simple(struct EventStruct *event, int pconfigIndex)
{
  const uint8_t optionValues[] {
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_SINGLE),
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_DUAL),
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_TRIPLE),
    static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_QUAD)
  };
  constexpr int optionCount = NR_ELEMENTS(optionValues);

  sensorTypeHelper_webformLoad(event, pconfigIndex, optionCount, optionValues);
}

void sensorTypeHelper_Selector(const String& id, int optionCount, const uint8_t options[], Sensor_VType choice) {
  addSelector_Head(id);

  for (uint8_t x = 0; x < optionCount; x++)
  {
    const Sensor_VType optionVType = static_cast<Sensor_VType>(options[x]);
    addSelector_Item(getSensorTypeLabel(optionVType),
                     options[x],
                     choice == optionVType);
  }
  addSelector_Foot();
}

#if FEATURE_CUSTOM_TASKVAR_VTYPE
const char value_type_categories[] PROGMEM =
  "Basic|"       // 1024
  "Environment|" // 1025
  "Dust/Gases|"  // 1026
  "Energy|"      // 1027
  "Time|"        // 1028
  "Size|"        // 1029
  "Light|"       // 1030
  "Other|"       // 1031
;

String toValueTypeCategory(const uint32_t valueType) {
  char   tmp[12]{};
  String result;

  result = GetTextIndexed(tmp, sizeof(tmp), valueType, value_type_categories);

  return result;
}

/* *INDENT-OFF* */

const uint16_t value_type_map[] PROGMEM = {
  0,                                                                                          // None
  1024, 1,   5,   6,   7,                                                                     // Basic
  1025, 2,   3,   4,   8,   101, 102, 122, 21,  131, 143, 144, 151, 152,                      // Environment
  1026, 106, 107, 108, 110, 146, 121, 154, 128, 129, 148, 149, 150, 153,                      // Gases
  1027, 116, 117, 118, 119, 120, 127, 139, 141, 142, 147,                                     // Energy
  1028, 132, 133, 134,                                                                        // Time
  1029, 100, 104, 105, 109, 111, 115, 135, 136, 137, 138, 155, 156, 157, 158,                 // Size
  1030, 103, 112, 113, 114, 123, 124, 125, 126,                                               // Light
  1031, 10,  130, 11,  22,  20,  31,  32,  33,  40,  41, 42, 43, 50, 51, 60, 61, 70, 71, 140, // Other
        145,                                                                                  // Other
};

/* *INDENT-ON* */

void sensorTypeCategoriesHelper_Selector(const String& id,
                                         int           optionCount,
                                         const uint8_t options[],
                                         Sensor_VType  choice) {
  constexpr uint16_t asize = NR_ELEMENTS(value_type_map);
  bool firstGrp            = true;
  bool closeGrp            = false;
  const uint8_t iChoice    = static_cast<uint8_t>(choice);
  const std::vector<uint8_t> vOptions(options, options + optionCount);

  do_addSelector_Head(id, F("xwide"), EMPTY_STRING, false
                      # if FEATURE_TOOLTIPS
                      , F("¹: Used for MQTT AutoDiscovery")
                      # endif // if FEATURE_TOOLTIPS
                      );

  for (uint16_t idx = 0; idx < asize; ++idx) {
    const uint16_t vtIdx = pgm_read_word_near(&value_type_map[idx]);

    if (vtIdx < 1024) {
      const uint8_t vtIdx8 = vtIdx;

      if (std::find(vOptions.begin(), vOptions.end(), vtIdx8) != vOptions.end()) {
        const Sensor_VType vt = static_cast<Sensor_VType>(vtIdx8);
        addSelector_Item(
          concat(getSensorTypeLabel(vt), isMQTTDiscoverySensorType(vt) ? F("¹") : EMPTY_STRING),
          vtIdx,
          iChoice == vtIdx8);
      }
    }

    if (vtIdx >= 1024) {
      uint16_t vtIdx2   = 0;
      bool     addGroup = false; // Check if group should be included

      for (uint8_t idx2 = idx + 1; !addGroup && (vtIdx2 < 1024) && (idx2 < asize); ++idx2) {
        vtIdx2 = pgm_read_word_near(&value_type_map[idx2]);

        if (vtIdx2 < 1024) {
          const uint8_t vtIdx8 = vtIdx2;

          if (std::find(vOptions.begin(), vOptions.end(), vtIdx8) != vOptions.end()) {
            addGroup = true;
          }
        }
      }

      if (addGroup) {
        if (!firstGrp) {
          addSelector_OptGroupFoot();
          closeGrp = true;
        }
        addSelector_OptGroup(toValueTypeCategory(vtIdx - 1024));
        firstGrp = false;
        closeGrp = false;
      }
    }

    if ((idx & 0x07) == 0) { delay(0); }
  }

  if (!firstGrp && !closeGrp) {
    addSelector_OptGroupFoot();
  }
  addSelector_Foot();
}

#endif // if FEATURE_CUSTOM_TASKVAR_VTYPE

void sensorTypeHelper_webformLoad(struct EventStruct *event, int pconfigIndex, int optionCount, const uint8_t options[])
{
  sensorTypeHelper_webformLoad(event, pconfigIndex, optionCount, options, true, 0);
}

void sensorTypeHelper_webformLoad(struct EventStruct *event,
                                  int                 pconfigIndex,
                                  int                 optionCount,
                                  const uint8_t       options[],
                                  bool                showSubHeader,
                                  int                 valueIndex)
{
  if (showSubHeader) {
    addFormSubHeader(F("Output Configuration"));
  }

  if ((pconfigIndex < 0) || (pconfigIndex >= PLUGIN_CONFIGVAR_MAX)) {
    return;
  }
  Sensor_VType choice             = static_cast<Sensor_VType>(PCONFIG(pconfigIndex));
  const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(event->TaskIndex);

  if (showSubHeader) {
    if (!validDeviceIndex(DeviceIndex)) {
      // FIXME TD-er: Should we even continue here?
      choice                = Sensor_VType::SENSOR_TYPE_NONE;
      PCONFIG(pconfigIndex) = static_cast<uint8_t>(choice);
    } else if (getValueCountFromSensorType(choice) != getValueCountForTask(event->TaskIndex)) {
      // Invalid value
      if (checkDeviceVTypeForTask(event) >= 0) {
        choice                = event->sensorType;
        PCONFIG(pconfigIndex) = static_cast<uint8_t>(choice);
      }
    }
  }

  const __FlashStringHelper *outputTypeLabel = F("Output Data Type");

  if (validDeviceIndex(DeviceIndex) && (Device[DeviceIndex].OutputDataType ==  Output_Data_type_t::Simple)) {
    if (!isSimpleOutputDataType(event->sensorType))
    {
      choice                = Device[DeviceIndex].VType;
      PCONFIG(pconfigIndex) = static_cast<uint8_t>(choice);
    }
    outputTypeLabel = F("Number Output Values");
  }

  if (showSubHeader) {
    addRowLabel(outputTypeLabel);
  } else if (valueIndex >= 0) {
    addRowLabel(strformat(F("Value %d type"), valueIndex));
  }

  #if FEATURE_CUSTOM_TASKVAR_VTYPE
  if (optionCount > 4) { // Don't handle the 'Simple' selector categorized
    sensorTypeCategoriesHelper_Selector(sensorTypeHelper_webformID(pconfigIndex), optionCount, options, choice);
  } else
  #endif // if FEATURE_CUSTOM_TASKVAR_VTYPE
  {
    sensorTypeHelper_Selector(sensorTypeHelper_webformID(pconfigIndex), optionCount, options, choice);
  }

  if (showSubHeader) {
    String note;
    note  = F("Changing '");
    note += outputTypeLabel;
    note += F("' may affect behavior of some controllers (e.g. Domoticz)");
    addFormNote(note);
  }
  String dummy;

  PluginCall(PLUGIN_WEBFORM_LOAD_OUTPUT_SELECTOR, event, dummy);
}

void sensorTypeHelper_saveOutputSelector(struct EventStruct *event, int pconfigIndex, uint8_t valueIndex, const String& defaultValueName)
{
  const bool isDefault = defaultValueName.equals(ExtraTaskSettings.TaskDeviceValueNames[valueIndex]);

  if (isDefault) {
    ExtraTaskSettings.clearTaskDeviceValueName(valueIndex);
  }
  ExtraTaskSettings.isDefaultTaskVarName(valueIndex, isDefault);
  pconfig_webformSave(event, pconfigIndex);
}

void pconfig_webformSave(struct EventStruct *event, int pconfigIndex)
{
  if ((pconfigIndex < 0) || (pconfigIndex >= PLUGIN_CONFIGVAR_MAX)) {
    return;
  }

  PCONFIG(pconfigIndex) = getFormItemInt(sensorTypeHelper_webformID(pconfigIndex), PCONFIG(pconfigIndex));
}

void sensorTypeHelper_loadOutputSelector(
  struct EventStruct *event, int pconfigIndex, uint8_t valuenr,
  int optionCount, const __FlashStringHelper *options[], const int indices[])
{
  if ((pconfigIndex < 0) || (pconfigIndex >= PLUGIN_CONFIGVAR_MAX)) {
    return;
  }
  const FormSelectorOptions selector(
    optionCount,
    options,
    indices);
  selector.addFormSelector(
    concat(F("Value "), valuenr + 1),
    sensorTypeHelper_webformID(pconfigIndex),
    PCONFIG(pconfigIndex));
}

void sensorTypeHelper_loadOutputSelector(
  struct EventStruct *event, int pconfigIndex, uint8_t valuenr,
  int optionCount, const String options[], const int indices[])
{
  if ((pconfigIndex < 0) || (pconfigIndex >= PLUGIN_CONFIGVAR_MAX)) {
    return;
  }
  const FormSelectorOptions selector(
    optionCount,
    options,
    indices);
  selector.addFormSelector(
    concat(F("Value "), valuenr + 1),
    sensorTypeHelper_webformID(pconfigIndex),
    PCONFIG(pconfigIndex));
}

String sensorTypeHelper_webformID(int pconfigIndex)
{
  if ((pconfigIndex >= 0) && (pconfigIndex < PLUGIN_CONFIGVAR_MAX)) {
    return concat(F("pconfigIndex_"), pconfigIndex);
  }
  return F("error");
}

#include "../Helpers/Scheduler.h"


#include "../../ESPEasy-Globals.h"

#include "../ControllerQueue/DelayQueueElements.h"

#include "../DataStructs/Scheduler_ConstIntervalTimerID.h"

#include "../Globals/Settings.h"

#include "../Helpers/ESPEasy_time_calc.h"
#include "../Helpers/Networking.h"
#include "../Helpers/PeriodicalActions.h"


/*********************************************************************************************\
* Interval Timer
* These timers set a new scheduled timer, based on the old value.
* This will make their interval as constant as possible.
\*********************************************************************************************/

// Interval where it is more important to actually run the scheduled job, instead of keeping the time drift to a minimum.
// For example running the PLUGIN_FIFTY_PER_SECOND calls probably need to run as fast as possible as they need to fetch data before a buffer
// overflow happens.
// For those it is more important to actually run it than keeping pace.
void ESPEasy_Scheduler::setNextTimeInterval(unsigned long& timer, const unsigned long step) {
  timer += step;
  const long passed = timePassedSince(timer);

  if (passed < 0) {
    // Event has not yet happened, which is fine.
    return;
  }

  if (static_cast<unsigned long>(passed) > step) {
    // No need to keep running behind, start again.
    timer = millis() + step;
    return;
  }

  // Try to get in sync again.
  timer = millis() + (step - passed);
}

// More strict interval where no time drift is more important than missing a scheduled interval.
// For example timing for repeating longPulse where 2 scheduled intervals need to be at constant 'distance' from each other.
void ESPEasy_Scheduler::setNextStrictTimeInterval(unsigned long     & timer,
                                                  const unsigned long step) {
  timer += step;
  const long passed = timePassedSince(timer);

  if (passed <= 0) {
    // Event has not yet happened, which is fine.
    return;
  }

  // Try to get in sync again.
  const unsigned long stepsMissed = static_cast<unsigned long>(passed) / step;

  timer += (stepsMissed + 1) * step;
}

void ESPEasy_Scheduler::setIntervalTimer(SchedulerIntervalTimer_e intervalTimer) {
  setIntervalTimer(intervalTimer, millis());
}

void ESPEasy_Scheduler::setIntervalTimerAt(SchedulerIntervalTimer_e intervalTimer, unsigned long newtimer) {
  const ConstIntervalTimerID timerID(intervalTimer);

  setNewTimerAt(timerID, newtimer);
}

void ESPEasy_Scheduler::setIntervalTimerOverride(SchedulerIntervalTimer_e intervalTimer, unsigned long msecFromNow) {
  unsigned long timer = millis();

  setNextTimeInterval(timer, msecFromNow);
  const ConstIntervalTimerID timerID(intervalTimer);

  setNewTimerAt(timerID, timer);
}

void ESPEasy_Scheduler::scheduleNextDelayQueue(SchedulerIntervalTimer_e intervalTimer, unsigned long nextTime) {
  if (nextTime != 0) {
    // Schedule for next process run.
    setIntervalTimerAt(intervalTimer, nextTime);
  }
}

void ESPEasy_Scheduler::setIntervalTimer(SchedulerIntervalTimer_e intervalTimer, unsigned long lasttimer) {
  // Set the initial timers for the regular runs
  unsigned long interval = 0;

  switch (intervalTimer) {
    case SchedulerIntervalTimer_e::TIMER_20MSEC:         interval = 20; break;
    case SchedulerIntervalTimer_e::TIMER_100MSEC:        interval = 100; break;
    case SchedulerIntervalTimer_e::TIMER_1SEC:           interval = 1000; break;
    case SchedulerIntervalTimer_e::TIMER_30SEC:
    case SchedulerIntervalTimer_e::TIMER_STATISTICS:     interval = 30000; break;
    case SchedulerIntervalTimer_e::TIMER_MQTT:           interval = timermqtt_interval; break;
    case SchedulerIntervalTimer_e::TIMER_GRATUITOUS_ARP: interval = timer_gratuitous_arp_interval; break;

    // Fall-through for all DelayQueue, which are just the fall-back timers.
    // The timers for all delay queues will be set according to their own settings as long as there is something to process.
    case SchedulerIntervalTimer_e::TIMER_MQTT_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C001_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C002_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C003_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C004_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C005_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C006_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C007_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C008_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C009_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C010_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C011_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C012_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C013_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C014_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C015_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C016_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C017_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C018_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C019_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C020_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C021_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C022_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C023_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C024_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C025_DELAY_QUEUE:
      // When extending this, search for EXTEND_CONTROLLER_IDS
      // in the code to find all places that need to be updated too.
      interval = 1000; break;
  }
  unsigned long timer = lasttimer;

  setNextTimeInterval(timer, interval);
  const ConstIntervalTimerID timerID(intervalTimer);

  setNewTimerAt(timerID, timer);
}

void ESPEasy_Scheduler::sendGratuitousARP_now() {
  sendGratuitousARP();

  if (Settings.gratuitousARP()) {
    timer_gratuitous_arp_interval = 100;
    setIntervalTimer(SchedulerIntervalTimer_e::TIMER_GRATUITOUS_ARP);
  }
}

void ESPEasy_Scheduler::process_interval_timer(SchedulerTimerID timerID, unsigned long lasttimer) {
  // Set the interval timer now, it may be altered by the commands below.
  // This is the default next-run-time.

  const ConstIntervalTimerID *tmp              = reinterpret_cast<const ConstIntervalTimerID *>(&timerID);
  const SchedulerIntervalTimer_e intervalTimer = tmp->getIntervalTimer();

  setIntervalTimer(intervalTimer, lasttimer);

  switch (intervalTimer) {
    case SchedulerIntervalTimer_e::TIMER_20MSEC:         run50TimesPerSecond(); break;
    case SchedulerIntervalTimer_e::TIMER_100MSEC:

      if (!UseRTOSMultitasking) {
        run10TimesPerSecond();
      }
      break;
    case SchedulerIntervalTimer_e::TIMER_1SEC:             runOncePerSecond();      break;
    case SchedulerIntervalTimer_e::TIMER_30SEC:            runEach30Seconds();      break;
    case SchedulerIntervalTimer_e::TIMER_MQTT:
#if FEATURE_MQTT
      runPeriodicalMQTT();
#endif // if FEATURE_MQTT
      break;
    case SchedulerIntervalTimer_e::TIMER_STATISTICS:       logTimerStatistics();    break;
    case SchedulerIntervalTimer_e::TIMER_GRATUITOUS_ARP:

      // Slowly increase the interval timer.
      timer_gratuitous_arp_interval = 2 * timer_gratuitous_arp_interval;

      if (timer_gratuitous_arp_interval > TIMER_GRATUITOUS_ARP_MAX) {
        timer_gratuitous_arp_interval = TIMER_GRATUITOUS_ARP_MAX;
      }

      if (Settings.gratuitousARP()) {
        sendGratuitousARP();
      }
      break;
    case SchedulerIntervalTimer_e::TIMER_MQTT_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C002_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C005_DELAY_QUEUE:
    case SchedulerIntervalTimer_e::TIMER_C006_DELAY_QUEUE:
#if FEATURE_MQTT
      processMQTTdelayQueue();
#endif // if FEATURE_MQTT
      break;
    case SchedulerIntervalTimer_e::TIMER_C001_DELAY_QUEUE:
  #ifdef USES_C001
      process_c001_delay_queue();
  #endif // ifdef USES_C001
      break;
    case SchedulerIntervalTimer_e::TIMER_C003_DELAY_QUEUE:
  #ifdef USES_C003
      process_c003_delay_queue();
  #endif // ifdef USES_C003
      break;
    case SchedulerIntervalTimer_e::TIMER_C004_DELAY_QUEUE:
  #ifdef USES_C004
      process_c004_delay_queue();
  #endif // ifdef USES_C004
      break;
    case SchedulerIntervalTimer_e::TIMER_C007_DELAY_QUEUE:
  #ifdef USES_C007
      process_c007_delay_queue();
  #endif // ifdef USES_C007
      break;
    case SchedulerIntervalTimer_e::TIMER_C008_DELAY_QUEUE:
  #ifdef USES_C008
      process_c008_delay_queue();
  #endif // ifdef USES_C008
      break;
    case SchedulerIntervalTimer_e::TIMER_C009_DELAY_QUEUE:
  #ifdef USES_C009
      process_c009_delay_queue();
  #endif // ifdef USES_C009
      break;
    case SchedulerIntervalTimer_e::TIMER_C010_DELAY_QUEUE:
  #ifdef USES_C010
      process_c010_delay_queue();
  #endif // ifdef USES_C010
      break;
    case SchedulerIntervalTimer_e::TIMER_C011_DELAY_QUEUE:
  #ifdef USES_C011
      process_c011_delay_queue();
  #endif // ifdef USES_C011
      break;
    case SchedulerIntervalTimer_e::TIMER_C012_DELAY_QUEUE:
  #ifdef USES_C012
      process_c012_delay_queue();
  #endif // ifdef USES_C012
      break;

    case SchedulerIntervalTimer_e::TIMER_C013_DELAY_QUEUE:
      /*
       #ifdef USES_C013
            process_c013_delay_queue();
       #endif
       */
      break;

    case SchedulerIntervalTimer_e::TIMER_C014_DELAY_QUEUE:
      /*
       #ifdef USES_C014
            process_c014_delay_queue();
       #endif
       */
      break;

    case SchedulerIntervalTimer_e::TIMER_C015_DELAY_QUEUE:
  #ifdef USES_C015
      process_c015_delay_queue();
  #endif // ifdef USES_C015
      break;
    case SchedulerIntervalTimer_e::TIMER_C016_DELAY_QUEUE:
  #ifdef USES_C016
      process_c016_delay_queue();
  #endif // ifdef USES_C016
      break;

    case SchedulerIntervalTimer_e::TIMER_C017_DELAY_QUEUE:
  #ifdef USES_C017
      process_c017_delay_queue();
  #endif // ifdef USES_C017
      break;

    case SchedulerIntervalTimer_e::TIMER_C018_DELAY_QUEUE:
  #ifdef USES_C018
      process_c018_delay_queue();
  #endif // ifdef USES_C018
      break;

    case SchedulerIntervalTimer_e::TIMER_C019_DELAY_QUEUE:
      /*
       #ifdef USES_C019
            process_c019_delay_queue();
       #endif
       */
      break;

    case SchedulerIntervalTimer_e::TIMER_C020_DELAY_QUEUE:
      /*
       #ifdef USES_C020
            process_c020_delay_queue();
       #endif
       */
      break;

    case SchedulerIntervalTimer_e::TIMER_C021_DELAY_QUEUE:
      /*
       #ifdef USES_C021
            process_c021_delay_queue();
       #endif
       */
      break;

    case SchedulerIntervalTimer_e::TIMER_C022_DELAY_QUEUE:
      /*
       #ifdef USES_C022
            process_c022_delay_queue();
       #endif
       */
      break;

    case SchedulerIntervalTimer_e::TIMER_C023_DELAY_QUEUE:
      /*
       #ifdef USES_C023
            process_c023_delay_queue();
       #endif
       */
      break;

    case SchedulerIntervalTimer_e::TIMER_C024_DELAY_QUEUE:
      /*
       #ifdef USES_C024
            process_c024_delay_queue();
       #endif
       */
      break;

    case SchedulerIntervalTimer_e::TIMER_C025_DELAY_QUEUE:
      /*
       #ifdef USES_C025
            process_c025_delay_queue();
       #endif
       */
      break;

      // When extending this, search for EXTEND_CONTROLLER_IDS
      // in the code to find all places that need to be updated too.
  }
}

#include "../Helpers/Scheduler.h"

#include "../DataStructs/Scheduler_PluginDeviceTimerID.h"
#include "../DataStructs/TimingStats.h"

#include "../DataTypes/ESPEasy_plugin_functions.h"

#include "../Helpers/_Plugin_init.h"

/*********************************************************************************************\
* Plugin Timer
* Essentially calling PLUGIN_DEVICETIMER_IN
* Similar to PLUGIN_TASKTIMER_IN, addressed to a plugin instead of a task.
\*********************************************************************************************/
void ESPEasy_Scheduler::setPluginTimer(unsigned long msecFromNow, pluginID_t pluginID, int Par1, int Par2, int Par3, int Par4, int Par5)
{
  // plugin number and par1 form a unique key that can be used to restart a timer
  // Use deviceIndex instead of pluginID, since the deviceIndex uses less bits.
  const deviceIndex_t deviceIndex = getDeviceIndex(pluginID);

  if (!validDeviceIndex(deviceIndex)) { return; }

  const PluginDeviceTimerID timerID(pluginID, Par1);

  systemTimerStruct timer_data;

  // PLUGIN_DEVICETIMER_IN does not address a task, so don't set TaskIndex
  timer_data.fromEvent(INVALID_TASK_INDEX, Par1, Par2, Par3, Par4, Par5);
  systemTimers[timerID.mixed_id] = timer_data;
  setNewTimerAt(timerID, millis() + msecFromNow);
}

void ESPEasy_Scheduler::process_plugin_timer(SchedulerTimerID id) {
  #ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  #endif // ifdef USE_SECOND_HEAP

  START_TIMER;
  auto it = systemTimers.find(id.mixed_id);

  if (it == systemTimers.end()) { return; }

  struct EventStruct TempEvent(it->second.toEvent());

  // PLUGIN_DEVICETIMER_IN does not address a task, so don't set TaskIndex

  // extract deviceID from timer id:
  const PluginDeviceTimerID *tmp  = reinterpret_cast<const PluginDeviceTimerID *>(&id);
  const deviceIndex_t deviceIndex = tmp->get_deviceIndex();

  // TD-er: Not sure if we have to keep original source for notifications.
  TempEvent.Source = EventValueSource::Enum::VALUE_SOURCE_SYSTEM;

  //  const deviceIndex_t deviceIndex = getDeviceIndex_from_TaskIndex(it->second.TaskIndex);

  /*
     String log = F("proc_system_timer: Pluginid: ");
     log += deviceIndex;
     log += F(" taskIndex: ");
     log += it->second.TaskIndex;
     log += F(" sysTimerID: ");
     log += id;
     addLog(LOG_LEVEL_INFO, log);
   */
  systemTimers.erase(id.mixed_id);

  if (validDeviceIndex(deviceIndex)) {
    String dummy;
    PluginCall(deviceIndex, PLUGIN_DEVICETIMER_IN, &TempEvent, dummy);
  }
  STOP_TIMER(PLUGIN_CALL_DEVICETIMER_IN);
}

#include "../Helpers/Hardware_GPIO.h"

#ifdef ESP32P4
# include <driver/gpio.h>

# include "../Globals/Settings.h"
# include "../Helpers/Hardware_device_info.h"

// ********************************************************************************
// Get info of a specific GPIO pin
// ********************************************************************************

bool getGpioInfo(int gpio, int& pinnr, bool& input, bool& output, bool& warning) {
  pinnr = -1; // ESP32 does not label the pins, they just use the GPIO number.

  input   = GPIO_IS_VALID_GPIO(gpio);
  output  = GPIO_IS_VALID_OUTPUT_GPIO(gpio);
  warning = isBootStrapPin(gpio);

  if ((gpio < 0) || !(GPIO_IS_VALID_GPIO(gpio))) { return false; }

  // P4 does not use GPIO-pins for accessing flash or PSRAM

# if FEATURE_ETHERNET

  // Check pins used for RMII Ethernet PHY
  if (NetworkMedium_t::Ethernet == Settings.NetworkMedium) {
    switch (gpio) {
      case 28: // RMII_RXDV
      case 29: // RMII_RXD0
      case 30: // RMII_RXD1
      case 34: // RMII_TXD0
      case 35: // RMII_TXD1
      case 49: // RMII_TXEN
      case 50: // RMII_CLK
      case 51: // PHY_RSTN
      case 52: // MDIO
        warning = true;
        break;
    }


    // FIXME TD-er: Must we also check for pins used for MDC/MDIO and Eth PHY power?
  }
# endif // if FEATURE_ETHERNET

  return (input || output);
}

bool isBootStrapPin(int gpio)
{

  return false;
}

bool getGpioPullResistor(int gpio, bool& hasPullUp, bool& hasPullDown) {
  hasPullDown = false;
  hasPullUp   = false;

  int  pinnr;
  bool input;
  bool output;
  bool warning;

  if (!getGpioInfo(gpio, pinnr, input, output, warning)) {
    return false;
  }

  hasPullUp   = true;
  hasPullDown = true;

  return true;
}

bool validGpio(int gpio) {
  if (gpio < 0) { return false; }

  if (!GPIO_IS_VALID_GPIO(gpio)) { return false; }

  int  pinnr;
  bool input;
  bool output;
  bool warning;

  return getGpioInfo(gpio, pinnr, input, output, warning);
}

// Get ADC related info for a given GPIO pin
// @param gpio_pin   GPIO pin number
// @param adc        Number of ADC unit
// @param ch         Channel number on ADC unit
// @param t          index of touch pad ID
bool getADC_gpio_info(int gpio_pin, int& adc, int& ch, int& t)
{
  adc = -1;
  ch  = -1;
  t   = -1;

  switch (gpio_pin) {
    case 16: adc = 1; ch = 0; break;
    case 17: adc = 1; ch = 1; break;
    case 18: adc = 1; ch = 2; break;
    case 19: adc = 1; ch = 3; break;
    case 20: adc = 1; ch = 4; break;
    case 21: adc = 1; ch = 5; break;
    case 22: adc = 1; ch = 6; break;
    case 23: adc = 1; ch = 7; break;
    case 49: adc = 2; ch = 0; break;
    case 50: adc = 2; ch = 1; break;
    case 51: adc = 2; ch = 2; break;
    case 52: adc = 2; ch = 3; break;
    case 53: adc = 2; ch = 4; break;
    case 54: adc = 2; ch = 5; break;
    default:
      return false;
  }

  return true;
}

#if SOC_TOUCH_SENSOR_SUPPORTED
int touchPinToGpio(int touch_pin)
{
  if (touch_pin >= 0 && touch_pin < SOC_TOUCH_SENSOR_NUM) {
    return touch_pin + 2;
  }
  return -1;
}
#endif

#if SOC_DAC_SUPPORTED
// Get DAC related info for a given GPIO pin
// @param gpio_pin   GPIO pin number
// @param dac        Number of DAC unit
bool getDAC_gpio_info(int gpio_pin, int& dac)
{
  return false;
}
#endif

#endif

#include "../Helpers/Hardware_device_info.h"

#ifdef ESP32P4


const __FlashStringHelper* getChipModel(uint32_t chip_model, uint32_t chip_revision, uint32_t pkg_version, bool single_core)
{
  if (18 == chip_model) { // ESP32-P4
    return F("ESP32-P4");
  }

  return F("Unknown");
}

#endif // ifdef ESP32P4

#include "../Helpers/ESPEasy_UnitOfMeasure.h"

#if FEATURE_TASKVALUE_UNIT_OF_MEASURE
# include "../DataTypes/SensorVType.h"

// *** DO NOT CHANGE ORDER, SAVED IN TASK SETTINGS! ***
const char unit_of_measure_list[] PROGMEM =
  "|"                                                                             // 0 = Empty/none
  "°C|°F|K|"                                                                      // 1..3
  "%|"                                                                            // 4
  "Pa|hPa|bar|mbar|inHg|psi|"                                                     // 5..10
  "W|kW|"                                                                         // 11..12
  "V|"                                                                            // 13
  "Wh|kWh|"                                                                       // 14..15
  "A|VA|"                                                                         // 16..17
  "mm|cm|m|km|"                                                                   // 18..21
  "L|mL|m³|ft³|"                                                                  // 22..25
  "m³/h|ft³/h|"                                                                   // 26..27
  "lx|"                                                                           // 28
  "UV index|"                                                                     // 29
  "µg/m³|mg/m³|p/m³|ppm|ppb|"                                                     // 30..34
  "°|"                                                                            // 35
  "€|$|¢|"                                                                        // 36..38
  "μs|ms|s|min|h|d|w|m|y|"                                                        // 39..47
  "in|ft|yd|mi|"                                                                  // 48..51
  "Hz|GHz|"                                                                       // 52..53
  "gal|fl. oz|"                                                                   // 54..55
  "m²|"                                                                           // 56
  "g|kg|mg|µg|"                                                                   // 57..60
  "oz|lb|"                                                                        // 61..62
  "µS/cm|"                                                                        // 63
  "W/m²|"                                                                         // 64
  "mm/h|"                                                                         // 65
  "mm/s|in/s|m/s|in/h|km/h|mph|"                                                  // 66..71
  "dB|dBm|"                                                                       // 72..73
  "bit|kbit|Mbit|Gbit|B|kB|MB|GB|TB|PB|EB|ZB|YB|KiB|MiB|GiB|TiB|PiB|EiB|ZiB|YiB|" // 74..94
  "bit/s|kbit/s|Mbit/s|Gbit/s|B/s|kB/s|MB/s|GB/s|KiB/s|MiB/s|GiB/s|"              // 95..105
  "ft/s|kn|"                                                                      // 106..107
  "mW|MW|GW|TW|"                                                                  // 108..111
  "BTU/(h⋅ft²)|"                                                                  // 112
  "pH|"                                                                           // 113
  "cbar|mmHg|kPa|"                                                                // 114..116
  "mA|µA|mV|µV|kV|"                                                               // 117..121
  "cm²|km²|mm²|in²|ft²|yd²|mi²|ac|ha|"                                            // 122..130
  "kHz|MHz|"                                                                      // 131..132
  "mWh|MWh|GWh|TWh|cal|kcal|Mcal|Gcal|J|kJ|MJ|GJ|"                                // 133..144
  "var|kvar|varh|kvarh|"                                                          // 145..148
  "st|"                                                                           // 149
  "mg/dL|mmol/L|"                                                                 // 150..151
  "μSv|μSv/h|"                                                                    // 152..153
  "m³/s|ft³/min|L/h|L/min|L/s|gal/min|mL/s|"                                      // 154..160
  "g/m³|kWh/100km|Wh/km|mi/kWh|km/kWh|"                                           // 161..165
  "in/d|mm/d|"                                                                    // 166 .. 167
  "Ah|"                                                                           // 168
;                                                                                 // *** DO NOT CHANGE ORDER, SAVED IN TASK SETTINGS! ***


// Not stored, when UoM index >= 1024 it's a label-index with 1024 subtracted
const char unit_of_measure_labels[] PROGMEM =
  "Apparent power|Air quality/CO/CO2|Area|(Atmospheric) Pressure|" // A 1024..1027
  "Blood glucose concentr.|"                                       // B 1028
  "Data rate|Data size|Distance|Duration|"                         // D 1029..1032
  "Energy distance|Energy(-storage)|"                              // E 1033..1034
  "Frequency|"                                                     // F 1035
  "Gas|"                                                           // G 1036
  "Percent Hum./Batt./Moist.|"                                     // H 1037
  "Illuminance|Irradiance|"                                        // I 1038..1039
  "Monetary|"                                                      // M 1040
  "Nitrogen (di-/mon-)oxide|"                                      // N 1041
  "Voc/Ozone|"                                                     // O 1042
  "Ph|PM/CO/CO2/NO(x)/Voc/Ozone|Power|"                            // P 1043..1045
  "Radiation|Reactive energy/power|"                               // R 1046..1047
  "Signal strength|Sound pressure|Speed|"                          // S 1048..1050
  "Temperature|"                                                   // T 1051
  "Voltage/Current|Volume/Water cons.|Volume flow rate|"           // V 1052..1054
  "Weight|Wind direction/angle|"                                   // W 1055..1056
  "Various units|"                                                 // Additional 1057
;

const uint16_t unit_of_measure_map[] PROGMEM = {
  1051, 1,   2,    3,                                                                                                    // Temperature
  1037, 4,                                                                                                               // Percent Battery,
                                                                                                                         // Humidity,
                                                                                                                         // Moisture
  1027, 8,   6,    116,    7,      115,     10,     5,      114,    9,                                                   // (Atmospheric)
                                                                                                                         // Pressure
  1052, 13,  119,  120,    121,    16,      117,    118,                                                                 // Voltage/Current
  1045, 11,  12,   108,    109,    110,     111,                                                                         // Power
  1024, 17,                                                                                                              // Apparent power
  1047, 145, 146,  147,    148,    168,                                                                                  // Reactive
                                                                                                                         // power/energy
  1044, 30,  31,   32,     33,     34,                                                                                   // Particle matter
  1031, 18,  19,   20,     21,     48,      49,     50,     51,                                                          // Distance
  1055, 57,  58,   59,     60,     61,      62,     149,                                                                 // Weight
  1053, 22,  23,   24,     25,     54,      55,                                                                          // Volume/Water
  1054, 26,  27,   153,    154,    155,     156,    157,    158,    159,   160,                                          // Volume flow rate
  1032, 39,  40,   41,     42,     43,      44,     45,     46,     47,                                                  // Duration
  1034, 14,  15,   133,    134,    135,     136,    137,    138,    139,   140,   141, 142, 143, 144,                    // Energy(-storage)
  1033, 162, 163,  164,    165,                                                                                          // Energy distance
  1050, 66,  67,   68,     69,     70,      71,     65,     106,    107,                                                 // Speed
  1056, 35,                                                                                                              // (Wind)
                                                                                                                         // direction/angle
  1038, 28,                                                                                                              // Illuminance
  1039, 64,  112,                                                                                                        // Irradiance
  1046, 152, 153,                                                                                                        // Radiation
  1057, 29,  63,   161,    166,    167,                                                                                  // Various units
  1035, 52,  53,   131,    132,                                                                                          // Frequency
  1043, 113,                                                                                                             // Potential
                                                                                                                         // hydrogen
  1026, 56,  122,  123,    124,    125,     126,    127,    128,    129,   130,                                          // Area
  1029, 95,  96,   97,     98,     99,      100,    101,    102,    103,   104,   105,                                   // Data rate
  1030, 74,  75,   76,     77,     78,      79,     80,     81,     82,    83,    84,  85,  86,  87,88, 89, 90, 91, 92, 93, 94, // Data size
  1049, 72,  73,                                                                                                         // Sound pressure
  1028, 150, 151,                                                                                                        // Blood glucose
  1040, 36,  37,   38,                                                                                                   // Monetary
};
const uint16_t unit_of_measure_map_size = NR_ELEMENTS(unit_of_measure_map);

String toUnitOfMeasureName(const uint32_t unitOfMeasureIndex,
                           const String & defUoM) {
  char   tmp[26]{}; // "PM/CO/CO2/NO(x)/Voc/Ozone" + \0
  String result;

  if (unitOfMeasureIndex < 1024) {
    result = GetTextIndexed(tmp, sizeof(tmp), unitOfMeasureIndex, unit_of_measure_list);
  } else {
    result = GetTextIndexed(tmp, sizeof(tmp), unitOfMeasureIndex - 1024, unit_of_measure_labels);
  }

  return result.isEmpty() ? defUoM : result;
}

int getUnitOfMeasureIndex(const String& uomName) {
  return GetCommandCode(uomName.c_str(), unit_of_measure_list);
}

/**
 * getUoMGroupForUoM: Get the selector group for the requested Unit of Measurement name
 */
uint16_t getUoMGroupForUoM(const String& uom) {
  return getUoMGroupForUoM(getUnitOfMeasureIndex(uom));
}

/**
 * getUoMGroupForUoM: Get the selector group for the requested Unit of Measurement index
 */
uint16_t getUoMGroupForUoM(const int findUom) {
  if ((findUom < 0)) { return 0; }
  const uint16_t find = findUom;
  uint16_t result     = 0;
  uint16_t last       = 0;

  for (uint16_t idx = 0; idx < unit_of_measure_map_size; ++idx) {
    const uint16_t uomIdx = pgm_read_word_near(&unit_of_measure_map[idx]);

    if (uomIdx < 1024) {
      if (find == uomIdx) {
        result = last;
        break;
      }
    } else {
      last = uomIdx;
    }
  }
  return result;
}

/**
 * getDefaultUoMforSensorVType: get the default UoM parameters into Par64[] array for Sensor_VType::SENSOR_TYPE_TEMP_HUM,
 * Sensor_VType::SENSOR_TYPE_TEMP_BARO, Sensor_VType::SENSOR_TYPE_TEMP_EMPTY_BARO and Sensor_VType::SENSOR_TYPE_TEMP_HUM_BARO
 */
bool getDefaultUoMforSensorVType(EventStruct *event) {
  switch (event->sensorType) { // These are the only default UoM values to be handled
    case Sensor_VType::SENSOR_TYPE_TEMP_HUM:
      event->Par64_1 = UOM_GROUP_TEMPERATURE;
      event->Par64_2 = UOM_GROUP_PERC_HUM_BAT_MOIST;
      return true;
    case Sensor_VType::SENSOR_TYPE_TEMP_BARO:
      event->Par64_1 = UOM_GROUP_TEMPERATURE;
      event->Par64_2 = UOM_GROUP_PRESSURE;
      return true;
    case Sensor_VType::SENSOR_TYPE_TEMP_EMPTY_BARO:
      event->Par64_1 = UOM_GROUP_TEMPERATURE;
      event->Par64_2 = UOM_GROUP_NONE;
      event->Par64_3 = UOM_GROUP_PRESSURE;
      return true;
    case Sensor_VType::SENSOR_TYPE_TEMP_HUM_BARO:
      event->Par64_1 = UOM_GROUP_TEMPERATURE;
      event->Par64_2 = UOM_GROUP_PERC_HUM_BAT_MOIST;
      event->Par64_3 = UOM_GROUP_PRESSURE;
      return true;
    default:
      break;
  }
  return false;
}

#endif // if FEATURE_TASKVALUE_UNIT_OF_MEASURE

#include "../Helpers/ESPEasyStatistics.h"


#if FEATURE_TIMING_STATS

#include "../DataStructs/TimingStats.h"
#include "../WebServer/ESPEasy_WebServer.h"
#include "../Helpers/Convert.h"
#include "../Helpers/_Plugin_init.h"


void stream_json_timing_stats(const TimingStats& stats, long timeSinceLastReset) {
  uint32_t minVal, maxVal;
  uint32_t  count = stats.getMinMax(minVal, maxVal);
  float call_per_sec = static_cast<float>(count) / static_cast<float>(timeSinceLastReset) * 1000.0f;

  json_number(F("count"), ull2String(count));
  json_number(F("call-per-sec"),   toString(call_per_sec, 2));
  json_number(F("min"),   ull2String(minVal));
  json_number(F("max"),   ull2String(maxVal));
  json_number(F("avg"),   toString(stats.getAvg(), 2));
  json_prop(F("unit"), F("usec"));
}

void jsonStatistics(bool clearStats) {
  bool firstPlugin     = true;
  deviceIndex_t  currentDeviceIndex = INVALID_DEVICE_INDEX;
  long timeSinceLastReset = timePassedSince(timingstats_last_reset);


  json_open(true, F("plugin"));

  for (auto& x: pluginStats) {
    if (!x.second.isEmpty()) {
      const deviceIndex_t deviceIndex = deviceIndex_t::toDeviceIndex(x.first >> 8);

      if (deviceIndex != currentDeviceIndex) {
        // new plugin
        currentDeviceIndex = deviceIndex;
        if (!firstPlugin) {
          json_close();
          json_close(true); // close previous function list
          json_close();     // close previous plugin
        }
        // Start new plugin stream
        json_open(); // open new plugin
        json_prop(F("name"), getPluginNameFromDeviceIndex(deviceIndex));
        json_prop(F("id"),   String(getPluginID_from_DeviceIndex(deviceIndex).value));
        json_open(true, F("function")); // open function
        json_open(); // open first function element
      }

      // Stream function timing stats
      json_open(false, getPluginFunctionName(x.first % 256));
      {
        stream_json_timing_stats(x.second, timeSinceLastReset);
      }
      json_close(false);
      firstPlugin = false;
    }
  }
  if (!firstPlugin) {
    // We added some, so we must make sure to close the last entry
    json_close();     // close first function element
    json_close(true); // close previous function
    json_close();     // close previous plugin
  }
  json_close(true);   // Close plugin list


  json_open(true, F("controller"));
  bool firstController = true;
  int  currentProtocolIndex = -1;
  for (auto& x: controllerStats) {
    if (!x.second.isEmpty()) {      
      const int ProtocolIndex = x.first / 256;
      if (currentProtocolIndex != ProtocolIndex) {
        // new protocol
        currentProtocolIndex = ProtocolIndex;
        if (!firstController) {
          json_close();
          json_close(true); // close previous function list
          json_close();     // close previous protocol
        }
        // Start new protocol stream
        json_open(); // open new plugin
        json_prop(F("name"), getCPluginNameFromProtocolIndex(ProtocolIndex));
        json_prop(F("id"),   String(getCPluginID_from_ProtocolIndex(ProtocolIndex)));
        json_open(true, F("function")); // open function
        json_open(); // open first function element

      }
      // Stream function timing stats
      json_open(false, getCPluginCFunctionName(static_cast<CPlugin::Function>(x.first % 256)));
      {
        stream_json_timing_stats(x.second, timeSinceLastReset);
      }
      json_close(false);
      firstController = false;
    }
  }
  if (!firstController) {
    // We added some, so we must make sure to close the last entry
    json_close();     // close first function element
    json_close(true); // close previous function
    json_close();     // close previous plugin
  }

  json_close(true);   // Close controller list


  json_open(true, F("misc"));
  for (auto& x: miscStats) {
    if (!x.second.isEmpty()) {
      json_open(); // open new misc item
      json_prop(F("name"), getMiscStatsName(x.first));
      json_prop(F("id"),   String(static_cast<int>(x.first)));
      json_open(true, F("function")); // open function
      json_open(); // open first function element
      // Stream function timing stats
      json_open(false, to_internal_string(getMiscStatsName(x.first), '-'));
      {
        stream_json_timing_stats(x.second, timeSinceLastReset);
      }
      json_close(false);
      json_close();     // close first function element
      json_close(true); // close function
      json_close();     // close misc item
    }
  }

  json_close(true);   // Close misc list

  if (clearStats) {
    pluginStats.clear();
    controllerStats.clear();
    miscStats.clear();
    timingstats_last_reset = millis();
  }
}


#endif // if FEATURE_TIMING_STATS
#include "../Helpers/_Plugin_Helper_GPIO.h"

#include "../ESPEasyCore/ESPEasyGPIO.h"
#include "../Globals/GlobalMapPortStatus.h"
#include "../Globals/RuntimeData.h"
#include "../Globals/Settings.h"
#include "../Helpers/PortStatus.h"


GPIO_plugin_helper_data_t::GPIO_plugin_helper_data_t(
  pluginID_t pluginNumber,
  uint16_t   pin,
  uint32_t   debounceInterval_ms,
  uint32_t   doubleClickMaxInterval_ms,
  uint32_t   longpressMinInterval_ms,
  uint8_t    dcMode,
  uint8_t    longpressEvent,
  bool       safeButton,
  bool       sendBootState,
  uint8_t    switchType,
  uint8_t    dimmerValue) :
  _portStatus_key(createKey(pluginNumber, pin)),
  _debounceInterval_ms(debounceInterval_ms),
  _doubleClickMaxInterval_ms(doubleClickMaxInterval_ms),
  _longpressMinInterval_ms(longpressMinInterval_ms),
  _doubleClickCounter(0),
  _safeButtonCounter(0),
  _pin(pin),
  _pluginNumber(pluginNumber),
  _dcMode(dcMode),
  _longpressEvent(longpressEvent),
  _switchType(switchType),
  _dimmerValue(dimmerValue),
  _safeButton(safeButton),
  _sendBootState(sendBootState),
  _longpressFired(false)
{
  // store millis for debounce, doubleclick and long press
  const unsigned long cur_millis = millis();

  _debounceTimer    = cur_millis; // debounce timer
  _doubleClickTimer = cur_millis; // doubleclick timer
  _longpressTimer   = cur_millis; // longpress timer
}

GPIO_plugin_helper_data_t::~GPIO_plugin_helper_data_t()
{
  if (_portStatus_key != 0) {
    removeTaskFromPort(_portStatus_key);
  }
}

bool GPIO_plugin_helper_data_t::init(
  struct EventStruct *event,
  int8_t              pinState,
  uint8_t             pinModeValue)
{
  // Read current status or create empty if it does not exist
  portStatusStruct newStatus = globalMapPortStatus[_portStatus_key];

  newStatus.state  = pinState;
  newStatus.output = newStatus.state; // FIXME TD-er: Is this correct to set output value based on pinState?
  newStatus.mode   = pinModeValue;

  // @giig1967g: if it is in the device list we assume it's an input pin
  // add this GPIO/port as a task
  if (newStatus.task < 3) {
    newStatus.task++;
  }

  // if boot pinState must be send, inverse default pinState
  // this is done to force the trigger in PLUGIN_TEN_PER_SECOND
  if (_sendBootState) {
    newStatus.state = !newStatus.state;

    if (_pluginNumber == PLUGIN_GPIO) {
      // FIXME TD-er: This was only done in P001, should this be done for PCF/MCP too?
      // Or maybe this is wrong to do for internal GPIO?
      newStatus.output = !newStatus.output;
    }
  }

  // setPinState(PLUGIN_ID_009, _mcpPin, PIN_MODE_INPUT, switchstate[event->TaskIndex]);
  savePortStatus(_portStatus_key, newStatus);

  // @giig1967g-20181022: set initial UserVar of the switch
  if ((newStatus.state != -1) && Settings.TaskDevicePin1Inversed[event->TaskIndex]) {
    UserVar.setFloat(event->TaskIndex, 0, !newStatus.state);
  } else {
    UserVar.setFloat(event->TaskIndex, 0, newStatus.state);
  }

  return true;
}

void GPIO_plugin_helper_data_t::tenPerSecond(
  struct EventStruct        *event,
  const __FlashStringHelper *monitorEventString,
  int8_t                     pinState)
{
  // Avoid 10xSEC in case of a non-fully configured device (no port defined yet)
  if (pinState == -1) {
    // FIXME TD-er: This will cause the last check at the end of this function not to be executed.
    // Is this correct?
    return;
  }

  /**************************************************************************\
     20181022 - @giig1967g: new doubleclick logic is:
     if there is a 'pinState' change, check debounce period.
     Then if doubleclick interval exceeded, reset _doubleClickCounter to 0
     _doubleClickCounter contains the current status for doubleclick:
     0: start counting
     1: 1st click
     2: 2nd click
     3: 3rd click = doubleclick event if inside interval (calculated as: '3rd click time' minus '1st click time')
     Returned EVENT value is = 3 always for doubleclick
     In rules this can be checked:
     on Button#State=3 do //will fire if doubleclick
  \**************************************************************************/

  // WARNING operator [],creates an entry in map if key doesn't exist:
  portStatusStruct currentStatus = globalMapPortStatus[_portStatus_key];

  // CASE 1: using SafeButton, so wait 1 more 100ms cycle to acknowledge the status change
  // QUESTION: MAYBE IT'S BETTER TO WAIT 2 CYCLES??
  if (_safeButton && (pinState != currentStatus.state) && (_safeButtonCounter == 0))
  {
#ifndef BUILD_NO_DEBUG
    addLog(LOG_LEVEL_DEBUG, concat(monitorEventString, F(" : SafeButton 1st click")));
#endif // ifndef BUILD_NO_DEBUG
    _safeButtonCounter = 1;

    return;
  }

  // CASE 2: not using SafeButton, or already waited 1 more 100ms cycle, so proceed.
  if ((pinState != currentStatus.state) || currentStatus.forceEvent)
  {
    // Reset SafeButton counter
    _safeButtonCounter = 0;

    // reset timer for long press
    _longpressTimer = millis();
    _longpressFired = false;

    const unsigned long debounceTime = timePassedSince(_debounceTimer);

    if (debounceTime >= _debounceInterval_ms) // de-bounce check
    {
      const unsigned long deltaDC = timePassedSince(_doubleClickTimer);

      if ((deltaDC >= _doubleClickMaxInterval_ms) ||
          (_doubleClickCounter == 3))
      {
        // reset timer for doubleclick
        _doubleClickCounter = 0;
        _doubleClickTimer   = millis();
      }

      // check settings for doubleclick according to the settings
      if (_doubleClickCounter == 0) {
        if ((_dcMode == SWITCH_DC_BOTH) ||                      // "Active on LOW & HIGH (EVENT=3)"
            ((_dcMode == SWITCH_DC_LOW) && (pinState == 0)) ||  // "Active only on LOW (EVENT=3)"
            ((_dcMode == SWITCH_DC_HIGH) && (pinState == 1))) { // "Active only on HIGH (EVENT=3)"
          _doubleClickCounter++;
        }
      } else {
        _doubleClickCounter++;
      }

      if ((currentStatus.mode == PIN_MODE_OFFLINE) ||
          (currentStatus.mode == PIN_MODE_UNDEFINED))
      {
        currentStatus.mode = PIN_MODE_INPUT_PULLUP; // changed from offline to online
      }

      currentStatus.state = pinState;
      const bool currentOutputState = currentStatus.output;
      bool new_outputState          = currentOutputState;

      switch (_switchType)
      {
        case SWITCH_TYPE_NORMAL_SWITCH:
          new_outputState = pinState;
          break;
        case SWITCH_TYPE_PUSH_ACTIVE_LOW:

          if (!pinState)
          {
            new_outputState = !currentOutputState;
          }
          break;
        case SWITCH_TYPE_PUSH_ACTIVE_HIGH:

          if (pinState)
          {
            new_outputState = !currentOutputState;
          }
          break;
      }

      // send if output needs to be changed
      if ((currentOutputState != new_outputState) || currentStatus.forceEvent)
      {
        uint8_t output_value;
        currentStatus.output = new_outputState;
        bool sendState = new_outputState;

        if (Settings.TaskDevicePin1Inversed[event->TaskIndex])
        {
          sendState = !sendState;
        }

        if ((_doubleClickCounter == 3) && (_dcMode > 0))
        {
          output_value = 3;                 // double click
        } else {
          output_value = sendState ? 1 : 0; // single click
        }
        event->sensorType = Sensor_VType::SENSOR_TYPE_SWITCH;

        if (_switchType == SWITCH_TYPE_DIMMER)
        {
          if (sendState)
          {
            output_value = _dimmerValue;

            // Only set type to being dimmer when setting a value else it is "switched off".
            event->sensorType = Sensor_VType::SENSOR_TYPE_DIMMER;
          }
        }
        UserVar.setFloat(event->TaskIndex, 0, output_value);

#ifndef BUILD_NO_DEBUG


        if (loglevelActiveFor(LOG_LEVEL_INFO))
        {
          // Need to split this log creation or else uncrustify would fail
          String log = monitorEventString;
          log += strformat(F("  : Port=%d State=%d"), _pin, pinState);
          log += output_value == 3 ? F(" Doubleclick=") : F(" Output value=");
          log += output_value;
          addLogMove(LOG_LEVEL_INFO, log);
        }

#endif // ifndef BUILD_NO_DEBUG

        // send task event
        sendData(event);

        // send monitor event
        if (currentStatus.monitor)
        {
          sendMonitorEvent(monitorEventString, _pin, output_value);
        }

        // reset Userdata so it displays the correct state value in the web page
        UserVar.setFloat(event->TaskIndex, 0, sendState ? 1 : 0);
      }
      _debounceTimer = millis();
    }

    // Reset forceEvent
    currentStatus.forceEvent = 0;

    savePortStatus(_portStatus_key, currentStatus);
    return;
  }

  // CASE 3: status unchanged. Checking longpress:
  // Check if LP is enabled and if LP has not fired yet
  if (!_longpressFired) {
    if ((_longpressEvent == SWITCH_LONGPRESS_BOTH) ||                     // "Active on LOW & HIGH (EVENT= 10 or 11)"
        ((_longpressEvent == SWITCH_LONGPRESS_LOW) && (pinState == 0)) || // "Active only on LOW (EVENT= 10 [NORMAL] or 11 [INVERSED])"
        ((_longpressEvent == SWITCH_LONGPRESS_HIGH) && (pinState == 1)))  // "Active only on HIGH (EVENT= 11 [NORMAL] or 10 [INVERSED])"
    {
      /**************************************************************************\
         20181009 - @giig1967g: new longpress logic is:
         if there is no 'state' change, check if longpress interval reached
         When reached send longpress event.
         Returned Event value = state + 10
         So if state = 0 => EVENT longpress = 10
         if state = 1 => EVENT longpress = 11
         So we can trigger longpress for high or low contact

         In rules this can be checked:
         on Button#State=10 do //will fire if longpress when state = 0
         on Button#State=11 do //will fire if longpress when state = 1
      \**************************************************************************/

      // Reset SafeButton counter
      _safeButtonCounter = 0;

      const unsigned long deltaLP = timePassedSince(_longpressTimer);

      if (deltaLP >= _longpressMinInterval_ms)
      {
        uint8_t output_value;
        bool    needToSendEvent = false;

        _longpressFired = true;

        switch (_switchType)
        {
          case SWITCH_TYPE_NORMAL_SWITCH:
            needToSendEvent = true;
            break;
          case SWITCH_TYPE_PUSH_ACTIVE_LOW:

            if (!pinState)
            {
              needToSendEvent = true;
            }
            break;
          case SWITCH_TYPE_PUSH_ACTIVE_HIGH:

            if (pinState)
            {
              needToSendEvent = true;
            }
            break;
        }

        if (needToSendEvent)
        {
          bool sendState = pinState;

          if (Settings.TaskDevicePin1Inversed[event->TaskIndex])
          {
            sendState = !sendState;
          }
          output_value = sendState ? 11 : 10;

          // output_value = output_value + 10;
          UserVar.setFloat(event->TaskIndex, 0, output_value);

#ifndef BUILD_NO_DEBUG

          if (loglevelActiveFor(LOG_LEVEL_INFO))
          {
            // Need to split this log creation or else uncrustify would fail
            String log = monitorEventString;
            log += strformat(
              F(" : LongPress: Port=%d State=%d Output value=%d"),
              _pin,
              pinState ? 1 : 0,
              output_value);
            addLogMove(LOG_LEVEL_INFO, log);
          }
#endif // ifndef BUILD_NO_DEBUG

          // send task event
          sendData(event);

          // send monitor event
          if (currentStatus.monitor)
          {
            sendMonitorEvent(monitorEventString, _pin, output_value);
          }

          // reset Userdata so it displays the correct state value in the web page
          UserVar.setFloat(event->TaskIndex, 0, sendState ? 1 : 0);
        }
        savePortStatus(_portStatus_key, currentStatus);
      }
      return;
    }
  }

  if (_safeButtonCounter == 1)
  {
    // Safe Button detected. Send EVENT value = 4
    constexpr uint8_t SAFE_BUTTON_EVENT = 4;

    // Reset SafeButton counter
    _safeButtonCounter = 0;

    // Create EVENT with value = 4 for SafeButton false positive detection
    const int tempUserVar = lround(UserVar[event->BaseVarIndex]);
    UserVar.setFloat(event->TaskIndex, 0, SAFE_BUTTON_EVENT);

#ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO))
    {
      addLogMove(LOG_LEVEL_INFO,
                 concat(monitorEventString,
                        strformat(
                          F(" : SafeButton: false positive detected. Port=%d State=%d"),
                          _pin,
                          tempUserVar)));
    }
#endif // ifndef BUILD_NO_DEBUG

    // send task event: DO NOT SEND TASK EVENT
    // sendData(event);
    // send monitor event
    if (currentStatus.monitor)
    {
      sendMonitorEvent(monitorEventString, _pin, SAFE_BUTTON_EVENT);
    }

    // reset Userdata so it displays the correct state value in the web page
    UserVar.setFloat(event->TaskIndex, 0, tempUserVar);
    return;
  }

  if ((pinState != currentStatus.state) && (pinState == -1)) {
    // set UserVar and switchState = -1 and send EVENT to notify user
    UserVar.setFloat(event->TaskIndex, 0, pinState);
    currentStatus.mode = PIN_MODE_OFFLINE;

    // switchstate[event->TaskIndex] = pinState;
#ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO,
             concat(monitorEventString,
                    strformat(
                      F(" : Port=%d is offline (EVENT= -1)"),
                      _pin)));
    }
#endif // ifndef BUILD_NO_DEBUG

    // send task event
    sendData(event);

    // send monitor event
    if (currentStatus.monitor)
    {
      sendMonitorEvent(monitorEventString, _pin, -1);
    }

    savePortStatus(_portStatus_key, currentStatus);
  }
}

#include "../Helpers/_CPlugin_DomoticzHelper.h"

#if FEATURE_DOMOTICZ

# include "../DataStructs/ESPEasy_EventStruct.h"
# include "../DataTypes/TaskIndex.h"

# include "../ESPEasyCore/ESPEasy_Log.h"
# include "../ESPEasyCore/ESPEasyWifi.h"

# include "../Globals/Cache.h"

# include "../Helpers/Convert.h"
# include "../Helpers/StringConverter.h"

# include "../../ESPEasy-Globals.h"

# ifdef USES_C002
#  include <ArduinoJson.h>
# endif // ifdef USES_C002


// HUM_STAT can be one of:

// 0=Normal
// 1=Comfortable
// 2=Dry
// 3=Wet
int humStatDomoticz(struct EventStruct *event, uint8_t rel_index) {
  userVarIndex_t userVarIndex = event->BaseVarIndex + rel_index;

  if (validTaskVarIndex(rel_index) && validUserVarIndex(userVarIndex)) {
    const int hum = UserVar[userVarIndex];

    if (hum < 30) { return 2; }

    if (hum < 40) { return 0; }

    if (hum < 59) { return 1; }
  }
  return 3;
}

int mapRSSItoDomoticz() { 
  return GetRSSI_quality(); 
}

int mapVccToDomoticz() {
  # if FEATURE_ADC_VCC

  // Voltage range from 2.6V .. 3.6V => 0..100%
  if (vcc < 2.6f) { return 0; }
  return (vcc - 2.6f) * 100;
  # else // if FEATURE_ADC_VCC
  return 255;
  # endif // if FEATURE_ADC_VCC
}

String formatDomoticzSensorType(struct EventStruct *event) {
  String values;

  const Sensor_VType sensorType = event->getSensorType();

  if (isSimpleOutputDataType(sensorType) 
      || isIntegerOutputDataType(sensorType)
#if FEATURE_EXTENDED_TASK_VALUE_TYPES
      || isDoubleOutputDataType(sensorType)
#endif
      ) {
    const uint8_t valueCount = getValueCountFromSensorType(sensorType);

    for (uint8_t i = 0; i < valueCount; ++i) {
      values += formatUserVarNoCheck(event, i);
      values += ';';
    }
  } else {
    switch (sensorType)
    {
      case Sensor_VType::SENSOR_TYPE_TEMP_HUM:      // temp + hum + hum_stat, used for DHT11
      case Sensor_VType::SENSOR_TYPE_TEMP_HUM_BARO: // temp + hum + hum_stat + bar + bar_fore, used for BME280
        // http://www.domoticz.com/wiki/Domoticz_API/JSON_URL%27s#Temperature.2Fhumidity
        values = strformat(
          F("%s;%s;%d;"),
          formatUserVarNoCheck(event, 0).c_str(), // TEMP = Temperature
          formatUserVarNoCheck(event, 1).c_str(), // HUM = Humidity
          humStatDomoticz(event, 1));             // HUM_STAT = Humidity status

        if (sensorType == Sensor_VType::SENSOR_TYPE_TEMP_HUM_BARO) {
          values += formatUserVarNoCheck(event, 2); // BAR = Barometric pressure
          values += F(";0;");                       // BAR_FOR = Barometer forecast
        }
        break;
      case Sensor_VType::SENSOR_TYPE_TEMP_BARO:       // temp + hum + hum_stat + bar + bar_fore, used for BMP085
      case Sensor_VType::SENSOR_TYPE_TEMP_EMPTY_BARO: // temp + bar + bar_fore, used for BMP280
      {
        // http://www.domoticz.com/wiki/Domoticz_API/JSON_URL%27s#Temperature.2Fbarometer
        const int baroIndex = sensorType == Sensor_VType::SENSOR_TYPE_TEMP_BARO ? 1 : 2;
        values = strformat(
          F("%s;%s;0;0;"),
          formatUserVarNoCheck(event, 0).c_str(),   // TEMP = Temperature
          formatUserVarNoCheck(event, baroIndex).c_str());  // BAR = Barometric pressure
                                                    // BAR_FOR = Barometer forecast
                                                    // ALTITUDE= Not used at the moment, can be 0
        break;
      }
      case Sensor_VType::SENSOR_TYPE_WIND:

        // WindDir in degrees; WindDir as text; Wind speed average ; Wind speed gust; 0
        // http://www.domoticz.com/wiki/Domoticz_API/JSON_URL%27s#Wind
        values = strformat(
          F("%s;%s;%d;%d;0;0;"),
          formatUserVarNoCheck(event, 0).c_str(),                   // WB = Wind bearing (0-359)
          String(getBearing(UserVar[event->BaseVarIndex])).c_str(), // WD = Wind direction (S, SW, NNW, etc.)
          static_cast<int>(UserVar[event->BaseVarIndex + 1] * 10),  // WS = 10 * Wind speed [m/s]
          static_cast<int>(UserVar[event->BaseVarIndex + 2] * 10)); // WG = 10 * Gust [m/s]
        break;
      case Sensor_VType::SENSOR_TYPE_SWITCH:
      case Sensor_VType::SENSOR_TYPE_DIMMER:

        // Too specific for HTTP/MQTT
        break;
      case Sensor_VType::SENSOR_TYPE_STRING:
        values = event->String2;
        break;
      default:
      {
        # ifndef BUILD_NO_DEBUG

        if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
          String log = F("Domoticz Controller: Not yet implemented sensor type: ");
          log += static_cast<uint8_t>(event->sensorType);
          log += F(" idx: ");
          log += event->idx;
          addLogMove(LOG_LEVEL_ERROR, log);
        }
        # endif // ifndef BUILD_NO_DEBUG
        break;
      }
    }
  }

  // Now strip trailing semi colon.
  int index_last_char = values.length() - 1;

  if ((index_last_char > 0) && (values.charAt(index_last_char) == ';')) {
    values.setCharAt(index_last_char, ' ');
  }
  values.trim();
  {
    # ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      String log = F(" Domoticz: Sensortype: ");
      log += static_cast<uint8_t>(event->sensorType);
      log += F(" idx: ");
      log += event->idx;
      log += F(" values: ");
      log += values;
      addLogMove(LOG_LEVEL_INFO, log);
    }
    # endif // ifndef BUILD_NO_DEBUG
  }
  return values;
}

# ifdef USES_C002
#  include <ArduinoJson.h>

bool deserializeDomoticzJson(const String& json,
                             unsigned int& idx, float& nvalue, long& nvaluealt,
                             String& svalue1, String& switchtype) {
  uint16_t jsonlength = 512;

  DynamicJsonDocument root(jsonlength);

  deserializeJson(root, json);

  if (root.isNull()) {
    return false;
  }

  // Use long here as intermediate object type to prevent ArduinoJSON from adding a new template variant to the code.
  const long idx_long = root[F("idx")];

  idx       = idx_long;
  nvalue    = root[F("nvalue")];
  nvaluealt = root[F("nvalue")];

  // const char* name = root["name"]; // Not used
  // const char* svalue = root["svalue"]; // Not used
  const char *svalue1_c = root[F("svalue1")];

  if (svalue1_c != nullptr) {
    svalue1 = svalue1_c;
  }

  // const char* svalue2 = root["svalue2"]; // Not used
  // const char* svalue3 = root["svalue3"]; // Not used
  const char *switchtype_c = root[F("switchType")]; // Expect "On/Off" or "dimmer"

  // FIXME TD-er: Is this compare even useful?
  // nvalue is already assigned the same value as nvaluealt and not changed since.
  if (essentiallyZero(nvalue)) {
    nvalue = nvaluealt;
  }

  if (switchtype_c == nullptr) {
    switchtype = '?';
  } else {
    switchtype = switchtype_c;
  }
  return true;
}

String serializeDomoticzJson(struct EventStruct *event)
{
  String json;
  {
    json += '{';
    json += to_json_object_value(F("idx"), static_cast<int>(event->idx));
    json += ',';
    json += to_json_object_value(F("RSSI"), mapRSSItoDomoticz());
    #  if FEATURE_ADC_VCC
    json += ',';
    json += to_json_object_value(F("Battery"), mapVccToDomoticz());
    #  endif // if FEATURE_ADC_VCC

    const Sensor_VType sensorType = event->getSensorType();

    if (sensorType == Sensor_VType::SENSOR_TYPE_SWITCH ||
        sensorType == Sensor_VType::SENSOR_TYPE_DIMMER) {
      json += ',';
      json += to_json_object_value(F("command"), F("switchlight"));
      json += ',';

      const bool value_zero = essentiallyZero(UserVar[event->BaseVarIndex]);
      if (sensorType == Sensor_VType::SENSOR_TYPE_DIMMER && !value_zero)
      {
        json += to_json_object_value(F("Set%20Level"), toString(UserVar[event->BaseVarIndex], 2));
      } else {
        json += to_json_object_value(F("switchcmd"), value_zero ? F("Off") : F("On"));
      }
    } else {
      json += ',';
      json += to_json_object_value(F("nvalue"), 0);
      json += ',';
      json += to_json_object_value(F("svalue"), formatDomoticzSensorType(event), true);
    }
    json += '}';
  }

  return json;
}

# endif // ifdef USES_C002

#endif  // if FEATURE_DOMOTICZ
#include "../Helpers/Hardware_PWM.h"

#include "../ESPEasyCore/ESPEasyGPIO.h"

#include "../Helpers/PortStatus.h"
#include "../Helpers/StringConverter.h"

#include "../Globals/GlobalMapPortStatus.h"


#if defined(ESP32)
# if ESP_IDF_VERSION_MAJOR < 5
#  include <soc/soc_caps.h>

// https://github.com/espressif/arduino-esp32/blob/master/cores/esp32/esp32-hal-ledc.c
#  ifndef LEDC_CHANNELS
#   ifdef SOC_LEDC_SUPPORT_HS_MODE
#    define LEDC_CHANNELS           (SOC_LEDC_CHANNEL_NUM << 1)
#   else // ifdef SOC_LEDC_SUPPORT_HS_MODE
#    define LEDC_CHANNELS           (SOC_LEDC_CHANNEL_NUM)
#   endif // ifdef SOC_LEDC_SUPPORT_HS_MODE
#  endif // ifndef LEDC_CHANNELS
int8_t   ledChannelPin[LEDC_CHANNELS];
uint32_t ledChannelFreq[LEDC_CHANNELS] = { 0 };
# endif // if ESP_IDF_VERSION_MAJOR < 5
#endif  // if defined(ESP32)


// ********************************************************************************
// Manage PWM state of GPIO pins.
// ********************************************************************************
void initAnalogWrite()
{
  #if defined(ESP32)
  # if ESP_IDF_VERSION_MAJOR < 5
  constexpr unsigned nrLedChannelPins = NR_ELEMENTS(ledChannelPin);

  for (uint8_t x = 0; x < nrLedChannelPins; x++) {
    ledChannelPin[x]  = -1;
    ledChannelFreq[x] = ledcSetup(x, 1000, 10); // Clear the channel
  }
  # endif // if ESP_IDF_VERSION_MAJOR < 5
  #endif // if defined(ESP32)
  #ifdef ESP8266

  // See https://github.com/esp8266/Arduino/commit/a67986915512c5304bd7c161cf0d9c65f66e0892
  analogWriteRange(1023);
  #endif // ifdef ESP8266
}

#if defined(ESP32)

// Match frequency and resolution and optionally adjust duty accordingly
// @return adjusted duty cycle
uint32_t adapt_ledc_frequency_resolution_duty(uint32_t& frequency, uint8_t& resolution, uint32_t duty = 0)
{
  while ((40000000u >> resolution) < frequency && resolution > 1) {
    --resolution;
    duty >>= 1;
  }
  return std::min(duty, static_cast<uint32_t>((1 << resolution) - 1));
}

# if ESP_IDF_VERSION_MAJOR < 5

int8_t attachLedChannel(int pin, uint32_t frequency, uint8_t resolution)
{
  static bool initialized = false;

  constexpr unsigned nrLedChannelPins = NR_ELEMENTS(ledChannelPin);

  if (!initialized) {
    for (uint8_t x = 0; x < nrLedChannelPins; x++) {
      ledChannelPin[x]  = -1;
      ledChannelFreq[x] = 0;
    }
    initialized = true;
  }


  // find existing channel if this pin has been used before
  int8_t ledChannel = -1;
  bool   mustSetup  = false;

  for (uint8_t x = 0; x < nrLedChannelPins; x++) {
    if (ledChannelPin[x] == pin) {
      ledChannel = x;
    }
  }

  if (ledChannel == -1)                                                  // no channel set for this pin
  {
    for (uint8_t x = 0; x < nrLedChannelPins && ledChannel == -1; ++x) { // find free channel
      if (ledChannelPin[x] == -1)
      {
        if (static_cast<uint32_t>(ledcReadFreq(x)) == ledChannelFreq[x]) {
          // Channel is not used by some other piece of code.
          ledChannel = x;
          mustSetup  = true;
          break;
        }
      }
    }
  }

  if (ledChannel == -1) { return ledChannel; }

  if (frequency != 0) {
    if (ledChannelFreq[ledChannel] != frequency)
    {
      // Frequency is given and has changed
      mustSetup = true;
    }
    ledChannelFreq[ledChannel] = frequency;
  } else if (ledChannelFreq[ledChannel] == 0) {
    mustSetup = true;

    // Set some default frequency
    ledChannelFreq[ledChannel] = 1000;
  }

  if (mustSetup) {
    // setup channel to resolution nr of bits and set frequency.
    ledChannelFreq[ledChannel] = ledcSetup(ledChannel, ledChannelFreq[ledChannel], 10);
    ledChannelPin[ledChannel]  = pin; // store pin nr
    ledcAttachPin(pin, ledChannel);   // attach to this pin
    //    pinMode(pin, OUTPUT);
  }

  return ledChannel;
}

void detachLedChannel(int pin)
{
  int8_t ledChannel = -1;

  constexpr unsigned nrLedChannelPins = NR_ELEMENTS(ledChannelPin);

  for (uint8_t x = 0; x < nrLedChannelPins; x++) {
    if (ledChannelPin[x] == pin) {
      ledChannel = x;
    }
  }

  if (ledChannel != -1) {
    ledcWrite(ledChannel, 0);
    ledcDetachPin(pin);
    ledChannelPin[ledChannel]  = -1;
    ledChannelFreq[ledChannel] = 0;
  }
}

# else // if ESP_IDF_VERSION_MAJOR < 5

// ESP_IDF >= 5.x finally manages the channels in the SDK

int8_t attachLedChannel(int pin, uint32_t frequency, uint8_t resolution)
{
  if (frequency == 0) {
    frequency = ESPEASY_PWM_DEFAULT_FREQUENCY;
  }
  ledcDetach(pin);  // See: https://github.com/espressif/arduino-esp32/issues/9212
  return ledcAttach(pin, frequency, resolution) ? 0 : -1;
}

void detachLedChannel(int pin)
{
  ledcDetach(pin);
}

# endif // if ESP_IDF_VERSION_MAJOR < 5

uint32_t analogWriteESP32(int pin, int value, uint32_t frequency)
{
  if (value == 0) {
    detachLedChannel(pin);
    return 0;
  }

  // find existing channel if this pin has been used before
  uint8_t resolution = 10;

  value = adapt_ledc_frequency_resolution_duty(frequency, resolution, value);
  int8_t ledChannel = attachLedChannel(pin, frequency, resolution);

  if (ledChannel != -1) {
    # if ESP_IDF_VERSION_MAJOR < 5
    ledcWrite(ledChannel, value);
    return ledChannelFreq[ledChannel];
    # else // if ESP_IDF_VERSION_MAJOR < 5
    ledcWrite(pin,        value);
    return ledcReadFreq(pin);
    # endif // if ESP_IDF_VERSION_MAJOR < 5
  }
  return 0;
}

#endif // if defined(ESP32)

bool set_Gpio_PWM_pct(int gpio, float dutyCycle_f, uint32_t frequency) {
  uint32_t dutyCycle = dutyCycle_f * 10.23f;

  return set_Gpio_PWM(gpio, dutyCycle, frequency);
}

bool set_Gpio_PWM(int gpio, uint32_t dutyCycle, uint32_t frequency) {
  uint32_t key;

  return set_Gpio_PWM(gpio, dutyCycle, 0, frequency, key);
}

bool set_Gpio_PWM(int gpio, uint32_t dutyCycle, uint32_t fadeDuration_ms, uint32_t& frequency, uint32_t& key)
{
  // For now, we only support the internal GPIO pins.
  if (!checkValidPortRange(PLUGIN_GPIO, gpio)) {
    return false;
  }
  portStatusStruct tempStatus;
  if (frequency == 0) frequency = 1000;

  // FIXME TD-er: PWM values cannot be stored very well in the portStatusStruct.
  key = createKey(PLUGIN_GPIO, gpio);

  // WARNING: operator [] creates an entry in the map if key does not exist
  // So the next command should be part of each command:
  tempStatus = globalMapPortStatus[key];

#if defined(ESP8266)
  pinMode(gpio, OUTPUT);

  if ((frequency > 0) && (frequency <= 40000)) {
    analogWriteFreq(frequency);
  }
#endif // if defined(ESP8266)

#if ESP_IDF_VERSION_MAJOR >= 5

  if (fadeDuration_ms == 0)
  {
    analogWriteESP32(gpio, dutyCycle, frequency);
  } else {
    uint8_t  resolution  = 10;
    uint32_t start_duty  = 0;
    uint32_t target_duty = dutyCycle;

    if (/*(tempStatus.mode == PIN_MODE_PWM) && */ (ledcReadFreq(gpio) != 0)) {
      start_duty = ledcRead(gpio);
    } else {
      // No PWM active, pick proper estimated start_duty based on pin state
      if (digitalRead(gpio)) {
        start_duty = (1 << resolution);
      }
    }

    if (frequency > 0) {
      // Adjust the frequency and resolution to keep them in the range of the used timer frequency.
      while ((40000000u >> resolution) < frequency && resolution > 7) {
        --resolution;

        if (start_duty > target_duty) {
          ++start_duty;
        }
        else {
          ++target_duty;
        }
        start_duty  >>= 1;
        target_duty >>= 1;
      }
    }
    start_duty  = std::min(start_duty, static_cast<uint32_t>((1 << resolution) - 1));
    target_duty = std::min(target_duty, static_cast<uint32_t>((1 << resolution) - 1));

    //    ledcDetach(gpio);

    if (!ledcWrite(gpio, start_duty)) {
      // Pin not yet attached
      if (!ledcAttach(gpio, frequency, resolution)) {
        addLog(LOG_LEVEL_ERROR, strformat(
          F("PWM : ledcAttach failed  gpio:%d freq:%d res:%d"),
          gpio, frequency, resolution));
        return false;
      }
    }

    if (!ledcFade(gpio, start_duty, target_duty, fadeDuration_ms)) {
      addLog(LOG_LEVEL_ERROR, F("PWM : ledcFade failed"));
      return false;
    }
  }

#else // if ESP_IDF_VERSION_MAJOR < 5

  if (fadeDuration_ms == 0)
  {
# if defined(ESP8266)
    analogWrite(gpio, dutyCycle);
# endif // if defined(ESP8266)
# if defined(ESP32)
    frequency = analogWriteESP32(gpio, dutyCycle, frequency);
# endif // if defined(ESP32)
  } else {
    const int32_t resolution_factor = (1 << 12);
    const uint8_t prev_mode         = tempStatus.mode;
    int32_t prev_value              = tempStatus.getDutyCycle();

    // Check to see if pin was already set to PWM.
    // If not, then set previous duty cycle close to logic pin state.
    if (prev_mode != PIN_MODE_PWM) {
      prev_value = (tempStatus.getValue() == 0) ? 0 : 1023;
    }

    const int32_t step_value = ((static_cast<int32_t>(dutyCycle) - prev_value) * resolution_factor) / static_cast<int32_t>(fadeDuration_ms);
    int32_t curr_value       = prev_value * resolution_factor;

    int i = fadeDuration_ms;

    while (i--) {
      curr_value += step_value;
      const int16_t new_value = curr_value / resolution_factor;
            # if defined(ESP8266)
      analogWrite(gpio, new_value);
            # endif // if defined(ESP8266)
            # if defined(ESP32)
      frequency = analogWriteESP32(gpio, new_value, frequency);
            # endif // if defined(ESP32)
      delay(1);
    }
  }

#endif // if ESP_IDF_VERSION_MAJOR >= 5

  // setPinState(pluginID, gpio, PIN_MODE_PWM, dutyCycle);
  tempStatus.mode      = PIN_MODE_PWM;
  tempStatus.dutyCycle = dutyCycle;
  tempStatus.command   = 1; // set to 1 in order to display the status in the PinStatus page

  savePortStatus(key, tempStatus);
  return true;
}

#include "../Helpers/Hardware_ADC.h"


#ifdef ESP32
# include "../Helpers/Hardware_GPIO.h"
#endif // ifdef ESP32

#ifdef ESP8266
#include "../Globals/ESPEasyWiFiEvent.h"


bool Hardware_ADC_t::init() {
  return true;
}


int  Hardware_ADC_t::read() {
  if (!WiFiEventData.wifiConnectInProgress) {
# if FEATURE_ADC_VCC
    _lastADCvalue = ESP.getVcc();
# else // if FEATURE_ADC_VCC
    _lastADCvalue = analogRead(A0);
# endif // if FEATURE_ADC_VCC
  }
  return _lastADCvalue;
}

#endif // ifdef ESP8266


#ifdef ESP32
Hardware_ADC_t::~Hardware_ADC_t()
{
# if ESP_IDF_VERSION_MAJOR >= 5
  adc_oneshot_del_unit(_adc_handle);
# else
  // FIXME TD-er: Should we reset the GPIO config for this pin?
# endif // if ESP_IDF_VERSION_MAJOR >= 5
}

bool Hardware_ADC_t::init(int pin, adc_atten_t attenuation)
{
  int ch{};
  int t = -1;

  if (!getADC_gpio_info(pin, _adc, ch, t)) {
    return false;
  }
  _pin = pin;
#if defined(SOC_TOUCH_SENSOR_SUPPORTED) && SOC_TOUCH_SENSOR_SUPPORTED
  _isTouchPin = t >= 0;
#endif
  _attenuation = attenuation;

# if ESP_IDF_VERSION_MAJOR >= 5
  _channel = static_cast<adc_channel_t>(ch);
#  if HAS_ADC2
  const adc_unit_t unit = (_adc == 1) ? ADC_UNIT_1 : ADC_UNIT_2;
#  else // if HAS_ADC2

  if (_adc != 1) {
    return false;
  }
  const adc_unit_t unit = ADC_UNIT_1;
#  endif // if HAS_ADC2

  adc_oneshot_unit_init_cfg_t init_config = {
    .unit_id = unit
  };

  if (ESP_OK != adc_oneshot_new_unit(&init_config, &_adc_handle)) {
    return false;
  }

  adc_oneshot_chan_cfg_t config = {
    .atten    = attenuation,
    .bitwidth = ADC_BITWIDTH_DEFAULT,
  };
  return ESP_OK == adc_oneshot_config_channel(_adc_handle, _channel, &config);

# else // if ESP_IDF_VERSION_MAJOR >= 5

  if ((_adc == 1) || (_adc == 2)) {
    analogSetPinAttenuation(_pin, static_cast<adc_attenuation_t>(_attenuation));
  }

  return true;
# endif // if ESP_IDF_VERSION_MAJOR >= 5
}

bool Hardware_ADC_t::adc_calibration_init()
{
  _useFactoryCalibration = _adc_cali_handle.init(_pin, _attenuation);
  return _useFactoryCalibration;
}

int Hardware_ADC_t::read(bool readAsTouch) {
# if HAS_HALL_EFFECT_SENSOR

  if (_adc == 0) {
    return hallRead();
  }
# endif // if HAS_HALL_EFFECT_SENSOR

# if ESP_IDF_VERSION_MAJOR >= 5
  // Starting with IDF 5.x, you can apparently now use ADC2 along with WiFi running.
  // See: https://docs.espressif.com/projects/esp-idf/en/v5.1.1/esp32/api-reference/peripherals/adc_oneshot.html#hardware-limitations
  const bool canread = true;
#else
  // ADC2 and WiFi affect each other.
  // See:
  // https://docs.espressif.com/projects/esp-idf/en/v4.4.6/esp32/api-reference/peripherals/adc.html#adc-limitations
  // ADC2 is shared with WiFi, so don't read ADC2 when WiFi is on.
  const bool canread = _adc == 1 || WiFi.getMode() == WIFI_OFF;
#endif

  if (canread) {
#if defined(SOC_TOUCH_SENSOR_SUPPORTED) && SOC_TOUCH_SENSOR_SUPPORTED

    if (readAsTouch && _isTouchPin) {
      return touchRead(_pin);
    }
# endif

# if ESP_IDF_VERSION_MAJOR >= 5

    int adc_raw{};

    if (ESP_OK == adc_oneshot_read(_adc_handle, _channel, &adc_raw)) {
      _lastADCvalue = adc_raw;
    }
# else // if ESP_IDF_VERSION_MAJOR >= 5

    _lastADCvalue = analogRead(_pin);

# endif // if ESP_IDF_VERSION_MAJOR >= 5
  }
  return _lastADCvalue;
}

float Hardware_ADC_t::applyFactoryCalibration(float rawValue) {
  return _adc_cali_handle.applyFactoryCalibration(rawValue);
}

#endif // ifdef ESP32

#include "../Helpers/BusCmd_Helper.h"

#include <GPIO_Direct_Access.h>
#include "../Globals/RulesCalculate.h"

// These commands (not case-sensitive) must have the same order as the BusCmd_Commands_e enum class
const char BusCmd_commands[] PROGMEM =
  "n|g|p|r|w|s|t|e|c|v|d|a|z|i|l|"
  #if FEATURE_BUSCMD_STRING && FEATURE_STRING_VARIABLES
  "m|"
  #endif // if FEATURE_BUSCMD_STRING && FEATURE_STRING_VARIABLES
;
const char BusCmd_commandsLong[] PROGMEM =
  "nop|get|put|read|write|read16|write16|eval|calc|value|delay|enable|reset|if|let|"
  #if FEATURE_BUSCMD_STRING && FEATURE_STRING_VARIABLES
  "letstr|"
  #endif // if FEATURE_BUSCMD_STRING && FEATURE_STRING_VARIABLES
;

// Supported data formats, _ == undefined, not processed
const char BusCmd_dataFormats[] PROGMEM =
  "_|"
  "u8|"
  "u16|"
  "u24|"
  "u32|"
  "u16le|"
  "u24le|"
  "u32le|"
  "8|"
  "16|"
  "24|"
  "32|"
  "16le|"
  "24le|"
  "32le|"
  "b|"
  "w|"
  #if FEATURE_BUSCMD_STRING
  "str|"
  #endif // if FEATURE_BUSCMD_STRING
;

/**
 * Constructor BusCmd_Command_struct
 */
BusCmd_Command_struct::BusCmd_Command_struct(BusCmd_Command_e    _command,
                                             BusCmd_DataFormat_e _format,
                                             uint16_t            _reg,
                                             int64_t             _data,
                                             uint32_t            _len,
                                             String              _calculation,
                                             String              _variable)
  :command(_command), format(_format), reg(_reg), len(_len), calculation(_calculation), variable(_variable) {
  switch (format) {
    case BusCmd_DataFormat_e::undefined: d0_uint32_t = (uint32_t)_data; break; // Special case
    case BusCmd_DataFormat_e::uint8_t: d0_uint8_t    = (uint8_t)_data; break;
    case BusCmd_DataFormat_e::uint16_t:
    case BusCmd_DataFormat_e::uint16_t_LE: d0_uint16_t = (uint16_t)_data; break;
    case BusCmd_DataFormat_e::uint24_t:
    case BusCmd_DataFormat_e::uint32_t:
    case BusCmd_DataFormat_e::uint24_t_LE:
    case BusCmd_DataFormat_e::uint32_t_LE: d0_uint32_t = (uint32_t)_data; break;
    case BusCmd_DataFormat_e::int8_t: d0_int8_t        = _data; break;
    case BusCmd_DataFormat_e::int16_t:
    case BusCmd_DataFormat_e::int16_t_LE: d0_int16_t = _data; break;
    case BusCmd_DataFormat_e::int24_t:
    case BusCmd_DataFormat_e::int32_t:
    case BusCmd_DataFormat_e::int24_t_LE:
    case BusCmd_DataFormat_e::int32_t_LE: d0_int32_t = _data; break;
    case BusCmd_DataFormat_e::bytes:
    case BusCmd_DataFormat_e::words:
    #if FEATURE_BUSCMD_STRING
    case BusCmd_DataFormat_e::string:
    #endif // if FEATURE_BUSCMD_STRING
      break;
  }
}

/**
 * Destructor BusCmd_Command_struct
 */
BusCmd_Command_struct::~BusCmd_Command_struct() {
  data_b.clear();
  data_w.clear();
  calculation.clear();
}

int64_t BusCmd_Command_struct::getIntValue() {
  int64_t data{};

  switch (format) {
    case BusCmd_DataFormat_e::undefined: break;
    case BusCmd_DataFormat_e::uint8_t: data = d0_uint8_t; break;
    case BusCmd_DataFormat_e::uint16_t:
    case BusCmd_DataFormat_e::uint16_t_LE: data = d0_uint16_t; break;
    case BusCmd_DataFormat_e::uint24_t:
    case BusCmd_DataFormat_e::uint32_t:
    case BusCmd_DataFormat_e::uint24_t_LE:
    case BusCmd_DataFormat_e::uint32_t_LE: data = d0_uint32_t; break;
    case BusCmd_DataFormat_e::int8_t: data      = d0_int8_t; break;
    case BusCmd_DataFormat_e::int16_t:
    case BusCmd_DataFormat_e::int16_t_LE: data = d0_int16_t; break;
    case BusCmd_DataFormat_e::int24_t:
    case BusCmd_DataFormat_e::int32_t:
    case BusCmd_DataFormat_e::int24_t_LE:
    case BusCmd_DataFormat_e::int32_t_LE: data = d0_int32_t; break;
    case BusCmd_DataFormat_e::bytes:
    case BusCmd_DataFormat_e::words:
    #if FEATURE_BUSCMD_STRING
    case BusCmd_DataFormat_e::string:
    #endif // if FEATURE_BUSCMD_STRING
      break;
  }
  return data;
}

String BusCmd_Command_struct::getHexValue(const bool withPrefix) {
  return withPrefix ? concat(F("0x"), getHexValue()) : getHexValue();
}

String BusCmd_Command_struct::getHexValue() {
  uint64_t data64 = getUIntValue();

  if (BusCmd_DataFormat_e::bytes == format) {
    return formatToHex_array(&data_b[0], data_b.size());
  } else if (BusCmd_DataFormat_e::words == format) {
    return formatToHex_wordarray(&data_w[0], data_w.size());
    #if FEATURE_BUSCMD_STRING
  } else if (BusCmd_DataFormat_e::string == format) {
    return EMPTY_STRING;
    #endif // if FEATURE_BUSCMD_STRING
  }
  return formatToHex(data64);
}

#if FEATURE_BUSCMD_STRING
String BusCmd_Command_struct::getString() {
  if (BusCmd_DataFormat_e::string == format) {
    return variable;
  }
  return EMPTY_STRING;
}

#endif // if FEATURE_BUSCMD_STRING

#ifndef LIMIT_BUILD_SIZE
String BusCmd_Command_struct::toString() {
  char cmd[8]{};
  char cmdS[3]{};
  char fmt[6]{};

  GetTextIndexed(cmd,  sizeof(cmd),  static_cast<uint32_t>(command), BusCmd_commandsLong);
  GetTextIndexed(cmdS, sizeof(cmdS), static_cast<uint32_t>(command), BusCmd_commands);
  GetTextIndexed(fmt,  sizeof(fmt),  static_cast<uint32_t>(format),  BusCmd_dataFormats);
  String data;

  if ((BusCmd_DataFormat_e::bytes == format) || (BusCmd_DataFormat_e::words == format)) {
    data = getHexValue(true);
  }

  int64_t val{};

  if ((BusCmd_Command_e::Delay == command) || (BusCmd_Command_e::Value == command)) {
    val = d0_uint32_t;
  } else {
    val = getIntValue();
  }
  String result = strformat(F("cmd: '%s' (%s), fmt: '%s', reg: 0x%x, data: %d (%s), len: %lu"),
                            cmd, cmdS, fmt, reg, static_cast<int32_t>(val), formatToHex(static_cast<uint32_t>(val)).c_str(), len);

  if (!data.isEmpty()) {
    result = concat(result, strformat(F(", data_b/w: %s"), data.c_str()));
  }

  if (!variable.isEmpty()) {
    result = concat(result, strformat(
                      # if FEATURE_BUSCMD_STRING
                      BusCmd_DataFormat_e::string == format ? F(", string: %s") :
                      # endif // if FEATURE_BUSCMD_STRING
                      F(", variable: %s"), variable.c_str()));
  }

  if (!calculation.isEmpty()) {
    result = concat(result, strformat(F(", calculation: %s"), calculation.c_str()));
  }
  return result;
}

#endif // ifndef LIMIT_BUILD_SIZE

/**
 * Constructor BusCmd_Buffer
 */
BusCmd_Buffer::BusCmd_Buffer(const String& name,
                             const String& line)
  :cacheName(name), commandSet(line) {}

/**
 *  Constructor BusCmd_Helper_struct
 */
BusCmd_Helper_struct::BusCmd_Helper_struct(IBusCmd_Handler*busCmd_Handler,
                                           taskIndex_t     taskIndex,
                                           int16_t         enPin,
                                           int16_t         rstPin,
                                           uint8_t         loopLimit)
  : _iBusCmd_Handler(busCmd_Handler), _taskIndex(taskIndex), _enPin(enPin), _rstPin(rstPin), _loopLimit(loopLimit) {}

/**
 *  Destructor BusCmd_Helper_struct
 */
BusCmd_Helper_struct::~BusCmd_Helper_struct() {
  _commandCache.clear();
  _commands.clear();
  _buffer.clear();
}

const __FlashStringHelper * BusCmd_Helper_struct::cacheSuffix(BusCmd_CommandSource_e source) {
  switch (source) {
    case BusCmd_CommandSource_e::PluginIdle:
    case BusCmd_CommandSource_e::PluginGetConfigVar:
    case BusCmd_CommandSource_e::PluginRead:
    case BusCmd_CommandSource_e::PluginWrite: return F("");
    case BusCmd_CommandSource_e::PluginOncePerSecond: return F("_1ps");
    case BusCmd_CommandSource_e::PluginTenPerSecond: return F("_10ps");
    case BusCmd_CommandSource_e::PluginFiftyPerSecond: return F("_50ps");
  }
  return F("");
}

void BusCmd_Helper_struct::setBuffer(uint8_t       index,
                                     const String& name,
                                     const String& line) {
  if (_buffer.size() < index + 1u) {
    _buffer.resize(max(index + 1, VARS_PER_TASK), BusCmd_Buffer(EMPTY_STRING, EMPTY_STRING));
  }
  _buffer[index] = BusCmd_Buffer(name, line);
}

/*********************************************************************************************
 * Handle I2C-command processing, optionally stored to command cache
 * Format: <cmd>.<dataformat>[.<len>][.<register>][.<data>];...
 ********************************************************************************************/
std::vector<BusCmd_Command_struct>BusCmd_Helper_struct::parseBusCmdCommands(const String& name,
                                                                            const String& line) {
  return parseBusCmdCommands(name, line, false);
}

std::vector<BusCmd_Command_struct>BusCmd_Helper_struct::parseBusCmdCommands(const String& name,
                                                                            const String& line,
                                                                            const bool    update) {
  std::vector<BusCmd_Command_struct> commands;

  if ((nullptr == _iBusCmd_Handler) || !_iBusCmd_Handler->init()) { // Handler is required and initialized
    return commands;
  }

  const String key = parseString(name, 1);
  String keyPostfix;
  const bool parseAndLogOK = ((BusCmd_CommandSource_e::PluginRead == _commandSource) ||
                              (BusCmd_CommandSource_e::PluginGetConfigVar == _commandSource) ||
                              (BusCmd_CommandSource_e::PluginWrite == _commandSource)
                              );

  if (!key.isEmpty() && (_commandCache.count(key) == 1) && !update) {
    commands = _commandCache.find(key)->second;

    if (loglevelActiveFor(LOG_LEVEL_INFO) && _showLog && parseAndLogOK) {
      addLog(LOG_LEVEL_INFO, strformat(F("BUSCMD: Retrieve '%s' from cache with %d commands."), name.c_str(), commands.size()));
    }
  }

  if (!line.isEmpty() && ((commands.empty()) || update) && parseAndLogOK) {
    int evt = 1;

    while (evt > 0) {
      keyPostfix.clear();
      String evtAll = parseStringKeepCaseNoTrim(line, evt, BUSCMD_EVENT_SEPARATOR);

      if (evtAll.equalsIgnoreCase(F("1ps"))) { // PLUGIN_ONCE_A_SECOND
        ++evt;
        keyPostfix = BusCmd_Helper_struct::cacheSuffix(BusCmd_CommandSource_e::PluginOncePerSecond);
        _has1ps    = true;
      } else
      if (evtAll.equalsIgnoreCase(F("10ps"))) { // PLUGIN_TEN_PER_SECOND
        ++evt;
        keyPostfix = BusCmd_Helper_struct::cacheSuffix(BusCmd_CommandSource_e::PluginTenPerSecond);
        _has10ps   = true;
      } else
      if (evtAll.equalsIgnoreCase(F("50ps"))) { // PLUGIN_FIFTY_PER_SECOND
        ++evt;
        keyPostfix = BusCmd_Helper_struct::cacheSuffix(BusCmd_CommandSource_e::PluginFiftyPerSecond);
        _has50ps   = true;
      }

      if (!keyPostfix.isEmpty()) { // Next part is I2C command sequence
        evtAll = parseStringKeepCaseNoTrim(line, evt, BUSCMD_EVENT_SEPARATOR);
      }

      if (!evtAll.isEmpty()) {
        ++evt;
        commands.clear();
      } else {
        evt = 0;
      }

      // parse line
      int idx = 1;

      while (idx > 0 && evt > 0) {
        const String cmdAll = parseStringKeepCaseNoTrim(evtAll, idx, BUSCMD_COMMAND_SEPARATOR);
        bool addCmd         = true;

        if (!cmdAll.isEmpty()) {
          std::vector<String> args;

          uint8_t i    = 1;
          String  arg0 = parseStringKeepCaseNoTrim(cmdAll, i, BUSCMD_ARGUMENT_SEPARATOR);

          while (i < 5 || !arg0.isEmpty()) { // Read at least 4 arguments
            args.push_back(arg0);
            ++i;
            arg0 = parseStringKeepCaseNoTrim(cmdAll, i, BUSCMD_ARGUMENT_SEPARATOR);
          }

          #ifndef LIMIT_BUILD_SIZE

          if (loglevelActiveFor(LOG_LEVEL_INFO) && _showLog && parseAndLogOK) {
            addLog(LOG_LEVEL_INFO, strformat(F("BUSCMD: Arguments parsed: %d (%s)"), args.size(), cmdAll.c_str()));
          }
          #endif // ifndef LIMIT_BUILD_SIZE

          args[0].toLowerCase();
          String sFmt = args[1];
          sFmt.toLowerCase();
          const int arg2i = GetCommandCode(sFmt.c_str(), BusCmd_dataFormats);
          int arg1i       = GetCommandCode(args[0].c_str(), BusCmd_commands);

          if (arg1i < 0) {
            arg1i = GetCommandCode(args[0].c_str(), BusCmd_commandsLong);
          }
          uint8_t arg = 2;

          uint16_t reg            = 0;
          uint32_t len            = 0;
          BusCmd_DataFormat_e fmt = BusCmd_DataFormat_e::uint8_t;
          String calculation;
          String variable;

          if (arg2i > -1) {
            fmt = static_cast<BusCmd_DataFormat_e>(arg2i);
          }

          if ((BusCmd_DataFormat_e::bytes == fmt) || (BusCmd_DataFormat_e::words == fmt)
              #if FEATURE_BUSCMD_STRING
              || (BusCmd_DataFormat_e::string == fmt)
              #endif // if FEATURE_BUSCMD_STRING
              ) {
            if (!validUIntFromString(args[arg], len) && !args[arg].isEmpty()) {
              variable = args[arg];
              len      = std::numeric_limits<uint32_t>::max();
            }
            ++arg;
          }

          if (arg1i > -1) {
            BusCmd_Command_e cmd = static_cast<BusCmd_Command_e>(arg1i);
            int64_t val          = 0;
            validInt64FromString(args[arg], val);

            switch (cmd) {
              case BusCmd_Command_e::NOP: break;
              case BusCmd_Command_e::Read:  // get - g.<format>
                break;
              case BusCmd_Command_e::Write: // put - p.<format>.<value>

                #if FEATURE_BUSCMD_STRING

                if (BusCmd_DataFormat_e::string == fmt) {
                  variable = args[arg];
                }
                #endif // if FEATURE_BUSCMD_STRING
                break;
              case BusCmd_Command_e::Calculate: // calc - c.<calculation>
              case BusCmd_Command_e::If:        // if - i.<calculation>[.skip]
                fmt         = BusCmd_DataFormat_e::undefined;
                val         = 0;
                calculation = args[arg - 1];
                stripEscapeCharacters(calculation);

                if ((BusCmd_Command_e::If == cmd) && !args[arg].isEmpty()) {
                  validUIntFromString(args[arg], len); // destination, 0 = exit, 1.. skip n commands
                }
                break;
              case BusCmd_Command_e::Let:              // let - l.<variable>.<calculation>
              #if FEATURE_BUSCMD_STRING && FEATURE_STRING_VARIABLES
              case BusCmd_Command_e::LetStr:           // letstr - m.<variable>.<calculation>
              #endif // if FEATURE_BUSCMD_STRING && FEATURE_STRING_VARIABLES
                fmt         = BusCmd_DataFormat_e::undefined;
                val         = 0;
                variable    = args[arg - 1];
                calculation = args[arg];
                stripEscapeCharacters(variable);
                stripEscapeCharacters(calculation);
                break;
              case BusCmd_Command_e::Eval:  // eval - e
                fmt = BusCmd_DataFormat_e::undefined;
                break;
              case BusCmd_Command_e::Value: // value - v.<valueIndex>
              case BusCmd_Command_e::Delay: // delay - d.<ms>
              {
                fmt = BusCmd_DataFormat_e::undefined;
                const bool isInt = validInt64FromString(args[arg - 1], val);

                if ((BusCmd_Command_e::Value == cmd) && !isInt) {
                  val = findDeviceValueIndexByName(args[arg - 1], _taskIndex);
                }
                break;
              }
              case BusCmd_Command_e::RegisterRead:   // read - r.<format>.<reg>
              case BusCmd_Command_e::Register16Read: // read16 - s.<format>.<reg16>
                reg = val;
                val = 0;
                break;
              case BusCmd_Command_e::RegisterWrite:   // write - w.<format>.<reg>.<data>
              case BusCmd_Command_e::Register16Write: // write16 - t.<format>.<reg16>.<data>
                reg = val;
                val = 0;

                if (!((BusCmd_DataFormat_e::bytes == fmt) || (BusCmd_DataFormat_e::words == fmt)
                      #if FEATURE_BUSCMD_STRING
                      || (BusCmd_DataFormat_e::string == fmt)
                      #endif // if FEATURE_BUSCMD_STRING
                      )) {
                  ++arg;

                  validInt64FromString(args[arg], val);
                }

                #if FEATURE_BUSCMD_STRING

                if (BusCmd_DataFormat_e::string == fmt) {
                  ++arg;
                  variable = args[arg];
                }
                #endif // if FEATURE_BUSCMD_STRING
                break;
              case BusCmd_Command_e::EnableGPIO: // enable - l.<state>
              case BusCmd_Command_e::ResetGPIO:  // reset - z.<state>.<msec>
                fmt = BusCmd_DataFormat_e::undefined;
                val = 0;
                validInt64FromString(args[arg - 1], val);

                if ((val < 0) || (val > 1)) { // Range check
                  addCmd = false;
                } else {
                  reg = val;                  // State

                  if (BusCmd_Command_e::ResetGPIO == cmd) {
                    val = 0;

                    if (!validInt64FromString(args[arg], val) || (val < 0)) { // msec
                      addCmd = false;
                    }
                  }
                }
                break;
            }

            if (addCmd) {
              commands.push_back(BusCmd_Command_struct(cmd, fmt, reg, val, len, calculation, variable));

              if (((BusCmd_Command_e::Write == cmd) || (BusCmd_Command_e::RegisterWrite == cmd) ||
                   (BusCmd_Command_e::Register16Write == cmd)) &&
                  ((BusCmd_DataFormat_e::bytes == fmt) || (BusCmd_DataFormat_e::words == fmt))) {
                const size_t currentCommandsIdx = commands.size() - 1;

                while (arg < args.size()) {
                  uint32_t val = 0;

                  if (validUIntFromString(args[arg], val)) {
                    if (BusCmd_DataFormat_e::bytes == fmt) { // bytes
                      commands[currentCommandsIdx].data_b.push_back((uint8_t)val);
                    } else {                                 // words
                      commands[currentCommandsIdx].data_w.push_back((uint16_t)val);
                    }
                  }
                  ++arg;
                }
              }
            }
          }
          ++idx;   // next
        } else {
          idx = 0; // done
        }
      }

      if (evt > 0) {
        #ifndef LIMIT_BUILD_SIZE

        if (loglevelActiveFor(LOG_LEVEL_INFO) && _showLog) {
          for (auto it = commands.begin(); it != commands.end(); ++it) {
            addLog(LOG_LEVEL_INFO, strformat(F("BUSCMD: Parsing command: %s, name: %s"), it->toString().c_str(), name.c_str()));
          }
        }
        #endif // ifndef LIMIT_BUILD_SIZE

        if (!key.isEmpty()) {
          _commandCache[concat(key, keyPostfix)] = commands;

          if (loglevelActiveFor(LOG_LEVEL_INFO) && _showLog) {
            addLog(LOG_LEVEL_INFO, strformat(F("BUSCMD: Insert '%s%s' into cache with %d commands."),
                                             name.c_str(), keyPostfix.c_str(), commands.size()));
          }
        }
      }
    }
  }

  return commands;
}

/*********************************************************************************************
 * Execute I2C commands
 ********************************************************************************************/
bool BusCmd_Helper_struct::executeBusCmdCommands() {
  bool result = false;

  if ((nullptr == _iBusCmd_Handler) || !_iBusCmd_Handler->init()) {
    return result;
  }
  const bool parseAndLogOK = ((BusCmd_CommandSource_e::PluginRead == _commandSource) ||
                              (BusCmd_CommandSource_e::PluginGetConfigVar == _commandSource) ||
                              (BusCmd_CommandSource_e::PluginWrite == _commandSource)
                              );

  if (BusCmd_CommandState_e::Processing == _commandState) {
    _it = _commands.begin();
  }

  if ((BusCmd_CommandState_e::Processing == _commandState) &&
      (_taskIndex != INVALID_TASK_INDEX) &&
      (_varIndex != INVALID_TASKVAR_INDEX) &&
      !_valueIsSet) {
    _value = UserVar.getFloat(_taskIndex, _varIndex);
  }

  if (BusCmd_CommandState_e::WaitingForDelay == _commandState) { // Returning from a delay
    _commandState = BusCmd_CommandState_e::Processing;

    if (validGpio(_rstPin) && (BusCmd_Command_e::ResetGPIO == _lastCommand)) {
      DIRECT_pinWrite(_rstPin, _lastReg ? LOW : HIGH); // Revert ResetGPIO state
    }
  }
  uint32_t toSkip{};                                   // Skip nr of commands after if.<cond>.<skip>

  while (_it != _commands.end() && BusCmd_CommandState_e::Processing == _commandState) {
    bool wideReg = false;
    uint32_t du32{};
    uint16_t du16{};
    uint32_t _len = _it->len;

    if (_len == std::numeric_limits<uint32_t>::max()) {
      String _var(replacePluginValues(_it->variable)); // preprocessing
      const String _variable = parseTemplate(_var);
      ESPEASY_RULES_FLOAT_TYPE tmp{};

      if (Calculate(_variable, tmp) == CalculateReturnCode::OK) {
        _len = tmp;
      } else {
        _len = 0u;
      }
    }
    _lastCommand = _it->command; // We only need this for long ResetGPIO pulses
    _lastReg     = _it->reg;

    switch (_it->command) {
      case BusCmd_Command_e::NOP: break;
      case BusCmd_Command_e::Read:

        switch (_it->format) {
          case BusCmd_DataFormat_e::undefined: break;
          case BusCmd_DataFormat_e::uint8_t:
          case BusCmd_DataFormat_e::int8_t:
            _it->d0_uint8_t = _iBusCmd_Handler->read8u();
            break;
          case BusCmd_DataFormat_e::uint16_t:
          case BusCmd_DataFormat_e::int16_t:
            _it->d0_uint16_t = _iBusCmd_Handler->read16u();
            break;
          case BusCmd_DataFormat_e::uint24_t:
            _it->d0_uint32_t = _iBusCmd_Handler->read24u();
            break;
          case BusCmd_DataFormat_e::uint32_t: // Fall through
          case BusCmd_DataFormat_e::int32_t:
            _it->d0_uint32_t = _iBusCmd_Handler->read32u();
            break;
          case BusCmd_DataFormat_e::uint16_t_LE:
            du16             = _iBusCmd_Handler->read16u();
            _it->d0_uint16_t = (du16 << 8) | (du16 >> 8);
            break;
          case BusCmd_DataFormat_e::uint24_t_LE:
            du32             = _iBusCmd_Handler->read24u();
            _it->d0_uint32_t = ((du32 & 0xFF0000) >> 16) | (du32 & 0xFF00) | ((du32 & 0xFF) << 16);
            break;
          case BusCmd_DataFormat_e::uint32_t_LE:
            du32             = _iBusCmd_Handler->read32u();
            _it->d0_uint32_t = ((du32 & 0xFF000000) >> 24) | ((du32 & 0xFF0000) >> 16) | ((du32 & 0xFF00) << 8) | ((du32 & 0xFF) << 24);
            break;
          case BusCmd_DataFormat_e::int16_t_LE:
          {
            du16 = _iBusCmd_Handler->read16u();
            const bool isNeg = bitRead(du16, 15);
            du16            &= ((1 << 15) - 1);
            du16             = ((du16 << 8) | (du16 >> 8)) | (isNeg ? (1 << 15) : 0);
            _it->d0_uint16_t = du16;
            break;
          }
          case BusCmd_DataFormat_e::int24_t:
          {
            du32 = _iBusCmd_Handler->read24u();
            const bool isNeg = bitRead(du32, 23);
            du32            &= ((1 << 23) - 1);
            _it->d0_uint32_t = du32 | (isNeg ? (1 << 31) : 0);
            break;
          }
          case BusCmd_DataFormat_e::int24_t_LE:
          {
            du32 = _iBusCmd_Handler->read24u();
            const bool isNeg = bitRead(du32, 23);
            du32            &= ((1 << 23) - 1);
            _it->d0_uint32_t = (((du32 & 0xFF0000) >> 16) | (du32 & 0xFF00) | ((du32 & 0xFF) << 16))
                               | (isNeg ? (1 << 31) : 0);
            break;
          }
          case BusCmd_DataFormat_e::int32_t_LE:
          {
            du32 = _iBusCmd_Handler->read32u();
            const bool isNeg = bitRead(du32, 31);
            du32            &= ((uint32_t)(1 << 31u) - 1);
            _it->d0_uint32_t = (((du32 & 0xFF000000) >> 24) |
                                ((du32 & 0xFF0000) >> 16) |
                                ((du32 & 0xFF00) << 8) |
                                ((du32 & 0xFF) << 24))
                               | (isNeg ? (1 << 31) : 0);
            break;
          }
          case BusCmd_DataFormat_e::bytes:
            _it->data_b = _iBusCmd_Handler->read8uB(_len);
            break;
          case BusCmd_DataFormat_e::words:
            _it->data_w = _iBusCmd_Handler->read16uW(_len);
            break;
          #if FEATURE_BUSCMD_STRING
          case BusCmd_DataFormat_e::string:
            _it->variable = _iBusCmd_Handler->readString(_len);
            break;
          #endif // if FEATURE_BUSCMD_STRING
        }
        result = true;
        break;
      case BusCmd_Command_e::Write:

        switch (_it->format) {
          case BusCmd_DataFormat_e::undefined: break;
          case BusCmd_DataFormat_e::uint8_t:
          case BusCmd_DataFormat_e::int8_t:
            result = _iBusCmd_Handler->write8u(_it->d0_uint8_t);
            break;
          case BusCmd_DataFormat_e::uint16_t:
          case BusCmd_DataFormat_e::int16_t:
            result = _iBusCmd_Handler->write16u(_it->d0_uint16_t);
            break;
          case BusCmd_DataFormat_e::uint24_t:
            result = _iBusCmd_Handler->write24u(_it->d0_uint32_t);
            break;
          case BusCmd_DataFormat_e::uint32_t:
          case BusCmd_DataFormat_e::int32_t:
            result = _iBusCmd_Handler->write32u(_it->d0_uint32_t);
            break;
          case BusCmd_DataFormat_e::uint16_t_LE:
            result = _iBusCmd_Handler->write16u((_it->d0_uint16_t << 8) | (_it->d0_uint16_t >> 8));
            break;
          case BusCmd_DataFormat_e::uint24_t_LE:
            du32   = ((_it->d0_uint32_t & 0xFF0000) >> 16) | (_it->d0_uint32_t & 0xFF00) | ((_it->d0_uint32_t & 0xFF) << 16);
            result = _iBusCmd_Handler->write24u(du32);
            break;
          case BusCmd_DataFormat_e::uint32_t_LE:
            du32 = ((_it->d0_uint32_t & 0xFF000000) >> 24) | ((_it->d0_uint32_t & 0xFF0000) >> 16) |
                   (_it->d0_uint32_t & 0xFF00) | ((_it->d0_uint32_t & 0xFF) << 16);
            result = _iBusCmd_Handler->write32u(du32);
            break;
          case BusCmd_DataFormat_e::int16_t_LE:
          {
            du16 = _it->d0_uint16_t;
            const bool isNeg = bitRead(du16, 15);
            du16  &= ((1 << 15) - 1);
            du16   = ((du16 << 8) | (du16 >> 8)) | (isNeg ? (1 << 15) : 0);
            result = _iBusCmd_Handler->write16u(du16);
            break;
          }
          case BusCmd_DataFormat_e::int24_t:
            result = _iBusCmd_Handler->write24u(_it->d0_uint32_t);
            break;
          case BusCmd_DataFormat_e::int24_t_LE:
          {
            du32 = _it->d0_uint32_t;
            const bool isNeg = bitRead(du32, 31);
            du32 &= ((1 << 23) - 1);
            du32  = (((du32 & 0xFF0000) >> 16) | (du32 & 0xFF00) | ((du32 & 0xFF) << 16))
                    | isNeg ? (1 << 23) : 0;
            result = _iBusCmd_Handler->write24u(du32);
            break;
          }
          case BusCmd_DataFormat_e::int32_t_LE:
          {
            du32 = _it->d0_uint32_t;
            const bool isNeg = bitRead(du32, 31);
            du32 = (((du32 & 0xFF000000) >> 24) |
                    ((du32 & 0xFF0000) >> 16) |
                    (du32 & 0xFF00) |
                    ((du32 & 0xFF) << 16))
                   | (isNeg ? (1 << 31) : 0);
            result = _iBusCmd_Handler->write32u(du32);
            break;
          }
          case BusCmd_DataFormat_e::bytes:
            result = _iBusCmd_Handler->write8uB(_it->data_b);
            break;
          case BusCmd_DataFormat_e::words:
            result = _iBusCmd_Handler->write16uW(_it->data_w);
            break;
          #if FEATURE_BUSCMD_STRING
          case BusCmd_DataFormat_e::string:
            result = _iBusCmd_Handler->writeString(_it->variable);
            break;
          #endif // if FEATURE_BUSCMD_STRING
        }
        break;
      case BusCmd_Command_e::Register16Read:
        wideReg = true; // fall through
      case BusCmd_Command_e::RegisterRead:

        switch (_it->format) {
          case BusCmd_DataFormat_e::undefined: break;
          case BusCmd_DataFormat_e::uint8_t:
          case BusCmd_DataFormat_e::int8_t:
            _it->d0_uint8_t = _iBusCmd_Handler->read8uREG(_it->reg, wideReg);
            break;
          case BusCmd_DataFormat_e::uint16_t:
          case BusCmd_DataFormat_e::int16_t:
            _it->d0_uint16_t = _iBusCmd_Handler->read16uREG(_it->reg, wideReg);
            break;
          case BusCmd_DataFormat_e::uint24_t:
            _it->d0_uint32_t = _iBusCmd_Handler->read24uREG(_it->reg, wideReg);
            break;
          case BusCmd_DataFormat_e::uint32_t: // Fall through
          case BusCmd_DataFormat_e::int32_t:
            _it->d0_uint32_t = _iBusCmd_Handler->read32uREG(_it->reg, wideReg);
            break;
          case BusCmd_DataFormat_e::uint16_t_LE:
            du16             = _iBusCmd_Handler->read16uREG(_it->reg, wideReg);
            _it->d0_uint16_t = (du16 << 8) | (du16 >> 8);
            break;
          case BusCmd_DataFormat_e::uint24_t_LE:
            du32             = _iBusCmd_Handler->read24uREG(_it->reg, wideReg);
            _it->d0_uint32_t = ((du32 & 0xFF0000) >> 16) | (du32 & 0xFF00) | ((du32 & 0xFF) << 16);
            break;
          case BusCmd_DataFormat_e::uint32_t_LE:
            du32             = _iBusCmd_Handler->read32uREG(_it->reg, wideReg);
            _it->d0_uint32_t = ((du32 & 0xFF000000) >> 24) | ((du32 & 0xFF0000) >> 16) | ((du32 & 0xFF00) << 8) | ((du32 & 0xFF) << 24);
            break;
          case BusCmd_DataFormat_e::int16_t_LE:
          {
            du16 = _iBusCmd_Handler->read16uREG(_it->reg, wideReg);
            const bool isNeg = bitRead(du16, 15);
            du16            &= ((1 << 15) - 1);
            du16             = ((du16 << 8) | (du16 >> 8)) | (isNeg ? (1 << 15) : 0);
            _it->d0_uint16_t = du16;
            break;
          }
          case BusCmd_DataFormat_e::int24_t:
          {
            du32 = _iBusCmd_Handler->read24uREG(_it->reg, wideReg);
            const bool isNeg = bitRead(du32, 23);
            du32            &= ((1 << 23) - 1);
            _it->d0_uint32_t = du32 | (isNeg ? (1 << 31) : 0);
            break;
          }
          case BusCmd_DataFormat_e::int24_t_LE:
          {
            du32 = _iBusCmd_Handler->read24uREG(_it->reg, wideReg);
            const bool isNeg = bitRead(du32, 23);
            du32            &= ((1 << 23) - 1);
            _it->d0_uint32_t = (((du32 & 0xFF0000) >> 16) | (du32 & 0xFF00) | ((du32 & 0xFF) << 16))
                               | (isNeg ? (1 << 31) : 0);
            break;
          }
          case BusCmd_DataFormat_e::int32_t_LE:
          {
            du32 = _iBusCmd_Handler->read32uREG(_it->reg, wideReg);
            const bool isNeg = bitRead(du32, 31);
            du32            &= ((uint32_t)(1 << 31u) - 1);
            _it->d0_uint32_t = (((du32 & 0xFF000000) >> 24) |
                                ((du32 & 0xFF0000) >> 16) |
                                ((du32 & 0xFF00) << 8) |
                                ((du32 & 0xFF) << 24))
                               | (isNeg ? (1 << 31) : 0);
            break;
          }
          case BusCmd_DataFormat_e::bytes:
            _it->data_b = _iBusCmd_Handler->read8uBREG(_it->reg, _len, wideReg);
            break;
          case BusCmd_DataFormat_e::words:
            _it->data_w = _iBusCmd_Handler->read16uWREG(_it->reg, _len, wideReg);
            break;
          #if FEATURE_BUSCMD_STRING
          case BusCmd_DataFormat_e::string:
            _it->variable = _iBusCmd_Handler->readStringREG(_it->reg, _len, wideReg);
            break;
          #endif // if FEATURE_BUSCMD_STRING
        }
        result = true;
        break;
      case BusCmd_Command_e::Register16Write:
        wideReg = true; // fall through
      case BusCmd_Command_e::RegisterWrite:

        switch (_it->format) {
          case BusCmd_DataFormat_e::undefined: break;
          case BusCmd_DataFormat_e::uint8_t:
          case BusCmd_DataFormat_e::int8_t:
            result = _iBusCmd_Handler->write8uREG(_it->reg, _it->d0_uint8_t, wideReg);
            break;
          case BusCmd_DataFormat_e::uint16_t:
          case BusCmd_DataFormat_e::int16_t:
            result = _iBusCmd_Handler->write16uREG(_it->reg, _it->d0_uint16_t, wideReg);
            break;
          case BusCmd_DataFormat_e::uint24_t:
            result = _iBusCmd_Handler->write24uREG(_it->reg, _it->d0_uint32_t, wideReg);
            break;
          case BusCmd_DataFormat_e::uint32_t:
          case BusCmd_DataFormat_e::int32_t:
            result = _iBusCmd_Handler->write32uREG(_it->reg, _it->d0_uint32_t, wideReg);
            break;
          case BusCmd_DataFormat_e::uint16_t_LE:
            result = _iBusCmd_Handler->write16uREG(_it->reg, (_it->d0_uint16_t << 8) | (_it->d0_uint16_t >> 8), wideReg);
            break;
          case BusCmd_DataFormat_e::uint24_t_LE:
            du32   = ((_it->d0_uint32_t & 0xFF0000) >> 16) | (_it->d0_uint32_t & 0xFF00) | ((_it->d0_uint32_t & 0xFF) << 16);
            result = _iBusCmd_Handler->write24uREG(_it->reg, du32, wideReg);
            break;
          case BusCmd_DataFormat_e::uint32_t_LE:
            du32 = ((_it->d0_uint32_t & 0xFF000000) >> 24) | ((_it->d0_uint32_t & 0xFF0000) >> 16) |
                   (_it->d0_uint32_t & 0xFF00) | ((_it->d0_uint32_t & 0xFF) << 16);
            result = _iBusCmd_Handler->write32uREG(_it->reg, du32, wideReg);
            break;
          case BusCmd_DataFormat_e::int16_t_LE:
          {
            du16 = _it->d0_uint16_t;
            const bool isNeg = bitRead(du16, 15);
            du16  &= ((1 << 15) - 1);
            du16   = ((du16 << 8) | (du16 >> 8)) | (isNeg ? (1 << 15) : 0);
            result = _iBusCmd_Handler->write16uREG(_it->reg, du16, wideReg);
            break;
          }
          case BusCmd_DataFormat_e::int24_t:
            result = _iBusCmd_Handler->write24uREG(_it->reg, _it->d0_uint32_t, wideReg);
            break;
          case BusCmd_DataFormat_e::int24_t_LE:
          {
            du32 = _it->d0_uint32_t;
            const bool isNeg = bitRead(du32, 31);
            du32 &= ((1 << 23) - 1);
            du32  = (((du32 & 0xFF0000) >> 16) | (du32 & 0xFF00) | ((du32 & 0xFF) << 16))
                    | isNeg ? (1 << 23) : 0;
            result = _iBusCmd_Handler->write24uREG(_it->reg, du32, wideReg);
            break;
          }
          case BusCmd_DataFormat_e::int32_t_LE:
          {
            du32 = _it->d0_uint32_t;
            const bool isNeg = bitRead(du32, 31);
            du32 = (((du32 & 0xFF000000) >> 24) |
                    ((du32 & 0xFF0000) >> 16) |
                    (du32 & 0xFF00) |
                    ((du32 & 0xFF) << 16))
                   | (isNeg ? (1 << 31) : 0);
            result = _iBusCmd_Handler->write32uREG(_it->reg, du32, wideReg);
            break;
          }
          case BusCmd_DataFormat_e::bytes:
            result = _iBusCmd_Handler->write8uBREG(_it->reg, _it->data_b, wideReg);
            break;
          case BusCmd_DataFormat_e::words:
            result = _iBusCmd_Handler->write16uWREG(_it->reg, _it->data_w, wideReg);
            break;
          #if FEATURE_BUSCMD_STRING
          case BusCmd_DataFormat_e::string:
            result = _iBusCmd_Handler->writeStringReg(_it->reg, _it->variable, wideReg);
            break;
          #endif // if FEATURE_BUSCMD_STRING
        }
        break;
      case BusCmd_Command_e::Value:

        if ((_it->d0_uint32_t > 0) && (_it->d0_uint32_t <= VARS_PER_TASK)) {
          UserVar.setFloat(_taskIndex, _it->d0_uint32_t - 1, _value);
          _valueIsSet = true;
        }
        result = true;
        break;
      case BusCmd_Command_e::Calculate:
      case BusCmd_Command_e::If:
      case BusCmd_Command_e::Let:
      #if FEATURE_BUSCMD_STRING && FEATURE_STRING_VARIABLES
      case BusCmd_Command_e::LetStr:
      #endif // if FEATURE_BUSCMD_STRING && FEATURE_STRING_VARIABLES

        result = true;

        if (!_it->calculation.isEmpty()) {
          String toCalc(replacePluginValues(_it->calculation));
          const String newCalc = parseTemplate(toCalc); // Process like rules
          ESPEASY_RULES_FLOAT_TYPE tmp{};

          #if FEATURE_BUSCMD_STRING && FEATURE_STRING_VARIABLES

          if ((BusCmd_Command_e::LetStr == _it->command)) {
            String toVar(replacePluginValues(_it->variable));
            const String newVar = parseTemplate(toVar); // Process like rules

            if (!newVar.isEmpty()) {
              setCustomStringVar(newVar, newCalc);      // Assign string value to a string variable

              if (loglevelActiveFor(LOG_LEVEL_INFO) && _showLog && parseAndLogOK) {
                addLog(LOG_LEVEL_INFO, strformat(F("BUSCMD: Calculation: %s -> LetStr,%s,%s"),
                                                 toCalc.c_str(), newVar.c_str(), newCalc.c_str()));
              }
            }
          } else
          #endif // if FEATURE_BUSCMD_STRING && FEATURE_STRING_VARIABLES

          if (Calculate(newCalc, tmp) == CalculateReturnCode::OK) {
            if (loglevelActiveFor(LOG_LEVEL_INFO) && _showLog && parseAndLogOK) {
              addLog(LOG_LEVEL_INFO, strformat(F("BUSCMD: Calculation: %s, result: %s"), toCalc.c_str(), doubleToString(tmp).c_str()));
            }

            if (BusCmd_Command_e::If == _it->command) {
              if (essentiallyZero(tmp)) { // 0 = false => cancel execution
                if (0 == _it->len) {
                  _commandState = BusCmd_CommandState_e::ConditionalExit;
                  result        = false;  // PLUGIN_READ failed
                } else {
                  // skip <len> commands
                  toSkip = _it->len;
                }
              }
            } else if (BusCmd_Command_e::Let == _it->command) {
              String toVar(replacePluginValues(_it->variable));
              const String newVar = parseTemplate(toVar); // Process like rules

              if (!newVar.isEmpty() && ExtraTaskSettings.checkInvalidCharInNames(newVar.c_str())) {
                setCustomFloatVar(newVar, tmp);
              } else {
                result = false;
              }
            } else {
              _value = tmp;
            }
          }
        }
        break;
      case BusCmd_Command_e::Eval:

        if (_it != _commands.begin()) { // No previous entry
          _evalCommand = _it;
          --_evalCommand;               // Previous command holds the desired value
          _evalIsSet = true;
        }
        result = true;
        break;
      case BusCmd_Command_e::Delay:
      {
        const uint32_t mx = ((BusCmd_CommandSource_e::PluginOncePerSecond == _commandSource) ||
                             (BusCmd_CommandSource_e::PluginTenPerSecond == _commandSource) ||
                             (BusCmd_CommandSource_e::PluginFiftyPerSecond == _commandSource) ||
                             (BusCmd_CommandSource_e::PluginGetConfigVar == _commandSource)) ? 10u : 500u;
        const uint32_t ms = min(_it->d0_uint32_t, mx); // Reasonable limit, max 10 msec for repeating events

        if (ms <= 10) {
          delay(ms);
          result = true;
        } else {
          _commandState = BusCmd_CommandState_e::StartingDelay;
          Scheduler.schedule_task_device_timer(_taskIndex, millis() + ms);
          result = false; // Don't publish result yet
        }
        break;
      }
      case BusCmd_Command_e::EnableGPIO:

        if (validGpio(_enPin)) {
          DIRECT_pinWrite(_enPin, _it->reg ? HIGH : LOW);
          result = true;
        }
        break;
      case BusCmd_Command_e::ResetGPIO:

        if (validGpio(_rstPin)) {
          const uint32_t ms = min(_it->d0_uint32_t, static_cast<uint32_t>(500u)); // Reasonable limit

          DIRECT_pinWrite(_rstPin, _it->reg ? HIGH : LOW);                        // Set ResetGPIO state

          if (ms <= 10) {
            delay(ms);
            DIRECT_pinWrite(_rstPin, _it->reg ? LOW : HIGH); // Revert ResetGPIO state
            result = true;
          } else {
            _commandState = BusCmd_CommandState_e::StartingDelay;
            Scheduler.schedule_task_device_timer(_taskIndex, millis() + ms);
            result = false; // Don't publish result yet
          }
        }
        break;
    }

    if ((BusCmd_CommandState_e::Processing == _commandState) && !_valueIsSet) {
      switch (_it->format) {
        case BusCmd_DataFormat_e::undefined: break;
        case BusCmd_DataFormat_e::uint8_t:
        case BusCmd_DataFormat_e::uint16_t:
        case BusCmd_DataFormat_e::uint16_t_LE:
        case BusCmd_DataFormat_e::uint24_t:
        case BusCmd_DataFormat_e::uint32_t:
        case BusCmd_DataFormat_e::uint24_t_LE:
        case BusCmd_DataFormat_e::uint32_t_LE:
          _value = _it->getUIntValue();
          break;
        case BusCmd_DataFormat_e::int8_t:
        case BusCmd_DataFormat_e::int16_t:
        case BusCmd_DataFormat_e::int16_t_LE:
        case BusCmd_DataFormat_e::int24_t:
        case BusCmd_DataFormat_e::int32_t:
        case BusCmd_DataFormat_e::int24_t_LE:
        case BusCmd_DataFormat_e::int32_t_LE:
          _value = _it->getIntValue();
          break;
        case BusCmd_DataFormat_e::bytes:
        case BusCmd_DataFormat_e::words:
          break;
        #if FEATURE_BUSCMD_STRING
        case BusCmd_DataFormat_e::string:
          # if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
          validDoubleFromString(_it->variable, _value);
          # else // if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
          validFloatFromString(_it->variable, _value);
          # endif // if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
          break;
        #endif // if FEATURE_BUSCMD_STRING
      }
    }
#ifndef LIMIT_BUILD_SIZE

    if (loglevelActiveFor(LOG_LEVEL_INFO) && _showLog && parseAndLogOK) {
      # if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
      const String valStr = doubleToString(_value, 2, true);
      # else // if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
      const String valStr = toString(_value, 2, true);
      # endif // if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE

      addLog(LOG_LEVEL_INFO, strformat(F("BUSCMD: Executing command: %s, value[%d]:(%c): %s"),
                                       _it->toString().c_str(), _varIndex, _valueIsSet ? 't' : 'f', valStr.c_str()));
    }
#endif // ifndef LIMIT_BUILD_SIZE
    ++_it; // Next command

    while (toSkip > 0 && _it != _commands.end()) {
      ++_it;
      --toSkip; // Skip commands, forward-only, for now
    }
  }

  if ((BusCmd_CommandState_e::Processing == _commandState) &&
      (_taskIndex != INVALID_TASK_INDEX) &&
      (_varIndex != INVALID_TASKVAR_INDEX) &&
      !_valueIsSet &&
      (BusCmd_CommandSource_e::PluginRead == _commandSource)) {
    UserVar.setFloat(_taskIndex, _varIndex, _value);
  }

  return result;
}

String BusCmd_Helper_struct::replacePluginValues(const String& inVar) {
  String result(inVar);

  result.replace(F("%pvalue%"), toString(_value)); // %pvalue%

  if (_evalIsSet) {
    result.replace(F("%value%"),
                   #if FEATURE_BUSCMD_STRING
                   BusCmd_DataFormat_e::string == _evalCommand->format ? _evalCommand->getString() :
                   #endif // if FEATURE_BUSCMD_STRING
                   toString(_evalCommand->getIntValue()));     // %value%
    result.replace(F("%h%"), _evalCommand->getHexValue(true)); // %h%

    if (BusCmd_DataFormat_e::bytes == _evalCommand->format) {
      if (result.indexOf(F("%b")) > -1) {
        for (uint8_t i = 0; i < _evalCommand->data_b.size(); ++i) {
          result.replace(strformat(F("%%b%d%%"), i),  String(_evalCommand->data_b[i]));                   // %b<n>%
          result.replace(strformat(F("%%bx%d%%"), i), formatToHex_no_prefix(_evalCommand->data_b[i], 2)); // %bx<n>%
        }
      }
    } else if (BusCmd_DataFormat_e::words == _evalCommand->format) {
      if (result.indexOf(F("%w")) > -1) {
        for (uint8_t i = 0; i < _evalCommand->data_w.size(); ++i) {
          result.replace(strformat(F("%%w%d%%"), i),  String(_evalCommand->data_w[i]));                   // %w<n>%
          result.replace(strformat(F("%%wx%d%%"), i), formatToHex_no_prefix(_evalCommand->data_w[i], 4)); // %wx<n>%
        }
      }
    }
  }

  return result;
}

bool BusCmd_Helper_struct::parseAndExecute(BusCmd_CommandSource_e source,
                                           const String         & line,
                                           const String         & logFormat) {
  bool result = false;

  _commandSource = source;
  _commands      = parseBusCmdCommands(EMPTY_STRING, line);

  if (!_commands.empty()) {
    if (loglevelActiveFor(LOG_LEVEL_INFO) && _showLog) {
      addLog(LOG_LEVEL_INFO, strformat(logFormat, _commands.size()));
    }
    _commandState = BusCmd_CommandState_e::Processing;
    result        = executeBusCmdCommands();
    _commands.clear();
    _commandState = BusCmd_CommandState_e::Idle;
  }
  _commandSource = BusCmd_CommandSource_e::PluginIdle;
  return result;
}

/**
 * Event handlers
 */
bool BusCmd_Helper_struct::plugin_once_a_second(struct EventStruct *event) {
  if (((BusCmd_CommandSource_e::PluginIdle != _commandSource) &&
       (BusCmd_CommandSource_e::PluginOncePerSecond != _commandSource)) ||
      !_has1ps) {
    return false;
  }
  _commandSource = BusCmd_CommandSource_e::PluginOncePerSecond;
  return processCommands(event);
}

bool BusCmd_Helper_struct::plugin_ten_per_second(struct EventStruct *event) {
  if (((BusCmd_CommandSource_e::PluginIdle != _commandSource) &&
       (BusCmd_CommandSource_e::PluginTenPerSecond != _commandSource)) ||
      !_has10ps) {
    return false;
  }
  _commandSource = BusCmd_CommandSource_e::PluginTenPerSecond;
  return processCommands(event);
}

bool BusCmd_Helper_struct::plugin_fifty_per_second(struct EventStruct *event) {
  if (((BusCmd_CommandSource_e::PluginIdle != _commandSource) &&
       (BusCmd_CommandSource_e::PluginFiftyPerSecond != _commandSource)) ||
      !_has50ps) {
    return false;
  }
  _commandSource = BusCmd_CommandSource_e::PluginFiftyPerSecond;
  return processCommands(event);
}

bool BusCmd_Helper_struct::plugin_read(struct EventStruct *event) {
  if ((BusCmd_CommandSource_e::PluginIdle != _commandSource) &&
      (BusCmd_CommandSource_e::PluginRead != _commandSource)) {
    return false;
  }
  _commandSource = BusCmd_CommandSource_e::PluginRead;
  return processCommands(event);
}

bool BusCmd_Helper_struct::processCommands(struct EventStruct *event) {
  bool result       = true;
  BusCmd_Buffer buf = BusCmd_Buffer(EMPTY_STRING, EMPTY_STRING);

  if (BusCmd_CommandState_e::Idle == _commandState) {
    _commandState = BusCmd_CommandState_e::Processing;
    _loop         = 0;
    _loopMax      = _loopLimit;
    _value        = 0.0;
    _valueIsSet   = false; // init

    // loadStrings(event);
    buf = _buffer[_loop];

    if (!(buf.cacheName.isEmpty() && buf.commandSet.isEmpty())) {
      const String cacheName = buf.cacheName.isEmpty()
                               ? EMPTY_STRING
                               : concat(buf.cacheName, BusCmd_Helper_struct::cacheSuffix(_commandSource));

      _commands = parseBusCmdCommands(cacheName, // PluginOnce/Ten/Fifty-PerSecond must come from cache
                                      (BusCmd_CommandSource_e::PluginRead == _commandSource) ||
                                      (BusCmd_CommandSource_e::PluginGetConfigVar == _commandSource) ||
                                      (BusCmd_CommandSource_e::PluginWrite == _commandSource)
                                       ? buf.commandSet : EMPTY_STRING);
    }
    _varIndex = _loop;
  }

  while (BusCmd_CommandState_e::StartingDelay != _commandState && BusCmd_CommandState_e::ConditionalExit != _commandState &&
         _loop < _loopMax) {
    if (!_commands.empty()) {
      result &= executeBusCmdCommands();
    }

    if (BusCmd_CommandState_e::Processing == _commandState) {
      ++_loop;
      buf = _buffer[_loop];
      const String cacheName = buf.cacheName.isEmpty()
                               ? EMPTY_STRING
                               : concat(buf.cacheName, BusCmd_Helper_struct::cacheSuffix(_commandSource));

      _commands = parseBusCmdCommands(cacheName, // PluginOnce/Ten/Fifty-PerSecond must come from cache
                                      (BusCmd_CommandSource_e::PluginRead == _commandSource) ||
                                      (BusCmd_CommandSource_e::PluginGetConfigVar == _commandSource) ||
                                      (BusCmd_CommandSource_e::PluginWrite == _commandSource)
                                       ? buf.commandSet : EMPTY_STRING);
      _varIndex = _loop;
    }
  }

  if ((BusCmd_CommandState_e::Processing == _commandState) || (BusCmd_CommandState_e::ConditionalExit == _commandState)) {
    if (_valueIsSet && (BusCmd_CommandSource_e::PluginRead != _commandSource) && (BusCmd_CommandState_e::ConditionalExit != _commandState)) {
      sendData(event); // Publish changes on succesful Value command from another action than PLUGIN_READ
    }
    _commandState = BusCmd_CommandState_e::Idle;
    _commands.clear();
    _valueIsSet    = false; // reset afterward
    _commandSource = BusCmd_CommandSource_e::PluginIdle;
  } else if (BusCmd_CommandState_e::StartingDelay == _commandState) {
    _commandState = BusCmd_CommandState_e::WaitingForDelay;
  }

  return result;
}

void BusCmd_Helper_struct::setCommands(std::vector<BusCmd_Command_struct>commands,
                                       taskVarIndex_t                    taskVarIndex,
                                       uint8_t                           loopStart,
                                       uint8_t                           loopMax,
                                       BusCmd_CommandState_e             commandState) {
  _commands     = commands;
  _varIndex     = taskVarIndex;
  _loop         = loopStart;
  _loopMax      = loopMax;
  _commandState = commandState;
}

#ifndef LIMIT_BUILD_SIZE
bool BusCmd_Helper_struct::plugin_get_config(struct EventStruct *event,
                                             String            & string) {
  const String cmd = parseString(string, 1);

  if (cmd.isEmpty() || (string.indexOf(',') == -1)) { return false; }

  if (equals(cmd, F("log"))) { // Get current logging state
    string = String(_showLog ? 1 : 0);
    return true;
  }

  if (BusCmd_CommandSource_e::PluginIdle != _commandSource) { // Execute commands when not busy
    return false;
  }
  bool success = false;
  std::vector<BusCmd_Command_struct> cmds;
  taskVarIndex_t taskVar = INVALID_TASKVAR_INDEX;

  _commandSource = BusCmd_CommandSource_e::PluginGetConfigVar;
  cmds           = parseBusCmdCommands(EMPTY_STRING, parseStringKeepCaseNoTrim(string, 2));

  if (!cmds.empty() && (BusCmd_CommandState_e::Idle == _commandState)) {
    setCommands(cmds,
                taskVar,
                0,
                1, // Process single entry
                BusCmd_CommandState_e::Processing);
    success = processCommands(event);

    if (success) {
      string = strformat(F("%g"), _value);
    }
  }
  # ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { // Not logging log value or early return
    addLog(LOG_LEVEL_DEBUG, strformat(F("BUSCMD: GET_CONFIG, result: %s, success: %d, value: %g, cmds: %d"),
                                      string.c_str(),
                                      success,
                                      _value,
                                      cmds.size()));
  }
  # endif // ifndef BUILD_NO_DEBUG

  return success;
}

#endif // ifndef LIMIT_BUILD_SIZE

#include "../Helpers/Hardware_GPIO.h"

#ifdef ESP32C6
# include <driver/gpio.h>

# include "../Helpers/Hardware_device_info.h"

// ********************************************************************************
// Get info of a specific GPIO pin
// ********************************************************************************

bool getGpioInfo(int gpio, int& pinnr, bool& input, bool& output, bool& warning) {
  pinnr = -1; // ESP32 does not label the pins, they just use the GPIO number.

  input   = GPIO_IS_VALID_GPIO(gpio);
  output  = GPIO_IS_VALID_OUTPUT_GPIO(gpio);
  warning = isBootStrapPin(gpio);

  if (!(GPIO_IS_VALID_GPIO(gpio))) { return false; }

  if (gpio == 27) {
    // By default VDD_SPI is the power supply pin for embedded flash or external flash. It can only be used as GPIO
    // only when the chip is connected to an external flash, and this flash is powered by an external power supply
    input   = false;
    output  = false;
    warning = true;
  }

  if (isFlashInterfacePin_ESPEasy(gpio)) {
    // Connected to the integrated SPI flash.
    input   = false;
    output  = false;
    warning = true;
  }

  if ((gpio == PIN_USB_D_MIN) || (gpio == PIN_USB_D_PLUS)) {
    // USB OTG and USB Serial/JTAG function. USB signal is a differential
    // signal transmitted over a pair of D+ and D- wires.
    warning = true;
  }

  return (input || output);
}

bool isBootStrapPin(int gpio) 
{ 
  // See: https://www.espressif.com/sites/default/files/documentation/esp32-c6_datasheet_en.pdf 
  // Chapter 3 - Boot Configurations 
  if (gpio == 8) { 
    // Strapping pin which must be high during flashing 
    // ROM message printing 
    return true; 
  } 
 
  if (gpio == 9) { 
    // Strapping pin to force download mode (like GPIO-0 on ESP8266/ESP32-classic) 
    // internal Weak pull-up, must be pulled down to enter download boot mode. 
    return true; 
  } 
 
  if (gpio == 4 || gpio == 5) { 
    // SDIO Sampling and Driving Clock Edge 
    //  MTMS = GPIO-4 
    //  MTDI = Gpio-5 
    return true; 
  } 
 
  if (gpio == 15) { 
    // JTAG signal source 
    return true; 
  } 
 
  return false; 
} 

bool getGpioPullResistor(int gpio, bool& hasPullUp, bool& hasPullDown) {
  hasPullDown = false;
  hasPullUp   = false;

  if (validGpio(gpio)) {
    hasPullUp   = true;
    hasPullDown = true;
  }

  return true;
}

bool validGpio(int gpio) {
  if (!GPIO_IS_VALID_GPIO(gpio)) { return false; }

  int  pinnr;
  bool input;
  bool output;
  bool warning;

  return getGpioInfo(gpio, pinnr, input, output, warning);
}

// Get ADC related info for a given GPIO pin
// @param gpio_pin   GPIO pin number
// @param adc        Number of ADC unit (0 == Hall effect)
// @param ch         Channel number on ADC unit
// @param t          index of touch pad ID
bool getADC_gpio_info(int gpio_pin, int& adc, int& ch, int& t)
{
  adc = -1;
  ch  = -1;
  t   = -1;

  if ((gpio_pin >= 0) && (gpio_pin <= 6)) {
    adc = 1;
    ch  = gpio_pin;
    return true;
  }
  return false;
}

#if SOC_TOUCH_SENSOR_SUPPORTED
int touchPinToGpio(int touch_pin)
{
  // No touch pin support
  return -1;
}
#endif

#if SOC_DAC_SUPPORTED
// Get DAC related info for a given GPIO pin
// @param gpio_pin   GPIO pin number
// @param dac        Number of DAC unit
bool getDAC_gpio_info(int gpio_pin, int& dac)
{
  // ESP32-C3, ESP32-S3, ESP32-C2, ESP32-C6 and ESP32-H2 don't have a DAC onboard
  return false;
}
#endif

#endif // ifdef ESP32C6

#include "../Helpers/Hardware_GPIO.h"

#ifdef ESP8266

// ********************************************************************************
// Get info of a specific GPIO pin
// ********************************************************************************


// return true when pin can be used.
bool getGpioInfo(int gpio, int& pinnr, bool& input, bool& output, bool& warning) {
  pinnr  = -1;
  input  = true;
  output = true;

  warning = isBootStrapPin(gpio);

  switch (gpio) {
    case  0: pinnr =  3; break;
    case  1: pinnr = 10; break;
    case  2: pinnr =  4; break;
    case  3: pinnr =  9; break;
    case  4: pinnr =  2; break;
    case  5: pinnr =  1; break;
    case  6:                    // GPIO 6 .. 8  is used for flash
    case  7:
    case  8: pinnr = -1; break;
    case  9: pinnr = 11; break; // On ESP8266 used for flash
    case 10: pinnr = 12; break; // On ESP8266 used for flash
    case 11: pinnr = -1; break;
    case 12: pinnr =  6; break;
    case 13: pinnr =  7; break;
    case 14: pinnr =  5; break;

    // GPIO-15 Can't be used as an input. There is an external pull-down on this pin.
    case 15: pinnr =  8; input = false; break;
    case 16: pinnr =  0; break; // This is used by the deep-sleep mechanism
  }

  if (isFlashInterfacePin_ESPEasy(gpio)) {
    if (isESP8285()) {
      if ((gpio == 9) || (gpio == 10)) {
        // Usable on ESP8285
      } else {
        warning = true;
      }
    } else {
      warning = true;

      // On ESP8266 GPIO 9 & 10 are only usable if not connected to flash
      if (gpio == 9) {
        // GPIO9 is internally used to control the flash memory.
        input  = false;
        output = false;
      } else if (gpio == 10) {
        // GPIO10 can be used as input only.
        output = false;
      }
    }
  }

  if ((pinnr < 0) || (pinnr > 16)) {
    input  = false;
    output = false;
  }
  return input || output;
}

bool isBootStrapPin(int gpio)
{
    return (gpio == 0 || gpio == 2 || gpio == 15);
}

bool getGpioPullResistor(int gpio, bool& hasPullUp, bool& hasPullDown) {
  hasPullDown = false;
  hasPullUp   = false;

  if (!validGpio(gpio)) {
    return false;
  }

  if (gpio == 16) {
    hasPullDown = true;
  } else {
    hasPullUp = true;
  }
  return true;
}


bool validGpio(int gpio) {
  if (gpio < 0) { return false; }
  if (gpio > MAX_GPIO) { return false; }

  int pinnr;
  bool input;
  bool output;
  bool warning;

  return getGpioInfo(gpio, pinnr, input, output, warning);
}


#endif // ifdef ESP8266
#include "../Helpers/Hardware_device_info.h"

#ifdef ESP32C3

// See: https://github.com/espressif/esptool/blob/master/esptool/targets/esp32c3.py


# include <soc/soc.h>
# include <soc/efuse_reg.h>
# include <soc/spi_reg.h>
# include <soc/spi_pins.h>
# include <soc/rtc.h>
# include <esp_chip_info.h>
# include <bootloader_common.h>


/** EFUSE_FLASH_CAP : R; bitpos: [29:27]; default: 0;
 * register: EFUSE_RD_MAC_SPI_SYS_3_REG
 */
# define EFUSE_FLASH_CAP    0x00000007U
# define EFUSE_FLASH_CAP_M  (EFUSE_FLASH_CAP_V << EFUSE_FLASH_CAP_S)
# define EFUSE_FLASH_CAP_V  0x00000007U
# define EFUSE_FLASH_CAP_S  27


/** EFUSE_VDD_SPI_AS_GPIO : RO; bitpos: [26]; default: 0;
 *  Set this bit to vdd spi pin function as gpio.
 *  register: EFUSE_RD_REPEAT_ERR0_REG
 */
# define EFUSE_VDD_SPI_AS_GPIO    (BIT(26))
# define EFUSE_VDD_SPI_AS_GPIO_M  (EFUSE_VDD_SPI_AS_GPIO_V << EFUSE_VDD_SPI_AS_GPIO_S)
# define EFUSE_VDD_SPI_AS_GPIO_V  0x00000001U
# define EFUSE_VDD_SPI_AS_GPIO_S  26

bool isFlashInterfacePin_ESPEasy(int gpio) {
  // GPIO-11: Flash voltage selector
  // GPIO-12 ... 17: Connected to flash
  // return (gpio) >= 12 && (gpio) <= 17;
  switch (gpio) {
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 0)
    case SPI_IOMUX_PIN_NUM_HD:
    case SPI_IOMUX_PIN_NUM_CS:
    case SPI_IOMUX_PIN_NUM_MOSI:
    case SPI_IOMUX_PIN_NUM_CLK:
    case SPI_IOMUX_PIN_NUM_MISO:
    case SPI_IOMUX_PIN_NUM_WP:
#else
    case MSPI_IOMUX_PIN_NUM_HD:
    case MSPI_IOMUX_PIN_NUM_WP:
    case MSPI_IOMUX_PIN_NUM_CS0:
    case MSPI_IOMUX_PIN_NUM_CLK:
    case MSPI_IOMUX_PIN_NUM_MOSI:
    case MSPI_IOMUX_PIN_NUM_MISO:
#endif
      return true;
  }
  return false;
}

bool flashVddPinCanBeUsedAsGPIO()
{
  const bool efuse_vdd_spi_as_gpio = REG_GET_FIELD(EFUSE_RD_REPEAT_ERR0_REG, EFUSE_VDD_SPI_AS_GPIO) != 0;

  return efuse_vdd_spi_as_gpio;
}

int32_t getEmbeddedFlashSize()
{
  const uint32_t flash_cap = REG_GET_FIELD(EFUSE_RD_MAC_SPI_SYS_3_REG, EFUSE_FLASH_CAP);

  switch (flash_cap) {
    case 0: return 0;
    case 1: return 4;
    case 2: return 2;
    case 3: return 1;
    case 4: return 8;
  }

  // Unknown value, thus mark as negative value
  return -1 *  static_cast<int32_t>(flash_cap);
}

int32_t getEmbeddedPSRAMSize()
{
  // Doesn't have PSRAM
  return 0;
}

# ifndef isPSRAMInterfacePin
bool isPSRAMInterfacePin(int gpio) {
  return false;
}

# endif // ifndef isPSRAMInterfacePin


const __FlashStringHelper* getChipModel(uint32_t chip_model, uint32_t chip_revision, uint32_t pkg_version, bool single_core)
{
  if (CHIP_ESP32C3 == chip_model) { // ESP32-C3
    /* esptool:
       def get_pkg_version(self):
       num_word = 3
       block1_addr = self.EFUSE_BASE + 0x044
       word3 = self.read_reg(block1_addr + (4 * num_word))
       pkg_version = (word3 >> 21) & 0x0F
       return pkg_version
     */
    uint32_t chip_ver = REG_GET_FIELD(EFUSE_RD_MAC_SPI_SYS_3_REG, EFUSE_PKG_VERSION);
    pkg_version = chip_ver & 0x7;

    //    pkg_version = esp_efuse_get_pkg_ver();

    //    AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("HDW: ESP32 Model %d, Revision %d, Core %d, Package %d"), chip_info.model, chip_revision,
    // chip_info.cores, chip_ver);

    switch (pkg_version) {
      case 0: return F("ESP32-C3 (QFN32)");    // Max 160MHz, Single core, QFN 5*5, ESP32-C3-WROOM-02, ESP32-C3-DevKitC-02
      // case 1: return F("ESP32-C3FH4");      // Max 160MHz, Single core, QFN 5*5, 4MB embedded flash, ESP32-C3-MINI-1, ESP32-C3-DevKitM-1
      case 1: return F("ESP8685 (QFN28)");     // Max 160MHz, Single core, QFN 5*5, 4MB embedded flash, ESP32-C3-MINI-1, ESP32-C3-DevKitM-1
      case 2: return F("ESP32-C3 AZ (QFN32)"); // QFN32
      case 3: return F("ESP8686 (QFN24)");     // QFN24
    }
    return F("ESP32-C3");
  }

  return F("Unknown");
}

#endif // ifdef ESP32C3

#include "../Helpers/RulesHelper.h"

#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../Globals/Settings.h"
#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringProvider.h"

/********************************************************************************************\
   Test for common mistake
   Return true if mistake was found (and corrected)
 \*********************************************************************************************/
bool rules_replace_common_mistakes(const String& from, const String& to, String& line)
{
  if (line.indexOf(from) == -1) {
    return false; // Nothing replaced
  }

  if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
    String log;
    if (reserve_special(log, 32 + from.length() + to.length() + line.length())) {
      log  = F("Rules (Syntax Error, auto-corrected): '");
      log += from;
      log += F("' => '");
      log += to;
      log += F("' in: '");
      log += line;
      log += '\'';
      addLogMove(LOG_LEVEL_ERROR, log);
    }
  }
  line.replace(from, to);
  return true;
}

/********************************************************************************************\
   Check for common mistakes
   Return true if nothing strange found
 \*********************************************************************************************/
bool check_rules_line_user_errors(String& line)
{
  bool res = true;

  if (rules_replace_common_mistakes(F("if["), F("if ["), line)) {
    res = false;
  }

  if (rules_replace_common_mistakes(F("if%"), F("if %"), line)) {
    res = false;
  }

  return res;
}

/********************************************************************************************\
   Strip comment from the line.
   Return true when comment was stripped.
 \*********************************************************************************************/
bool rules_strip_trailing_comments(String& line)
{
  // Strip trailing comments
  int comment = line.indexOf(F("//"));

  if (comment >= 0) {
    bool firstSlash = false;
    bool haveColon  = false;
    char wrapQuote  = '\0';
    comment = -1; // No comment confirmed yet

    // Find first comment '//' that's not quoted or prefixed with a colon '://'
    for (size_t i = 0; i < line.length() && comment == -1; i++) {
      switch (line[i]) {
        case ':':
          haveColon = true;
          break;
        case '/':

          if ((wrapQuote == '\0') && !haveColon) {
            if (firstSlash) {
              comment = i - 1u; // Found a valid comment-start
            } else {
              firstSlash = true;
            }
          } else {
            firstSlash = false;
            haveColon  = false;
          }
          break;
        case '"':
        case '\'':
        case '`':

          if ((wrapQuote == '\0') && (i > 0) && ((line[i - 1] == ' ') || (line[i - 1] == ','))) { // Start-quote?
            wrapQuote = line[i];                                                                  // Start quoted range
          } else if ((line[i] == wrapQuote) &&                                                    // End-quote equals start-quote?
                                                                                                  // And next is a separator: ' ,/'
                     (((i < line.length() - 1) && ((line[i + 1] == ' ') || (line[i + 1] == ',') || (line[i + 1] == '/'))) ||
                      (i == line.length() - 1))) {                                                // Or end of line?
            wrapQuote = '\0';                                                                     // No longer in quoted range
          }

        // Fall through
        default:
          firstSlash = false;
          haveColon  = false;
          break;
      }
    }
  }

  if (comment >= 0) {
    line = line.substring(0, comment);
    line.trim();
    return true;
  }
  return false;
}

RulesHelperClass::RulesHelperClass()
{}

RulesHelperClass::~RulesHelperClass()
{
  closeAllFiles();
}

bool RulesHelperClass::findMatchingRule(const String& event, String& filename, size_t& pos)
{
  if (!_eventCache.isInitialized()) {
    init();
  }
  RulesEventCache_vector::const_iterator it = _eventCache.findMatchingRule(event, Settings.EnableRulesEventReorder());

  if (it == _eventCache.end()) { return false; }

  filename = it->_filename;
  pos      = it->_posInFile;
  return true;
}

void RulesHelperClass::init()
{
  if (_eventCache.isInitialized()) { return; }

  // Read all files to populate caches.

  for (uint8_t x = 0; x < RULESETS_MAX; x++) {
    // Read files
    const String filename        = getRulesFileName(x);
    size_t pos                   = 0;
    bool   moreAvailable         = true;
    const bool searchNextOnBlock = false;

    while (moreAvailable) {
      const size_t pos_start_line = pos;
      const String rulesLine      = readLn(filename, pos, moreAvailable, searchNextOnBlock);

      if (_eventCache.addLine(
            rulesLine,
            filename,
            pos_start_line)) {
#ifndef BUILD_NO_DEBUG

        if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
          addLogMove(LOG_LEVEL_DEBUG, strformat(
            F("Cache rules event: %s pos: %u %s"),
            filename.c_str(),
            pos_start_line,
            rulesLine.c_str()));
        }
#endif // ifndef BUILD_NO_DEBUG
      }
    }
  }
  _eventCache.initialize();
}

void RulesHelperClass::closeAllFiles() {
  for (auto it = _fileHandleMap.begin(); it != _fileHandleMap.end();) {
    #ifdef CACHE_RULES_IN_MEMORY
    it = _fileHandleMap.erase(it);
    #else // ifdef CACHE_RULES_IN_MEMORY
    it->second.close();
    it = _fileHandleMap.erase(it);
    #endif // ifdef CACHE_RULES_IN_MEMORY
  }
  _eventCache.clear();
}

#ifndef CACHE_RULES_IN_MEMORY
size_t RulesHelperClass::read(const String& filename, size_t& pos, uint8_t *buffer, size_t length)
{
  if (!Settings.UseRules || !fileExists(filename)) {
    return 0;
  }
  auto it = _fileHandleMap.begin();

  if (it != _fileHandleMap.end()) {
    if (!it->first.equals(filename)) {
      // Switched to a new file.
      // Close this handle first to make sure we don't keep too many file handles open.
      it->second.close();
      _fileHandleMap.erase(it);
      it = _fileHandleMap.end();
    }
  }

  if (it == _fileHandleMap.end()) {
    // No open file handle found, so try to open it.
    _fileHandleMap.emplace(filename, tryOpenFile(filename, "r"));
    it = _fileHandleMap.find(filename);
  }

  if (!it->second) {
    return 0;
  }

  if (it->second.position() != pos) {
    if (!it->second.seek(pos)) {
      return 0;
    }
  }
  const size_t ret = it->second.read(buffer, length);

  pos = it->second.position();
  return ret;
}

#endif // ifndef CACHE_RULES_IN_MEMORY

bool RulesHelperClass::addChar(char c, String& line,   bool& firstNonSpaceRead)
{
  switch (c)
  {
    case '\n':
    {
      // Line end, parse rule
      line.trim();

      if ((line.length() > 0) && !line.startsWith(F("//"))) {
        rules_strip_trailing_comments(line);
        check_rules_line_user_errors(line);
        return true;
      }

      // Prepare for new line
      line.clear();
      firstNonSpaceRead = false;
      break;
    }
    case '\r': // Just skip this character
      break;
    case '\t': // tab
    case ' ':  // space
    {
      // Strip leading spaces.
      if (firstNonSpaceRead) {
        line += ' ';
      }
      break;
    }
    default: // Any other character
    {
      firstNonSpaceRead = true;

      line += c;
      break;
    }
  }
  return false;
}

#ifdef CACHE_RULES_IN_MEMORY
String RulesHelperClass::readLn(const String& filename,
                                size_t      & pos,
                                bool        & moreAvailable,
                                bool          searchNextOnBlock)
{
  moreAvailable = false;
  auto it = _fileHandleMap.find(filename);

  if (it == _fileHandleMap.end()) {
    // Read lines from the file
    fs::File f = tryOpenFile(filename, "r");

    if (f) {
      RulesLines lines;
      String     tmpStr;
      bool firstNonSpaceRead = false;

      // Keep track of which line we're reading for the event cache.
      size_t readPos = 0;

      while (f.available()) {
        if (addChar(char(f.read()), tmpStr, firstNonSpaceRead)) {
          lines.push_back(std::move(move_special(std::move(tmpStr))));
          ++readPos;

          firstNonSpaceRead = false;
          tmpStr.clear();
        }
      }

      if (tmpStr.length() > 0) {
        rules_strip_trailing_comments(tmpStr);
        check_rules_line_user_errors(tmpStr);
        lines.push_back(std::move(move_special(std::move(tmpStr))));
        tmpStr.clear();
      }
# ifndef BUILD_NO_DEBUG

      if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
        addLogMove(LOG_LEVEL_DEBUG, strformat(
                     F("Rules : Read %u  lines from %s"),
                     lines.size(),
                     filename.c_str()));
      }
# endif // ifndef BUILD_NO_DEBUG
      _fileHandleMap.emplace(std::make_pair(filename, std::move(lines)));
      it = _fileHandleMap.find(filename);
    }
  }

  if (it != _fileHandleMap.end()) {
    while (pos < it->second.size()) {
      ++pos;
      moreAvailable = pos < it->second.size();

      if (!searchNextOnBlock ||
          it->second[pos - 1].substring(0, 3).equalsIgnoreCase(F("on "))) {
        return it->second[pos - 1];
      }
    }
  }
  return EMPTY_STRING;
}

#else // ifdef CACHE_RULES_IN_MEMORY

String RulesHelperClass::readLn(const String& filename,
                                size_t      & pos,
                                bool        & moreAvailable,
                                bool          searchNextOnBlock)
{
  # ifdef USE_SECOND_HEAP

  // Do not store in 2nd heap, this is only temporary and needs to be as fast as possible
  HeapSelectDram ephemeral;
  # endif // ifdef USE_SECOND_HEAP

  std::vector<uint8_t> buf;

  buf.resize(RULES_BUFFER_SIZE);

  bool firstNonSpaceRead = false;

  // Try to get the best possible estimate on line length based on earlier parsing of the rules.
  static size_t longestLineSize = RULES_BUFFER_SIZE;
  String line;

  line.reserve(longestLineSize);

  bool done = false;

  while (!done) {
    const size_t startPos = pos;
    int len               = read(filename, pos, &buf[0], RULES_BUFFER_SIZE);
    moreAvailable = len != 0;

    // Due to change in Arduino code, pos may now also be (size_t)-1
    // See: https://github.com/espressif/arduino-esp32/commit/0ab2c58b6c14f6dbc8b9ab0e61d776cd3ac5de66
    constexpr size_t errorcode = (size_t)-1;

    if (pos == errorcode) {
      moreAvailable = false;
    }

    if (!moreAvailable) { done = true; }

    for (int x = 0; x < len; x++) {
      int data = buf[x];

      if (addChar(char(data), line, firstNonSpaceRead)) {
        if (line.length() > longestLineSize) {
          longestLineSize = line.length();
        }

        // A line may end on every position in the buffer,
        // so we must make sure the position is reflecting the end of the line.
        pos = startPos + x;

        if (!searchNextOnBlock ||
            line.substring(0, 3).equalsIgnoreCase(F("on ")))
        {
          done = true;

          return line;
        } else {
          // Not starting with "on " which we need, so continue to search for a matching line
          line.clear();
        }
      }
    }
  }
  rules_strip_trailing_comments(line);
  check_rules_line_user_errors(line);

  // Make sure there is no left-over reserved data 
  // and allocate it to the appropriate memory area.
  return move_special(std::move(line));
}

#endif // ifdef CACHE_RULES_IN_MEMORY


#include "../Helpers/StringGenerator_Web.h"
#include "../WebServer/HTML_wrappers.h"

/**
 * start a datalist definition
 * see: datalistAddValue, datalistFinish
 */
void datalistStart(const __FlashStringHelper *id) {
  datalistStart(String(id));
}

void datalistStart(const String& id) {
  addHtml(F("<datalist id=\""));
  addHtml(id);
  addHtml(F("\">"));
}

/**
 * add a value to a datalist
 * see: datalistStart, datalistFinish
 */
void datalistAddValue(const String& value) {
  addHtml(F("<option value=\""));
  addHtml(value);
  addHtml(F("\">"));
  addHtml(value);
  addHtml(F("</option>"));
}

/**
 * finish the datalist definition
 * see: datalistStart, datalistAddValue
 */
void datalistFinish() {
  addHtml(F("</datalist>"));
}

#include "../Helpers/Hardware_device_info.h"

#ifdef ESP32H2


const __FlashStringHelper* getChipModel(uint32_t chip_model, uint32_t chip_revision, uint32_t pkg_version, bool single_core)
{
  if (10 == chip_model) return F("ESP32-H2(beta1)");
  if (14 == chip_model) return F("ESP32-H2(beta2)");
  if (CHIP_ESP32H2 == chip_model) { // ESP32-H2
    return F("ESP32-H2");
  }

  return F("Unknown");
}

#endif // ifdef ESP32H2

#include "../Helpers/ESPEasy_math.h"
#ifndef BUILD_NO_DEBUG
#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../Helpers/StringConverter.h"
#endif // ifndef BUILD_NO_DEBUG

#include <Arduino.h> 
// Need to include Arduino.h first, then cmath
// See: https://github.com/esp8266/Arduino/issues/8922#issuecomment-1542301697
#include <cmath>

#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
constexpr double ESPEASY_DOUBLE_EPSILON     = ESPEASY_DOUBLE_EPSILON_FACTOR * std::numeric_limits<double>::epsilon();
constexpr double ESPEASY_DOUBLE_EPSILON_NEG = -1.0 * ESPEASY_DOUBLE_EPSILON_FACTOR * std::numeric_limits<double>::epsilon();
#endif
constexpr float  ESPEASY_FLOAT_EPSILON      = ESPEASY_FLOAT_EPSILON_FACTOR * std::numeric_limits<float>::epsilon();
constexpr float  ESPEASY_FLOAT_EPSILON_NEG  = -1.0f * ESPEASY_FLOAT_EPSILON_FACTOR * std::numeric_limits<float>::epsilon();

#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
int maxNrDecimals_fpType(const double& value)
{
  int res       = ESPEASY_DOUBLE_NR_DECIMALS;
  double factor = 1;

  while ((value / factor) > 10 && res > 2) {
    factor *= 10.0;
    --res;
  }
  return res;
}
#endif
int maxNrDecimals_fpType(const float& value)
{
  int res       = ESPEASY_FLOAT_NR_DECIMALS;
  float factor = 1;

  while ((value / factor) > 10 && res > 2) {
    factor *= 10.0f;
    --res;
  }
  return res;
}

uint64_t computeDecimalFactorForDecimals(int nrDecimals)
{
  uint64_t factor = 1;
  while (nrDecimals > 0) {
    factor *= 10;
    --nrDecimals;
  }
  return factor;
}

#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
double estimateDoubleEpsilon(const double& a, const double& b)
{
  return std::max(std::abs(a), std::abs(b)) * ESPEASY_DOUBLE_EPSILON;
}
#endif
float estimateEpsilon(const float& a, const float& b)
{
  return std::max(std::abs(a), std::abs(b)) * ESPEASY_FLOAT_EPSILON;
}


#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
bool approximatelyEqual(const double& a, const double& b) {
  return approximatelyEqual(a, b, estimateDoubleEpsilon(a, b));
}
#endif

bool approximatelyEqual(const float& a, const float& b) {
  return approximatelyEqual(a, b, estimateEpsilon(a, b));
}

#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
bool approximatelyEqual(const double& a, const double& b, double estimatedEpsilon)
{
  return std::abs(a - b) <= estimatedEpsilon;
}
#endif

bool approximatelyEqual(const float& a, const float& b, float estimatedEpsilon)
{
  return std::abs(a - b) <= estimatedEpsilon;
}

#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
bool definitelyGreaterThan(const double& a, const double& b) {
  return definitelyGreaterThan(a, b, estimateDoubleEpsilon(a, b));
}
#endif

bool definitelyGreaterThan(const float& a, const float& b) {
  return definitelyGreaterThan(a, b, estimateEpsilon(a, b));
}

#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
bool definitelyGreaterThan(const double& a, const double& b, double estimatedEpsilon)
{
  #ifndef BUILD_NO_DEBUG
  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLog(LOG_LEVEL_DEBUG, strformat(F("definitelyGreaterThan double a:%f b:%f ep:%.8g"),
                                      a, b, estimatedEpsilon));
  }
  #endif // ifndef BUILD_NO_DEBUG
  return (a - b) > estimatedEpsilon;
}
#endif

bool definitelyGreaterThan(const float& a, const float& b, float estimatedEpsilon)
{
  #ifndef BUILD_NO_DEBUG
  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLog(LOG_LEVEL_DEBUG, strformat(F("definitelyGreaterThan float a:%f b:%f ep:%.8g"),
                                      a, b, estimatedEpsilon));
  }
  #endif // ifndef BUILD_NO_DEBUG
  return (a - b) > estimatedEpsilon;
}

#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
bool definitelyLessThan(const double& a, const double& b) {
  return definitelyLessThan(a, b, estimateDoubleEpsilon(a, b));
}
#endif

bool definitelyLessThan(const float& a, const float& b) {
  return definitelyLessThan(a, b, estimateEpsilon(a, b));
}

#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
bool definitelyLessThan(const double& a, const double& b, double estimatedEpsilon)
{
  #ifndef BUILD_NO_DEBUG
  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLog(LOG_LEVEL_DEBUG, strformat(F("definitelyLessThan double a:%f b:%f ep:%.8g"),
                                      a, b, estimatedEpsilon));
  }
  #endif // ifndef BUILD_NO_DEBUG
  return (b - a) > estimatedEpsilon;
}
#endif

bool definitelyLessThan(const float& a, const float& b, float estimatedEpsilon)
{
  #ifndef BUILD_NO_DEBUG
  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLog(LOG_LEVEL_DEBUG, strformat(F("definitelyLessThan float a:%f b:%f ep:%.8g"),
                                      a, b, estimatedEpsilon));
  }
  #endif // ifndef BUILD_NO_DEBUG
  return (b - a) > estimatedEpsilon;
}

#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
bool essentiallyEqual(const double& a, const double& b) {
  return essentiallyEqual(a, b, estimateDoubleEpsilon(a, b));
}
#endif

bool essentiallyEqual(const float& a, const float& b) {
  return essentiallyEqual(a, b, estimateEpsilon(a, b));
}

#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
bool essentiallyEqual(const double& a, const double& b, double estimatedEpsilon)
{
  return std::abs(a - b) <= estimatedEpsilon;
}
#endif

bool essentiallyEqual(const float& a, const float& b, float estimatedEpsilon)
{
  return std::abs(a - b) <= estimatedEpsilon;
}

#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
bool essentiallyZero(const double& a)
{
  return ESPEASY_DOUBLE_EPSILON_NEG <= a &&
         a <= ESPEASY_DOUBLE_EPSILON;
}
#endif

bool essentiallyZero(const float& a)
{
  return ESPEASY_FLOAT_EPSILON_NEG <= a &&
         a <= ESPEASY_FLOAT_EPSILON;
}

/*========================================================================*/
/*  Functions that would otherwise duplicate code                         */
/*  For example due to being implemented as macros                        */
/*  Or duplications as it is being implemented both for double and float  */
/*  Another factor is that converting from double to float on each call   */
/*  also adds up                                                          */
/*========================================================================*/

#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE

// Since we already are using the double implementation of those functions, 
// no need to also add implementation of the float versions of those.
// The functions are declared in math.h
// We're simply overriding their toolchain implementation to reduce build size.


float powf(const float x, const float y)
{
  return (float)(pow((double)x, (double)y));
}

float ceilf(const float x)
{
  return (float)(ceil((double)x));
}

float floorf(const float x)
{
  return (float)(floor((double)x));
}

float fabsf(const float x)
{
  return (float)(fabs((double)x));
}

float acosf(const float x)
{
  return (float)(acos((double)x));
}

float cosf(const float x)
{
  return (float)(cos((double)x));
}

float asinf(const float x)
{
  return (float)(asin((double)x));
}

float sinf(const float x)
{
  return (float)(sin((double)x));
}

float atanf(const float x)
{
  return (float)(atan((double)x));
}

float tanf(const float x)
{
  return (float)(tan((double)x));
}

float sqrtf(const float x)
{
  return (float)(sqrt((double)x));
}



#else


// We only need the float precision when calling these functions.
// Declaration is in math.h
// But we're using a trick here to implement them like this, thus not using 
// the double implementation of those functions.
//
// N.B. this is only working because we concatenate the .cpp files in this Helpers directory.
// So if those functions are called from a .cpp file  outside this folder, it will use the double implementation.
// All rules related code is in this Helpers folder.

double pow(const double x, const double y)
{
  return (double)(powf((float)x, (float)y));
}

double ceil(const double x)
{
  return (double)(ceilf((float)x));
}

double floor(const double x)
{
  return (double)(floorf((float)x));
}

double fabs(const double x)
{
  return (double)(fabsf((float)x));
}

double acos(const double x)
{
  return (double)(acosf((float)x));
}

double cos(const double x)
{
  return (double)(cosf((float)x));
}

double asin(const double x)
{
  return (double)(asinf((float)x));
}

double sin(const double x)
{
  return (double)(sinf((float)x));
}

double atan(const double x)
{
  return (double)(atanf((float)x));
}

double tan(const double x)
{
  return (double)(tanf((float)x));
}

double sqrt(const double x)
{
  return (double)(sqrtf((float)x));
}



#endif

ESPEASY_RULES_FLOAT_TYPE mapADCtoFloat(ESPEASY_RULES_FLOAT_TYPE float_value,
                                       ESPEASY_RULES_FLOAT_TYPE adc1,
                                       ESPEASY_RULES_FLOAT_TYPE adc2,
                                       ESPEASY_RULES_FLOAT_TYPE out1,
                                       ESPEASY_RULES_FLOAT_TYPE out2)
{
  // FIXME TD-er: When ADC1/2 are equal, shouldn't we just output either out1, out2 or some average of those?
  if (!approximatelyEqual(adc1, adc2))
  {
    const ESPEASY_RULES_FLOAT_TYPE normalized = (float_value - adc1) / (adc2 - adc1);
    float_value = normalized * (out2 - out1) + out1;
  }
  return float_value;
}

#include "../Helpers/_NPlugin_init.h"

#if FEATURE_NOTIFIER

#include "../DataStructs/ESPEasy_EventStruct.h"
#include "../DataStructs/TimingStats.h"

#include "../DataTypes/ESPEasy_plugin_functions.h"

#include "../Globals/NPlugins.h"
#include "../Globals/Settings.h"

#include "../Helpers/Misc.h"

// ********************************************************************************
// Initialize all Controller NPlugins that where defined earlier
// and initialize the function call pointer into the CNPlugin array
// ********************************************************************************

// Uncrustify must not be used on macros, so turn it off.
// *INDENT-OFF*
#define ADDNPLUGIN(NNN, ID) if (addNPlugin(ID, x)) { NPlugin_ptr[x++] = &NPlugin_##NNN; }
// Uncrustify must not be used on macros, but we're now done, so turn Uncrustify on again.
// *INDENT-ON*

void NPluginInit()
{
  #ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  #endif
//  ProtocolIndex_to_NPlugin_id[NPLUGIN_MAX] = INVALID_N_PLUGIN_ID;
  uint8_t x;

  // Clear pointer table for all plugins
  for (x = 0; x < NPLUGIN_MAX; x++)
  {
    NPlugin_ptr[x] = nullptr;
    NPlugin_id[x]  = INVALID_N_PLUGIN_ID;
//    ProtocolIndex_to_NPlugin_id[x] = INVALID_N_PLUGIN_ID;
    // Do not initialize NPlugin_id_to_ProtocolIndex[x] to an invalid value. (it is map)
  }

  x = 0;

#ifdef USES_N001
  ADDNPLUGIN(001, 1)
#endif

#ifdef USES_N002
  ADDNPLUGIN(002, 2)
#endif

#ifdef USES_N003
  ADDNPLUGIN(003, 3)
#endif

#ifdef USES_N004
  ADDNPLUGIN(004, 4)
#endif

#ifdef USES_N005
  ADDNPLUGIN(005, 5)
#endif

#ifdef USES_N006
  ADDNPLUGIN(006, 6)
#endif

#ifdef USES_N007
  ADDNPLUGIN(007, 7)
#endif

#ifdef USES_N008
  ADDNPLUGIN(008, 8)
#endif

#ifdef USES_N009
  ADDNPLUGIN(009, 9)
#endif

#ifdef USES_N010
  ADDNPLUGIN(010, 10)
#endif

#ifdef USES_N011
  ADDNPLUGIN(011, 11)
#endif

#ifdef USES_N012
  ADDNPLUGIN(012, 12)
#endif

#ifdef USES_N013
  ADDNPLUGIN(013, 13)
#endif

#ifdef USES_N014
  ADDNPLUGIN(014, 14)
#endif

#ifdef USES_N015
  ADDNPLUGIN(015, 15)
#endif

#ifdef USES_N016
  ADDNPLUGIN(016, 16)
#endif

#ifdef USES_N017
  ADDNPLUGIN(017, 17)
#endif

#ifdef USES_N018
  ADDNPLUGIN(018, 18)
#endif

#ifdef USES_N019
  ADDNPLUGIN(019, 19)
#endif

#ifdef USES_N020
  ADDNPLUGIN(020, 20)
#endif

#ifdef USES_N021
  ADDNPLUGIN(021, 21)
#endif

#ifdef USES_N022
  ADDNPLUGIN(022, 22)
#endif

#ifdef USES_N023
  ADDNPLUGIN(023, 23)
#endif

#ifdef USES_N024
  ADDNPLUGIN(024, 24)
#endif

#ifdef USES_N025
  ADDNPLUGIN(025, 25)
#endif

#ifdef USES_N026
  ADDNPLUGIN(026, 26)
#endif

#ifdef USES_N027
  ADDNPLUGIN(027, 27)
#endif

#ifdef USES_N028
  ADDNPLUGIN(028, 28)
#endif

#ifdef USES_N029
  ADDNPLUGIN(029, 29)
#endif

#ifdef USES_N030
  ADDNPLUGIN(030, 30)
#endif

#ifdef USES_N031
  ADDNPLUGIN(031, 31)
#endif

#ifdef USES_N032
  ADDNPLUGIN(032, 32)
#endif

#ifdef USES_N033
  ADDNPLUGIN(033, 33)
#endif

#ifdef USES_N034
  ADDNPLUGIN(034, 34)
#endif

#ifdef USES_N035
  ADDNPLUGIN(035, 35)
#endif

#ifdef USES_N036
  ADDNPLUGIN(036, 36)
#endif

#ifdef USES_N037
  ADDNPLUGIN(037, 37)
#endif

#ifdef USES_N038
  ADDNPLUGIN(038, 38)
#endif

#ifdef USES_N039
  ADDNPLUGIN(039, 39)
#endif

#ifdef USES_N040
  ADDNPLUGIN(040, 40)
#endif

#ifdef USES_N041
  ADDNPLUGIN(041, 41)
#endif

#ifdef USES_N042
  ADDNPLUGIN(042, 42)
#endif

#ifdef USES_N043
  ADDNPLUGIN(043, 43)
#endif

#ifdef USES_N044
  ADDNPLUGIN(044, 44)
#endif

#ifdef USES_N045
  ADDNPLUGIN(045, 45)
#endif

#ifdef USES_N046
  ADDNPLUGIN(046, 46)
#endif

#ifdef USES_N047
  ADDNPLUGIN(047, 47)
#endif

#ifdef USES_N048
  ADDNPLUGIN(048, 48)
#endif

#ifdef USES_N049
  ADDNPLUGIN(049, 49)
#endif

#ifdef USES_N050
  ADDNPLUGIN(050, 50)
#endif

#ifdef USES_N051
  ADDNPLUGIN(051, 51)
#endif

#ifdef USES_N052
  ADDNPLUGIN(052, 52)
#endif

#ifdef USES_N053
  ADDNPLUGIN(053, 53)
#endif

#ifdef USES_N054
  ADDNPLUGIN(054, 54)
#endif

#ifdef USES_N055
  ADDNPLUGIN(055, 55)
#endif

#ifdef USES_N056
  ADDNPLUGIN(056, 56)
#endif

#ifdef USES_N057
  ADDNPLUGIN(057, 57)
#endif

#ifdef USES_N058
  ADDNPLUGIN(058, 58)
#endif

#ifdef USES_N059
  ADDNPLUGIN(059, 59)
#endif

#ifdef USES_N060
  ADDNPLUGIN(060, 60)
#endif

#ifdef USES_N061
  ADDNPLUGIN(061, 61)
#endif

#ifdef USES_N062
  ADDNPLUGIN(062, 62)
#endif

#ifdef USES_N063
  ADDNPLUGIN(063, 63)
#endif

#ifdef USES_N064
  ADDNPLUGIN(064, 64)
#endif

#ifdef USES_N065
  ADDNPLUGIN(065, 65)
#endif

#ifdef USES_N066
  ADDNPLUGIN(066, 66)
#endif

#ifdef USES_N067
  ADDNPLUGIN(067, 67)
#endif

#ifdef USES_N068
  ADDNPLUGIN(068, 68)
#endif

#ifdef USES_N069
  ADDNPLUGIN(069, 69)
#endif

#ifdef USES_N070
  ADDNPLUGIN(070, 70)
#endif

#ifdef USES_N071
  ADDNPLUGIN(071, 71)
#endif

#ifdef USES_N072
  ADDNPLUGIN(072, 72)
#endif

#ifdef USES_N073
  ADDNPLUGIN(073, 73)
#endif

#ifdef USES_N074
  ADDNPLUGIN(074, 74)
#endif

#ifdef USES_N075
  ADDNPLUGIN(075, 75)
#endif

#ifdef USES_N076
  ADDNPLUGIN(076, 76)
#endif

#ifdef USES_N077
  ADDNPLUGIN(077, 77)
#endif

#ifdef USES_N078
  ADDNPLUGIN(078, 78)
#endif

#ifdef USES_N079
  ADDNPLUGIN(079, 79)
#endif

#ifdef USES_N080
  ADDNPLUGIN(080, 80)
#endif

#ifdef USES_N081
  ADDNPLUGIN(081, 81)
#endif

#ifdef USES_N082
  ADDNPLUGIN(082, 82)
#endif

#ifdef USES_N083
  ADDNPLUGIN(083, 83)
#endif

#ifdef USES_N084
  ADDNPLUGIN(084, 84)
#endif

#ifdef USES_N085
  ADDNPLUGIN(085, 85)
#endif

#ifdef USES_N086
  ADDNPLUGIN(086, 86)
#endif

#ifdef USES_N087
  ADDNPLUGIN(087, 87)
#endif

#ifdef USES_N088
  ADDNPLUGIN(088, 88)
#endif

#ifdef USES_N089
  ADDNPLUGIN(089, 89)
#endif

#ifdef USES_N090
  ADDNPLUGIN(090, 90)
#endif

#ifdef USES_N091
  ADDNPLUGIN(091, 91)
#endif

#ifdef USES_N092
  ADDNPLUGIN(092, 92)
#endif

#ifdef USES_N093
  ADDNPLUGIN(093, 93)
#endif

#ifdef USES_N094
  ADDNPLUGIN(094, 94)
#endif

#ifdef USES_N095
  ADDNPLUGIN(095, 95)
#endif

#ifdef USES_N096
  ADDNPLUGIN(096, 96)
#endif

#ifdef USES_N097
  ADDNPLUGIN(097, 97)
#endif

#ifdef USES_N098
  ADDNPLUGIN(098, 98)
#endif

#ifdef USES_N099
  ADDNPLUGIN(099, 99)
#endif

#ifdef USES_N100
  ADDNPLUGIN(100, 100)
#endif

#ifdef USES_N101
  ADDNPLUGIN(101, 101)
#endif

#ifdef USES_N102
  ADDNPLUGIN(102, 102)
#endif

#ifdef USES_N103
  ADDNPLUGIN(103, 103)
#endif

#ifdef USES_N104
  ADDNPLUGIN(104, 104)
#endif

#ifdef USES_N105
  ADDNPLUGIN(105, 105)
#endif

#ifdef USES_N106
  ADDNPLUGIN(106, 106)
#endif

#ifdef USES_N107
  ADDNPLUGIN(107, 107)
#endif

#ifdef USES_N108
  ADDNPLUGIN(108, 108)
#endif

#ifdef USES_N109
  ADDNPLUGIN(109, 109)
#endif

#ifdef USES_N110
  ADDNPLUGIN(110, 110)
#endif

#ifdef USES_N111
  ADDNPLUGIN(111, 111)
#endif

#ifdef USES_N112
  ADDNPLUGIN(112, 112)
#endif

#ifdef USES_N113
  ADDNPLUGIN(113, 113)
#endif

#ifdef USES_N114
  ADDNPLUGIN(114, 114)
#endif

#ifdef USES_N115
  ADDNPLUGIN(115, 115)
#endif

#ifdef USES_N116
  ADDNPLUGIN(116, 116)
#endif

#ifdef USES_N117
  ADDNPLUGIN(117, 117)
#endif

#ifdef USES_N118
  ADDNPLUGIN(118, 118)
#endif

#ifdef USES_N119
  ADDNPLUGIN(119, 119)
#endif

#ifdef USES_N120
  ADDNPLUGIN(120, 120)
#endif

#ifdef USES_N121
  ADDNPLUGIN(121, 121)
#endif

#ifdef USES_N122
  ADDNPLUGIN(122, 122)
#endif

#ifdef USES_N123
  ADDNPLUGIN(123, 123)
#endif

#ifdef USES_N124
  ADDNPLUGIN(124, 124)
#endif

#ifdef USES_N125
  ADDNPLUGIN(125, 125)
#endif

#ifdef USES_N126
  ADDNPLUGIN(126, 126)
#endif

#ifdef USES_N127
  ADDNPLUGIN(127, 127)
#endif

#ifdef USES_N128
  ADDNPLUGIN(128, 128)
#endif

#ifdef USES_N129
  ADDNPLUGIN(129, 129)
#endif

#ifdef USES_N130
  ADDNPLUGIN(130, 130)
#endif

#ifdef USES_N131
  ADDNPLUGIN(131, 131)
#endif

#ifdef USES_N132
  ADDNPLUGIN(132, 132)
#endif

#ifdef USES_N133
  ADDNPLUGIN(133, 133)
#endif

#ifdef USES_N134
  ADDNPLUGIN(134, 134)
#endif

#ifdef USES_N135
  ADDNPLUGIN(135, 135)
#endif

#ifdef USES_N136
  ADDNPLUGIN(136, 136)
#endif

#ifdef USES_N137
  ADDNPLUGIN(137, 137)
#endif

#ifdef USES_N138
  ADDNPLUGIN(138, 138)
#endif

#ifdef USES_N139
  ADDNPLUGIN(139, 139)
#endif

#ifdef USES_N140
  ADDNPLUGIN(140, 140)
#endif

#ifdef USES_N141
  ADDNPLUGIN(141, 141)
#endif

#ifdef USES_N142
  ADDNPLUGIN(142, 142)
#endif

#ifdef USES_N143
  ADDNPLUGIN(143, 143)
#endif

#ifdef USES_N144
  ADDNPLUGIN(144, 144)
#endif

#ifdef USES_N145
  ADDNPLUGIN(145, 145)
#endif

#ifdef USES_N146
  ADDNPLUGIN(146, 146)
#endif

#ifdef USES_N147
  ADDNPLUGIN(147, 147)
#endif

#ifdef USES_N148
  ADDNPLUGIN(148, 148)
#endif

#ifdef USES_N149
  ADDNPLUGIN(149, 149)
#endif

#ifdef USES_N150
  ADDNPLUGIN(150, 150)
#endif

#ifdef USES_N151
  ADDNPLUGIN(151, 151)
#endif

#ifdef USES_N152
  ADDNPLUGIN(152, 152)
#endif

#ifdef USES_N153
  ADDNPLUGIN(153, 153)
#endif

#ifdef USES_N154
  ADDNPLUGIN(154, 154)
#endif

#ifdef USES_N155
  ADDNPLUGIN(155, 155)
#endif

#ifdef USES_N156
  ADDNPLUGIN(156, 156)
#endif

#ifdef USES_N157
  ADDNPLUGIN(157, 157)
#endif

#ifdef USES_N158
  ADDNPLUGIN(158, 158)
#endif

#ifdef USES_N159
  ADDNPLUGIN(159, 159)
#endif

#ifdef USES_N160
  ADDNPLUGIN(160, 160)
#endif

#ifdef USES_N161
  ADDNPLUGIN(161, 161)
#endif

#ifdef USES_N162
  ADDNPLUGIN(162, 162)
#endif

#ifdef USES_N163
  ADDNPLUGIN(163, 163)
#endif

#ifdef USES_N164
  ADDNPLUGIN(164, 164)
#endif

#ifdef USES_N165
  ADDNPLUGIN(165, 165)
#endif

#ifdef USES_N166
  ADDNPLUGIN(166, 166)
#endif

#ifdef USES_N167
  ADDNPLUGIN(167, 167)
#endif

#ifdef USES_N168
  ADDNPLUGIN(168, 168)
#endif

#ifdef USES_N169
  ADDNPLUGIN(169, 169)
#endif

#ifdef USES_N170
  ADDNPLUGIN(170, 170)
#endif

#ifdef USES_N171
  ADDNPLUGIN(171, 171)
#endif

#ifdef USES_N172
  ADDNPLUGIN(172, 172)
#endif

#ifdef USES_N173
  ADDNPLUGIN(173, 173)
#endif

#ifdef USES_N174
  ADDNPLUGIN(174, 174)
#endif

#ifdef USES_N175
  ADDNPLUGIN(175, 175)
#endif

#ifdef USES_N176
  ADDNPLUGIN(176, 176)
#endif

#ifdef USES_N177
  ADDNPLUGIN(177, 177)
#endif

#ifdef USES_N178
  ADDNPLUGIN(178, 178)
#endif

#ifdef USES_N179
  ADDNPLUGIN(179, 179)
#endif

#ifdef USES_N180
  ADDNPLUGIN(180, 180)
#endif

#ifdef USES_N181
  ADDNPLUGIN(181, 181)
#endif

#ifdef USES_N182
  ADDNPLUGIN(182, 182)
#endif

#ifdef USES_N183
  ADDNPLUGIN(183, 183)
#endif

#ifdef USES_N184
  ADDNPLUGIN(184, 184)
#endif

#ifdef USES_N185
  ADDNPLUGIN(185, 185)
#endif

#ifdef USES_N186
  ADDNPLUGIN(186, 186)
#endif

#ifdef USES_N187
  ADDNPLUGIN(187, 187)
#endif

#ifdef USES_N188
  ADDNPLUGIN(188, 188)
#endif

#ifdef USES_N189
  ADDNPLUGIN(189, 189)
#endif

#ifdef USES_N190
  ADDNPLUGIN(190, 190)
#endif

#ifdef USES_N191
  ADDNPLUGIN(191, 191)
#endif

#ifdef USES_N192
  ADDNPLUGIN(192, 192)
#endif

#ifdef USES_N193
  ADDNPLUGIN(193, 193)
#endif

#ifdef USES_N194
  ADDNPLUGIN(194, 194)
#endif

#ifdef USES_N195
  ADDNPLUGIN(195, 195)
#endif

#ifdef USES_N196
  ADDNPLUGIN(196, 196)
#endif

#ifdef USES_N197
  ADDNPLUGIN(197, 197)
#endif

#ifdef USES_N198
  ADDNPLUGIN(198, 198)
#endif

#ifdef USES_N199
  ADDNPLUGIN(199, 199)
#endif

#ifdef USES_N200
  ADDNPLUGIN(200, 200)
#endif

#ifdef USES_N201
  ADDNPLUGIN(201, 201)
#endif

#ifdef USES_N202
  ADDNPLUGIN(202, 202)
#endif

#ifdef USES_N203
  ADDNPLUGIN(203, 203)
#endif

#ifdef USES_N204
  ADDNPLUGIN(204, 204)
#endif

#ifdef USES_N205
  ADDNPLUGIN(205, 205)
#endif

#ifdef USES_N206
  ADDNPLUGIN(206, 206)
#endif

#ifdef USES_N207
  ADDNPLUGIN(207, 207)
#endif

#ifdef USES_N208
  ADDNPLUGIN(208, 208)
#endif

#ifdef USES_N209
  ADDNPLUGIN(209, 209)
#endif

#ifdef USES_N210
  ADDNPLUGIN(210, 210)
#endif

#ifdef USES_N211
  ADDNPLUGIN(211, 211)
#endif

#ifdef USES_N212
  ADDNPLUGIN(212, 212)
#endif

#ifdef USES_N213
  ADDNPLUGIN(213, 213)
#endif

#ifdef USES_N214
  ADDNPLUGIN(214, 214)
#endif

#ifdef USES_N215
  ADDNPLUGIN(215, 215)
#endif

#ifdef USES_N216
  ADDNPLUGIN(216, 216)
#endif

#ifdef USES_N217
  ADDNPLUGIN(217, 217)
#endif

#ifdef USES_N218
  ADDNPLUGIN(218, 218)
#endif

#ifdef USES_N219
  ADDNPLUGIN(219, 219)
#endif

#ifdef USES_N220
  ADDNPLUGIN(220, 220)
#endif

#ifdef USES_N221
  ADDNPLUGIN(221, 221)
#endif

#ifdef USES_N222
  ADDNPLUGIN(222, 222)
#endif

#ifdef USES_N223
  ADDNPLUGIN(223, 223)
#endif

#ifdef USES_N224
  ADDNPLUGIN(224, 224)
#endif

#ifdef USES_N225
  ADDNPLUGIN(225, 225)
#endif

#ifdef USES_N226
  ADDNPLUGIN(226, 226)
#endif

#ifdef USES_N227
  ADDNPLUGIN(227, 227)
#endif

#ifdef USES_N228
  ADDNPLUGIN(228, 228)
#endif

#ifdef USES_N229
  ADDNPLUGIN(229, 229)
#endif

#ifdef USES_N230
  ADDNPLUGIN(230, 230)
#endif

#ifdef USES_N231
  ADDNPLUGIN(231, 231)
#endif

#ifdef USES_N232
  ADDNPLUGIN(232, 232)
#endif

#ifdef USES_N233
  ADDNPLUGIN(233, 233)
#endif

#ifdef USES_N234
  ADDNPLUGIN(234, 234)
#endif

#ifdef USES_N235
  ADDNPLUGIN(235, 235)
#endif

#ifdef USES_N236
  ADDNPLUGIN(236, 236)
#endif

#ifdef USES_N237
  ADDNPLUGIN(237, 237)
#endif

#ifdef USES_N238
  ADDNPLUGIN(238, 238)
#endif

#ifdef USES_N239
  ADDNPLUGIN(239, 239)
#endif

#ifdef USES_N240
  ADDNPLUGIN(240, 240)
#endif

#ifdef USES_N241
  ADDNPLUGIN(241, 241)
#endif

#ifdef USES_N242
  ADDNPLUGIN(242, 242)
#endif

#ifdef USES_N243
  ADDNPLUGIN(243, 243)
#endif

#ifdef USES_N244
  ADDNPLUGIN(244, 244)
#endif

#ifdef USES_N245
  ADDNPLUGIN(245, 245)
#endif

#ifdef USES_N246
  ADDNPLUGIN(246, 246)
#endif

#ifdef USES_N247
  ADDNPLUGIN(247, 247)
#endif

#ifdef USES_N248
  ADDNPLUGIN(248, 248)
#endif

#ifdef USES_N249
  ADDNPLUGIN(249, 249)
#endif

#ifdef USES_N250
  ADDNPLUGIN(250, 250)
#endif

#ifdef USES_N251
  ADDNPLUGIN(251, 251)
#endif

#ifdef USES_N252
  ADDNPLUGIN(252, 252)
#endif

#ifdef USES_N253
  ADDNPLUGIN(253, 253)
#endif

#ifdef USES_N254
  ADDNPLUGIN(254, 254)
#endif

#ifdef USES_N255
  ADDNPLUGIN(255, 255)
#endif

// When extending this, search for EXTEND_CONTROLLER_IDS 
// in the code to find all places that need to be updated too.

  #ifndef BUILD_NO_RAM_TRACKER
  logMemUsageAfter(F("ADDNPLUGIN(...)"));
  #endif

  NPluginCall(NPlugin::Function::NPLUGIN_PROTOCOL_ADD, 0);
  #ifndef BUILD_NO_RAM_TRACKER
  logMemUsageAfter(F("NPLUGIN_PROTOCOL_ADD"));
  #endif

}
#endif
#include "../Helpers/AdafruitGFX_helper.h"
#include "../../_Plugin_Helper.h"

#ifdef PLUGIN_USES_ADAFRUITGFX

# include "../Helpers/StringConverter.h"
# include "../Helpers/StringGenerator_Web.h"
# include "../WebServer/Markup_Forms.h"

# if ADAGFX_FONTS_INCLUDED
#  include "../Static/Fonts/Seven_Segment24pt7b.h"
#  include "../Static/Fonts/Seven_Segment18pt7b.h"
#  include "../Static/Fonts/FreeSans9pt7b.h"
#  ifdef ADAGFX_FONTS_EXTRA_5PT_INCLUDED
#   ifdef ADAGFX_FONTS_EXTRA_5PT_TOMTHUMB
#    include <Fonts/TomThumb.h> // Available in Adafruit_GFX library
#   endif // ifdef ADAGFX_FONTS_EXTRA_5PT_TOMTHUMB
#  endif // ifdef ADAGFX_FONTS_EXTRA_5PT_INCLUDED
#  ifdef ADAGFX_FONTS_EXTRA_8PT_INCLUDED
#   ifdef ADAGFX_FONTS_EXTRA_8PT_ANGELINA
#    include "../Static/Fonts/angelina8pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_8PT_ANGELINA
#   ifdef ADAGFX_FONTS_EXTRA_8PT_NOVAMONO
#    include "../Static/Fonts/NovaMono8pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_8PT_NOVAMONO
#   ifdef ADAGFX_FONTS_EXTRA_8PT_UNISPACE
#    include "../Static/Fonts/unispace8pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_8PT_UNISPACE
#   ifdef ADAGFX_FONTS_EXTRA_8PT_UNISPACEITALIC
#    include "../Static/Fonts/unispace_italic8pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_8PT_UNISPACEITALIC
#   ifdef ADAGFX_FONTS_EXTRA_8PT_WHITERABBiT
#    include "../Static/Fonts/whitrabt8pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_8PT_WHITERABBiT
#   ifdef ADAGFX_FONTS_EXTRA_8PT_ROBOTO
#    include "../Static/Fonts/Roboto_Regular8pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_8PT_ROBOTO
#   ifdef ADAGFX_FONTS_EXTRA_8PT_ROBOTOCONDENSED
#    include "../Static/Fonts/RobotoCondensed_Regular8pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_8PT_ROBOTOCONDENSED
#   ifdef ADAGFX_FONTS_EXTRA_8PT_ROBOTOMONO
#    include "../Static/Fonts/RobotoMono_Regular8pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_8PT_ROBOTOMONO
#  endif // ifdef ADAGFX_FONTS_EXTRA_8PT_INCLUDED
#  ifdef ADAGFX_FONTS_EXTRA_12PT_INCLUDED
#   ifdef ADAGFX_FONTS_EXTRA_12PT_ANGELINA
#    include "../Static/Fonts/angelina12pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_12PT_ANGELINA
#   ifdef ADAGFX_FONTS_EXTRA_12PT_NOVAMONO
#    include "../Static/Fonts/NovaMono12pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_12PT_NOVAMONO
#   ifdef ADAGFX_FONTS_EXTRA_12PT_REPETITIONSCROLLiNG
#    include "../Static/Fonts/RepetitionScrolling12pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_12PT_REPETITIONSCROLLiNG
#   ifdef ADAGFX_FONTS_EXTRA_12PT_UNISPACE
#    include "../Static/Fonts/unispace12pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_12PT_UNISPACE
#   ifdef ADAGFX_FONTS_EXTRA_12PT_UNISPACEITALIC
#    include "../Static/Fonts/unispace_italic12pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_12PT_UNISPACEITALIC
#   ifdef ADAGFX_FONTS_EXTRA_12PT_WHITERABBiT
#    include "../Static/Fonts/whitrabt12pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_12PT_WHITERABBiT
#   ifdef ADAGFX_FONTS_EXTRA_12PT_ROBOTO
#    include "../Static/Fonts/Roboto_Regular12pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_12PT_ROBOTO
#   ifdef ADAGFX_FONTS_EXTRA_12PT_ROBOTOCONDENSED
#    include "../Static/Fonts/RobotoCondensed_Regular12pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_12PT_ROBOTOCONDENSED
#   ifdef ADAGFX_FONTS_EXTRA_12PT_ROBOTOMONO
#    include "../Static/Fonts/RobotoMono_Regular12pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_12PT_ROBOTOMONO
#  endif // ifdef ADAGFX_FONTS_EXTRA_12PT_INCLUDED
#  ifdef ADAGFX_FONTS_EXTRA_16PT_INCLUDED
#   ifdef ADAGFX_FONTS_EXTRA_16PT_AMERIKASANS
#    include "../Static/Fonts/AmerikaSans16pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_16PT_AMERIKASANS
#   ifdef ADAGFX_FONTS_EXTRA_16PT_WHITERABBiT
#    include "../Static/Fonts/whitrabt16pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_16PT_WHITERABBiT
#   ifdef ADAGFX_FONTS_EXTRA_16PT_ROBOTO
#    include "../Static/Fonts/Roboto_Regular16pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_16PT_ROBOTO
#   ifdef ADAGFX_FONTS_EXTRA_16PT_ROBOTOCONDENSED
#    include "../Static/Fonts/RobotoCondensed_Regular16pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_16PT_ROBOTOCONDENSED
#   ifdef ADAGFX_FONTS_EXTRA_16PT_ROBOTOMONO
#    include "../Static/Fonts/RobotoMono_Regular16pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_16PT_ROBOTOMONO
#  endif // ifdef ADAGFX_FONTS_EXTRA_16PT_INCLUDED
#  ifdef ADAGFX_FONTS_EXTRA_18PT_INCLUDED
#   ifdef ADAGFX_FONTS_EXTRA_18PT_WHITERABBiT
#    include "../Static/Fonts/whitrabt18pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_18PT_WHITERABBiT
#   ifdef ADAGFX_FONTS_EXTRA_18PT_SEVENSEG_B
#    include "../Static/Fonts/7segment18pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_18PT_SEVENSEG_B
#   ifdef ADAGFX_FONTS_EXTRA_18PT_LCD14COND
#    include "../Static/Fonts/LCD14cond18pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_18PT_LCD14COND
#  endif // ifdef ADAGFX_FONTS_EXTRA_18PT_INCLUDED
#  ifdef ADAGFX_FONTS_EXTRA_20PT_INCLUDED
#   ifdef ADAGFX_FONTS_EXTRA_20PT_WHITERABBiT
#    include "../Static/Fonts/whitrabt20pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_20PT_WHITERABBiT
#  endif // ifdef ADAGFX_FONTS_EXTRA_20PT_INCLUDED
#  ifdef ADAGFX_FONTS_EXTRA_24PT_INCLUDED
#   ifdef ADAGFX_FONTS_EXTRA_24PT_SEVENSEG_B
#    include "../Static/Fonts/7segment24pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_24PT_SEVENSEG_B
#   ifdef ADAGFX_FONTS_EXTRA_24PT_LCD14COND
#    include "../Static/Fonts/LCD14cond24pt7b.h"
#   endif // ifdef ADAGFX_FONTS_EXTRA_24PT_LCD14COND
#  endif // ifdef ADAGFX_FONTS_EXTRA_24PT_INCLUDED
# endif  // if ADAGFX_FONTS_INCLUDED (this should be the closing #endif)

/* Marker: C) Keep the last #endif, above, and insert an #ifdef/#ifdef/#include/#endif/#endif to conditinally include the specific font .h
   file  *DON'T REMOVE* */

# if FEATURE_SD && defined(ADAGFX_ENABLE_BMP_DISPLAY)
#  include <SD.h>
# endif // if FEATURE_SD && defined(ADAGFX_ENABLE_BMP_DISPLAY)

/**
   ------------------------------------------
   Adding a custom font to the list of fonts.
   ------------------------------------------
   (Advanced development topic)

   As we can't have all fonts combined in all builds for .bin-size reasons, each extra font has to be guarded by compile-time defines.
   This does make the readability of the source quite poor, but flexible in what is eventually included.
   For historic reasons, there are 2 types of compile-time defines, some that are either just defined, or not,
   and some that should be defined as 0 for Off/Disabled, and non-0 (usually 1, sometimes 'true') for On/Enabled. This of course adds to the
   confusion.

   A few sources for finding truetype (ttf) fonts:
   https://www.fonts.google.com
   https://www.1001freefonts.com
   https://www.fontsquirrel.com
   Most usable for displaying text and numeric data in a structured way is to use a monospaced font, where all characters use the same
   width. When only displaying numeric data, some proportionally spaced fonts have monospaced digits (same width), but certainly not all
   fonts do!
   When sharing your font in the ESPEasy repository, please select a font that's free to use, as we don't have funding for commercial fonts
   or copyrighted fonts available! Please share the link to the font that was used, so the legal stuff can be validated.
   If you want to use a non-free or licensed font, then please don't share it in the ESPEasy repository, to avoid legal issues.

   For this explanation I'll use a 90pt font named 7segment90pt7b.
   The desired font can be generated from https://rop.nl/truetype2gfx/ where you can upload a .ttf font file, select the size, 'preview' the
   font (mind you, the site shows the .ttf font, not the generated bitmap font!), and generate a .h file using the Get GFX Font File button.
   The generated .h file should be copied to the src/src/Static/Fonts folder of the repository.

   The steps are numbered a) to g)

   a)
   We start by adding the defines to AdafruitGFX_helper.h

   To add a 90pt font, find 'Marker: A)' to insert #define ADAGFX_FONTS_EXTRA_90PT_INCLUDED (above the marker) to guard this group of fonts.

   b)
   At 'Marker: B)' add #define ADAGFX_FONTS_EXTRA_90PT_SEVENSEG_B (above the marker) to have a way to include or exclude this new font.
   The last part of the name should best match the name of the font

   c)
   Now we continue in AdafruitGFX_helper.cpp

   Just above 'Marker: C)' is the last #endif of a large #ifdef/#ifdef/#include/#endif/#endif list.
   Just before that last #endif, a new check should be added, like this: (example names used, the name for the include-file should be
   adjusted too):

 #  ifdef ADAGFX_FONTS_EXTRA_90PT_INCLUDED
 #   ifdef ADAGFX_FONTS_EXTRA_90PT_SEVENSEG_B
 #    include "../Static/Fonts/7segment90pt7b.h"
 #   endif // ifdef ADAGFX_FONTS_EXTRA_90PT_SEVENSEG_B
 #  endif // ifdef ADAGFX_FONTS_EXTRA_90PT_INCLUDED

   This filename has to be copied exactly as it is on disk, as the builds are created mostly on Linux, that uses a case-sensitive file
   system.

   d)
   Next, the include-file generated before needs a few adjustments to make it work with the ESPEasy code:
   In that font .h file we'll insert, before the first line, a single-include check:

 #ifndef FONTS_7SEGMENT90PT7B_H
 #define FONTS_7SEGMENT90PT7B_H

   and at the end of that file:

 #endif // ifndef FONTS_7SEGMENT90PT7B_H

   The name of the #define should match the filename, any periods, slashes and dashes replaced by underscores, and all uppercase, by
   convention.

   e)
   Now we'll reteurn to AdafruitGFX_helper.cpp, to extend the list of font-names:
   That font-names list ends just above 'Marker E)', with an empty set of quotes and a semicolon: "";
   Just before that line, include this conditional code:

 #  ifdef ADAGFX_FONTS_EXTRA_90PT_INCLUDED
 #   ifdef ADAGFX_FONTS_EXTRA_90PT_SEVENSEG_B
   "sevenseg90b|"
 #   endif // ifdef ADAGFX_FONTS_EXTRA_90PT_SEVENSEG_B
 #  endif // ifdef ADAGFX_FONTS_EXTRA_90PT_INCLUDED

   assigning the name sevenseg90b to the font. This name should match the fontname and probably the point size,
   but _must_ be all lowercase characters! (required!).
   The name-separator character | must also be added so the name can be recognized as a unique name in the list.

   f)
   Next we have to add the font data to the font-list array.
   At the end of that array, above 'Marker F)', we'll have to add the address of the GFXFont from the new font .h file (select your
      font.h file, of course) and parameters for the font:

 # ifdef ADAGFX_FONTS_EXTRA_90PT_INCLUDED
 #  ifdef ADAGFX_FONTS_EXTRA_90PT_SEVENSEG_B
   { &_7segment90pt7b,               96,              120, 122,  false, 101u },
 #  endif // ifdef ADAGFX_FONTS_EXTRA_90PT_SEVENSEG_B
 # endif  // ifdef ADAGFX_FONTS_EXTRA_90PT_INCLUDED

   The name of the GFXFont definition can be found near the end of the font.h file, and should be autocompleted by VSCode Intellisense.

   The numbers are:
   - width (usually a few pixels wider than the font-size)
   - height (usually the font-size multiplied by ~1.4)
   - offset (line-height offset, some fonts need 0, for most fonts it should be the font-height + a few pixels for the line-spacing)
   and a boolean that's true for proportional spacing and false for monospaced fonts.
   These numbers are an approximation, and have to be tried on a real display, by trial and error.
   The last number, 101u in the example, is the font-id. That has to be unique (check the rest of the list), used 101 as the first
   user-custom font.
   The exisiting font-ids should NOT be changed, as they are stored in plugin settings as the default font to use, and would invalidate
   existing configurations if modified!

   g)
   Next step is to compile a build with this code enabled. Don't compile for ESP8266, as then all extra fonts are disabled (not going to fit
   in the binary).
   An ESP32 MAX build is a good candidate to compile, as that includes all plugins, and most fonts, though these added custom fonts have to
   be manually enabled.
   Also, a Custom build can be built, where you define in a Custom.h file, copied from Custom-sample.h, and enable one or more of USES_P095,
   USES_P096, USES_P116, USES_P131, USES_P141 and USES_P165 to have the AdafruitGFX_helper auto-enabled. (P165 doesn't have much space for
   large fonts though)
   In the Custom.h file, you can also enable the #define for the font-group (ADAGFX_FONTS_EXTRA_90PT_INCLUDED) and optionally the font
   (ADAGFX_FONTS_EXTRA_90PT_SEVENSEG_B), so in the AdafruitGFX_helper.h file they can be disabled by default, and only enabled in the Custom
   build when desired.

 */

/******************************************************************************************
 * get the display text for a 'text print mode' enum value
 *****************************************************************************************/
const __FlashStringHelper* toString(const AdaGFXTextPrintMode& mode) {
  switch (mode) {
    case AdaGFXTextPrintMode::ContinueToNextLine: return F("Continue to next line");
    case AdaGFXTextPrintMode::TruncateExceedingMessage: return F("Truncate exceeding message");
    case AdaGFXTextPrintMode::ClearThenTruncate: return F("Clear then truncate exceeding message");
    case AdaGFXTextPrintMode::TruncateExceedingCentered: return F("Truncate, centered if maxWidth set");
    case AdaGFXTextPrintMode::MAX: break;
  }
  return F("None");
}

/******************************************************************************************
 * get the display text for a color depth enum value
 *****************************************************************************************/
const __FlashStringHelper* toString(const AdaGFXColorDepth& colorDepth) {
  switch (colorDepth) {
    case AdaGFXColorDepth::Monochrome: return F("Monochrome");
    case AdaGFXColorDepth::BlackWhiteRed: return F("Monochrome + 1 color");
    case AdaGFXColorDepth::BlackWhite2Greyscales: return F("Monochrome + 2 grey levels");
    # if ADAGFX_SUPPORT_7COLOR
    case AdaGFXColorDepth::SevenColor: return F("eInk - 7 colors");
    # endif // if ADAGFX_SUPPORT_7COLOR
    # if ADAGFX_SUPPORT_8and16COLOR
    case AdaGFXColorDepth::EightColor: return F("TFT - 8 colors");
    case AdaGFXColorDepth::SixteenColor: return F("TFT - 16 colors");
    # endif // if ADAGFX_SUPPORT_8and16COLOR
    case AdaGFXColorDepth::FullColor: return F("Full color - 65535 colors");
  }
  return F("None");
}

# if ADAGFX_ENABLE_BUTTON_DRAW

/******************************************************************************************
 * get the display text for a button type enum value
 *****************************************************************************************/
const __FlashStringHelper* toString(const Button_type_e button) {
  switch (button) {
    case Button_type_e::None: return F("None");
    case Button_type_e::Square: return F("Square");
    case Button_type_e::Rounded: return F("Rounded");
    case Button_type_e::Circle: return F("Circle");
    case Button_type_e::ArrowLeft: return F("Arrow, left");
    case Button_type_e::ArrowUp: return F("Arrow, up");
    case Button_type_e::ArrowRight: return F("Arrow, right");
    case Button_type_e::ArrowDown: return F("Arrow, down");
  }
  return F("Unsupported!");
}

/******************************************************************************************
 * get the display text for a button layout enum value
 *****************************************************************************************/
const __FlashStringHelper* toString(const Button_layout_e layout) {
  switch (layout) {
    case Button_layout_e::CenterAligned: return F("Centered");
    case Button_layout_e::LeftAligned: return F("Left-aligned");
    case Button_layout_e::TopAligned: return F("Top-aligned");
    case Button_layout_e::RightAligned: return F("Right-aligned");
    case Button_layout_e::BottomAligned: return F("Bottom-aligned");
    case Button_layout_e::LeftTopAligned: return F("Left-Top-aligned");
    case Button_layout_e::RightTopAligned: return F("Right-Top-aligned");
    case Button_layout_e::RightBottomAligned: return F("Right-Bottom-aligned");
    case Button_layout_e::LeftBottomAligned: return F("Left-Bottom-aligned");
    case Button_layout_e::NoCaption: return F("No Caption");
    #  if ADAGFX_ENABLE_BMP_DISPLAY
    case Button_layout_e::Bitmap: return F("Bitmap image");
    #  endif // if ADAGFX_ENABLE_BMP_DISPLAY
    #  if ADAGFX_ENABLE_BUTTON_SLIDER
    case Button_layout_e::Slider: return F("Slide control");
    #  endif // if ADAGFX_ENABLE_BUTTON_SLIDER
  }
  return F("Unsupported!");
}

# endif // if ADAGFX_ENABLE_BUTTON_DRAW

/*****************************************************************************************
 * Show a selector for all available 'Text print mode' options, for use in PLUGIN_WEBFORM_LOAD
 ****************************************************************************************/
void AdaGFXFormTextPrintMode(const __FlashStringHelper *id,
                             uint8_t                    selectedIndex) {
  const __FlashStringHelper *textModes[] = { // Be sure to use all available modes from enum!
    toString(AdaGFXTextPrintMode::ContinueToNextLine),
    toString(AdaGFXTextPrintMode::TruncateExceedingMessage),
    toString(AdaGFXTextPrintMode::ClearThenTruncate),
    toString(AdaGFXTextPrintMode::TruncateExceedingCentered),
  };

  constexpr int count = NR_ELEMENTS(textModes);
  FormSelectorOptions selector(count, textModes);

  selector.default_index = 1;

  selector.addFormSelector(F("Text print Mode"), id, selectedIndex);
}

void AdaGFXFormColorDepth(const __FlashStringHelper *id,
                          uint16_t                   selectedIndex,
                          bool                       enabled) {
  # if ADAGFX_SUPPORT_7COLOR
  #  if ADAGFX_SUPPORT_8and16COLOR
  const int colorDepthCount = 7 + 1;
  #  else // if ADAGFX_SUPPORT_8and16COLOR
  const int colorDepthCount = 5 + 1;
  #  endif // if ADAGFX_SUPPORT_8and16COLOR
  # else // if ADAGFX_SUPPORT_7COLOR
  #  if ADAGFX_SUPPORT_8and16COLOR
  const int colorDepthCount = 6 + 1;
  #  else // if ADAGFX_SUPPORT_8and16COLOR
  const int colorDepthCount = 4 + 1;
  #  endif // if ADAGFX_SUPPORT_8and16COLOR
  # endif // if ADAGFX_SUPPORT_7COLOR
  const __FlashStringHelper *colorDepths[colorDepthCount] = { // Be sure to use all available modes from enum!
    toString(static_cast<AdaGFXColorDepth>(0)),               // include None
    toString(AdaGFXColorDepth::Monochrome),
    toString(AdaGFXColorDepth::BlackWhiteRed),
    toString(AdaGFXColorDepth::BlackWhite2Greyscales),
    # if ADAGFX_SUPPORT_7COLOR
    toString(AdaGFXColorDepth::SevenColor),
    # endif // if ADAGFX_SUPPORT_7COLOR
    # if ADAGFX_SUPPORT_8and16COLOR
    toString(AdaGFXColorDepth::EightColor),
    toString(AdaGFXColorDepth::SixteenColor),
    # endif // if ADAGFX_SUPPORT_8and16COLOR
    toString(AdaGFXColorDepth::FullColor)
  };
  const int colorDepthOptions[colorDepthCount] = {
    0,
    static_cast<int>(AdaGFXColorDepth::Monochrome),
    static_cast<int>(AdaGFXColorDepth::BlackWhiteRed),
    static_cast<int>(AdaGFXColorDepth::BlackWhite2Greyscales),
    # if ADAGFX_SUPPORT_7COLOR
    static_cast<int>(AdaGFXColorDepth::SevenColor),
    # endif // if ADAGFX_SUPPORT_7COLOR
    # if ADAGFX_SUPPORT_8and16COLOR
    static_cast<int>(AdaGFXColorDepth::EightColor),
    static_cast<int>(AdaGFXColorDepth::SixteenColor),
    # endif // if ADAGFX_SUPPORT_8and16COLOR
    static_cast<int>(AdaGFXColorDepth::FullColor)
  };

  addRowLabel_tr_id(F("Display Color-depth"), id);
  FormSelectorOptions selector(colorDepthCount, colorDepths, colorDepthOptions);
  selector.enabled = enabled;
  selector.addSelector(id, selectedIndex);
}

/*****************************************************************************************
 * Show a selector for Rotation options, supported by Adafruit_GFX
 ****************************************************************************************/
void AdaGFXFormRotation(const __FlashStringHelper *id,
                        uint8_t                    selectedIndex) {
  const __FlashStringHelper *rotationOptions[] = { F("Normal"), F("+90&deg;"), F("+180&deg;"), F("+270&deg;") };
  const FormSelectorOptions  selector(NR_ELEMENTS(rotationOptions), rotationOptions);

  selector.addFormSelector(F("Rotation"), id, selectedIndex);
}

/*****************************************************************************************
 * Show a checkbox & note to disable background-fill for text
 ****************************************************************************************/
void AdaGFXFormTextBackgroundFill(const __FlashStringHelper *id,
                                  uint8_t                    selectedIndex) {
  addFormCheckBox(F("Background-fill for text"), id, selectedIndex);
  # ifndef LIMIT_BUILD_SIZE
  addFormNote(F("Fill entire line-height with background color."));
  # endif // ifndef LIMIT_BUILD_SIZE
}

/*****************************************************************************************
 * Show a checkbox & note to enable col/row mode for txp, txz and txtfull subcommands
 ****************************************************************************************/
void AdaGFXFormTextColRowMode(const __FlashStringHelper *id,
                              bool                       selectedState) {
  addFormCheckBox(F("Text Coordinates in col/row"), id, selectedState);
  # ifndef LIMIT_BUILD_SIZE
  addFormNote(F("Unchecked: Coordinates in pixels. Applies only to 'txp', 'txz' and 'txtfull' subcommands."));
  # endif // ifndef LIMIT_BUILD_SIZE
}

/*****************************************************************************************
 * Show a checkbox & note to enable -1 px compatibility mode for txp and txtfull subcommands
 ****************************************************************************************/
void AdaGFXFormOnePixelCompatibilityOption(const __FlashStringHelper *id,
                                           uint8_t                    selectedIndex) {
  addFormCheckBox(F("Use -1px offset for txp &amp; txtfull"), id, selectedIndex);
  # ifndef LIMIT_BUILD_SIZE
  addFormNote(F("This is for compatibility with the original plugin implementation."));
  # endif // ifndef LIMIT_BUILD_SIZE
}

/*****************************************************************************************
 * Show 2 input fields for Foreground and Background color, translated to known color names or hex with # prefix
 ****************************************************************************************/
void AdaGFXFormForeAndBackColors(const __FlashStringHelper *foregroundId,
                                 uint16_t                   foregroundColor,
                                 const __FlashStringHelper *backgroundId,
                                 uint16_t                   backgroundColor,
                                 AdaGFXColorDepth           colorDepth) {
  String color = AdaGFXcolorToString(foregroundColor, colorDepth);

  AdaGFXHtmlColorDepthDataList(F("adagfxFGBGcolors"), colorDepth);
  addRowLabel(F("Foreground color"));
  addTextBox(foregroundId, color, 11, false, false,
             EMPTY_STRING, F("")
             # if FEATURE_TOOLTIPS
             , F("Foreground color")
             # endif // if FEATURE_TOOLTIPS
             , F("adagfxFGBGcolors")
             );
  color = AdaGFXcolorToString(backgroundColor, colorDepth);
  addRowLabel(F("Background color"));
  addTextBox(backgroundId, color, 11, false, false,
             EMPTY_STRING, F("")
             # if FEATURE_TOOLTIPS
             , F("Background color")
             # endif // if FEATURE_TOOLTIPS
             , F("adagfxFGBGcolors")
             );
  # ifndef LIMIT_BUILD_SIZE
  addFormNote(F("Use Color name, '#RGB565' (# + 1..4 hex nibbles) or '#RRGGBB' (# + 6 hex nibbles RGB color)."));
  addFormNote(F("NB: Colors stored as RGB565 value!"));
  # else // ifndef LIMIT_BUILD_SIZE
  addFormNote(F("Use Color name, # + 1..4 hex RGB565 or # + 6 hex nibbles RGB color."));
  # endif // ifndef LIMIT_BUILD_SIZE
}

/*****************************************************************************************
 * Show a pin selector and percentage 1..100 for Backlight settings
 ****************************************************************************************/
void AdaGFXFormBacklight(const __FlashStringHelper *backlightPinId,
                         int8_t                     backlightPin,
                         const __FlashStringHelper *backlightPercentageId,
                         uint16_t                   backlightPercentage) {
  addFormPinSelect(PinSelectPurpose::Generic_output, formatGpioName_output_optional(F("Backlight ")), backlightPinId, backlightPin);

  addFormNumericBox(F("Backlight percentage"), backlightPercentageId, backlightPercentage, 0, 100);
  addUnit(F("0-100%"));
}

/*****************************************************************************************
 * Show pin selector, inverse option and timeout inputs for Displaybutton settings
 ****************************************************************************************/
void AdaGFXFormDisplayButton(const __FlashStringHelper *buttonPinId,
                             int8_t                     buttonPin,
                             const __FlashStringHelper *buttonInverseId,
                             bool                       buttonInverse,
                             const __FlashStringHelper *displayTimeoutId,
                             int                        displayTimeout) {
  addFormPinSelect(PinSelectPurpose::Generic_input, F("Display button"), buttonPinId, buttonPin);

  addFormCheckBox(F("Inversed Logic"), buttonInverseId, buttonInverse);

  addFormNumericBox(F("Display Timeout"), displayTimeoutId, displayTimeout, 0);
  addUnit(F("0 = off"));
}

/*****************************************************************************************
 * Show a numeric input 1..10 for Font scaling setting
 ****************************************************************************************/
void AdaGFXFormFontScaling(const __FlashStringHelper *fontScalingId,
                           uint8_t                    fontScaling,
                           uint8_t                    maxScale) {
  addFormNumericBox(F("Font scaling"), fontScalingId, fontScaling, 1, maxScale);
  String unit = F("1x..");

  unit += maxScale;
  unit += 'x';
  addUnit(unit);
}

/*****************************************************************************************
 * Show a selector for line-spacing setting, supported by Adafruit_GFX
 ****************************************************************************************/
void AdaGFXFormLineSpacing(const __FlashStringHelper *id,
                           uint8_t                    selectedIndex) {
  String lineSpacings[16];

  for (uint8_t i = 0; i < 16; ++i) {
    if (15 == i) {
      # ifndef LIMIT_BUILD_SIZE
      lineSpacings[i] = F("Auto, using font height * scaling");
      # else // ifndef LIMIT_BUILD_SIZE
      lineSpacings[i] = F("Auto");
      # endif // ifndef LIMIT_BUILD_SIZE
    } else {
      lineSpacings[i] = i;
    }
  }
  const FormSelectorOptions selector(16, lineSpacings);
  selector.addFormSelector(F("Linespacing"), id, selectedIndex);
  addUnit(F("px"));
}

/****************************************************************************
 * AdaGFXparseTemplate: Replace variables and adjust unicode special characters to Adafruit font
 ***************************************************************************/
String AdaGFXparseTemplate(const String      & tmpString,
                           const uint8_t       lineSize,
                           AdafruitGFX_helper *gfxHelper) {
  # if ADAGFX_PARSE_SUBCOMMAND

  String result = tmpString;

  if (nullptr != gfxHelper) {
    String trigger = gfxHelper->getTrigger();

    if (!trigger.isEmpty()) {
      int16_t prefixTrigger  = result.indexOf(ADAGFX_PARSE_PREFIX);
      int16_t postfixTrigger = result.indexOf(ADAGFX_PARSE_POSTFIX, prefixTrigger + 1);

      while ((prefixTrigger > -1) && (postfixTrigger > -1) && (postfixTrigger > prefixTrigger)) { // Might be valid
        String subcommand = result.substring(prefixTrigger + ADAGFX_PARSE_POSTFIX_LEN, postfixTrigger);

        if (!subcommand.isEmpty()) {
          const String command = strformat(F("%s,%s"), trigger.c_str(), subcommand.c_str());

          #  ifndef BUILD_NO_DEBUG

          if (loglevelActiveFor(ADAGFX_LOG_LEVEL)) {
            addLogMove(ADAGFX_LOG_LEVEL, concat(F("AdaGFX: inline cmd: "), command));
          }
          #  endif // ifndef BUILD_NO_DEBUG

          if (gfxHelper->processCommand(command)) { // Execute command and remove from result incl. pre/postfix
            result.remove(prefixTrigger, (postfixTrigger - prefixTrigger) + ADAGFX_PARSE_POSTFIX_LEN);
            prefixTrigger  = result.indexOf(ADAGFX_PARSE_PREFIX);
            postfixTrigger = result.indexOf(ADAGFX_PARSE_POSTFIX, prefixTrigger + 1);
          } else { // If the command fails, exit further processing
            prefixTrigger  = -1;
            postfixTrigger = -1;
            #  ifndef BUILD_NO_DEBUG
            addLog(ADAGFX_LOG_LEVEL, F("AdaGFX: inline cmd: unknown"));
            #  endif // ifndef BUILD_NO_DEBUG
          }
        } else {
          prefixTrigger  = -1;
          postfixTrigger = -1;
        }
      }
    }
  }
  result = parseTemplate_padded(result, lineSize);
  # else // if ADAGFX_PARSE_SUBCOMMAND
  String result = parseTemplate_padded(tmpString, lineSize);
  # endif // if ADAGFX_PARSE_SUBCOMMAND

  const char euro[4]       = { 0xe2, 0x82, 0xac, 0 }; // Unicode euro symbol
  const char euro_ascii[2] = { 0xED, 0 };             // Euro symbol
  result.replace(euro, euro_ascii);

  char unicodePrefix = 0xc2;

  if (result.indexOf(unicodePrefix) != -1) {
    const char degree[3]       = { 0xc2, 0xb0, 0 };  // Unicode degree symbol
    const char degree_ascii[2] = { 0xf7, 0 };        // degree symbol
    result.replace(degree, degree_ascii);

    const char pound[3]       = { 0xc2, 0xa3, 0 };   // Unicode pound symbol
    const char pound_ascii[2] = { 0x9C, 0 };         // pound symbol
    result.replace(pound, pound_ascii);

    const char yen[3]       = { 0xc2, 0xa5, 0 };     // Unicode yen symbol
    const char yen_ascii[2] = { 0x9D, 0 };           // yen symbol
    result.replace(yen, yen_ascii);

    const char cent[3]       = { 0xc2, 0xa2, 0 };    // Unicode cent symbol
    const char cent_ascii[2] = { 0x9B, 0 };          // cent symbol
    result.replace(cent, cent_ascii);

    const char mu[3]       = { 0xc2, 0xb5, 0 };      // Unicode mu/micro (µ) symbol
    const char mu_ascii[2] = { 0xe5, 0 };            // mu/micro symbol
    result.replace(mu, mu_ascii);

    const char plusmin[3]       = { 0xc2, 0xb1, 0 }; // Unicode plusminus symbol
    const char plusmin_ascii[2] = { 0xf0, 0 };       // plusminus symbol
    result.replace(plusmin, plusmin_ascii);

    const char laquo[3]       = { 0xc2, 0xab, 0 };   // Unicode left aquo symbol
    const char laquo_ascii[2] = { 0xae, 0 };         // left aquo symbol
    result.replace(laquo, laquo_ascii);

    const char raquo[3]       = { 0xc2, 0xbb, 0 };   // Unicode right aquote symbol
    const char raquo_ascii[2] = { 0xaf, 0 };         // right aquote symbol
    result.replace(raquo, raquo_ascii);

    const char half[3]       = { 0xc2, 0xbd, 0 };    // Unicode half 1/2 symbol
    const char half_ascii[2] = { 0xab, 0 };          // half 1/2 symbol
    result.replace(half, half_ascii);

    const char quart[3]       = { 0xc2, 0xbc, 0 };   // Unicode quart 1/4 symbol
    const char quart_ascii[2] = { 0xac, 0 };         // quart 1/4 symbol
    result.replace(quart, quart_ascii);

    const char sup2[3]       = { 0xc2, 0xb2, 0 };    // Unicode superscript 2 symbol
    const char sup2_ascii[2] = { 0xfc, 0 };          // superscript 2 symbol
    result.replace(sup2, sup2_ascii);

    // Unsupported characters, replace by something useful
    const char sup1[3]       = { 0xc2, 0xb9, 0 };   // Unicode superscript 1 symbol
    const char sup1_ascii[2] = { 0x31, 0 };         // regular 1 (missing from font)
    result.replace(sup1, sup1_ascii);

    const char sup3[3]       = { 0xc2, 0xb3, 0 };   // Unicode superscript 3 symbol
    const char sup3_ascii[2] = { 0x33, 0 };         // regular 3 (missing from font)
    result.replace(sup3, sup3_ascii);

    const char frac34[3]       = { 0xc2, 0xbe, 0 }; // Unicode fraction 3/4 symbol
    const char frac34_ascii[2] = { 0x5c, 0 };       // regular \ (missing from font)
    result.replace(frac34, frac34_ascii);
    delay(0);
  }

  unicodePrefix = 0xc3;

  if (result.indexOf(unicodePrefix) != -1) {
    // See: https://github.com/letscontrolit/ESPEasy/issues/2081

    const char umlautAE_uni[3]   = { 0xc3, 0x84, 0 };  // Unicode Umlaute AE
    const char umlautAE_ascii[2] = { 0x8e, 0 };        // Umlaute A
    result.replace(umlautAE_uni, umlautAE_ascii);

    const char umlaut_ae_uni[3]  = { 0xc3, 0xa4, 0 };  // Unicode Umlaute ae
    const char umlautae_ascii[2] = { 0x84, 0 };        // Umlaute a
    result.replace(umlaut_ae_uni, umlautae_ascii);

    const char umlautOE_uni[3]   = { 0xc3, 0x96, 0 };  // Unicode Umlaute OE
    const char umlautOE_ascii[2] = { 0x99, 0 };        // Umlaute O
    result.replace(umlautOE_uni, umlautOE_ascii);

    const char umlaut_oe_uni[3]  = { 0xc3, 0xb6, 0 };  // Unicode Umlaute oe
    const char umlautoe_ascii[2] = { 0x94, 0 };        // Umlaute o
    result.replace(umlaut_oe_uni, umlautoe_ascii);

    const char umlautUE_uni[3]   = { 0xc3, 0x9c, 0 };  // Unicode Umlaute UE
    const char umlautUE_ascii[2] = { 0x9a, 0 };        // Umlaute U
    result.replace(umlautUE_uni, umlautUE_ascii);

    const char umlaut_ue_uni[3]  = { 0xc3, 0xbc, 0 };  // Unicode Umlaute ue
    const char umlautue_ascii[2] = { 0x81, 0 };        // Umlaute u
    result.replace(umlaut_ue_uni, umlautue_ascii);

    const char divide_uni[3]   = { 0xc3, 0xb7, 0 };    // Unicode divide symbol
    const char divide_ascii[2] = { 0xf5, 0 };          // Divide symbol
    result.replace(divide_uni, divide_ascii);

    const char umlaut_sz_uni[3]   = { 0xc3, 0x9f, 0 }; // Unicode Umlaute sz
    const char umlaut_sz_ascii[2] = { 0xe0, 0 };       // Umlaute B
    result.replace(umlaut_sz_uni, umlaut_sz_ascii);

    // Unsupported characters, replace by something useful
    const char times[3]       = { 0xc3, 0x97, 0 }; // Unicode multiplication symbol
    const char times_ascii[2] = { 0x78, 0 };       // regular x (missing from font)
    result.replace(times, times_ascii);
    delay(0);
  }

  // Handle '{0xNN...}' hex values in template, where NN can be any hex value from 01..FF (practically 20..FF).
  int16_t hexPrefix = 0;
  int16_t hexPostfix;
  const String hexSeparators = F(" ,.:;-");

  while (((hexPrefix = result.indexOf(F("{0x"), hexPrefix)) > -1) &&
         ((hexPostfix = result.indexOf('}', hexPrefix)) > -1)) {
    String replace;

    for (int16_t ci = hexPrefix + 3; ci < hexPostfix - 1; ci += 2) { // Multiple of 2 only
      uint32_t hexValue = hexToUL(result.substring(ci, ci + 2));

      if (hexValue > 0) {
        replace += static_cast<char>(hexValue);
      }

      while (hexSeparators.indexOf(result.substring(ci + 2, ci + 3)) > -1 && ci < hexPostfix) {
        ci++;
      }
    }

    if (!replace.isEmpty()) {
      result.replace(result.substring(hexPrefix, hexPostfix + 1), replace);
    }
  }

  for (uint16_t l = result.length(); l > 0 && isSpace(result[l - 1]); --l) { // Right-trim
    result.remove(l - 1);
  }

  # ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(ADAGFX_LOG_LEVEL)) {
    addLogMove(ADAGFX_LOG_LEVEL, strformat(F("AdaGFX: parse result: '%s'"), result.c_str()));
  }
  # endif // ifndef BUILD_NO_DEBUG
  return result;
}

// AdafruitGFX_helper class methods

/****************************************************************************
 * parameterized constructors
 ***************************************************************************/
AdafruitGFX_helper::AdafruitGFX_helper(Adafruit_GFX              *display,
                                       const String             & trigger,
                                       const uint16_t             res_x,
                                       const uint16_t             res_y,
                                       const AdaGFXColorDepth   & colorDepth,
                                       const AdaGFXTextPrintMode& textPrintMode,
                                       const uint8_t              fontscaling,
                                       const uint16_t             fgcolor,
                                       const uint16_t             bgcolor,
                                       const bool                 useValidation,
                                       const bool                 textBackFill,
                                       const uint8_t              defaultFontId)
  : _display(display), _trigger(trigger), _res_x(res_x), _res_y(res_y), _colorDepth(colorDepth),
  _textPrintMode(textPrintMode), _fontscaling(fontscaling), _fgcolor(fgcolor), _bgcolor(bgcolor),
  _useValidation(useValidation), _textBackFill(textBackFill), _defaultFontId(defaultFontId)
{
  addLog(LOG_LEVEL_INFO, F("AdaGFX_helper: GFX Init."));
}

# if ADAGFX_ENABLE_BMP_DISPLAY
AdafruitGFX_helper::AdafruitGFX_helper(Adafruit_SPITFT           *display,
                                       const String             & trigger,
                                       const uint16_t             res_x,
                                       const uint16_t             res_y,
                                       const AdaGFXColorDepth   & colorDepth,
                                       const AdaGFXTextPrintMode& textPrintMode,
                                       const uint8_t              fontscaling,
                                       const uint16_t             fgcolor,
                                       const uint16_t             bgcolor,
                                       const bool                 useValidation,
                                       const bool                 textBackFill,
                                       const uint8_t              defaultFontId)
  : _tft(display), _trigger(trigger), _res_x(res_x), _res_y(res_y), _colorDepth(colorDepth),
  _textPrintMode(textPrintMode), _fontscaling(fontscaling), _fgcolor(fgcolor), _bgcolor(bgcolor),
  _useValidation(useValidation), _textBackFill(textBackFill), _defaultFontId(defaultFontId)
{
  _display = _tft;
  addLog(LOG_LEVEL_INFO, F("AdaGFX_helper: TFT Init."));
}

# endif // if ADAGFX_ENABLE_BMP_DISPLAY

/****************************************************************************
 * common initialization, called from constructors
 ***************************************************************************/
void AdafruitGFX_helper::initialize() {
  _trigger.toLowerCase(); // store trigger in lowercase
  # ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(ADAGFX_LOG_LEVEL)) {
    addLogMove(ADAGFX_LOG_LEVEL, strformat(F("AdaGFX: Init, x: %d, y: %d, colors: %d, trigger: %s, %s"),
                                           _res_x, _res_y, static_cast<uint16_t>(_colorDepth),
                                           _trigger.c_str(), getFeatures().c_str()));
  }
  # endif // ifndef BUILD_NO_DEBUG

  _display_x = _res_x; // Store initial resolution
  _display_y = _res_y;

  # if ADAGFX_ENABLE_FRAMED_WINDOW
  defineWindow(0, 0, _res_x, _res_y, 0, 0); // Add window 0 at rotation 0
  # endif // if ADAGFX_ENABLE_FRAMED_WINDOW

  if (_fontscaling < 1) { _fontscaling = 1; }

  if (nullptr != _display) {
    # if ADAGFX_FONTS_INCLUDED
    setFontById(_defaultFontId);
    # endif // if ADAGFX_FONTS_INCLUDED
    _display->setTextSize(_fontscaling);
    _display->setTextColor(_fgcolor, _bgcolor); // initialize text colors
    _display->setTextWrap(_textPrintMode == AdaGFXTextPrintMode::ContinueToNextLine);
  }
}

/****************************************************************************
 * Show enabled features of the helper
 ***************************************************************************/
String AdafruitGFX_helper::getFeatures() {
  String log = F("Features:"
                 # if (defined(ADAGFX_USE_ASCIITABLE) && ADAGFX_USE_ASCIITABLE)
                 " asciitable,"
                 # endif // if (defined(ADAGFX_USE_ASCIITABLE) && ADAGFX_USE_ASCIITABLE)
                 # if (defined(ADAGFX_ENABLE_EXTRA_CMDS) && ADAGFX_ENABLE_EXTRA_CMDS)
                 " lm/lmr,"
                 # endif // if (defined(ADAGFX_ENABLE_EXTRA_CMDS) && ADAGFX_ENABLE_EXTRA_CMDS)
                 # if (defined(ADAGFX_ENABLE_BMP_DISPLAY) && ADAGFX_ENABLE_BMP_DISPLAY)
                 " bmp,"
                 # endif // if (defined(ADAGFX_ENABLE_BMP_DISPLAY) && ADAGFX_ENABLE_BMP_DISPLAY)
                 # if (defined(ADAGFX_ENABLE_BUTTON_DRAW) && ADAGFX_ENABLE_BUTTON_DRAW)
                 " btn,"
                 # endif // if (defined(ADAGFX_ENABLE_BUTTON_DRAW) && ADAGFX_ENABLE_BUTTON_DRAW)`
                 # if (defined(ADAGFX_ENABLE_FRAMED_WINDOW) && ADAGFX_ENABLE_FRAMED_WINDOW)
                 " win,"
                 # endif // if (defined(ADAGFX_ENABLE_FRAMED_WINDOW) && ADAGFX_ENABLE_FRAMED_WINDOW)
                 # if (defined(ADAGFX_ENABLE_GET_CONFIG_VALUE) && ADAGFX_ENABLE_GET_CONFIG_VALUE)
                 " getconf,"
                 # endif // if (defined(ADAGFX_ENABLE_GET_CONFIG_VALUE) && ADAGFX_ENABLE_GET_CONFIG_VALUE)
                 );

  if (log.endsWith(F(","))) {
    log.remove(log.length() - 1);
  }
  return log;
}

/****************************************************************************
 * getCursorXY: get the current (text) cursor coordinates, either in pixels or cols/rows, depending on related setting
 ***************************************************************************/
void AdafruitGFX_helper::getCursorXY(int16_t& currentX,
                                     int16_t& currentY) {
  _lastX = _display->getCursorX();
  _lastY = _display->getCursorY();

  if (_columnRowMode && (_lastX != 0)) { _lastX /= _fontwidth; }

  if (_columnRowMode && (_lastY != 0)) { _lastY /= _fontheight; }
  currentX = _lastX;
  currentY = _lastY;
}

/****************************************************************************
 * setTxtfullCompensation: x and/or y values defined here are subtracted from x and y position
 * - set to 1 for x-1/y-1 pixel for P095 and P096
 * - set to 2 for y+1 pixel
 * - set to 3 for x+1 pixel
 ***************************************************************************/
void AdafruitGFX_helper::setTxtfullCompensation(uint8_t compensation) {
  switch (compensation) {
    case 1: // P095
    {
      _x_compensation = 1;
      _y_compensation = 1;
      break;
    }
    case 2:
    {
      _x_compensation = 0;
      _y_compensation = -1;
      break;
    }
    case 3:
    {
      _x_compensation = -1;
      _y_compensation = 0;
      break;
    }
    default:
    {
      _x_compensation = 0;
      _y_compensation = 0;
      break;
    }
  }
}

/****************************************************************************
 * invertDisplay(): Store display-inverted state and proxy to _display
 ***************************************************************************/
void AdafruitGFX_helper::invertDisplay(bool i) {
  _displayInverted = i;
  _display->invertDisplay(_displayInverted);
}

/****************************************************************************
 * processCommand: Parse string to <command>,<subcommand>[,<arguments>...] and execute that command
 ***************************************************************************/
const char adagfx_commands[] PROGMEM =
  "txt|txp|txz|txl|txc|txs|txtfull|clear|rot|tpm|" // 0..9
  # if ADAGFX_USE_ASCIITABLE
  "asciitable|"
  # endif // if ADAGFX_USE_ASCIITABLE
  "font|l|lh|lv|"
  # if ADAGFX_ENABLE_EXTRA_CMDS
  "lm|lmr|"
  # endif // if ADAGFX_ENABLE_EXTRA_CMDS
  "r|rf|c|" // 10..19
  "cf|t|tf|rr|rrf|px|pxh|pxv|"
  # if ADAGFX_ENABLE_BMP_DISPLAY
  "bmp|"
  # endif // if ADAGFX_ENABLE_BMP_DISPLAY
  # if ADAGFX_ENABLE_BUTTON_DRAW
  "btn|"              // 20..29
  # endif // if ADAGFX_ENABLE_BUTTON_DRAW
  # if ADAGFX_ENABLE_FRAMED_WINDOW
  "win|defwin|delwin" // 30..
  # endif // if ADAGFX_ENABLE_FRAMED_WINDOW
;
enum class adagfx_commands_e : int8_t {
  invalid = -1,
  txt     = 0, // 0
  txp,
  txz,
  txl,
  txc,
  txs,
  txtfull,
  clear,
  rot,
  tpm,        // 9
  # if ADAGFX_USE_ASCIITABLE
  asciitable, // 10
  # endif // if ADAGFX_USE_ASCIITABLE
  font,
  l,
  lh,
  lv,
  # if ADAGFX_ENABLE_EXTRA_CMDS
  lm,
  lmr,
  # endif // if ADAGFX_ENABLE_EXTRA_CMDS
  r,
  rf,
  c,  // 19
  cf, // 20
  t,
  tf,
  rr,
  rrf,
  px,
  pxh,
  pxv,
  # if ADAGFX_ENABLE_BMP_DISPLAY
  bmp,
  # endif // if ADAGFX_ENABLE_BMP_DISPLAY
  # if ADAGFX_ENABLE_BUTTON_DRAW
  btn, // 29
  # endif // if ADAGFX_ENABLE_BUTTON_DRAW
  # if ADAGFX_ENABLE_FRAMED_WINDOW
  win, // 30
  defwin,
  delwin,
  # endif // if ADAGFX_ENABLE_FRAMED_WINDOW
};
# if ADAGFX_FONTS_INCLUDED

// *** Don't forget to add the | separator at the end of a (new) font-name! (except for the last one in the list)
const char adagfx_fonts[] PROGMEM =
  "default|sevenseg24|sevenseg18|freesans|"
  #  ifdef ADAGFX_FONTS_EXTRA_5PT_INCLUDED
  #   ifdef ADAGFX_FONTS_EXTRA_5PT_TOMTHUMB
  "tomthumb|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_5PT_TOMTHUMB
  #  endif // ifdef ADAGFX_FONTS_EXTRA_5PT_INCLUDED
  #  ifdef ADAGFX_FONTS_EXTRA_8PT_INCLUDED
  #   ifdef ADAGFX_FONTS_EXTRA_8PT_ANGELINA
  "angelina8prop|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_8PT_ANGELINA
  #   ifdef ADAGFX_FONTS_EXTRA_8PT_NOVAMONO
  "novamono8pt|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_8PT_NOVAMONO
  #   ifdef ADAGFX_FONTS_EXTRA_8PT_UNISPACE
  "unispace8pt|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_8PT_UNISPACE
  #   ifdef ADAGFX_FONTS_EXTRA_8PT_UNISPACEITALIC
  "unispaceitalic8pt|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_8PT_UNISPACEITALIC
  #   ifdef ADAGFX_FONTS_EXTRA_8PT_WHITERABBiT
  "whiterabbit8pt|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_8PT_WHITERABBiT
  #   ifdef ADAGFX_FONTS_EXTRA_8PT_ROBOTO
  "roboto8pt|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_8PT_ROBOTO
  #   ifdef ADAGFX_FONTS_EXTRA_8PT_ROBOTOCONDENSED
  "robotocond8pt|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_8PT_ROBOTOCONDENSED
  #   ifdef ADAGFX_FONTS_EXTRA_8PT_ROBOTOMONO
  "robotomono8pt|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_8PT_ROBOTOMONO
  #  endif // ifdef ADAGFX_FONTS_EXTRA_8PT_INCLUDED
  #  ifdef ADAGFX_FONTS_EXTRA_12PT_INCLUDED
  #   ifdef ADAGFX_FONTS_EXTRA_12PT_ANGELINA
  "angelina12prop|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_12PT_ANGELINA
  #   ifdef ADAGFX_FONTS_EXTRA_12PT_NOVAMONO
  "novamono12pt|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_12PT_NOVAMONO
  #   ifdef ADAGFX_FONTS_EXTRA_12PT_REPETITIONSCROLLiNG
  "repetitionscrolling12pt|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_12PT_REPETITIONSCROLLiNG
  #   ifdef ADAGFX_FONTS_EXTRA_12PT_UNISPACE
  "unispace12pt|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_12PT_UNISPACE
  #   ifdef ADAGFX_FONTS_EXTRA_12PT_UNISPACEITALIC
  "unispaceitalic12pt|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_12PT_UNISPACEITALIC
  #   ifdef ADAGFX_FONTS_EXTRA_12PT_WHITERABBiT
  "whiterabbit12pt|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_12PT_WHITERABBiT
  #   ifdef ADAGFX_FONTS_EXTRA_12PT_ROBOTO
  "roboto12pt|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_12PT_ROBOTO
  #   ifdef ADAGFX_FONTS_EXTRA_12PT_ROBOTOCONDENSED
  "robotocond12pt|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_12PT_ROBOTOCONDENSED
  #   ifdef ADAGFX_FONTS_EXTRA_12PT_ROBOTOMONO
  "robotomono12pt|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_12PT_ROBOTOMONO
  #  endif // ifdef ADAGFX_FONTS_EXTRA_12PT_INCLUDED
  #  ifdef ADAGFX_FONTS_EXTRA_16PT_INCLUDED
  #   ifdef ADAGFX_FONTS_EXTRA_16PT_AMERIKASANS
  "amerikasans16pt|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_16PT_AMERIKASANS
  #   ifdef ADAGFX_FONTS_EXTRA_16PT_WHITERABBiT
  "whiterabbit16pt|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_16PT_WHITERABBiT
  #   ifdef ADAGFX_FONTS_EXTRA_16PT_ROBOTO
  "roboto16pt|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_16PT_ROBOTO
  #   ifdef ADAGFX_FONTS_EXTRA_16PT_ROBOTOCONDENSED
  "robotocond16pt|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_16PT_ROBOTOCONDENSED
  #   ifdef ADAGFX_FONTS_EXTRA_16PT_ROBOTOMONO
  "robotomono16pt|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_16PT_ROBOTOMONO
  #  endif // ifdef ADAGFX_FONTS_EXTRA_16PT_INCLUDED
  #  ifdef ADAGFX_FONTS_EXTRA_18PT_INCLUDED
  #   ifdef ADAGFX_FONTS_EXTRA_18PT_WHITERABBiT
  "whiterabbit18pt|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_18PT_WHITERABBiT
  #   ifdef ADAGFX_FONTS_EXTRA_18PT_SEVENSEG_B
  "sevenseg18b|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_18PT_SEVENSEG_B
  #   ifdef ADAGFX_FONTS_EXTRA_18PT_LCD14COND
  "lcd14cond18pt|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_18PT_LCD14COND
  #  endif // ifdef ADAGFX_FONTS_EXTRA_18PT_INCLUDED
  #  ifdef ADAGFX_FONTS_EXTRA_20PT_INCLUDED
  #   ifdef ADAGFX_FONTS_EXTRA_20PT_WHITERABBiT
  "whiterabbit20pt|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_20PT_WHITERABBiT
  #  endif // ifdef ADAGFX_FONTS_EXTRA_20PT_INCLUDED
  #  ifdef ADAGFX_FONTS_EXTRA_24PT_INCLUDED
  #   ifdef ADAGFX_FONTS_EXTRA_24PT_SEVENSEG_B
  "sevenseg24b|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_24PT_SEVENSEG_B
  #   ifdef ADAGFX_FONTS_EXTRA_24PT_LCD14COND
  "lcd14cond24pt|"
  #   endif // ifdef ADAGFX_FONTS_EXTRA_24PT_LCD14COND
  #  endif // ifdef ADAGFX_FONTS_EXTRA_24PT_INCLUDED
  "";

/* Marker E) Add the new fontname with #ifdef checks _above_ the line with only: "";  *DON'T REMOVE* */

struct tFontArgs {
  constexpr tFontArgs(const GFXfont *f,
                      uint8_t        width,
                      uint8_t        height,
                      int8_t         offset,
                      bool           proportional,
                      uint8_t        fontId)
    : _f(f), _width(width), _height(height), _offset(offset),
    _proportional(proportional), _fontId(fontId) {}

  const GFXfont *_f;
  uint8_t        _width;
  uint8_t        _height;
  int8_t         _offset;
  bool           _proportional;
  uint8_t        _fontId;
};

/* *INDENT-OFF* */
constexpr tFontArgs fontargs[] =
{
  { nullptr,                        6,                9,   0,   false,  0u },
  { &Seven_Segment24pt7b,           21,               42,  35,  true,   1u },
  { &Seven_Segment18pt7b,           16,               33,  26,  true,   2u },
  { &FreeSans9pt7b,                 10,               16,  12,  false,  3u },
  # ifdef ADAGFX_FONTS_EXTRA_5PT_INCLUDED
  #  ifdef ADAGFX_FONTS_EXTRA_5PT_TOMTHUMB
  { &TomThumb,                      5,                 6,   5,  false,  4u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_5PT_TOMTHUMB
  # endif // ifdef ADAGFX_FONTS_EXTRA_5PT_INCLUDED
  # ifdef ADAGFX_FONTS_EXTRA_8PT_INCLUDED
  #  ifdef ADAGFX_FONTS_EXTRA_8PT_ANGELINA
  { &angelina8pt7b,                 6,                16,  12,  true,   5u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_8PT_ANGELINA
  #  ifdef ADAGFX_FONTS_EXTRA_8PT_NOVAMONO
  { &NovaMono8pt7b,                 9,                16,  12,  false,  6u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_8PT_NOVAMONO
  #  ifdef ADAGFX_FONTS_EXTRA_8PT_UNISPACE
  { &unispace8pt7b,                 13,               24,  20,  false,  7u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_8PT_UNISPACE
  #  ifdef ADAGFX_FONTS_EXTRA_8PT_UNISPACEITALIC
  { &unispace_italic8pt7b,          13,               24,  20,  false,  8u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_8PT_UNISPACEITALIC
  #  ifdef ADAGFX_FONTS_EXTRA_8PT_WHITERABBiT
  { &whitrabt8pt7b,                 10,               16,  12,  false,  9u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_8PT_WHITERABBiT
  #  ifdef ADAGFX_FONTS_EXTRA_8PT_ROBOTO
  { &Roboto_Regular8pt7b,           10,               16,  12,  true,  10u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_8PT_ROBOTO
  #  ifdef ADAGFX_FONTS_EXTRA_8PT_ROBOTOCONDENSED
  { &RobotoCondensed_Regular8pt7b,  9,                16,  12,  true,  11u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_8PT_ROBOTOCONDENSED
  #  ifdef ADAGFX_FONTS_EXTRA_8PT_ROBOTOMONO
  { &RobotoMono_Regular8pt7b,       10,               16,  12,  false, 12u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_8PT_ROBOTOMONO
  # endif  // ifdef ADAGFX_FONTS_EXTRA_8PT_INCLUDED
  # ifdef ADAGFX_FONTS_EXTRA_12PT_INCLUDED
  #  ifdef ADAGFX_FONTS_EXTRA_12PT_ANGELINA
  { &angelina12pt7b,                8,                22,  18,  true,  13u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_12PT_ANGELINA
  #  ifdef ADAGFX_FONTS_EXTRA_12PT_NOVAMONO
  { &NovaMono12pt7b,                13,               26,  22,  false, 14u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_12PT_NOVAMONO
  #  ifdef ADAGFX_FONTS_EXTRA_12PT_REPETITIONSCROLLiNG
  { &RepetitionScrolling12pt7b,     13,               22,  18,  false, 15u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_12PT_REPETITIONSCROLLiNG
  #  ifdef ADAGFX_FONTS_EXTRA_12PT_UNISPACE
  { &unispace12pt7b,                18,               30,  26,  false, 16u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_12PT_UNISPACE
  #  ifdef ADAGFX_FONTS_EXTRA_12PT_UNISPACEITALIC
  { &unispace_italic12pt7b,         18,               30,  26,  false, 17u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_12PT_UNISPACEITALIC
  #  ifdef ADAGFX_FONTS_EXTRA_12PT_WHITERABBiT
  { &whitrabt12pt7b,                13,               20,  16,  false, 18u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_12PT_WHITERABBiT
  #  ifdef ADAGFX_FONTS_EXTRA_12PT_ROBOTO
  { &Roboto_Regular12pt7b,          13,               20,  20,  true,  19u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_12PT_ROBOTO
  #  ifdef ADAGFX_FONTS_EXTRA_12PT_ROBOTOCONDENSED
  { &RobotoCondensed_Regular12pt7b, 13,               20,  20,  true,  20u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_12PT_ROBOTOCONDENSED
  #  ifdef ADAGFX_FONTS_EXTRA_12PT_ROBOTOMONO
  { &RobotoMono_Regular12pt7b,      13,               20,  20,  false, 21u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_12PT_ROBOTOMONO
  # endif  // ifdef ADAGFX_FONTS_EXTRA_12PT_INCLUDED
  # ifdef ADAGFX_FONTS_EXTRA_16PT_INCLUDED
  #  ifdef ADAGFX_FONTS_EXTRA_16PT_AMERIKASANS
  { &AmerikaSans16pt7b,             17,               30,  26,  true,  22u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_16PT_AMERIKASANS
  #  ifdef ADAGFX_FONTS_EXTRA_16PT_WHITERABBiT
  { &whitrabt16pt7b,                18,               26,  22,  false, 23u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_16PT_WHITERABBiT
  #  ifdef ADAGFX_FONTS_EXTRA_16PT_ROBOTO
  { &Roboto_Regular16pt7b,          18,               27,  23,  true,  24u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_16PT_ROBOTO
  #  ifdef ADAGFX_FONTS_EXTRA_16PT_ROBOTOCONDENSED
  { &RobotoCondensed_Regular16pt7b, 18,               27,  23,  true,  25u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_16PT_ROBOTOCONDENSED
  #  ifdef ADAGFX_FONTS_EXTRA_16PT_ROBOTOMONO
  { &RobotoMono_Regular16pt7b,      18,               27,  23,  false, 26u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_16PT_ROBOTOMONO
  # endif  // ifdef ADAGFX_FONTS_EXTRA_16PT_INCLUDED
  # ifdef ADAGFX_FONTS_EXTRA_18PT_INCLUDED
  #  ifdef ADAGFX_FONTS_EXTRA_18PT_WHITERABBiT
  { &whitrabt18pt7b,                21,               30,  26,  false, 27u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_18PT_WHITERABBiT
  #  ifdef ADAGFX_FONTS_EXTRA_18PT_SEVENSEG_B
  { &_7segment18pt7b,               21,               30,  30,  false, 28u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_18PT_SEVENSEG_B
  #  ifdef ADAGFX_FONTS_EXTRA_18PT_LCD14COND
  { &LCD14cond18pt7b,               24,               30,  30,  false, 29u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_18PT_LCD14COND
  # endif // ifdef ADAGFX_FONTS_EXTRA_18PT_INCLUDED
  # ifdef ADAGFX_FONTS_EXTRA_20PT_INCLUDED
  #  ifdef ADAGFX_FONTS_EXTRA_20PT_WHITERABBiT
  { &whitrabt20pt7b,                24,               32,  28,  false, 30u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_20PT_WHITERABBiT
  # endif  // ifdef ADAGFX_FONTS_EXTRA_20PT_INCLUDED
  # ifdef ADAGFX_FONTS_EXTRA_24PT_INCLUDED
  #  ifdef ADAGFX_FONTS_EXTRA_24PT_SEVENSEG_B
  { &_7segment24pt7b,               26,               34,  38,  false, 31u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_24PT_SEVENSEG_B
  #  ifdef ADAGFX_FONTS_EXTRA_24PT_LCD14COND
  { &LCD14cond24pt7b,               26,               34,  38,  false, 32u },
  #  endif // ifdef ADAGFX_FONTS_EXTRA_24PT_LCD14COND
  # endif  // ifdef ADAGFX_FONTS_EXTRA_24PT_INCLUDED
};
/* *INDENT-ON* */
# endif // if ADAGFX_FONTS_INCLUDED

/* Marker F) Add the font data address and parameters to the above array, just above the closing curly brace  *DON'T REMOVE* */

String AdaGFXgetFontName(uint8_t fontId, bool includeFontId) {
  # if ADAGFX_FONTS_INCLUDED
  const uint32_t idx = AdaGFXgetFontIndexForFontId(fontId);
  char   tmp[30]{}; // Longest name so far is 23 + \0
  String fontName(GetTextIndexed(tmp, sizeof(tmp), idx, adagfx_fonts));

  if (includeFontId) {
    fontName = strformat(F("%s (%d)"), tmp, fontargs[idx]._fontId);
  }
  return fontName;
  # else // if ADAGFX_FONTS_INCLUDED
  return EMPTY_STRING;
  # endif // if ADAGFX_FONTS_INCLUDED
}

uint32_t AdaGFXgetFontIndexForFontId(uint8_t fontId) {
  # if ADAGFX_FONTS_INCLUDED
  constexpr uint32_t font_max = NR_ELEMENTS(fontargs);

  for (uint32_t idx = 0; idx < font_max; ++idx) {
    if (fontargs[idx]._fontId == fontId) {
      return idx;
    }
  }
  # endif // if ADAGFX_FONTS_INCLUDED
  return 0;
}

void AdaGFXFormDefaultFont(const __FlashStringHelper *id,
                           uint8_t                    selectedIndex) {
  # if ADAGFX_FONTS_INCLUDED
  constexpr uint32_t font_max = NR_ELEMENTS(fontargs);

  addRowLabel_tr_id(F("Default font"), id);
  addSelector_Head(id);

  char tmp[30]{}; // Longest name so far is 23 + \0

  for (uint32_t idx = 0; idx < font_max; ++idx) {
    const bool selected = (fontargs[idx]._fontId == selectedIndex);
    GetTextIndexed(tmp, sizeof(tmp), idx, adagfx_fonts);
    addSelector_Item(strformat(F("%s (%d)"), tmp, fontargs[idx]._fontId),
                     fontargs[idx]._fontId,
                     selected);
  }
  addSelector_Foot();
  # endif // if ADAGFX_FONTS_INCLUDED
}

# if ADAGFX_FONTS_INCLUDED
void AdafruitGFX_helper::setFontById(uint8_t fontId) {
  constexpr int font_max = NR_ELEMENTS(fontargs);
  const int     font_i   = AdaGFXgetFontIndexForFontId(fontId);

  if ((font_i >= 0) && (font_i < font_max)) {
    _fontId = fontargs[font_i]._fontId;
    _display->setFont(fontargs[font_i]._f);
    calculateTextMetrics(fontargs[font_i]._width,
                         fontargs[font_i]._height,
                         fontargs[font_i]._offset,
                         fontargs[font_i]._proportional);
  }
}

# endif // if ADAGFX_FONTS_INCLUDED

bool AdafruitGFX_helper::processCommand(const String& string) {
  bool success = false;

  if ((nullptr == _display) || _trigger.isEmpty()) { return success; }

  const String cmd        = parseString(string, 1); // lower case
  const String subcommand = parseString(string, 2);

  # if ADAGFX_ENABLE_FRAMED_WINDOW || ADAGFX_ARGUMENT_VALIDATION
  uint16_t res_x = _res_x;
  uint16_t res_y = _res_y;
  # endif // if ADAGFX_ENABLE_FRAMED_WINDOW || ADAGFX_ARGUMENT_VALIDATION
  uint16_t _xo = 0;
  uint16_t _yo = 0;

  # if ADAGFX_ENABLE_FRAMED_WINDOW
  getWindowLimits(res_x, res_y);
  getWindowOffsets(_xo, _yo);
  # endif // if ADAGFX_ENABLE_FRAMED_WINDOW

  if (!(cmd.equals(_trigger) ||
        isAdaGFXTrigger(cmd)) ||
      subcommand.isEmpty()) { return success; } // Only support own trigger, and at least a non=empty subcommand

  std::vector<String>  sParams;
  std::vector<int32_t> nParams;
  uint8_t emptyCount = 0;
  int     argCount   = 0;
  bool    loop       = true;

  while (loop) { // Process all provided arguments
    // 0-offset + 1st and 2nd argument used by trigger/subcommand, don't trim off spaces
    sParams.push_back(parseStringKeepCaseNoTrim(string, argCount + 3));
    nParams.push_back(0);
    validIntFromString(sParams[argCount], nParams[argCount]);

    if (sParams[argCount].isEmpty()) {
      emptyCount++;
    } else {
      emptyCount = 0;                                           // Reset empty counter
    }
    loop = emptyCount < 3 || argCount <= ADAGFX_PARSE_MAX_ARGS; // Keep picking up arguments until we have the last 3 empty

    # ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG_DEV)) {
      addLog(LOG_LEVEL_DEBUG_DEV, strformat(F(":%d %s"), argCount, sParams[argCount].c_str()));
    }
    # endif // ifndef BUILD_NO_DEBUG

    argCount++;
  }
  argCount -= emptyCount; // Not counting the empty arguments

  # ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(ADAGFX_LOG_LEVEL)) {
    addLog(ADAGFX_LOG_LEVEL, strformat(F("AdaGFX: command: %s argCount: %d:%s"), _trigger.c_str(), argCount, string.c_str()));
  }
  # endif // ifndef BUILD_NO_DEBUG

  const int subcommand_i = GetCommandCode(subcommand.c_str(), adagfx_commands);

  if (subcommand_i < 0) { return false; } // Fail fast

  const adagfx_commands_e subcmd = static_cast<adagfx_commands_e>(subcommand_i);
  const bool currentColRowState  = _columnRowMode;

  # if ADAGFX_ARGUMENT_VALIDATION

  // Optimize some coordinate checks, with less than 3 occurrences there is no gain
  const bool invCoord_0_1     = argCount >= 2 && invalidCoordinates(nParams[0], nParams[1]);
  const bool invCoord_2_3     = argCount >= 4 && invalidCoordinates(nParams[2], nParams[3]);
  const bool invCoord_0_2_1_3 = argCount >= 4 && invalidCoordinates(nParams[0] + nParams[2],
                                                                    nParams[1] + nParams[3]);
  # endif // if ADAGFX_ARGUMENT_VALIDATION

  switch (subcmd)
  {
    case adagfx_commands_e::invalid:
      break;
    case adagfx_commands_e::txt:                                    // txt: Print text at last cursor position, ends at next line!
      _display->println(parseStringToEndKeepCaseNoTrim(string, 3)); // Print entire rest of provided line
      success = true;
      break;
    case adagfx_commands_e::txp:                                    // txp: Text position

      if (argCount == 2) {
        # if ADAGFX_ARGUMENT_VALIDATION

        if (!invalidCoordinates(nParams[0], nParams[1], _columnRowMode))
        # endif // if ADAGFX_ARGUMENT_VALIDATION
        {
          if (_columnRowMode) {
            _display->setCursor(nParams[0] * _fontwidth + _xo, nParams[1] * _fontheight + _yo);
          } else {
            _display->setCursor(nParams[0] + _xo - _x_compensation, nParams[1] + _yo - _y_compensation);
          }
        }
        success = true;
      }
      break;
    case adagfx_commands_e::txz: // txz: Text at position

      if (argCount >= 3) {
        # if ADAGFX_ARGUMENT_VALIDATION

        if (!invalidCoordinates(nParams[0], nParams[1], _columnRowMode))
        # endif // if ADAGFX_ARGUMENT_VALIDATION
        {
          printText(parseStringToEndKeepCaseNoTrim(string, 5).c_str(),
                    nParams[0] + _xo - _x_compensation,
                    nParams[1] + _yo - _y_compensation,
                    _fontscaling,
                    _fgcolor,
                    _bgcolor);
        }
        success = true;
      }
      break;
    case adagfx_commands_e::txl: // txl: Text at line(s)

      if (argCount >= 2) {
        uint8_t _line   = 0;
        uint8_t _column = 0;
        uint8_t idx     = 0;
        setColumnRowMode(true); // this command is by default set to Column/Row mode

        while (idx < argCount && !sParams[idx + 1].isEmpty()) {
          if (nParams[idx] > 0) {
            _line = nParams[idx];
          } else {
            _line++;
          }
          printText(sParams[idx + 1].c_str(), _column, _line - 1, _fontscaling, _fgcolor, _bgcolor);
          idx += 2;
        }
        setColumnRowMode(currentColRowState);
        success = true;
      }
      break;
    case adagfx_commands_e::txc: // txc: Textcolor, fg and opt. bg colors

      if ((argCount == 1) || (argCount == 2)) {
        _fgcolor = AdaGFXparseColor(sParams[0], _colorDepth);

        if (argCount == 1) {
          _bgcolor = _fgcolor; // Transparent background
          _display->setTextColor(_fgcolor);
        } else {               // argCount=2
          _bgcolor = AdaGFXparseColor(sParams[1], _colorDepth);
          _display->setTextColor(_fgcolor, _bgcolor);
        }
        success = true;
      }
      break;
    case adagfx_commands_e::txs: // txs: Text size = font scaling, 1..10

      if ((argCount == 1) && (nParams[0] >= 0) && (nParams[0] <= 10)) {
        _fontscaling = nParams[0];
        _display->setTextSize(_fontscaling);
        calculateTextMetrics(_fontwidth, _fontheight, _heightOffset, _isProportional);
        success = true;
      }
      break;
    case adagfx_commands_e::txtfull: // txtfull: Text at position, with size and color

      if ((argCount >= 3) && (argCount <= 8)) {
        uint16_t par3color = argCount < 5 || sParams[3].isEmpty() ? _fgcolor : AdaGFXparseColor(sParams[3], _colorDepth);
        uint16_t par4color = argCount < 6 || sParams[4].isEmpty() ? _bgcolor : AdaGFXparseColor(sParams[4], _colorDepth);

        # if ADAGFX_ARGUMENT_VALIDATION

        if (!invalidCoordinates(nParams[0] - _x_compensation,
                                nParams[1] - _y_compensation,
                                _columnRowMode))
        # endif // if ADAGFX_ARGUMENT_VALIDATION
        {
          success = true;

          switch (argCount) {
            case 3: // single text

              printText(sParams[2].c_str(),
                        nParams[0] - _x_compensation,
                        nParams[1] - _y_compensation,
                        _fontscaling,
                        _fgcolor,
                        _bgcolor);
              break;
            case 4: // text + size

              printText(sParams[3].c_str(),
                        nParams[0] - _x_compensation,
                        nParams[1] - _y_compensation,
                        nParams[2],
                        _fgcolor,
                        _bgcolor);
              break;
            case 5: // text + size + color

              printText(sParams[4].c_str(),
                        nParams[0] - _x_compensation,
                        nParams[1] - _y_compensation,
                        nParams[2],
                        par3color,
                        par3color); // transparent bg
              break;
            case 6:                 // text + size + color + bkcolor

              printText(sParams[5].c_str(),
                        nParams[0] - _x_compensation,
                        nParams[1] - _y_compensation,
                        nParams[2],
                        par3color,
                        par4color);
              break;
            case 7: // 7: text + size + color + bkcolor + printmode
            case 8: // as 7 but: + maxwidth

            {
              AdaGFXTextPrintMode tmpPrintMode = _textPrintMode;

              if ((nParams[5] >= 0) && (nParams[5] < static_cast<int>(AdaGFXTextPrintMode::MAX))) {
                _textPrintMode = static_cast<AdaGFXTextPrintMode>(nParams[5]);
                _display->setTextWrap(_textPrintMode == AdaGFXTextPrintMode::ContinueToNextLine);
              }
              printText(sParams[argCount - 1].c_str(),
                        nParams[0] - _x_compensation,
                        nParams[1] - _y_compensation,
                        nParams[2],
                        par3color,
                        par4color,
                        argCount == 8 ? nParams[argCount - 2] : 0);

              if (_textPrintMode != tmpPrintMode) {
                _textPrintMode = tmpPrintMode;
                _display->setTextWrap(_textPrintMode == AdaGFXTextPrintMode::ContinueToNextLine);
              }
              break;
            }
            default:
              success = false;
              break;
          }
        }
      }
      break;
    case adagfx_commands_e::clear: // clear: Clear display
      # if ADAGFX_ENABLE_FRAMED_WINDOW

      if (_window == 0)
      # endif // if ADAGFX_ENABLE_FRAMED_WINDOW
      {
        _display->fillScreen(argCount == 0 ? _bgcolor : AdaGFXparseColor(sParams[0], _colorDepth));
      }
      # if ADAGFX_ENABLE_FRAMED_WINDOW
      else {
        // logWindows(F("clear ")); // Use for debugging only
        uint16_t _w = 0, _h = 0;
        getWindowLimits(_w, _h);
        _display->fillRect(_xo, _yo, _w, _h,
                           argCount == 0 ? _bgcolor : AdaGFXparseColor(sParams[0], _colorDepth));
      }
      # endif // if ADAGFX_ENABLE_FRAMED_WINDOW
      success = true;
      break;
    case adagfx_commands_e::rot: // rot: Rotation

      if ((argCount == 1) && (nParams[0] >= 0) && (nParams[0] <= 3)) {
        setRotation(nParams[0]);
        success = true;
      }
      break;
    case adagfx_commands_e::tpm: // tpm: Text Print Mode

      if ((argCount == 1) && !((nParams[0] < 0) || (nParams[0] >= static_cast<int>(AdaGFXTextPrintMode::MAX)))) {
        _textPrintMode = static_cast<AdaGFXTextPrintMode>(nParams[0]);
        _display->setTextWrap(_textPrintMode == AdaGFXTextPrintMode::ContinueToNextLine);
        success = true;
      }
      break;
    # if ADAGFX_USE_ASCIITABLE
    case adagfx_commands_e::asciitable: // Show ASCII table
    {
      String line;
      const int16_t start        = 0x80 + (argCount >= 1 && nParams[0] >= -4 && nParams[0] < 4 ? nParams[0] * 0x20 : 0);
      const uint8_t scale        = (argCount == 2 && nParams[1] > 0 && nParams[1] <= 10 ? nParams[1] : 2);
      const uint8_t currentScale = _fontscaling;

      if (_fontscaling != scale) { // Set fontscaling
        _fontscaling = scale;
        _display->setTextSize(_fontscaling);
        calculateTextMetrics(_fontwidth, _fontheight, _heightOffset, _isProportional);
      }
      line.reserve(_textcols);
      _display->setCursor(0, 0);
      int16_t row = 0;
      setColumnRowMode(true);

      for (int16_t i = start; i <= 0xFF && row < _textrows; ++i) {
        if ((i % 4 == 0) && (line.length() > (_textcols - 8u))) { // 8 = 4x space + char
          printText(line.c_str(), 0, row, _fontscaling, _fgcolor, _bgcolor);
          line.clear();
          row++;
        }

        if (line.isEmpty()) {
          line += formatToHex(i, 2);
        }
        line += ' ';
        line += static_cast<char>(((i == 0x0A) || (i == 0x0D) ? 0x20 : i)); // Show a space instead of CR/LF
      }

      if (row < _textrows) {
        printText(line.c_str(), 0, row, _fontscaling, _fgcolor, _bgcolor);
      }

      setColumnRowMode(currentColRowState); // Restore

      if (_fontscaling != currentScale) {   // Restore if needed
        _fontscaling = currentScale;
        _display->setTextSize(_fontscaling);
        calculateTextMetrics(_fontwidth, _fontheight, _heightOffset, _isProportional);
      }
      success = true;
      break;
    }
    # endif // if ADAGFX_USE_ASCIITABLE
    case adagfx_commands_e::font: // font: Change font

      # if ADAGFX_FONTS_INCLUDED

      if (argCount == 1) {
        int font_i = 0;
        sParams[0].toLowerCase();

        constexpr int font_max = NR_ELEMENTS(fontargs);

        if ((nParams[0] > 0) || equals(sParams[0], F("0"))) {
          font_i = AdaGFXgetFontIndexForFontId(nParams[0]); // Set font by fontId
        } else {
          font_i = GetCommandCode(sParams[0].c_str(), adagfx_fonts);
        }

        if ((font_i >= 0) && (font_i < font_max)) {
          _fontId = fontargs[font_i]._fontId;
          _display->setFont(fontargs[font_i]._f);
          calculateTextMetrics(fontargs[font_i]._width,
                               fontargs[font_i]._height,
                               fontargs[font_i]._offset,
                               fontargs[font_i]._proportional);
          success = true;
        }
      }
      # endif  // if ADAGFX_FONTS_INCLUDED
      break;
    case adagfx_commands_e::l: // l: Line

      if (argCount == 5) {
        # if ADAGFX_ARGUMENT_VALIDATION

        if (!(invCoord_0_1 ||
              invCoord_2_3))
        # endif // if ADAGFX_ARGUMENT_VALIDATION
        {
          _display->drawLine(nParams[0] + _xo, nParams[1] + _yo, nParams[2] + _xo, nParams[3] + _yo,
                             AdaGFXparseColor(sParams[4], _colorDepth));
          success = true;
        }
      }
      break;
    case adagfx_commands_e::lh: // lh: Horizontal line

      if (argCount == 3) {
        # if ADAGFX_ARGUMENT_VALIDATION

        if (!((nParams[0] < 0) || (nParams[0] > res_x)))
        # endif // if ADAGFX_ARGUMENT_VALIDATION
        {
          _display->drawFastHLine(_xo, nParams[0] + _yo, nParams[1], AdaGFXparseColor(sParams[2], _colorDepth));
          success = true;
        }
      }
      break;
    case adagfx_commands_e::lv: // lv: Vertical line

      if (argCount == 3)
      {
        # if ADAGFX_ARGUMENT_VALIDATION

        if (!((nParams[0] < 0) || (nParams[0] > res_y)))
        # endif // if ADAGFX_ARGUMENT_VALIDATION
        {
          _display->drawFastVLine(nParams[0] + _xo, _yo, nParams[1], AdaGFXparseColor(sParams[2], _colorDepth));
          success = true;
        }
      }
      break;
    # if ADAGFX_ENABLE_EXTRA_CMDS
    case adagfx_commands_e::lm:
    case adagfx_commands_e::lmr: // lm/lmr: Multi-line, multiple coordinates

      if (argCount >= 5) {
        uint16_t mcolor         = AdaGFXparseColor(sParams[0], _colorDepth);
        bool     mloop          = true;
        uint8_t  parCount       = 0;
        uint8_t  optCount       = 0;
        int  cx                 = -1;
        int  cy                 = -1;
        bool closeLine          = false;
        const bool relativeMode = (adagfx_commands_e::lmr == subcmd); // Use Relative mode
        success = true;

        while (mloop) {
          sParams[optCount] = parseString(string, parCount + 4);       // 0-offset + 1st and 2nd cmd-argument and 1 for color argument

          if (!validIntFromString(sParams[optCount], nParams[optCount]) && !sParams[optCount].isEmpty()) {
            mcolor = AdaGFXparseColor(sParams[optCount], _colorDepth); // Interpret as a color

            if (optCount > 0) { optCount--; }
          }
          mloop     = !sParams[optCount].isEmpty();
          closeLine = equals(sParams[optCount], 'c');

          if (mloop) { parCount++; optCount++; } // Next argument

          if ((optCount == 4) || closeLine) { // 0..3 = 4th argument or close the line
            if (relativeMode) {
              nParams[2] += nParams[0];
              nParams[3] += nParams[1];
            }
            #  if ADAGFX_ARGUMENT_VALIDATION

            if (invalidCoordinates(nParams[0], nParams[1]) ||
                invalidCoordinates(nParams[2], nParams[3])) {
              success = false;
              mloop   = false; // break out
            } else
            #  endif // if ADAGFX_ARGUMENT_VALIDATION
            {
              if (closeLine) {
                nParams[2] = cx;
                nParams[3] = cy;
                mloop      = false; // Exit after closing the line
              }
              #  ifndef BUILD_NO_DEBUG

              if (loglevelActiveFor(LOG_LEVEL_INFO)) {
                addLog(LOG_LEVEL_INFO, strformat(F("AdaGFX: cmd: lm x/y/x1/y1:%d/%d/%d/%d loop:%c color:%s"),
                                                 nParams[0], nParams[1], nParams[2], nParams[3],
                                                 mloop ? 'T' : 'f', AdaGFXcolorToString(mcolor, _colorDepth).c_str()));
              }
              #  endif // ifndef BUILD_NO_DEBUG
              _display->drawLine(nParams[0] + _xo, nParams[1] + _yo, nParams[2] + _xo, nParams[3] + _yo, mcolor);

              if ((cx == -1) && (cy == -1)) {
                cx = nParams[0];
                cy = nParams[1];
              }
              nParams[0] = nParams[2]; // Move second set to first set
              nParams[1] = nParams[3];
              optCount   = 2;          // Get second set of arguments only
            }
          }
        }
      }
      break;
    # endif // if ADAGFX_ENABLE_EXTRA_CMDS
    case adagfx_commands_e::r:  // r: Rectangle
    case adagfx_commands_e::rf: // rf: Rectangled, filled

      if ((argCount == 5) ||
          (argCount == 6)) {
        # if ADAGFX_ARGUMENT_VALIDATION

        if (!(invCoord_0_1 ||
              invCoord_0_2_1_3))
        # endif // if ADAGFX_ARGUMENT_VALIDATION
        {
          if ((adagfx_commands_e::rf == subcmd) && (argCount == 6)) {
            _display->fillRect(nParams[0] + _xo, nParams[1] + _yo, nParams[2], nParams[3], AdaGFXparseColor(sParams[5], _colorDepth));
          }
          _display->drawRect(nParams[0] + _xo, nParams[1] + _yo, nParams[2], nParams[3], AdaGFXparseColor(sParams[4], _colorDepth));
          success = true;
        }
      }
      break;
    case adagfx_commands_e::c:  // c: Circle
    case adagfx_commands_e::cf: // cf: Circle, filled

      if ((argCount == 4) ||
          (argCount == 5)) {
        # if ADAGFX_ARGUMENT_VALIDATION

        if (!(invCoord_0_1 ||
              invalidCoordinates(nParams[2], 0))) // Also check radius
        # endif  // if ADAGFX_ARGUMENT_VALIDATION
        {
          if ((adagfx_commands_e::cf == subcmd) && (argCount == 5)) {
            _display->fillCircle(nParams[0] + _xo, nParams[1] + _yo, nParams[2], AdaGFXparseColor(sParams[4], _colorDepth));
          }
          _display->drawCircle(nParams[0] + _xo, nParams[1] + _yo, nParams[2], AdaGFXparseColor(sParams[3], _colorDepth));
          success = true;
        }
      }
      break;
    case adagfx_commands_e::t:  // t: Triangle
    case adagfx_commands_e::tf: // tf: Triangle, filled

      if ((argCount == 7) ||
          (argCount == 8)) {
        # if ADAGFX_ARGUMENT_VALIDATION

        if (!(invCoord_0_1 ||
              invCoord_2_3 ||
              invalidCoordinates(nParams[4], nParams[5])))
        # endif // if ADAGFX_ARGUMENT_VALIDATION
        {
          if ((adagfx_commands_e::tf == subcmd) && (argCount == 8)) {
            _display->fillTriangle(nParams[0] + _xo,
                                   nParams[1] + _yo,
                                   nParams[2] + _xo,
                                   nParams[3] + _yo,
                                   nParams[4] + _xo,
                                   nParams[5] + _yo,
                                   AdaGFXparseColor(sParams[7], _colorDepth));
          }
          _display->drawTriangle(nParams[0] + _xo,
                                 nParams[1] + _yo,
                                 nParams[2] + _xo,
                                 nParams[3] + _yo,
                                 nParams[4] + _xo,
                                 nParams[5] + _yo,
                                 AdaGFXparseColor(sParams[6], _colorDepth));
          success = true;
        }
      }
      break;
    case adagfx_commands_e::rr:  // rr: Rounded rectangle
    case adagfx_commands_e::rrf: // rrf: Rounded rectangle, filled

      if ((argCount == 6) ||
          (argCount == 7)) {
        # if ADAGFX_ARGUMENT_VALIDATION

        if (!(invCoord_0_1 ||
              invCoord_0_2_1_3 ||
              invalidCoordinates(nParams[4], 0))) // Also check radius
        # endif  // if ADAGFX_ARGUMENT_VALIDATION
        {
          if ((adagfx_commands_e::rrf == subcmd) && (argCount == 7)) {
            _display->fillRoundRect(nParams[0] + _xo,
                                    nParams[1] + _yo,
                                    nParams[2],
                                    nParams[3],
                                    nParams[4],
                                    AdaGFXparseColor(sParams[6], _colorDepth));
          }
          _display->drawRoundRect(nParams[0] + _xo,
                                  nParams[1] + _yo,
                                  nParams[2],
                                  nParams[3],
                                  nParams[4],
                                  AdaGFXparseColor(sParams[5], _colorDepth));
          success = true;
        }
      }
      break;
    case adagfx_commands_e::px: // px: Pixel

      if (argCount == 3) {
        # if ADAGFX_ARGUMENT_VALIDATION

        if (!invCoord_0_1)
        # endif // if ADAGFX_ARGUMENT_VALIDATION
        {
          _display->drawPixel(nParams[0] + _xo, nParams[1] + _yo, AdaGFXparseColor(sParams[2], _colorDepth));
          success = true;
        }
      }
      break;
    case adagfx_commands_e::pxh:
    case adagfx_commands_e::pxv: // pxh/pxv: Pixels, hor./vert.

      if (argCount > 2) {
        // incremented merged loop is smaller than 2 separate loops
        # if ADAGFX_ARGUMENT_VALIDATION

        if (!invCoord_0_1)
        # endif // if ADAGFX_ARGUMENT_VALIDATION
        {
          _display->startWrite();
          _display->writePixel(nParams[0] + _xo, nParams[1] + _yo, AdaGFXparseColor(sParams[2], _colorDepth));
          loop = true;
          uint8_t h     = 0;
          uint8_t v     = 0;
          bool    isPxh = (adagfx_commands_e::pxh == subcmd);

          if (isPxh) {
            h++;
          } else {
            v++;
          }

          while (loop) {
            String color = parseString(string, h + v + 5); // 5 = 2 + 3 already parsed merged loop is smaller than 2 separate loops

            if (color.isEmpty()
            # if ADAGFX_ARGUMENT_VALIDATION
                || invalidCoordinates(nParams[0] + h + _xo, nParams[1] + v + _yo)
            # endif // if ADAGFX_ARGUMENT_VALIDATION
                ) {
              loop = false;
            } else {
              _display->writePixel(nParams[0] + h + _xo, nParams[1] + v + _yo, AdaGFXparseColor(color, _colorDepth));

              if (isPxh) {
                h++;
              } else {
                v++;
              }
            }
            delay(0);
          }
          _display->endWrite();
          success = true;
        }
      }
      break;
    # if ADAGFX_ENABLE_BMP_DISPLAY
    case adagfx_commands_e::bmp: // bmp,x,y,filename.bmp : show bmp from file

      if ((argCount == 3) && !sParams[2].isEmpty()) {
        success = showBmp(sParams[2], nParams[0] + _xo, nParams[1] + _yo);
      }
      break;
    # endif // if ADAGFX_ENABLE_BMP_DISPLAY
    # if ADAGFX_ENABLE_BUTTON_DRAW
    case adagfx_commands_e::btn:

      if ((argCount >= 8) && (nParams[7] != 0)) {
        // btn,state,m,x,y,w,h,id,type[,ONclr,OFFclr,Captionclr,fontscale,ONcaption,OFFcapt,Borderclr,DisabClr,DisabCaptclr],TaskIndex,Group,SelGrp,objectname
        // ev: 1     2 3 4 5 6 7  8     9     10     11         12        13        14      15        16       17,18,19,20,21
        // nP: 0     1 2 3 4 5 6  7     8     9      10         11        12        13      14        15       16,17,18,19,20
        // : Draw a button
        // m=mode: -2 = disabled, -1 = initial, 0 = default
        // state:  0 = off, 1 = on, -2 = off + disabled, -1 = on + disabled
        // id: < 0 = clear area
        // type & 0x0F: 0 = none, 1 = rectangle, 2 = rounded rect., 3 = circle,
        // type & 0xF0 = CenterAligned, LeftAligned, TopAligned, RightAligned, BottomAligned, LeftTopAligned, RightTopAligned,
        //               RightBottomAligned, LeftBottomAligned, NoCaption, Bitmap, Slider (not a button)
        // (*clr = color, TaskIndex, Group and SelGrp are ignored)
        #  if ADAGFX_ARGUMENT_VALIDATION

        if (!(invCoord_2_3 ||
              invalidCoordinates(nParams[2] + nParams[4], nParams[3] + nParams[5])))
        #  endif  // if ADAGFX_ARGUMENT_VALIDATION
        {
          // All checked out OK
          // Default values
          uint16_t onColor              = ADAGFX_BLUE;
          uint16_t offColor             = ADAGFX_RED;
          uint16_t captionColor         = ADAGFX_WHITE;
          uint8_t  fontScale            = 0;
          uint16_t borderColor          = ADAGFX_WHITE;
          uint16_t disabledColor        = 0x9410; // Medium grey
          uint16_t disabledCaptionColor = 0x5A69; // Dark grey
          success = true;

          if (!sParams[8].isEmpty()) { onColor = AdaGFXparseColor(sParams[8], _colorDepth); }

          if (!sParams[9].isEmpty()) { offColor = AdaGFXparseColor(sParams[9], _colorDepth); }

          if (!sParams[10].isEmpty()) { captionColor = AdaGFXparseColor(sParams[10], _colorDepth); }

          if ((nParams[11] > 0) && (nParams[11] <= 10)) { fontScale = nParams[11]; }

          if (!sParams[14].isEmpty()) { borderColor = AdaGFXparseColor(sParams[14], _colorDepth); }

          if (!sParams[15].isEmpty()) { disabledColor = AdaGFXparseColor(sParams[15], _colorDepth); }

          if (!sParams[16].isEmpty()) { disabledCaptionColor = AdaGFXparseColor(sParams[16], _colorDepth); }

          uint16_t   fillColor = onColor;
          uint16_t   textColor = captionColor;
          const bool clearArea = nParams[7] < 0;
          nParams[7] = std::abs(nParams[7]);

          const Button_type_e   buttonType   = static_cast<Button_type_e>(nParams[7] & 0x0F);
          const Button_layout_e buttonLayout = static_cast<Button_layout_e>(nParams[7] & 0xF0);

          // Check mode & state: -2, -1, 0, 1 to select used colors
          #  if ADAGFX_ENABLE_BUTTON_SLIDER

          if (buttonLayout == Button_layout_e::Slider) {
            if (nParams[1] == -2) {
              fillColor = disabledColor;
              textColor = disabledCaptionColor;
            } else if (clearArea) {
              fillColor   = _bgcolor; //
              borderColor = _bgcolor;
            }
          } else
          #  endif // if ADAGFX_ENABLE_BUTTON_SLIDER
          {
            if (nParams[0] == 0) {
              fillColor = offColor;
            }

            if ((nParams[1] == -2) || (nParams[0] < 0)) {
              fillColor = disabledColor;
              textColor = disabledCaptionColor;
            } else if (clearArea) {
              fillColor   = _bgcolor; //
              borderColor = _bgcolor;
            }
          }

          // Clear the area?
          if ((buttonType != Button_type_e::None) ||
              clearArea) {
            drawButtonShape(
              #  if ADAGFX_ENABLE_BUTTON_SLIDER
              buttonLayout == Button_layout_e::Slider ? Button_type_e::Square :
              #  endif // if ADAGFX_ENABLE_BUTTON_SLIDER
              buttonType, // Clear full square for slider
              nParams[2] + _xo, nParams[3] + _yo, nParams[4], nParams[5],
              _bgcolor, _bgcolor);
          }

          // Check button-type bits (mask: 0x0F) to draw correct shape
          if (!clearArea) {
            drawButtonShape(buttonType,
                            nParams[2] + _xo, nParams[3] + _yo, nParams[4], nParams[5],
                            fillColor, borderColor);
          }

          // Display caption? (or bitmap)
          if (!clearArea &&
              (buttonLayout != Button_layout_e::NoCaption)) {
            int16_t  x1, y1;
            uint16_t w1, h1, w2, h2;
            String   newString;

            // Determine alignment parameters
            if ((nParams[0] == 1) || (nParams[0] == -1) // 1 = on+enabled, -1 = on+disabled
                #  if ADAGFX_ENABLE_BUTTON_SLIDER
                || (buttonLayout == Button_layout_e::Slider)
                #  endif // if ADAGFX_ENABLE_BUTTON_SLIDER
                ) {
              newString = sParams[12].isEmpty() ? sParams[6] : sParams[12];
            } else {
              newString = sParams[13].isEmpty() ? sParams[6] : sParams[13];
            }
            newString = AdaGFXparseTemplate(newString, 20);

            _display->setTextSize(fontScale);                             // set scaling
            _display->getTextBounds(newString, 0, 0, &x1, &y1, &w1, &h1); // get caption length and height in pixels
            _display->getTextBounds(F(" "),    0, 0, &x1, &y1, &w2, &h2); // measure space width for little margins

            // Check button-alignment bits (mask 0xF0) for caption placement, modifies the x/y arguments passed!
            // Little margin is: from left/right: half of the width of a space, from top/bottom: half of height of the font used

            switch (buttonLayout) {
              case Button_layout_e::CenterAligned:
                nParams[2] += (nParams[4] / 2 - w1 / 2);  // center horizontically
                nParams[3] += (nParams[5] / 2 - h1 / 2);  // center vertically
                break;
              case Button_layout_e::LeftAligned:
                nParams[2] += w2 / 2;                     // A little margin from left
                nParams[3] += (nParams[5] / 2 - h1 / 2);  // center vertically
                break;
              case Button_layout_e::TopAligned:
                nParams[2] += (nParams[4] / 2 - w1 / 2);  // center horizontically
                nParams[3] += h1 / 2;                     // A little margin from top
                break;
              case Button_layout_e::RightAligned:
                nParams[2] += (nParams[4] - w1) - w2 / 2; // right-align + a little margin
                nParams[3] += (nParams[5] / 2 - h1 / 2);  // center vertically
                break;
              case Button_layout_e::BottomAligned:
                nParams[2] += (nParams[4] / 2 - w1 / 2);  // center horizontically
                nParams[3] += (nParams[5] - h1 * 1.5);    // bottom align + a little margin
                break;
              case Button_layout_e::LeftTopAligned:
                nParams[2] += w2 / 2;                     // A little margin from left
                nParams[3] += h1 / 2;                     // A little margin from top
                break;
              case Button_layout_e::RightTopAligned:
                nParams[2] += (nParams[4] - w1) - w2 / 2; // right-align + a little margin
                nParams[3] += h1 / 2;                     // A little margin from top
                break;
              case Button_layout_e::RightBottomAligned:
                nParams[2] += (nParams[4] - w1) - w2 / 2; // right-align + a little margin
                nParams[3] += (nParams[5] - h1 * 1.5);    // bottom align + a little margin
                break;
              case Button_layout_e::LeftBottomAligned:
                nParams[2] += w2 / 2;                     // A little margin from left
                nParams[3] += (nParams[5] - h1 * 1.5);    // bottom align + a little margin
                break;
              #  if ADAGFX_ENABLE_BMP_DISPLAY
              case Button_layout_e::Bitmap:
              {                     // Use ON/OFF caption to specify (full) bitmap filename
                if (!newString.isEmpty()) {
                  int32_t offX = 0; // Allow optional arguments for x and y offset values, usage:
                  int32_t offY = 0; // [x,[y,]]filename.bmp

                  if (newString.indexOf(',') > -1) {
                    String tmp = parseString(newString, 1);
                    validIntFromString(tmp, offX);
                    newString = parseStringToEndKeepCase(newString, 2);

                    if (newString.indexOf(',') > -1) {
                      tmp = parseString(newString, 1);
                      validIntFromString(tmp, offY);
                      newString = parseStringToEndKeepCase(newString, 2);
                    }
                  }
                  success = showBmp(newString, nParams[2] + _xo + offX, nParams[3] + _yo + offY);
                } else {
                  success = false;
                }
                break;
              }
              #  endif // if ADAGFX_ENABLE_BMP_DISPLAY
              case Button_layout_e::NoCaption:
              #  if ADAGFX_ENABLE_BUTTON_SLIDER
              case Button_layout_e::Slider: // Nothing to do here (yet)
              #  endif // if ADAGFX_ENABLE_BUTTON_SLIDER
                break;
            }

            if ((buttonLayout != Button_layout_e::NoCaption)
                #  if ADAGFX_ENABLE_BUTTON_SLIDER
                && (buttonLayout != Button_layout_e::Slider)
                #  endif // if ADAGFX_ENABLE_BUTTON_SLIDER
                #  if ADAGFX_ENABLE_BMP_DISPLAY
                && (buttonLayout != Button_layout_e::Bitmap)
                #  endif // if ADAGFX_ENABLE_BMP_DISPLAY
                ) {
              // Set position and colors, then print
              _display->setCursor(nParams[2] + _xo, nParams[3] + _yo);
              _display->setTextColor(textColor, textColor); // transparent bg results in button color
              _display->print(newString);

              // restore colors
              _display->setTextColor(_fgcolor, _bgcolor);
            }
            #  if ADAGFX_ENABLE_BUTTON_SLIDER

            if (buttonLayout == Button_layout_e::Slider) {
              // 1) Determine direction from w/h
              const bool isVertical   = nParams[4] < nParams[5]; // width < height
              const bool showAsCircle = borderColor == fillColor;

              // determine value and range
              int16_t offI2            = 5;  // half of indicator width
              int16_t offG2            = 3;  // half of Gauge width
              int16_t offP             = 0;  // Offset for indicator
              int16_t zeroLine         = -1; // Draw a range zero-line at this offset? only when >= 0
              int     percentage       = 0;
              float   gaugeValue       = 0.0f;
              int16_t lowRange         = 0;
              int16_t highRange        = 100;
              float   rangeFrom        = 0.0f;
              float   rangeTo          = 0.0f;
              float   range            = 100.0f; // For percentage the range is 100
              bool    useRange         = false;
              bool    hasRangeReversed = false;  // Range low value left or top, high value right or bottom

              if (!validFloatFromString(newString, gaugeValue)) {
                percentage = nParams[0];         // Value as provided
              }

              // Have a range?
              if (!sParams[13].isEmpty()) { // Off caption can hold range: <from>,<to>
                String tmp           = parseString(sParams[13], 1);
                const bool validFrom = validFloatFromString(tmp, rangeFrom);
                tmp = parseString(sParams[13], 2);

                if (validFrom && validFloatFromString(tmp, rangeTo) &&
                    !essentiallyEqual(rangeFrom, 0.0f) && !essentiallyEqual(rangeTo, 0.0f)) {
                  useRange         = true;
                  lowRange         = static_cast<uint16_t>(rangeFrom);
                  highRange        = static_cast<uint16_t>(rangeTo);
                  hasRangeReversed = lowRange > highRange; // Range high value left or top, low value right or bottom?
                }
              }

              // 2) Draw center-line from 0 to 100%
              // 3) Draw gauge for used/filled part
              // 4) Draw indicator at correct percentage index
              if (showAsCircle) { // Circle indicator or full width bar indicator
                offI2 = (isVertical ? nParams[4] : nParams[5]) / 4;
              }

              if (useRange) { // Calculate range-boundaries
                range = abs(max(rangeTo, rangeFrom) - min(rangeFrom, rangeTo));

                if (gaugeValue > max(rangeTo, rangeFrom)) {
                  gaugeValue = max(rangeTo, rangeFrom);
                } else if (gaugeValue < min(rangeFrom, rangeTo)) {
                  gaugeValue = min(rangeFrom, rangeTo);
                } else {
                  gaugeValue -= min(rangeFrom, rangeTo); // Give it the correct Offset
                }

                if (((lowRange < 0) && (highRange > 0)) ||
                    ((lowRange > 0) && (highRange < 0))) {
                  zeroLine = map(0, lowRange, highRange, 0, isVertical ? nParams[5] : nParams[4]);
                }
              }
              percentage = static_cast<int>(gaugeValue);

              offP = ((((isVertical ? nParams[5] : nParams[4]) - (2 * offI2)) / range) * percentage) - 1; // keep within button borders

              if (hasRangeReversed) {
                offP = (isVertical ? nParams[5] : nParams[4]) - (2 * offI2) - offP - 1;                   // flip
              }

              if (isVertical) {
                // centerline
                _display->drawLine(nParams[2] + _xo + nParams[4] / 2, nParams[3] + _yo + (nParams[5] - offI2 - 1),
                                   nParams[2] + _xo + nParams[4] / 2, nParams[3] + _yo + offI2, textColor);

                if (zeroLine > -1) {
                  _display->drawLine(nParams[2] + _xo, nParams[3] + _yo + (nParams[5] - zeroLine - 1),
                                     nParams[2] + _xo + nParams[4], nParams[3] + _yo + (nParams[5] - zeroLine - 1), textColor);
                }

                // Gauge
                if (hasRangeReversed) {
                  int16_t bar = nParams[5] - (2 * offI2);
                  _display->fillRoundRect(nParams[2] + _xo + (nParams[4] / 2) - offG2, nParams[3] + _yo + offI2 + 0,
                                          2 * offG2, bar - offP, offG2, textColor);
                } else {
                  _display->fillRoundRect(nParams[2] + _xo + (nParams[4] / 2) - offG2, nParams[3] + _yo + (nParams[5] - offI2 - offP - 1),
                                          2 * offG2, offP, offG2, textColor);
                }

                // Indicator/drag-handle
                if (showAsCircle) { // Circle indicator
                  _display->fillCircle(nParams[2] + _xo + nParams[4] / 2,
                                       nParams[3] + _yo + (nParams[5] - offI2 - offP - 2) + (percentage == 100 ? 1 : 0),
                                       trunc(nParams[4] / 4), textColor);
                } else {
                  _display->fillRoundRect(nParams[2] + _xo + 1,
                                          nParams[3] + _yo + (nParams[5] - (2 * offI2) - offP - (hasRangeReversed ? 0 : 1)),
                                          nParams[4] - 2, 2 * offI2, offI2, textColor);
                }
              } else { // : if !isVertical -> isHorizontal
                // centerline
                _display->drawLine(nParams[2] + _xo + offI2 + 1, nParams[3] + _yo + nParams[5] / 2,
                                   nParams[2] + _xo + nParams[4] - offI2, nParams[3] + _yo + nParams[5] / 2, textColor);

                if (zeroLine > -1) {
                  _display->drawLine(nParams[2] + _xo + zeroLine + 1, nParams[3] + _yo,
                                     nParams[2] + _xo + zeroLine + 1, nParams[3] + _yo + nParams[5], textColor);
                }

                // Gauge
                if (hasRangeReversed) {
                  int16_t bar = nParams[4] - (2 * offI2);
                  _display->fillRoundRect(nParams[2] + _xo + offI2 + offP + 2, nParams[3] + _yo + (nParams[5] / 2) - offG2,
                                          bar - offP, offG2 * 2, offG2, textColor);
                } else {
                  _display->fillRoundRect(nParams[2] + _xo + offI2 + 1, nParams[3] + _yo + (nParams[5] / 2) - offG2,
                                          offP, offG2 * 2, offG2, textColor);
                }

                // Indicator/drag-handle
                if (showAsCircle) { // Circle indicator
                  _display->fillCircle(nParams[2] + _xo + offP + offI2 + 1 - (percentage == 100 ? 1 : 0),
                                       nParams[3] + _yo + (nParams[5] / 2),
                                       trunc(nParams[5] / 4), textColor);
                } else {
                  _display->fillRoundRect(nParams[2] + _xo + offP + (hasRangeReversed ? 0 : 1), nParams[3] + _yo + 1,
                                          2 * offI2, nParams[5] - 2, offI2, textColor);
                }
              }

              // 5) Draw percentage in center if Fontsize > 0
              if (fontScale > 0) {
                nParams[2] += (nParams[4] / 2 - w1 / 2);      // center horizontically
                nParams[3] += (nParams[5] / 2 - h1 / 2);      // center vertically
                _display->setCursor(nParams[2] + _xo, nParams[3] + _yo);
                _display->setTextColor(textColor, fillColor); // regular bg color for readability
                _display->print(newString);
              }
            }
            #  endif // if ADAGFX_ENABLE_BUTTON_SLIDER

            // restore font scaling
            _display->setTextSize(_fontscaling);
          }
        }
      }
      break;
    # endif // if ADAGFX_ENABLE_BUTTON_DRAW
    # if ADAGFX_ENABLE_FRAMED_WINDOW
    case adagfx_commands_e::win: // win: select window by id

      if ((argCount >= 1) && (argCount <= 2)) {
        success = selectWindow(nParams[0], nParams[1]);
      }
      break;
    case adagfx_commands_e::defwin: // defwin: define window

      if ((argCount >= 5) && (argCount <= 6)) {
        const int8_t rot = _rotation;
        #  if ADAGFX_ARGUMENT_VALIDATION
        const int16_t curWin = getWindow();

        if (curWin != 0) { selectWindow(0); }           // Validate against raw window coordinates

        if (argCount == 6) { setRotation(nParams[5]); } // Use requested rotation

        if (invCoord_0_1 ||
            invCoord_0_2_1_3) {
          if (curWin != 0) { selectWindow(curWin); }  // restore current window

          if (rot != _rotation) { setRotation(rot); } // Restore rotation
        } else
        #  endif  // if ADAGFX_ARGUMENT_VALIDATION
        {
          #  if ADAGFX_ARGUMENT_VALIDATION

          if (curWin != 0) { selectWindow(curWin); } // restore current window
          #  endif // if ADAGFX_ARGUMENT_VALIDATION

          if (nParams[4] > 0) {                  // Window 0 is the raw window, having the full size, created at initialization of this
            success = true;

            // helper instance
            #  ifndef BUILD_NO_DEBUG
            int16_t win = // avoid compiler warning
            #  endif // ifndef BUILD_NO_DEBUG
            defineWindow(nParams[0],
                         nParams[1],
                         nParams[2],
                         nParams[3],
                         nParams[4],
                         argCount == 6 ? nParams[5] : _rotation);
            #  ifndef BUILD_NO_DEBUG

            if (loglevelActiveFor(LOG_LEVEL_INFO)) {
              addLogMove(LOG_LEVEL_INFO, strformat(F("AdaGFX defined window id: %d"), win));
            }
            #  endif // ifndef BUILD_NO_DEBUG

            if (rot != _rotation) { setRotation(rot); } // Restore rotation, also update new window
          }
        }
      }
      break;
    case adagfx_commands_e::delwin: // delwin: delete window, don't delete window 0

      if ((argCount == 1) && (nParams[0] > 0)) {
        // logWindows(F(" deLwin ")); // use for debugging only

        success = deleteWindow(nParams[0]);
      }
      break;
      # endif // if ADAGFX_ENABLE_FRAMED_WINDOW
  }
  return success;
}

/****************************************************************************
 * Get a config value from the plugin
 ***************************************************************************/
# if ADAGFX_ENABLE_GET_CONFIG_VALUE
const char adagfx_getcommands[] PROGMEM = "win|iswin|width|height|length|textheight|rot|txs|tpm"
                                          #  if ADAGFX_FONTS_INCLUDED
                                          "|font"
                                          #  endif // if ADAGFX_FONTS_INCLUDED
;
enum class adagfx_getcommands_e : int8_t {
  invalid = -1,
  win     = 0,
  iswin,
  width,
  height,
  length,
  textheight,
  rot,
  txs,
  tpm,
  #  if ADAGFX_FONTS_INCLUDED
  font,
  #  endif // if ADAGFX_FONTS_INCLUDED
};

bool AdafruitGFX_helper::pluginGetConfigValue(String& string) {
  bool success = false;
  char sep     = '.';

  if ((-1 == string.indexOf(sep)) && (string.indexOf(',') >= 0)) {
    sep = ',';
  }
  String command = parseString(string, 1, sep);

  const int command_i            = GetCommandCode(command.c_str(), adagfx_getcommands);
  const adagfx_getcommands_e cmd = static_cast<adagfx_getcommands_e>(command_i);

  switch (cmd) {
    case adagfx_getcommands_e::win:
    {                                   // win: get current window id
      #  if ADAGFX_ENABLE_FRAMED_WINDOW // if feature enabled
      string  = getWindow();
      success = true;
      #  endif // if ADAGFX_ENABLE_FRAMED_WINDOW
      break;
    }
    case adagfx_getcommands_e::iswin:
    {                                   // iswin: check if windows exists
      #  if ADAGFX_ENABLE_FRAMED_WINDOW // if feature enabled
      command = parseString(string, 2, sep);
      int32_t win = 0;

      if (validIntFromString(command, win)) {
        string = validWindow(static_cast<uint8_t>(win));
      } else {
        string = '0';
      }
      success = true; // Always correct, just return 'false' if wrong
      #  endif // if ADAGFX_ENABLE_FRAMED_WINDOW
      break;
    }
    case adagfx_getcommands_e::width:
    case adagfx_getcommands_e::height:
      // width/height: get window width or height
    {
      #  if ADAGFX_ENABLE_FRAMED_WINDOW // if feature enabled
      uint16_t w = 0, h = 0;
      getWindowLimits(w, h);

      if (adagfx_getcommands_e::width == cmd) {
        string = w;
      } else {
        string = h;
      }
      success = true;
      #  endif // if ADAGFX_ENABLE_FRAMED_WINDOW
      break;
    }
    case adagfx_getcommands_e::length:
    case adagfx_getcommands_e::textheight:
      // length/textheight: get text length or height
    {
      int16_t  x1{}, y1{};
      uint16_t w1{}, h1{};
      String   newString = parseStringToEndKeepCaseNoTrim(string, 2, sep);

      if (!newString.isEmpty()) {
        newString = AdaGFXparseTemplate(newString, 0);

        _display->getTextBounds(newString, 0, 0, &x1, &y1, &w1, &h1); // Count length and height
      }

      if (adagfx_getcommands_e::length == cmd) {
        string = w1;
      } else {
        string = h1;
      }
      success = true;
      break;
    }
    case adagfx_getcommands_e::rot:
    { // rot: get current rotation setting
      string  = _rotation;
      success = true;
      break;
    }
    case adagfx_getcommands_e::txs:
    { // txs: get current text scaling setting
      string  = _fontscaling;
      success = true;
      break;
    }
    case adagfx_getcommands_e::tpm:
    { // tpm: get current text print mode setting
      string  = static_cast<int>(_textPrintMode);
      success = true;
      break;
    }
    #  if ADAGFX_FONTS_INCLUDED
    case adagfx_getcommands_e::font:
      string  = AdaGFXgetFontName(_fontId);
      success = true;
      break;
    #  endif // if ADAGFX_FONTS_INCLUDED
    case adagfx_getcommands_e::invalid:
      break;
  }

  return success;
}

# endif // if ADAGFX_ENABLE_GET_CONFIG_VALUE

/****************************************************************************
 * draw a button shape with provided color, can also clear a previously drawn button
 ***************************************************************************/
# if ADAGFX_ENABLE_BUTTON_DRAW
void AdafruitGFX_helper::drawButtonShape(const Button_type_e& buttonType,
                                         const int          & x,
                                         const int          & y,
                                         const int          & w,
                                         const int          & h,
                                         const uint16_t     & fillColor,
                                         const uint16_t     & borderColor) {
  switch (buttonType) {
    case Button_type_e::Square: // Rectangle
    {
      _display->fillRect(x, y, w, h, fillColor);
      _display->drawRect(x, y, w, h, borderColor);
      break;
    }
    case Button_type_e::Rounded:     // Rounded Rectangle
    {
      int16_t radius = (w + h) / 20; // average 10 % corner radius w/h
      _display->fillRoundRect(x, y, w, h, radius, fillColor);
      _display->drawRoundRect(x, y, w, h, radius, borderColor);
      break;
    }
    case Button_type_e::Circle:     // Circle
    {
      int16_t radius = (w + h) / 4; // average radius
      _display->fillCircle(x + (w / 2), y + (h / 2), radius, fillColor);
      _display->drawCircle(x + (w / 2), y + (h / 2), radius, borderColor);
      break;
    }
    case Button_type_e::ArrowLeft:
    { // draw: left-center, right-top, right-bottom
      _display->fillTriangle(x, y + h / 2, x + w, y,
                             x + w, y + h, fillColor);
      _display->drawTriangle(x, y + h / 2, x + w, y,
                             x + w, y + h, borderColor);
      break;
    }
    case Button_type_e::ArrowUp:
    { // draw: top-center, right-bottom, left-bottom
      _display->fillTriangle(x + w / 2, y, x + w, y + h,
                             x, y + h, fillColor);
      _display->drawTriangle(x + w / 2, y, x + w, y + h,
                             x, y + h, borderColor);
      break;
    }
    case Button_type_e::ArrowRight:
    { // draw: left-top, right-center, left-bottom
      _display->fillTriangle(x, y, x + w, y + h / 2,
                             x, y + h, fillColor);
      _display->drawTriangle(x, y, x + w, y + h / 2,
                             x, y + h, borderColor);
      break;
    }
    case Button_type_e::ArrowDown:
    { // draw: left-top, right-top, bottom-center
      _display->fillTriangle(x, y, x + w, y,
                             x + w / 2, y + h, fillColor);
      _display->drawTriangle(x, y, x + w, y,
                             x + w / 2, y + h, borderColor);
      break;
    }
    case Button_type_e::None:
      break;
  }
}

# endif // if ADAGFX_ENABLE_BUTTON_DRAW

/****************************************************************************
 * printText: Print text on display at a specific pixel or column/row location
 ***************************************************************************/
void AdafruitGFX_helper::printText(const char     *string,
                                   const int16_t & X,
                                   const int16_t & Y,
                                   const uint8_t & textSize,
                                   const uint16_t& color,
                                   uint16_t        bkcolor,
                                   const uint16_t& maxWidth) {
  int16_t  _x        = X;
  int16_t  _y        = Y + (_heightOffset * textSize);
  uint16_t _w        = 0;
  int16_t  xText     = 0;
  int16_t  yText     = 0;
  uint16_t wText     = 0;
  uint16_t wChar     = 0;
  uint16_t hText     = 0;
  int16_t  oTop      = 0;
  int16_t  oBottom   = 0;
  int16_t  oLeft     = 0;
  uint16_t xOffset   = 0;
  uint16_t hChar1    = 0;
  uint16_t wChar1    = 0;
  String   newString = string;
  uint16_t res_x     = _res_x;

  # if ADAGFX_ENABLE_FRAMED_WINDOW
  uint16_t res_y   = _res_y;
  uint16_t yOffset = 0;
  # endif // if ADAGFX_ENABLE_FRAMED_WINDOW

  # if ADAGFX_ENABLE_FRAMED_WINDOW
  getWindowLimits(res_x, res_y);
  getWindowOffsets(xOffset, yOffset);
  _x += xOffset;
  _y += yOffset;
  # endif // if ADAGFX_ENABLE_FRAMED_WINDOW

  _display->setTextSize(textSize);
  _display->getTextBounds(String('A'), 0, 0, &xText, &yText, &wChar1, &hChar1); // Calculate ~1 char height

  if (_columnRowMode) {
    _x = X * (_fontwidth * textSize);                                           // We need this multiple times

    if (15 == _lineSpacing) {
      _y = (Y * (_fontheight * textSize)) + (_heightOffset * textSize);
    } else {
      _y = (Y * (hChar1 + _lineSpacing)) + _heightOffset; // Apply explicit line spacing
    }
  }

  _display->setCursor(_x, _y);
  _display->setTextColor(color, bkcolor);

  if (_textPrintMode != AdaGFXTextPrintMode::ContinueToNextLine) {
    # if ADAGFX_ENABLE_FRAMED_WINDOW

    if (0 == getWindow()) // Only on Window 0
    # endif // if ADAGFX_ENABLE_FRAMED_WINDOW
    {
      wChar = wChar1;
    }
    _display->getTextBounds(newString, _x, _y, &xText, &yText, &wText, &hText);   // Calculate length

    while ((newString.length() > 0) && (((_x - xOffset) + wText) > res_x + wChar)) {
      newString.remove(newString.length() - 1);                                   // Cut last character off
      _display->getTextBounds(newString, _x, _y, &xText, &yText, &wText, &hText); // Re-calculate length
    }
  }

  _display->getTextBounds(newString, _x, _y, &xText, &yText, &wText, &hText); // Calculate length

  if ((hText < hChar1) && _columnRowMode) {                                   // If the text-height is lower than the default height
    yText -= (hChar1 - hText);                                                // Subtract height-difference between A and current height
    hText  = hChar1;                                                          // Use default height
  }

  if ((maxWidth > 0) && ((_x - xOffset) + maxWidth <= res_x)) {
    res_x = (_x - xOffset) + maxWidth;
    _w    = maxWidth;

    if ((_textPrintMode == AdaGFXTextPrintMode::TruncateExceedingCentered) &&
        (maxWidth > wText)) {
      oLeft = (_w - (wText + 2 * (xText - _x))) / 2;
    }
  } else {
    _w = wText + 2 * (xText - _x);
  }

  if (_textBackFill && (color != bkcolor)) { // Fill extra space above and below text
    oTop    -= textSize;
    oBottom += textSize;
    _y      += textSize;
  }

  if ((_textPrintMode == AdaGFXTextPrintMode::ClearThenTruncate) ||
      (color != bkcolor)) { // Clear before print
    # ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      addLogMove(LOG_LEVEL_DEBUG, strformat(F("printText: clear: _x:%d, oTop:%d, _y:%d, xTx:%d, yTx:%d, wTx:%d,"
                                              " hTx:%d, oBot:%d, _res_x/max:%d/%d, str:%s"),
                                            _x, oTop, _y, xText, yText, wText,
                                            hText, oBottom, _res_x, res_x, newString.c_str()));
    }
    # endif // ifndef BUILD_NO_DEBUG

    if (bkcolor == color) { bkcolor = _bgcolor; } // To get at least the text readable

    if (_textPrintMode == AdaGFXTextPrintMode::ClearThenTruncate) { // oTop is negative so subtract to add...
      _display->fillRect(_x + oTop, yText, res_x - (_x - xOffset), hText + oBottom - oTop, bkcolor); // Clear text area to right edge of
                                                                                                     // screen
    } else {
      _display->fillRect(_x + oTop, yText, _w, hText + oBottom - oTop, bkcolor); // Clear text area
    }

    delay(0);
  }

  _display->setCursor(_x + oLeft, _y); // add left offset to center, _y may be updated
  _display->print(newString);
}

/****************************************************************************
 * getTextSize length and height in pixels
 ***************************************************************************/
uint16_t AdafruitGFX_helper::getTextSize(const String& text,
                                         uint16_t    & h) {
  int16_t  x;
  int16_t  y;
  uint16_t w;

  _display->getTextBounds(text.c_str(), 0, 0, &x, &y, &w, &h); // Count length and height in pixels
  return w;
}

/****************************************************************************
 * color565: convert r, g, b colors to rgb565 (by bit-shifting)
 ***************************************************************************/
uint16_t color565(const uint8_t& red,
                  const uint8_t& green,
                  const uint8_t& blue) {
  return ((red & 0xF8) << 8) | ((green & 0xFC) << 3) | (blue >> 3);
}

/****************************************************************************
 * AdaGFXparseColor: translate color name, rgb565 hex #rGgb or rgb hex #RRGGBB to an RGB565 value,
 * also applies color reduction to mono(2), duo(3), quadro(4), septo(7), octo(8), quinto(16)-chrome colors
 ***************************************************************************/

// Parse color string to RGB565 color
// param [in] s : The color string (white, red, ...)
// Param [in] colorDepth: The requiresed color depth, default: FullColor
// param [in] defaultWhite: Return White color if empty, default: true
// return : color (default ADAGFX_WHITE)
const char adagfx_colornames[] PROGMEM = "black|white|inverse|red|yellow|dark|light|green|blue|orange|navy|darkcyan|"
                                         "darkgreen|maroon|purple|olive|lightgrey|darkgrey|cyan|magenta|greenyellow|pink";
enum class adagfx_colornames_e : int8_t {
  invalid = -1,
  black   = 0,
  white,
  inverse,
  red,
  yellow,
  dark,
  light,
  green,
  blue,
  orange,
  navy,
  darkcyan,
  darkgreen,
  maroon,
  purple,
  olive,
  lightgrey,
  darkgrey,
  cyan,
  magenta,
  greenyellow,
  pink,
};

uint16_t AdaGFXparseColor(String                & s,
                          const AdaGFXColorDepth& colorDepth,
                          const bool              emptyIsBlack) {
  s.toLowerCase();
  int32_t   result  = -1; // No result yet
  const int color_i = GetCommandCode(s.c_str(), adagfx_colornames);

  const adagfx_colornames_e color = static_cast<adagfx_colornames_e>(color_i);

  if ((colorDepth == AdaGFXColorDepth::Monochrome) ||
      (colorDepth == AdaGFXColorDepth::BlackWhiteRed) ||
      (colorDepth == AdaGFXColorDepth::BlackWhite2Greyscales)) { // Only a limited set of colors is supported
    switch (color) {
      case adagfx_colornames_e::black: return static_cast<uint16_t>(AdaGFXMonoRedGreyscaleColors::ADAGFXEPD_BLACK);
      case adagfx_colornames_e::inverse: return static_cast<uint16_t>(AdaGFXMonoRedGreyscaleColors::ADAGFXEPD_INVERSE);
      case adagfx_colornames_e::yellow: // Synonym for red
      case adagfx_colornames_e::red: return static_cast<uint16_t>(AdaGFXMonoRedGreyscaleColors::ADAGFXEPD_RED);
      case adagfx_colornames_e::dark: return static_cast<uint16_t>(AdaGFXMonoRedGreyscaleColors::ADAGFXEPD_DARK);
      case adagfx_colornames_e::light: return static_cast<uint16_t>(AdaGFXMonoRedGreyscaleColors::ADAGFXEPD_LIGHT);

      // case adagfx_colornames_e::white: return static_cast<uint16_t>(AdaGFXMonoRedGreyscaleColors::ADAGFXEPD_WHITE);
      // If we get this far, return the default
      default:
        return static_cast<uint16_t>(AdaGFXMonoRedGreyscaleColors::ADAGFXEPD_WHITE);
    }
  # if ADAGFX_SUPPORT_7COLOR
  } else if (colorDepth == AdaGFXColorDepth::SevenColor) {
    switch (color) {
      case adagfx_colornames_e::black: result  = static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_BLACK); break;
      case adagfx_colornames_e::white: result  = static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_WHITE); break;
      case adagfx_colornames_e::green: result  = static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_GREEN); break;
      case adagfx_colornames_e::blue: result   = static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_BLUE); break;
      case adagfx_colornames_e::red: result    = static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_RED); break;
      case adagfx_colornames_e::yellow: result = static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_YELLOW); break;
      case adagfx_colornames_e::orange: result = static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_ORANGE); break;
      default:
        break;
    }
  # endif // if ADAGFX_SUPPORT_7COLOR
  } else { // Some predefined colors
    switch (color) {
      case adagfx_colornames_e::black: result       = ADAGFX_BLACK; break;
      case adagfx_colornames_e::navy: result        = ADAGFX_NAVY; break;
      case adagfx_colornames_e::darkgreen: result   = ADAGFX_DARKGREEN; break;
      case adagfx_colornames_e::darkcyan: result    = ADAGFX_DARKCYAN; break;
      case adagfx_colornames_e::maroon: result      = ADAGFX_MAROON; break;
      case adagfx_colornames_e::purple: result      = ADAGFX_PURPLE; break;
      case adagfx_colornames_e::olive: result       = ADAGFX_OLIVE; break;
      case adagfx_colornames_e::lightgrey: result   = ADAGFX_LIGHTGREY; break;
      case adagfx_colornames_e::darkgrey: result    = ADAGFX_DARKGREY; break;
      case adagfx_colornames_e::blue: result        = ADAGFX_BLUE; break;
      case adagfx_colornames_e::green: result       = ADAGFX_GREEN; break;
      case adagfx_colornames_e::cyan: result        = ADAGFX_CYAN; break;
      case adagfx_colornames_e::red: result         = ADAGFX_RED; break;
      case adagfx_colornames_e::magenta: result     = ADAGFX_MAGENTA; break;
      case adagfx_colornames_e::yellow: result      = ADAGFX_YELLOW; break;
      case adagfx_colornames_e::white: result       = ADAGFX_WHITE; break;
      case adagfx_colornames_e::orange: result      = ADAGFX_ORANGE; break;
      case adagfx_colornames_e::greenyellow: result = ADAGFX_GREENYELLOW; break;
      case adagfx_colornames_e::pink: result        = ADAGFX_PINK; break;
      default:
        break;
    }
  }

  // Parse default hex #rgb565 (hex) string (1-4 hex nibbles accepted!)
  if ((result == -1) && (s.length() >= 2) && (s.length() <= 5) && (s[0] == '#')) {
    result = hexToUL(&s[1]);
  }

  // Parse default hex #RRGGBB string (must be 6 hex nibbles!)
  if ((result == -1) && (s.length() == 7) && (s[0] == '#')) {
    // convrt to long value in base16, then split up into r, g, b values
    const uint32_t number = hexToUL(&s[1]);

    // uint32_t r = number >> 16 & 0xFF;
    // uint32_t g = number >> 8 & 0xFF;
    // uint32_t b = number & 0xFF;
    // convert to color565 (as used by adafruit lib)
    result = color565(number >> 16 & 0xFF, number >> 8 & 0xFF, number & 0xFF);
  }

  if ((result == -1) || (result == ADAGFX_WHITE)) { // Default & don't convert white
    # if ADAGFX_SUPPORT_8and16COLOR

    if (
      #  if ADAGFX_SUPPORT_7COLOR
      (colorDepth >= AdaGFXColorDepth::SevenColor) &&
      #  endif // if ADAGFX_SUPPORT_7COLOR
      (colorDepth <= AdaGFXColorDepth::SixteenColor)) {
      result = static_cast<uint16_t>(AdaGFXMonoRedGreyscaleColors::ADAGFXEPD_BLACK); // Monochrome fallback, compatible 7-color
    } else
    # endif // if ADAGFX_SUPPORT_8and16COLOR
    {
      if (emptyIsBlack) {
        result = ADAGFX_BLACK;
      } else {
        result = ADAGFX_WHITE; // Color fallback value
      }
    }
  } else {
    // Reduce colors?
    switch (colorDepth) {
      case AdaGFXColorDepth::Monochrome:
      case AdaGFXColorDepth::BlackWhiteRed:
      case AdaGFXColorDepth::BlackWhite2Greyscales:
        // Unsupported at this point, but compiler needs the cases because of the enum class
        break;
      # if ADAGFX_SUPPORT_7COLOR
      case AdaGFXColorDepth::SevenColor:
        result = AdaGFXrgb565ToColor7(result); // Convert
        break;
      # endif  // if ADAGFX_SUPPORT_7COLOR
      # if ADAGFX_SUPPORT_8and16COLOR
      case AdaGFXColorDepth::EightColor:
        result = color565((result >> 11 & 0x1F) / 4, (result >> 5 & 0x3F) / 4, (result & 0x1F) / 4); // reduce colors factor 4
        break;
      case AdaGFXColorDepth::SixteenColor:
        result = color565((result >> 11 & 0x1F) / 2, (result >> 5 & 0x3F) / 2, (result & 0x1F) / 2); // reduce colors factor 2
        break;
      # endif // if ADAGFX_SUPPORT_8and16COLOR
      case AdaGFXColorDepth::FullColor:
        // No color reduction
        break;
    }
  }
  return static_cast<uint16_t>(result);
}

const __FlashStringHelper* AdaGFXcolorToString_internal(const uint16_t        & color,
                                                        const AdaGFXColorDepth& colorDepth,
                                                        bool                    blackIsEmpty);

// Add a single optionvalue of a color to a datalist (internal/private)
void AdaGFXaddHtmlDataListColorOptionValue(uint16_t         color,
                                           AdaGFXColorDepth colorDepth) {
  const __FlashStringHelper *clr = AdaGFXcolorToString_internal(color, colorDepth, false);

  if (!equals(clr, '*')) {
    datalistAddValue(clr);
  }
}

/*****************************************************************************************
 * Generate a html 'datalist' of the colors available for selected colorDepth, with id provided
 ****************************************************************************************/
void AdaGFXHtmlColorDepthDataList(const __FlashStringHelper *id,
                                  const AdaGFXColorDepth   & colorDepth) {
  datalistStart(id);

  switch (colorDepth) {
    case AdaGFXColorDepth::BlackWhiteRed:
    case AdaGFXColorDepth::BlackWhite2Greyscales:
      AdaGFXaddHtmlDataListColorOptionValue(static_cast<uint16_t>(AdaGFXMonoRedGreyscaleColors::ADAGFXEPD_RED),   colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(static_cast<uint16_t>(AdaGFXMonoRedGreyscaleColors::ADAGFXEPD_DARK),  colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(static_cast<uint16_t>(AdaGFXMonoRedGreyscaleColors::ADAGFXEPD_LIGHT), colorDepth);

    // Fall through
    case AdaGFXColorDepth::Monochrome:
      AdaGFXaddHtmlDataListColorOptionValue(static_cast<uint16_t>(AdaGFXMonoRedGreyscaleColors::ADAGFXEPD_BLACK),   colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(static_cast<uint16_t>(AdaGFXMonoRedGreyscaleColors::ADAGFXEPD_WHITE),   colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(static_cast<uint16_t>(AdaGFXMonoRedGreyscaleColors::ADAGFXEPD_INVERSE), colorDepth);
      break;
    # if ADAGFX_SUPPORT_7COLOR
    case AdaGFXColorDepth::SevenColor:
    {
      AdaGFXaddHtmlDataListColorOptionValue(static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_BLACK),  colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_WHITE),  colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_GREEN),  colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_BLUE),   colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_RED),    colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_YELLOW), colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_ORANGE), colorDepth);
      break;
    }
    # endif // if ADAGFX_SUPPORT_7COLOR
    # if ADAGFX_SUPPORT_8and16COLOR
    case AdaGFXColorDepth::EightColor: // TODO: Sort out the actual 8/16 color options
    case AdaGFXColorDepth::SixteenColor:
    # endif // if ADAGFX_SUPPORT_8and16COLOR
    case AdaGFXColorDepth::FullColor:
    {
      AdaGFXaddHtmlDataListColorOptionValue(ADAGFX_BLACK,       colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(ADAGFX_NAVY,        colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(ADAGFX_DARKGREEN,   colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(ADAGFX_DARKCYAN,    colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(ADAGFX_MAROON,      colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(ADAGFX_PURPLE,      colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(ADAGFX_OLIVE,       colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(ADAGFX_LIGHTGREY,   colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(ADAGFX_DARKGREY,    colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(ADAGFX_BLUE,        colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(ADAGFX_GREEN,       colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(ADAGFX_CYAN,        colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(ADAGFX_RED,         colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(ADAGFX_MAGENTA,     colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(ADAGFX_YELLOW,      colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(ADAGFX_WHITE,       colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(ADAGFX_ORANGE,      colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(ADAGFX_GREENYELLOW, colorDepth);
      AdaGFXaddHtmlDataListColorOptionValue(ADAGFX_PINK,        colorDepth);
      break;
    }
  }
  datalistFinish();
}

/*****************************************************************************************
 * Convert an RGB565 color (number) to it's name or the #rgb565 hex string, based on depth
 ****************************************************************************************/
String AdaGFXcolorToString(const uint16_t        & color,
                           const AdaGFXColorDepth& colorDepth,
                           bool                    blackIsEmpty) {
  String result = AdaGFXcolorToString_internal(color, colorDepth, blackIsEmpty);

  if (equals(result, '*')) {
    result  = '#';
    result += String(color, HEX);
    result.toUpperCase();
  }
  return result;
}

const __FlashStringHelper* AdaGFXcolorToString_internal(const uint16_t        & color,
                                                        const AdaGFXColorDepth& colorDepth,
                                                        bool                    blackIsEmpty) {
  switch (colorDepth) {
    case AdaGFXColorDepth::Monochrome:
    case AdaGFXColorDepth::BlackWhiteRed:
    case AdaGFXColorDepth::BlackWhite2Greyscales:
    {
      switch (color) {
        case static_cast<uint16_t>(AdaGFXMonoRedGreyscaleColors::ADAGFXEPD_BLACK): return blackIsEmpty ? F("") : F("black");
        case ADAGFX_WHITE: // Fall through
        case static_cast<uint16_t>(AdaGFXMonoRedGreyscaleColors::ADAGFXEPD_WHITE): return F("white");
        case static_cast<uint16_t>(AdaGFXMonoRedGreyscaleColors::ADAGFXEPD_INVERSE): return F("inverse");
        case static_cast<uint16_t>(AdaGFXMonoRedGreyscaleColors::ADAGFXEPD_RED): return F("red");
        case static_cast<uint16_t>(AdaGFXMonoRedGreyscaleColors::ADAGFXEPD_DARK): return F("dark");
        case static_cast<uint16_t>(AdaGFXMonoRedGreyscaleColors::ADAGFXEPD_LIGHT): return F("light");
        default:
          break;
      }
      break;
    }
    # if ADAGFX_SUPPORT_7COLOR
    case AdaGFXColorDepth::SevenColor:
    {
      switch (color) {
        case static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_BLACK): return blackIsEmpty ? F("") : F("black");
        case ADAGFX_WHITE: // Fall through
        case static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_WHITE): return F("white");
        case static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_GREEN): return F("green");
        case static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_BLUE): return F("blue");
        case static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_RED): return F("red");
        case static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_YELLOW): return F("yellow");
        case static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_ORANGE): return F("orange");
        default:
          break;
      }
      break;
    }
    # endif // if ADAGFX_SUPPORT_7COLOR
    # if ADAGFX_SUPPORT_8and16COLOR
    case AdaGFXColorDepth::EightColor:
    case AdaGFXColorDepth::SixteenColor:
    # endif // if ADAGFX_SUPPORT_8and16COLOR
    case AdaGFXColorDepth::FullColor:
    {
      switch (color) {
        case ADAGFX_BLACK:  return blackIsEmpty ? F("") : F("black");
        case ADAGFX_NAVY: return F("navy");
        case ADAGFX_DARKGREEN: return F("darkgreen");
        case ADAGFX_DARKCYAN: return F("darkcyan");
        case ADAGFX_MAROON: return F("maroon");
        case ADAGFX_PURPLE: return F("purple");
        case ADAGFX_OLIVE: return F("olive");
        case ADAGFX_LIGHTGREY: return F("lightgrey");
        case ADAGFX_DARKGREY: return F("darkgrey");
        case ADAGFX_BLUE: return F("blue");
        case ADAGFX_GREEN: return F("green");
        case ADAGFX_CYAN: return F("cyan");
        case ADAGFX_RED: return F("red");
        case ADAGFX_MAGENTA: return F("magenta");
        case ADAGFX_YELLOW: return F("yellow");
        case ADAGFX_WHITE: return F("white");
        case ADAGFX_ORANGE: return F("orange");
        case ADAGFX_GREENYELLOW: return F("greenyellow");
        case ADAGFX_PINK: return F("pink");
        default:
          break;
      }
      break;
    }
  }
  return F("*");
}

# if ADAGFX_SUPPORT_7COLOR

/****************************************************************************
 * AdaGFXrgb565ToColor7: Convert a rgb565 color to the 7 colors supported by 7-color eInk displays
 * Borrowed from https://github.com/ZinggJM/GxEPD2 color7() routine
 ***************************************************************************/
uint16_t AdaGFXrgb565ToColor7(const uint16_t& color) {
  const uint16_t red   = (color & 0xF800);
  const uint16_t green = (color & 0x07E0) << 5;
  const uint16_t blue  = (color & 0x001F) << 11;
  uint16_t cv7         = static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_WHITE); // Default = white

  if ((red < 0x8000) && (green < 0x8000) && (blue < 0x8000)) {
    cv7 = static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_BLACK);                // black
  }
  else if ((red >= 0x8000) && (green >= 0x8000) && (blue >= 0x8000)) {
    cv7 = static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_WHITE);                // white
  }
  else if ((red >= 0x8000) && (blue >= 0x8000)) {
    if (red > blue) {
      cv7 = static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_RED);
    } else {
      cv7 = static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_BLUE); // red, blue
    }
  }
  else if ((green >= 0x8000) && (blue >= 0x8000)) {
    if (green > blue) {
      cv7 = static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_GREEN);
    } else {
      cv7 = static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_BLUE); // green, blue
    }
  }
  else if ((red >= 0x8000) && (green >= 0x8000)) {
    static const uint16_t y2o_lim = ((ADAGFX_YELLOW - ADAGFX_ORANGE) / 2 + (ADAGFX_ORANGE & 0x07E0)) << 5;

    if (green > y2o_lim) {
      cv7 = static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_YELLOW);
    } else {
      cv7 = static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_ORANGE); // yellow, orange
    }
  }
  else if (red >= 0x8000) {
    cv7 = static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_RED);   // red
  }
  else if (green >= 0x8000) {
    cv7 = static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_GREEN); // green
  }
  else {
    cv7 = static_cast<uint16_t>(AdaGFX7Colors::ADAGFX7C_BLUE);  // blue
  }
  return cv7;
}

# endif // if ADAGFX_SUPPORT_7COLOR

/*****************************************************************************
 * Convert rgb565 color to the closest matching rgb888 (RRGGBB) color
 * (some approximation because of loss of resolution in rgb565)
 ****************************************************************************/
uint32_t AdaGFXrgb565ToRgb888(uint16_t rgb565) {
  const uint16_t r = (((rgb565 & 0xF800) >> 11) * 255) / 31;
  const uint16_t g = (((rgb565 & 0x07E0) >> 5) * 255) / 63;
  const uint16_t b = ((rgb565 & 0x001F) * 255) / 31;

  return (r << 16) + (g << 8) + b;
}

/****************************************************************************
 * Convert rgb565 color to web-usable color (# prefix with 6 digit hex value)
 ***************************************************************************/
String AdaGFXrgb565ToWebColor(uint16_t rgb565) {
  return strformat(F("#%06X"), AdaGFXrgb565ToRgb888(rgb565));
}

/****************************************************************************
 * Convert a 24 bit RRGGBB color to rgb565 format. Some bits are lost
 ***************************************************************************/
uint16_t AdaGFXrgb888ToRgb565(uint32_t rgb888) {
  const uint8_t r = (rgb888 & 0xFF0000) >> 16;
  const uint8_t g = (rgb888 & 0xFF00) >> 8;
  const uint8_t b = (rgb888 & 0xFF);

  return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
}

/****************************************************************************
 * getTextMetrics: Returns the metrics related to current font
 ***************************************************************************/
void AdafruitGFX_helper::getTextMetrics(uint16_t& textcols,
                                        uint16_t& textrows,
                                        uint8_t & fontwidth,
                                        uint8_t & fontheight,
                                        uint8_t & fontscaling,
                                        uint8_t & heightOffset,
                                        uint16_t& xpix,
                                        uint16_t& ypix) {
  textcols     = _textcols;
  textrows     = _textrows;
  fontwidth    = _fontwidth;
  fontheight   = _fontheight;
  fontscaling  = _fontscaling;
  heightOffset = _heightOffset;
  # if ADAGFX_ENABLE_FRAMED_WINDOW
  getWindowLimits(xpix, ypix);
  # else // if ADAGFX_ENABLE_FRAMED_WINDOW
  xpix = _res_x;
  ypix = _res_y;
  # endif // if ADAGFX_ENABLE_FRAMED_WINDOW
}

/****************************************************************************
 * getColors: Returns the current text colors
 ***************************************************************************/
void AdafruitGFX_helper::getColors(uint16_t& fgcolor,
                                   uint16_t& bgcolor) {
  fgcolor = _fgcolor;
  bgcolor = _bgcolor;
}

/****************************************************************************
 * calculateTextMetrics: Recalculate the text mertics based on supplied font parameters
 ***************************************************************************/
void AdafruitGFX_helper::calculateTextMetrics(const uint8_t fontwidth,
                                              const uint8_t fontheight,
                                              const int8_t  heightOffset,
                                              const bool    isProportional) {
  uint16_t res_x = _res_x;
  uint16_t res_y = _res_y;

  # if ADAGFX_ENABLE_FRAMED_WINDOW
  getWindowLimits(res_x, res_y);
  # endif // if ADAGFX_ENABLE_FRAMED_WINDOW

  _fontwidth      = fontwidth;
  _fontheight     = fontheight;
  _heightOffset   = heightOffset;
  _isProportional = isProportional;
  _textcols       = res_x / (_fontwidth * _fontscaling);
  _textrows       = res_y / ((_fontheight + _heightOffset) * _fontscaling);

  # ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(ADAGFX_LOG_LEVEL)) {
    addLogMove(ADAGFX_LOG_LEVEL, strformat(F("AdaGFX: tr: %s x: %d, y: %d, text columns: %d rows: %d"),
                                           _trigger.c_str(), res_x, res_y, _textcols, _textrows));
  }
  # endif // ifndef BUILD_NO_DEBUG
}

# if ADAGFX_ARGUMENT_VALIDATION

/****************************************************************************
 * invalidCoordinates: Check if X/Y coordinates stay within the limits of the display,
 * default pixel-mode, colRowMode true = character mode.
 * If Y == 0 then X is allowed the max. value of the display size.
 * *** Returns TRUE when invalid !! ***
 ***************************************************************************/
bool AdafruitGFX_helper::invalidCoordinates(const int  X,
                                            const int  Y,
                                            const bool colRowMode) {
  uint16_t res_x = _res_x;
  uint16_t res_y = _res_y;

  #  if ADAGFX_ENABLE_FRAMED_WINDOW
  getWindowLimits(res_x, res_y);
  #  endif // if ADAGFX_ENABLE_FRAMED_WINDOW

  #  ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(ADAGFX_LOG_LEVEL)) {
    addLogMove(ADAGFX_LOG_LEVEL, strformat(F("invalidCoordinates: X:%d/%d Y:%d/%d"),
                                           X, colRowMode ? _textcols : res_x, Y, colRowMode ? _textrows : res_y));
  }
  #  endif // ifndef BUILD_NO_DEBUG

  if (!_useValidation) { return false; }

  if (colRowMode) {
    return !((X >= 0) && (X <= _textcols) &&
             (Y >= 0) && (Y <= _textrows));
  } else {
    if (Y == 0) { // Y == 0: Accept largest x/y size value for x
      return !((X >= 0) && (X <= std::max(res_x, res_y)));
    } else {
      return !((X >= 0) && (X <= res_x) &&
               (Y >= 0) && (Y <= res_y));
    }
  }
}

# endif // if ADAGFX_ARGUMENT_VALIDATION

void AdafruitGFX_helper::setValidation(const bool& state) {
  _useValidation = state;
}

/****************************************************************************
 * rotate the display (and all windows)
 ***************************************************************************/
void AdafruitGFX_helper::setRotation(uint8_t m) {
  const uint8_t rotation = m & 3;

  _display->setRotation(m); // Set rotation 0/1/2/3
  _rotation = rotation;

  switch (rotation) {
    case 0:
    case 2:
      _res_x = _display_x;
      _res_y = _display_y;
      break;
    case 1:
    case 3:
      _res_x = _display_y;
      _res_y = _display_x;
      break;
  }
  # if ADAGFX_ENABLE_FRAMED_WINDOW

  for (uint8_t i = 0; i < _windows.size(); ++i) {                // Swap x/y for all matching windows
    switch (rotation) {
      case 0:                                                    // 0 degrees
        _windows[i].top_left.x     = _windows[i].org_top_left.x; // All original
        _windows[i].top_left.y     = _windows[i].org_top_left.y;
        _windows[i].width_height.x = _windows[i].org_width_height.x;
        _windows[i].width_height.y = _windows[i].org_width_height.y;
        break;
      case 1:                                                        // +90 degrees
        _windows[i].top_left.x     = _windows[i].org_top_left.y;
        _windows[i].top_left.y     = _display_x - (_windows[i].org_top_left.x + _windows[i].org_width_height.x);
        _windows[i].width_height.x = _windows[i].org_width_height.y; // swapped width/height
        _windows[i].width_height.y = _windows[i].org_width_height.x;
        break;
      case 2:                                                        // +180 degrees
        _windows[i].top_left.x     = _display_x - (_windows[i].org_top_left.x + _windows[i].org_width_height.x);
        _windows[i].top_left.y     = _display_y - (_windows[i].org_top_left.y + _windows[i].org_width_height.y);
        _windows[i].width_height.x = _windows[i].org_width_height.x;
        _windows[i].width_height.y = _windows[i].org_width_height.y;
        break;
      case 3:                                                        // +270 degrees
        _windows[i].top_left.x     = _display_y - (_windows[i].org_top_left.y + _windows[i].org_width_height.y);
        _windows[i].top_left.y     = _windows[i].org_top_left.x;
        _windows[i].width_height.x = _windows[i].org_width_height.y; // swapped width/height
        _windows[i].width_height.y = _windows[i].org_width_height.x;
        break;
    }
    _windows[i].rotation = rotation;
  }

  // logWindows(F("rot ")); // For debugging only
  # endif // if ADAGFX_ENABLE_FRAMED_WINDOW
  calculateTextMetrics(_fontwidth, _fontheight, _heightOffset, _isProportional);
}

# if ADAGFX_ENABLE_BMP_DISPLAY

/****************************************************************************
 * CPA (Copy/paste/adapt) from Adafruit_ImageReader::coreBMP()
 * Changes:
 * - No 'load to memory' feature
 * - No special handling of SD Filesystem/FAT, but File only
 * - Adds support for non-SPI displays (like NeoPixel Matrix, and possibly I2C displays, once supported)
 ***************************************************************************/
bool AdafruitGFX_helper::showBmp(const String& filename,
                                 int16_t       x,
                                 int16_t       y) {
  uint32_t offset;               // Start of image data in file
  uint32_t headerSize;           // Indicates BMP version
  uint32_t compression = 0;      // BMP compression mode
  uint32_t colors      = 0;      // Number of colors in palette
  uint32_t rowSize;              // >bmpWidth if scanline padding
  uint8_t  sdbuf[3 * BUFPIXELS]; // BMP read buf (R+G+B/pixel)

  uint32_t destidx = 0;
  uint32_t bmpPos  = 0;          // Next pixel position in file
  int bmpWidth;                  // BMP width & height in pixels
  int bmpHeight;
  int loadWidth;
  int loadHeight;                // Region being loaded (clipped)
  int loadX;
  int loadY;                     // "
  int row;                       // Current pixel pos.
  int col;
  uint16_t *quantized = NULL;    // 16-bit 5/6/5 color palette
  uint16_t  tftbuf[BUFPIXELS];
  uint16_t *dest = tftbuf;       // TFT working buffer, or NULL if to canvas
  int16_t   drow = 0;
  int16_t   dcol = 0;
  uint8_t   planes;              // BMP planes
  uint8_t   depth;               // BMP bit depth
  uint8_t   r;                   // Current pixel colors
  uint8_t   g;
  uint8_t   b;
  uint8_t   bitIn = 0;           // Bit number for 1-bit data in

  #  if ((3 * BUFPIXELS) <= 255)
  uint8_t srcidx = sizeof sdbuf; // Current position in sdbuf
  #  else // if ((3 * BUFPIXELS) <= 255)
  uint16_t srcidx = sizeof sdbuf;
  #  endif // if ((3 * BUFPIXELS) <= 255)
  bool flip     = true;  // BMP is stored bottom-to-top
  bool transact = true;  // Enable transaction support to work proper with SD czrd, when enabled
  bool status   = false; // IMAGE_SUCCESS on valid file

  bool canTransact = (nullptr != _tft);

  // If BMP is being drawn off the right or bottom edge of the screen,
  // nothing to do here. NOT an error, just a trivial clip operation.
  if (_tft && ((x >= _tft->width()) || (y >= _tft->height()))) {
    addLog(LOG_LEVEL_INFO, F("showBmp: coordinates off display"));
    return false;
  }

  // Open requested file on storage
  // Search flash file system first, then SD if present
  file = tryOpenFile(filename, "r");

  if (!file) {
    addLog(LOG_LEVEL_ERROR, F("showBmp: file not found"));
    return false;
  }

  size_t myPos{};

  // Parse BMP header. 0x4D42 (ASCII 'BM') is the Windows BMP signature.
  // There are other values possible in a .BMP file but these are super
  // esoteric (e.g. OS/2 struct bitmap array) and NOT supported here!
  const uint16_t sig = readLE16();
  myPos += 2;

  if (sig == 0x4D42) {   // BMP signature
    (void)readLE32();    // Read & ignore file size
    (void)readLE32();    // Read & ignore creator bytes
    offset = readLE32(); // Start of image data
    // Read DIB header
    headerSize = readLE32();
    bmpWidth   = readLE32();
    bmpHeight  = readLE32();
    myPos     += 24;

    // If bmpHeight is negative, image is in top-down order.
    // This is not canon but has been observed in the wild.
    if (bmpHeight < 0) {
      bmpHeight = -bmpHeight;
      flip      = false;
    }
    planes = readLE16();
    depth  = readLE16(); // Bits per pixel
    myPos += 4;

    // Compression mode is present in later BMP versions (default = none)
    if (headerSize > 12) {
      compression = readLE32();
      (void)readLE32();    // Raw bitmap data size; ignore
      (void)readLE32();    // Horizontal resolution, ignore
      (void)readLE32();    // Vertical resolution, ignore
      colors = readLE32(); // Number of colors in palette, or 0 for 2^depth
      (void)readLE32();    // Number of colors used (ignore)
      // File position should now be at start of palette (if present)
      myPos += 24;
    }

    if (!colors) {
      colors = 1 << depth;
    }
    #  ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO, strformat(F("showBmp: bitmap w:%d, h:%d, dpt:%d, colors:%d, cmp:%d, pl:%d, x:%d, y:%d"),
                                       bmpWidth, bmpHeight, depth, colors, compression, planes, x, y));
    }
    #  endif // ifndef BUILD_NO_DEBUG

    loadWidth  = bmpWidth;
    loadHeight = bmpHeight;
    loadX      = 0;
    loadY      = 0;

    if (_display) {
      // Crop area to be loaded (if destination is TFT)
      if (x < 0) {
        loadX      = -x;
        loadWidth += x;
        x          = 0;
      }

      if (y < 0) {
        loadY       = -y;
        loadHeight += y;
        y           = 0;
      }

      if ((x + loadWidth) > _display->width()) {
        loadWidth = _display->width() - x;
      }

      if ((y + loadHeight) > _display->height()) {
        loadHeight = _display->height() - y;
      }
    }
    #  ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, strformat(F("showBmp: x:%d, y:%d, dw:%d, dh:%d"),
                                           x, y, _display->width(), _display->height()));
    }
    #  endif // ifndef BUILD_NO_DEBUG

    if ((planes == 1) && (compression == 0)) { // Only uncompressed is handled
      // BMP rows are padded (if needed) to 4-byte boundary
      rowSize = ((depth * bmpWidth + 31) / 32) * 4;

      if ((depth == 24) || (depth == 1)) { // BGR or 1-bit bitmap format
        // if (dest) {                     // Supported format, alloc OK, etc.
        status = true;

        if ((loadWidth > 0) && (loadHeight > 0)) { // Clip top/left
          _display->startWrite();                  // Start SPI (regardless of transact)

          if (canTransact) {
            _tft->setAddrWindow(x, y, loadWidth, loadHeight);
          }

          if ((depth >= 16) ||
              (quantized = (uint16_t *)malloc(colors * sizeof(uint16_t)))) {
            if (depth < 16) {
              // Load and quantize color table
              for (uint16_t c = 0; c < colors; ++c) {
                b = file.read();
                g = file.read();
                r = file.read();
                (void)file.read(); // Ignore 4th byte
                myPos       += 4;
                quantized[c] =     // -V522
                               ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
              }
            }

            for (row = 0; row < loadHeight; ++row) { // For each scanline...
              delay(0);                              // Keep ESP8266 happy

              // Seek to start of scan line.  It might seem labor-intensive
              // to be doing this on every line, but this method covers a
              // lot of gritty details like cropping, flip and scanline
              // padding. Also, the seek only takes place if the file
              // position actually needs to change (avoids a lot of cluster
              // math in SD library).
              if (flip) { // Bitmap is stored bottom-to-top order (normal BMP)
                bmpPos = offset + (bmpHeight - 1 - (row + loadY)) * rowSize;
              } else {    // Bitmap is stored top-to-bottom
                bmpPos = offset + (row + loadY) * rowSize;
              }

              if (depth == 24) {
                bmpPos += loadX * 3;
              } else {
                bmpPos += loadX / 8;
                bitIn   = 7 - (loadX & 7);
              }

              constexpr size_t errorcode = (size_t)-1;
              size_t pos                 = myPos; // file.position(); // position() doesn't seem to work for SD files

              if (pos == errorcode) {
                pos = 0;
              }

              if (pos != bmpPos) {                    // Need seek?
                if (transact && canTransact) {
                  _tft->dmaWait();
                  _tft->endWrite();                   // End TFT SPI transaction
                }
                file.seek(bmpPos);                    // Seek = SD transaction
                myPos  = bmpPos;                      // Re-sync
                srcidx = sizeof sdbuf;                // Force buffer reload
              }

              for (col = 0; col < loadWidth; ++col) { // For each pixel...
                if (srcidx >= sizeof sdbuf) {         // Time to load more?
                  if (transact && canTransact) {
                    _tft->dmaWait();
                    _tft->endWrite();                 // End TFT SPI transact
                  }
                  file.read(sdbuf, sizeof sdbuf);     // Load from SD
                  myPos += sizeof sdbuf;              // Let's assume we read a full buffer...

                  if (transact && canTransact) {
                    _display->startWrite();           // Start TFT SPI transact
                  }

                  if (destidx) {                      // If buffered TFT data
                    // Non-blocking writes (DMA) have been temporarily
                    // disabled until this can be rewritten with two
                    // alternating 'dest' buffers (else the nonblocking
                    // data out is overwritten in the dest[] write below).
                    // tft->writePixels(dest, destidx, false); // Write it
                    delay(0);

                    if (canTransact) {
                      _tft->writePixels(dest, destidx, true); // Write it
                    } else {
                      // loop over buffer

                      for (uint16_t p = 0; p < destidx; ++p) {
                        _display->drawPixel(x + p, y + drow, dest[p]);
                      }
                    }

                    if (col % 33 == 0) { delay(0); }
                    destidx = 0; // and reset dest index
                  }

                  srcidx = 0;    // Reset bmp buf index
                }

                if (depth == 24) {
                  // Convert each pixel from BMP to 565 format, save in dest
                  b               = sdbuf[srcidx++];
                  g               = sdbuf[srcidx++]; // -V557
                  r               = sdbuf[srcidx++]; // -V557
                  dest[destidx++] =
                    ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
                } else {
                  // Extract 1-bit color index
                  uint8_t n = (sdbuf[srcidx] >> bitIn) & 1;

                  if (!bitIn) {
                    srcidx++;
                    bitIn = 7;
                  } else {
                    bitIn--;
                  }

                  // Look up in palette, store in tft dest buf
                  dest[destidx++] = quantized[n];
                }
                dcol++;
              }                                           // end pixel loop

              if (_tft) {                                 // Drawing to TFT?
                delay(0);

                if (destidx) {                            // Any remainders?
                  // See notes above re: DMA
                  _tft->writePixels(dest, destidx, true); // Write it
                  destidx = 0;                            // and reset dest index
                }
                _tft->dmaWait();
                _tft->endWrite();                         // update display
              } else {
                // loop over buffer
                if (destidx) {
                  for (uint16_t p = 0; p < destidx; ++p) {
                    _display->drawPixel(x + p, y + drow, dest[p]);

                    if (p % 100 == 0) { delay(0); }
                  }
                  destidx = 0; // and reset dest index
                }
              }

              drow++;
              dcol = 0;
            }                  // end scanline loop

            if (quantized) {
              free(quantized); // Palette no longer needed
            }
            delay(0);
          }                    // end depth>24 or quantized malloc OK
        }                      // end top/left clip
        // }                         // end malloc check
      }                        // end depth check
    } // end planes/compression check

    if (status) {
    #  ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_INFO, F("showBmp: Done."));
    #  endif // ifndef BUILD_NO_DEBUG
    } else {
      addLog(LOG_LEVEL_ERROR, F("showBmp: Only uncompressed and 24 or 1 bit color-depth supported."));
    }
  } else { // end signature
    addLog(LOG_LEVEL_ERROR, strformat(F("showBmp: File signature error. (0x%04X)"), sig));
  }

  file.close();
  return status; // -V680

  // }
}

/*!
    @brief   Reads a little-endian 16-bit unsigned value from currently-
             open File, converting if necessary to the microcontroller's
             native endianism. (BMP files use little-endian values.)
    @return  Unsigned 16-bit value, native endianism.
 */
uint16_t AdafruitGFX_helper::readLE16(void) {
  // Big-endian or unknown. Byte-by-byte read will perform reversal if needed.
  return file.read() | ((uint16_t)file.read() << 8);
}

/*!
    @brief   Reads a little-endian 32-bit unsigned value from currently-
             open File, converting if necessary to the microcontroller's
             native endianism. (BMP files use little-endian values.)
    @return  Unsigned 32-bit value, native endianism.
 */
uint32_t AdafruitGFX_helper::readLE32(void) {
  // Big-endian or unknown. Byte-by-byte read will perform reversal if needed.
  return file.read() | ((uint32_t)file.read() << 8) |
         ((uint32_t)file.read() << 16) | ((uint32_t)file.read() << 24);
}

# endif // if ADAGFX_ENABLE_BMP_DISPLAY

# if ADAGFX_ENABLE_FRAMED_WINDOW

/****************************************************************************
 * Check if the requested id is a valid window id
 ***************************************************************************/
bool AdafruitGFX_helper::validWindow(const uint8_t& windowId) {
  return getWindowIndex(windowId) != -1;
}

/****************************************************************************
 * Select this window id as the default
 ***************************************************************************/
bool AdafruitGFX_helper::selectWindow(const uint8_t& windowId,
                                      const int8_t & rotation) {
  const int16_t result = getWindowIndex(windowId);

  if (result != -1) {
    _windowIndex = result;
    _window      = windowId;
  }
  return result != -1;
}

/****************************************************************************
 * Return the index of the windowId in _windows, -1 if not found
 ***************************************************************************/
int16_t AdafruitGFX_helper::getWindowIndex(const int16_t& windowId) {
  size_t result = 0;

  for (auto win = _windows.begin(); win != _windows.end(); win++, ++result) {
    if ((*win).id == windowId) {
      break;
    }
  }
  return result == _windows.size() ? -1 : result;
}

/****************************************************************************
 * Get the offset for the currently active window
 ***************************************************************************/
void AdafruitGFX_helper::getWindowOffsets(uint16_t& xOffset,
                                          uint16_t& yOffset) {
  xOffset = _windows[_windowIndex].top_left.x;
  yOffset = _windows[_windowIndex].top_left.y;
}

/****************************************************************************
 * Get the limits for the currently active window
 ***************************************************************************/
void AdafruitGFX_helper::getWindowLimits(uint16_t& xLimit,
                                         uint16_t& yLimit) {
  xLimit = _windows[_windowIndex].width_height.x;
  yLimit = _windows[_windowIndex].width_height.y;
}

/****************************************************************************
 * Define a window and return the ID
 ***************************************************************************/
uint8_t AdafruitGFX_helper::defineWindow(const int16_t& x,
                                         const int16_t& y,
                                         const int16_t& w,
                                         const int16_t& h,
                                         int16_t        windowId,
                                         const int8_t & rotation) {
  int16_t result = getWindowIndex(windowId);

  if (result < 0) {
    result = static_cast<int16_t>(_windows.size()); // previous size
    _windows.push_back(tWindowObject());            // add new

    if (windowId < 0) {
      windowId = 0;

      for (auto it = _windows.begin(); it != _windows.end(); it++) {
        if ((*it).id == windowId) { windowId++; } // Generate a new window id
      }
    }
    _windows[result].id = windowId;
  }
  _windows[result].top_left.x     = x;
  _windows[result].top_left.y     = y;
  _windows[result].width_height.x = w;
  _windows[result].width_height.y = h;

  if (rotation >= 0) {
    _windows[result].rotation = rotation & 3;
  } else {
    _windows[result].rotation = _rotation;
  }

  // Adjust original coordinate/sizes based on rotation
  switch (_windows[result].rotation) {
    case 0:                                    // 0 degrees
      _windows[result].org_top_left.x     = x; // All original
      _windows[result].org_top_left.y     = y;
      _windows[result].org_width_height.x = w;
      _windows[result].org_width_height.y = h;
      break;
    case 1:                                                       // +90 degrees
      _windows[result].org_top_left.x     = _display_x - (y + h); // swapped x/y
      _windows[result].org_top_left.y     = x;
      _windows[result].org_width_height.x = h;                    // swapped width/height
      _windows[result].org_width_height.y = w;
      break;
    case 2:                                                       // +180 degrees
      _windows[result].org_top_left.x     = _display_x - (x + w);
      _windows[result].org_top_left.y     = _display_y - (y + h);
      _windows[result].org_width_height.x = w;                    // unchanged
      _windows[result].org_width_height.y = h;
      break;
    case 3:                                                       // +270 degrees
      _windows[result].org_top_left.x     = y;
      _windows[result].org_top_left.y     = _display_x - (x + w);
      _windows[result].org_width_height.x = h;                    // swapped width/height
      _windows[result].org_width_height.y = w;
      break;
  }

  return _windows[result].id;
}

/****************************************************************************
 * Remove a window definition
 ***************************************************************************/
bool AdafruitGFX_helper::deleteWindow(const uint8_t& windowId) {
  const int16_t result = getWindowIndex(windowId);

  if (result > -1) {
    _windows.erase(_windows.begin() + result);
    return true;
  }
  return false;
}

/****************************************************************************
 * log all current known window definitions
 ***************************************************************************/
void AdafruitGFX_helper::logWindows(const String& prefix) {
  #  ifndef BUILD_NO_DEBUG

  for (auto it = _windows.begin(); it != _windows.end(); it++) {
    addLogMove(LOG_LEVEL_INFO, strformat(F("AdaGFX window %s: %d, x:%d, y:%d, w:%d, h:%d"
                                           ", rot%d, current: %d, org x:%d, y:%d, w:%d, h:%d"),
                                         prefix.c_str(), (*it).id, (*it).top_left.x, (*it).top_left.y,
                                         (*it).width_height.x, (*it).width_height.y,
                                         (*it).rotation, getWindow(), (*it).org_top_left.x, (*it).org_top_left.y,
                                         (*it).org_width_height.x, (*it).org_width_height.y));
  }
  #  endif // ifndef BUILD_NO_DEBUG
}

# endif // if ADAGFX_ENABLE_FRAMED_WINDOW

#endif // ifdef PLUGIN_USES_ADAFRUITGFX

#include "../Helpers/I2C_Plugin_Helper.h"

/***********************************************************************
 * checkI2CConfigValid_toHtml(taskIndex, onlyCheck)
 * Check if I2C is correctly configured and usable for this task
 * taskIndex: will be used in planned enhancements
 * outputToHtml = false: no html output is generated
 * Outputs an error message and returns false if not correct
 **********************************************************************/
bool checkI2CConfigValid_toHtml(taskIndex_t taskIndex,
                                bool        outputToHtml) {
  #if FEATURE_I2C_MULTIPLE
  const uint8_t i2cBus = Settings.getI2CInterface(taskIndex);
  #else
  const uint8_t i2cBus = 0;
  #endif // if FEATURE_I2C_MULTIPLE
  if ((Settings.getI2CSdaPin(i2cBus) == -1) || (Settings.getI2CSclPin(i2cBus) == -1)) {
    if (outputToHtml) { addHtml(F("Incomplete I2C configuration.")); }
    return false;
  }
  #if FEATURE_I2CMULTIPLEXER

  if ((Settings.getI2CMultiplexerType(i2cBus) != I2C_MULTIPLEXER_NONE) &&
      (Settings.getI2CMultiplexerAddr(i2cBus) == -1)) { // Multiplexer selected, but no port configured
    if (outputToHtml) { addHtml(F("Incomplete I2C Multiplexer configuration.")); }
    return false;
  }
  #endif // if FEATURE_I2CMULTIPLEXER
  return true;
}


#include "../Helpers/_CPlugin_Helper_mqtt.h"

#if FEATURE_MQTT
# include "../Commands/ExecuteCommand.h"

# if FEATURE_MQTT_DISCOVER
#  include "../Globals/MQTT.h"
#  include "../Helpers/StringGenerator_System.h"
# endif // if FEATURE_MQTT_DISCOVER
# include "../Helpers/SystemVariables.h"
# include "../Helpers/ESPEasy_UnitOfMeasure.h"

# ifdef USES_P001
#  include "../PluginStructs/P001_data_struct.h"
# endif // ifdef USES_P001

/***************************************************************************************
 * Parse MQTT topic for /cmd and /set ending to handle commands or TaskValueSet
 * Special C014 case: handleCmd = false and handleSet is true, so *only* pluginID 1, 9, 19, 33 & 86 are accepted
 **************************************************************************************/
bool MQTT_handle_topic_commands(struct EventStruct *event,
                                bool                handleCmd,
                                bool                handleSet,
                                bool                tryRemoteConfig) {
  bool handled = false;

  // Topic  : event->String1
  // Message: event->String2
  String cmd;
  int    lastindex           = event->String1.lastIndexOf('/');
  const String lastPartTopic = event->String1.substring(lastindex + 1);

  if (!handled && handleCmd && equals(lastPartTopic, F("cmd"))) {
    // Example:
    // Topic: ESP_Easy/Bathroom_pir_env/cmd
    // Message: gpio,14,0
    // Full command:  gpio,14,0

    cmd = event->String2;

    // SP_C005a: string= ;cmd=gpio,12,0 ;taskIndex=12 ;string1=ESPT12/cmd ;string2=gpio,12,0
    handled = true;
  }

  if (!handled && handleSet && equals(lastPartTopic, F("set"))) {
    // Example:
    // Topic: ESP_Easy/DummyTask/DummyVar/set
    // Message: 14
    // Full command: TaskValueSet,DummyTask,DummyVar,14
    const String topic = event->String1.substring(0, lastindex);
    lastindex = topic.lastIndexOf('/');

    if (lastindex > -1) {
      String taskName        = topic.substring(0, lastindex);
      const String valueName = topic.substring(lastindex + 1);
      lastindex = taskName.lastIndexOf('/');

      if (lastindex > -1) {
        taskName = taskName.substring(lastindex + 1);

        const taskIndex_t   taskIndex     = findTaskIndexByName(taskName);
        const deviceIndex_t deviceIndex   = getDeviceIndex_from_TaskIndex(taskIndex);
        uint8_t valueNr                   = findDeviceValueIndexByName(valueName, taskIndex);
        const taskVarIndex_t taskVarIndex = static_cast<taskVarIndex_t>(valueNr);

        if (validDeviceIndex(deviceIndex) && validTaskVarIndex(taskVarIndex) && Settings.TaskDeviceEnabled[taskIndex]) {
          # if defined(USES_P001) || defined(USES_P009) || defined(USES_P019) || defined(USES_P033) || defined(USES_P086)
          const int pluginID = Device[deviceIndex].Number;
          # endif // if defined(USES_P001) || defined(USES_P009) || defined(USES_P010) || defined(USES_P033) || defined(USES_P086)
          # ifdef USES_P001

          if (!handled && (pluginID == 1) && validGpio(Settings.TaskDevicePin[0][taskIndex])) { // Plugin 1 Switch, uses 1st GPIO only
            EventStruct   TempEvent(taskIndex);
            const uint8_t switchtype = P001_data_struct::P001_getSwitchType(&TempEvent);        // 0 = Switch
            const bool    inverted   = Settings.TaskDevicePin1Inversed[taskIndex];
            uint32_t value{};
            validUIntFromString(event->String2, value);

            if ((0 == switchtype) && inverted) {
              value = (0 == value) ? 1u : 0u;
            }
            cmd = strformat((0 == switchtype) ? F("gpio,%d,%d") : F("pwm,%d,%d"),
                            Settings.TaskDevicePin[0][taskIndex], value);
            handled = true;
          }
          # endif // ifdef USES_P001
          # if defined(USES_P009) || defined(USES_P019)

          if (!handled && ((pluginID == 9) || (pluginID == 19))) { // Plugin 9 MCP23017, Plugin 19 PCF8574
            EventStruct TempEvent(taskIndex);
            const bool  inverted = Settings.TaskDevicePin1Inversed[taskIndex];
            uint32_t    value{};
            validUIntFromString(event->String2, value);

            if (inverted) {
              value = (0 == value) ? 1u : 0u;
            }
            cmd = strformat((pluginID == 9) ? F("mcpgpio,%d,%d") : F("pcfgpio,%d,%d"),
                            Settings.TaskDevicePort[taskIndex], value);
            handled = true;
          }
          # endif // if defined(USES_P009) || defined(USES_P019)
          # ifdef USES_P033

          if (!handled && ((pluginID == 33) || // Plugin 33 Dummy Device,
                                               // backward compatible behavior: if handleCmd = true then execute TaskValueSet regardless of
                                               // AllowTaskValueSetAllPlugins
                           ((handleCmd || Settings.AllowTaskValueSetAllPlugins()) && (pluginID != 86)))) {
            // TaskValueSet,<task/device nr>,<value nr>,<value/formula (!ToDo) >, works only with new version of P033!

            if (validTaskVarIndex(taskVarIndex)) { // value Name identified, valueNr = uint8_t
              // Set a Dummy Device Value, device Number, var number and argument
              cmd     = strformat(F("TaskValueSet,%d,%d,%s"), taskIndex + 1, valueNr + 1, event->String2.c_str());
              handled = true;
            }
          }
          # endif // ifdef USES_P033
          # ifdef USES_P086

          if (!handled && (pluginID == 86)) { // Plugin 86 Homie receiver. Schedules the event defined in the plugin.
            // Does NOT store the value.
            // Use HomieValueSet to save the value. This will acknowledge back to the controller too.
            valueNr = findDeviceValueIndexByName(valueName, taskIndex);

            if (validTaskVarIndex(valueNr)) {
              cmd = strformat(F("event,%s="), valueName.c_str());

              if (Settings.TaskDevicePluginConfig[taskIndex][valueNr] == 3) {   // Quote String parameters. PLUGIN_086_VALUE_STRING
                cmd += wrapWithQuotes(event->String2);
              } else {
                if (Settings.TaskDevicePluginConfig[taskIndex][valueNr] == 4) { // Enumeration parameter, find Number of item.
                                                                                // PLUGIN_086_VALUE_ENUM
                  const String enumList = ExtraTaskSettings.TaskDeviceFormula[taskVarIndex];
                  int i                 = 1;
                  String part           = parseStringKeepCase(enumList, i);

                  while (!part.isEmpty()) {                      // lookup result in enum List, keep it backward compatible, but
                    if (part.equalsIgnoreCase(event->String2)) { // Homie spec says it should be case-sensitive...
                      break;
                    }
                    i++;
                    part = parseStringKeepCase(enumList, i);
                  }
                  cmd += i;
                  cmd += ',';
                }
                cmd += event->String2;
              }
              handled = true;
            }
          }
          # endif // ifdef USES_P086
        }
      }
    }
  }

  if (handled && !cmd.isEmpty()) {
    MQTT_execute_command(cmd, tryRemoteConfig);
  }
  return handled;
}

/*****************************************************************************************
 * Execute commands received via MQTT, sanitize event arguments with regard to commas vs =
 * event/asyncevent are added to queue, other commands executed immediately
 ****************************************************************************************/
void MQTT_execute_command(String& cmd,
                          bool    tryRemoteConfig) {
  # ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, strformat(F("MQTT : Executing command: %s"), cmd.c_str()));
  }
  # endif // ifndef BUILD_NO_DEBUG
  // in case of event, store to buffer and return...
  const String command = parseString(cmd, 1);

  if (equals(command, F("event")) || equals(command, F("asyncevent"))) {
    if (Settings.UseRules) {
      // Need to sanitize the event a bit to allow for sending event values as MQTT messages.
      // For example:
      // Publish topic: espeasy_node/cmd_arg2/event/myevent/2
      // Message: 1
      // Actual event:  myevent=1,2

      // Strip out the "event" or "asyncevent" part, leaving the actual event string
      String args = parseStringToEndKeepCase(cmd, 2);

      {
        // Get the first part upto a parameter separator
        // Example: "myEvent,1,2,3", which needs to be converted to "myEvent=1,2,3"
        // N.B. This may contain the first eventvalue too
        // e.g. "myEvent=1,2,3" => "myEvent=1"
        String eventName    = parseStringKeepCase(args, 1);
        String eventValues  = parseStringToEndKeepCase(args, 2);
        const int equal_pos = eventName.indexOf('=');

        if (equal_pos != -1) {
          // We found an '=' character, so the actual event name is everything before that char.
          eventName   = args.substring(0, equal_pos);
          eventValues = args.substring(equal_pos + 1); // Rest of the event, after the '=' char
        }

        if (eventValues.startsWith(F(","))) {
          // Need to reconstruct the event to get rid of calls like these:
          // myevent=,1,2
          eventValues = eventValues.substring(1);
        }

        // Now reconstruct the complete event
        // Without event values: "myEvent" (no '=' char)
        // With event values: "myEvent=1,2,3"

        // Re-using the 'cmd' String as that has pre-allocated memory which is
        // known to be large enough to hold the entire event.
        args = eventName;

        if (eventValues.length() > 0) {
          // Only append an = if there are eventvalues.
          args += '=';
          args += eventValues;
        }
      }

      // Check for duplicates, as sometimes a node may have multiple subscriptions to the same topic.
      // Then it may add several of the same events in a burst.
      eventQueue.addMove(std::move(args), true);
    }
  } else {
    ExecuteCommand(INVALID_TASK_INDEX, EventValueSource::Enum::VALUE_SOURCE_MQTT, cmd.c_str(), true, true, tryRemoteConfig);
  }
}

bool MQTT_protocol_send(EventStruct *event,
                        String       pubname,
                        bool         retainFlag) {
  bool success = false;

  // Check for %valname%
  const bool contains_valname = pubname.indexOf(F("%valname%")) != -1;

  # ifndef LIMIT_BUILD_SIZE

  // Small speed-up as there are lots of system variables starting with "%sys" and this is used in quite a lot of MQTT topics.
  // Not needed if build size really matters
  if (pubname.indexOf(F("%sysname%")) != -1) {
    pubname.replace(F("%sysname%"), SystemVariables::getSystemVariable(SystemVariables::SYSNAME));
  }

  // %tskname% will not change per taskvalue, so replace now
  if (pubname.indexOf(F("%tskname%")) != -1) {
    pubname.replace(F("%tskname%"), getTaskDeviceName(event->TaskIndex));
  }
  # endif // ifndef LIMIT_BUILD_SIZE

  const uint8_t valueCount = getValueCountForTask(event->TaskIndex);

  for (uint8_t x = 0; x < valueCount; ++x) {
    // MFD: skip publishing for values with empty labels (removes unnecessary publishing of unwanted values)
    if (getTaskValueName(event->TaskIndex, x).isEmpty()) {
      continue; // we skip values with empty labels
    }
    String tmppubname = pubname;

    if (contains_valname) {
      parseSingleControllerVariable(tmppubname, event, x, false);
    }
    parseControllerVariables(tmppubname, event, false);
    String value;

    if (event->sensorType == Sensor_VType::SENSOR_TYPE_STRING) {
      value = event->String2.substring(0, 20); // For the log
    } else {
      value = formatUserVarNoCheck(event, x);
    }
    # ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      addLog(LOG_LEVEL_DEBUG, strformat(
               F("MQTT %s : %s %s"),
               get_formatted_Controller_number(getCPluginID_from_ControllerIndex(event->ControllerIndex)).c_str(),
               tmppubname.c_str(),
               value.c_str()));
    }
    # endif // ifndef BUILD_NO_DEBUG

    // Small optimization so we don't try to copy potentially large strings
    if (event->sensorType == Sensor_VType::SENSOR_TYPE_STRING) {
      if (MQTTpublish(event->ControllerIndex, event->TaskIndex, tmppubname.c_str(), event->String2.c_str(),
                      retainFlag)) {
        success = true;
      }
    } else {
      // Publish using move operator, thus tmppubname and value are empty after this call
      if (MQTTpublish(event->ControllerIndex, event->TaskIndex, std::move(tmppubname), std::move(value),
                      retainFlag)) {
        success = true;
      }
    }
  }
  # if FEATURE_STRING_VARIABLES

  if (Settings.SendDerivedTaskValues(event->TaskIndex, event->ControllerIndex)) {
    String taskName = getTaskDeviceName(event->TaskIndex);
    taskName.toLowerCase();
    String postfix;
    const String search = getDerivedValueSearchAndPostfix(taskName, postfix);

    auto it = customStringVar.begin();

    while (it != customStringVar.end()) {
      if (it->first.startsWith(search) && it->first.endsWith(postfix)) {
        String tmppubname = pubname;

        String valueName    = it->first.substring(search.length(), it->first.indexOf('-'));
        const String vname2 = getDerivedValueName(taskName, valueName);

        if (!vname2.isEmpty()) {
          valueName = vname2;
        }

        if (contains_valname) {
          parseValNameVariable(tmppubname, valueName, false);
        }
        parseControllerVariables(tmppubname, event, false);

        if (!it->second.isEmpty()) {
          String value(it->second);
          value = parseTemplateAndCalculate(value);

          if (MQTTpublish(event->ControllerIndex, event->TaskIndex, std::move(tmppubname), std::move(value),
                          retainFlag)) {
            success = true;
          }
        }
      }
      else if (it->first.substring(0, search.length()).compareTo(search) > 0) {
        break;
      }
      ++it;
    }
  }
  # endif // if FEATURE_STRING_VARIABLES

  return success;
}

# if FEATURE_MQTT_DISCOVER

// Support function to fetch the user-configurable VTypes for a plugin
// use like:
// success = getDiscoveryVType(event, Plugin_085_QueryVType, P085_QUERY1_CONFIG_POS, event->Par5);
bool getDiscoveryVType(struct EventStruct *event, QueryVType_ptr func_ptr, uint8_t pConfigOffset, uint8_t nrVars)
{
  bool success           = false;
  constexpr uint8_t _max = PLUGIN_CONFIGVAR_MAX;
  constexpr uint8_t _par = NR_ELEMENTS(event->ParN) - 1;

  for (uint8_t i = 0; i < nrVars && i < _max; ++i) {
    const uint8_t choice = (255 == pConfigOffset) ? i : PCONFIG(i + pConfigOffset);

    if (i < _par) {
      event->ParN[i] = (func_ptr(choice));
    }
    success = true;
  }
  return success;
}

// helper functions to supply a single value VType to be used by getDiscoveryVType
int Plugin_QueryVType_BinarySensor(uint8_t value_nr) {
  return static_cast<int>(Sensor_VType::SENSOR_TYPE_SWITCH) | Sensor_VType_CAN_SET;
}

int Plugin_QueryVType_BinarySensorInv(uint8_t value_nr) {
  return static_cast<int>(Sensor_VType::SENSOR_TYPE_SWITCH_INVERTED) | Sensor_VType_CAN_SET;
}

int Plugin_QueryVType_Analog(uint8_t value_nr) {
  return static_cast<int>(Sensor_VType::SENSOR_TYPE_ANALOG_ONLY);
}

int Plugin_QueryVType_CO2(uint8_t value_nr) {
  return static_cast<int>(Sensor_VType::SENSOR_TYPE_CO2_ONLY);
}

int Plugin_QueryVType_Distance(uint8_t value_nr) {
  return static_cast<int>(Sensor_VType::SENSOR_TYPE_DISTANCE_ONLY);
}

int Plugin_QueryVType_DustPM2_5(uint8_t value_nr) {
  return static_cast<int>(Sensor_VType::SENSOR_TYPE_DUSTPM2_5_ONLY);
}

int Plugin_QueryVType_Lux(uint8_t value_nr) {
  return static_cast<int>(Sensor_VType::SENSOR_TYPE_LUX_ONLY);
}

int Plugin_QueryVType_Temperature(uint8_t value_nr) {
  return static_cast<int>(Sensor_VType::SENSOR_TYPE_TEMP_ONLY);
}

int Plugin_QueryVType_Weight(uint8_t value_nr) {
  return static_cast<int>(Sensor_VType::SENSOR_TYPE_WEIGHT_ONLY);
}

String makeHomeAssistantCompliantName(const String& name) {
  return makeRFCCompliantName(name, '_', '_', 0);
}

#  if FEATURE_MQTT_DEVICECLASS
const char mqtt_binary_deviceclass_names[] PROGMEM =
  "|"                                                               // Default/0 is empty value
  "power|light|plug|door|garage_door|cold|heat|lock|tamper|window|" // Guessed some often used options to be listed first
  "battery|battery_charging|carbon_monoxide|connectivity|gas|"      // switch is selected as the default
  "moisture|motion|moving|occupancy|opening|presence|problem|"      // *** DO NOT CHANGE VALUE ORDER!!!
  "running|safety|smoke|sound|update|vibration|"                    // *** Index is stored in task settings!!!
  "switch|outlet|"                                                  // switch devices
;

String MQTT_binary_deviceClassName(int devClassIndex) {
  char tmp[17]{}; // length: battery_charging + \0

  const String result(GetTextIndexed(tmp, sizeof(tmp), devClassIndex, mqtt_binary_deviceclass_names));

  return result;
}

int MQTT_binary_deviceClassIndex(const String& deviceClassName) {
  return GetCommandCode(deviceClassName.c_str(), mqtt_binary_deviceclass_names);
}

// TwoWay devices are marked with ² in the selector, and discovered as 'light' instead of 'binary_sensor'
bool MQTT_binary_deviceClassTwoWay(int devClassIndex) {
  switch (devClassIndex) { // Index into mqtt_binary_deviceclass_names
    case 1:                // power
    case 2:                // light
    case 3:                // plug
    case 5:                // garage_door
    case 8:                // lock
    case 26:               // sound
    case 28:               // vibration
    case 29:               // switch
    case 30:               // outlet
      return true;
    default:
      break;
  }
  return false;
}

// Switch devices are marked with ÷ in the selector, and discovered as 'switch' instead of 'light'
bool MQTT_binary_deviceClassSwitch(int devClassIndex) {
  switch (devClassIndex) { // Index into mqtt_binary_deviceclass_names
    case 29:               // switch
    case 30:               // outlet
      return true;
    default:
      break;
  }
  return false;
}

#  endif // if FEATURE_MQTT_DEVICECLASS

#  if FEATURE_MQTT_STATE_CLASS
const __FlashStringHelper* MQTT_sensor_StateClass(uint8_t index,
                                                  bool    display) {
  switch (index) {
    case 0: return F("");
    case 1: return display ? F("Measurement") : F("measurement");
    case 2: return display ? F("Measurement-angle") : F("measurement_angle");
    case 3: return display ? F("Total") : F("total");
    case 4: return display ? F("Total-increasing") : F("total_increasing");
  }
  return F("");
}

#  endif // if FEATURE_MQTT_STATE_CLASS

bool MQTT_SendAutoDiscovery(controllerIndex_t ControllerIndex, cpluginID_t CPluginID) {
  bool success = true;

  MakeControllerSettings(ControllerSettings); // -V522

  if (!AllocatedControllerSettings()) {
    return false;
  }
  LoadControllerSettings(ControllerIndex, *ControllerSettings);

  if (ControllerSettings->mqtt_autoDiscovery()

      // && (ControllerSettings->MqttAutoDiscoveryTrigger[0] != 0)
      && (ControllerSettings->MqttAutoDiscoveryTopic[0] != 0)) {
    #  ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO, concat(strformat(F("MQTT : AutoDiscovery for Controller %d CPlugin %d"),
                                              ControllerIndex + 1, CPluginID),
                                    validTaskIndex(mqttDiscoverOnlyTask) ?
                                    strformat(F(", Task %d"), mqttDiscoverOnlyTask + 1) :
                                    EMPTY_STRING));
    }
    #  endif // ifndef BUILD_NO_DEBUG


    // Dispatch autoDiscovery per supported CPlugin
    switch (CPluginID) {
      case 5: // CPLUGIN_ID_005 : Home assistant/openHAB
        success = MQTT_HomeAssistant_SendAutoDiscovery(ControllerIndex, *ControllerSettings);
        break;
    }
  }

  return success;
}

/********************************************************
 * Send MQTT AutoDiscovery in Home Assistant format
 *******************************************************/
bool MQTT_HomeAssistant_SendAutoDiscovery(controllerIndex_t         ControllerIndex,
                                          ControllerSettingsStruct& ControllerSettings) {
  bool success = false;

  // Send plugin info
  taskIndex_t fromTask = 0;
  taskIndex_t maxTask  = TASKS_MAX;

  if (validTaskIndex(mqttDiscoverOnlyTask)) {
    fromTask             = mqttDiscoverOnlyTask;
    maxTask              = mqttDiscoverOnlyTask + 1;
    mqttDiscoverOnlyTask = INVALID_TASK_INDEX; // Reset
  }

  for (taskIndex_t x = fromTask; x < maxTask; ++x) {
    const pluginID_t pluginID = Settings.getPluginID_for_task(x);

    if (validPluginID_fullcheck(pluginID)) {
      LoadTaskSettings(x);
      const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(x);

      // Device is enabled so send information
      if (validDeviceIndex(DeviceIndex) &&
          Device[DeviceIndex].SendDataOption &&           // do (can) we send data?
          Settings.TaskDeviceEnabled[x] &&                // task enabled?
          Settings.TaskDeviceSendData[ControllerIndex][x] // selected for this controller?
          ) {
        const String taskName   = getTaskDeviceName(x);
        const int    valueCount = getValueCountForTask(x);
        std::vector<DiscoveryItem> discoveryItems;

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          addLog(LOG_LEVEL_INFO, strformat(F("MQTT : Start AutoDiscovery for task %d, %s max. %d value%c"),
                                           x + 1, taskName.c_str(), valueCount, 1 != valueCount ? 's' : ' '));
        }

        String pluginDeviceClass;

        // Translate Device[].VType to usable VType per value, and fetch optional deviceClass, for MQTT AutoDiscovery
        if (MQTT_DiscoveryGetDeviceVType(x, discoveryItems, valueCount, pluginDeviceClass)) {
          const String hostName               = Settings.getHostname();
          const unsigned int groupId          = Settings.TaskDeviceID[ControllerIndex][x] & 0x3FFF; // Remove opt. flag bits
          const protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(ControllerIndex);
          const bool usesControllerIDX        = validProtocolIndex(ProtocolIndex) &&
                                                getProtocolStruct(ProtocolIndex).usesID;
          const bool useGroupId = groupId != 0 && !usesControllerIDX;

          const String elementName = useGroupId ?
                                     (Settings.Unit == groupId ? hostName : strformat(F("Group %u"), groupId)) :
                                     strformat(F("%s %s"),    hostName.c_str(), taskName.c_str());
          const String elementIds = useGroupId ?
                                     strformat(F("group_%u"), groupId) :
                                     strformat(F("%s_%s"),    hostName.c_str(), taskName.c_str());
          const String docLink = makeDocLink(strformat(
                                               F("Plugin/%s.html"),
                                               get_formatted_Plugin_number(Settings.getPluginID_for_task(x)).c_str()), true);

          // dev = device, ids = identifiers, mf = manufacturer, sw = sw_version, o = origin, url = support_url
          const String deviceElement = strformat(F(",\"dev\":{\"name\":\"%s\",\"ids\":[\"%s\"],"
                                                   "\"mf\":\"ESPEasy\",\"sw\":\"%s\"},"
                                                   "\"o\":{\"name\":\"%s\",\"sw\":\"%s\",\"url\":\"%s\"}"),
                                                 elementName.c_str(), elementIds.c_str(),
                                                 getSystemBuildString().c_str(),
                                                 elementName.c_str(), getSystemBuildString().c_str(), docLink.c_str());

          #  if FEATURE_STRING_VARIABLES

          if (Settings.SendDerivedTaskValues(x, ControllerIndex)) {
            String postfix;
            const String search = getDerivedValueSearchAndPostfix(taskName, postfix);

            auto it       = customStringVar.begin();
            uint8_t varNr = VARS_PER_TASK;

            while (it != customStringVar.end()) {
              if (it->first.startsWith(search) && it->first.endsWith(postfix)) {
                String valueName = it->first.substring(search.length(), it->first.indexOf('-'));
                String uom;
                String vTypeStr;
                const String vname2 = getDerivedValueNameUomAndVType(taskName, valueName, uom, vTypeStr);
                Sensor_VType vType  = Sensor_VType::SENSOR_TYPE_NONE;
                vTypeStr.toLowerCase();

                constexpr uint8_t maxVType = static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_NOT_SET);

                for (uint8_t i = 0; i < maxVType; ++i) {
                  const Sensor_VType vt = static_cast<Sensor_VType>(i);

                  if ((getValueCountFromSensorType(vt, false) == 1) &&
                      vTypeStr.equalsIgnoreCase(getSensorTypeLabel(vt))) {
                    vType = vt;
                    break;
                  }
                }

                if (Sensor_VType::SENSOR_TYPE_NONE != vType) {
                  if (!vname2.isEmpty()) {
                    valueName = vname2;
                  }
                  discoveryItems.push_back(DiscoveryItem(vType, 1, varNr, valueName, uom, false));

                  ++varNr;
                }
              }
              else if (it->first.substring(0, search.length()).compareTo(search) > 0) {
                break;
              }
              ++it;
            }
          }
          #  endif // if FEATURE_STRING_VARIABLES

          for (size_t s = 0; s < discoveryItems.size(); ++s) {
            struct EventStruct TempEvent(x);
            const uint8_t varCount = discoveryItems[s].varIndex + discoveryItems[s].valueCount;

            switch (discoveryItems[s].VType) {
              // VType values to support, mapped to device classes:
              case Sensor_VType::SENSOR_TYPE_SWITCH:
              case Sensor_VType::SENSOR_TYPE_SWITCH_INVERTED:
              {
                const bool inversedState = Sensor_VType::SENSOR_TYPE_SWITCH_INVERTED == discoveryItems[s].VType;

                for (uint8_t v = discoveryItems[s].varIndex; v < varCount; ++v) {
                  const String valuename  = MQTT_DiscoveryHelperGetValueName(x, v, discoveryItems[s]);
                  String valueDeviceClass = parseStringKeepCase(pluginDeviceClass, v + 1); // Device classes per value

                  if (valueDeviceClass.isEmpty()) { valueDeviceClass = F("switch"); } // default
                  #  if FEATURE_MQTT_STATE_CLASS
                  const String stateClass = MQTT_sensor_StateClass(Cache.getTaskVarStateClass(x, v), false);
                  #  else // if FEATURE_MQTT_STATE_CLASS
                  const String stateClass = EMPTY_STRING;
                  #  endif // if FEATURE_MQTT_STATE_CLASS
                  #  if FEATURE_MQTT_DEVICECLASS
                  const int  devClass = MQTT_binary_deviceClassIndex(valueDeviceClass);
                  const bool twoWay   = MQTT_binary_deviceClassTwoWay(devClass);
                  const bool isSwitch = MQTT_binary_deviceClassSwitch(devClass);
                  #  else // if FEATURE_MQTT_DEVICECLASS
                  const bool twoWay   = true;
                  const bool isSwitch = true;
                  #  endif // if FEATURE_MQTT_DEVICECLASS

                  // Discover 2-way as Light
                  const __FlashStringHelper*componentClass = twoWay && discoveryItems[s].canSet
                                                             ? (isSwitch ? F("switch") : F("light"))
                                                             : F("binary_sensor");
                  const String deviceClass = strformat(F("%s\",\"pl_on\":\"%d\",\"pl_off\":\"%d"),
                                                       valueDeviceClass.c_str(), !inversedState, inversedState);
                  const String uom = MQTT_DiscoveryHelperGetValueUoM(x, v, discoveryItems[s]);

                  if (discoveryItems[s].canSet) {
                    success &= MQTT_DiscoveryPublishWithStatusAndSet(x, v, valuename,
                                                                     ControllerIndex,
                                                                     ControllerSettings,
                                                                     F("device_automation"),
                                                                     EMPTY_STRING, // unused
                                                                     EMPTY_STRING, // No unit of measure used
                                                                     &TempEvent,
                                                                     deviceElement,
                                                                     EMPTY_STRING,
                                                                     success,
                                                                     true, false,
                                                                     useGroupId ? elementName : EMPTY_STRING, elementIds,
                                                                     true); // Send Trigger discovery
                  }
                  success &= MQTT_DiscoveryPublishWithStatusAndSet(x, v, valuename,
                                                                   ControllerIndex,
                                                                   ControllerSettings,
                                                                   componentClass,
                                                                   deviceClass,
                                                                   uom,
                                                                   &TempEvent,
                                                                   deviceElement,
                                                                   stateClass,
                                                                   success,
                                                                   discoveryItems[s].canSet, false,
                                                                   useGroupId ? elementName : EMPTY_STRING, elementIds);
                }
                break;
              }
              case Sensor_VType::SENSOR_TYPE_TEMP_HUM:
              case Sensor_VType::SENSOR_TYPE_TEMP_BARO:
              case Sensor_VType::SENSOR_TYPE_TEMP_HUM_BARO:
              case Sensor_VType::SENSOR_TYPE_TEMP_EMPTY_BARO:
              case Sensor_VType::SENSOR_TYPE_TEMP_ONLY:
              case Sensor_VType::SENSOR_TYPE_HUM_ONLY:
              case Sensor_VType::SENSOR_TYPE_BARO_ONLY:
              {
                uint8_t v = discoveryItems[s].varIndex;

                // Temperature
                if ((Sensor_VType::SENSOR_TYPE_HUM_ONLY != discoveryItems[s].VType) &&
                    (Sensor_VType::SENSOR_TYPE_BARO_ONLY != discoveryItems[s].VType)) {
                  uint8_t fromV = v;
                  uint8_t maxV  = v + 1;

                  if (Sensor_VType::SENSOR_TYPE_TEMP_ONLY == discoveryItems[s].VType) {
                    maxV = fromV + discoveryItems[s].valueCount; // SENSOR_TYPE_TEMP_ONLY Can have multiple values
                  }

                  for (v = fromV; v < maxV; ++v) {
                    const String valuename = MQTT_DiscoveryHelperGetValueName(x, v, discoveryItems[s]);
                    const String uom       = MQTT_DiscoveryHelperGetValueUoM(x, v, discoveryItems[s],
                                                                             getValueType2DefaultHAUoM(Sensor_VType::SENSOR_TYPE_TEMP_ONLY));
                    #  if FEATURE_MQTT_STATE_CLASS
                    const String stateClass = MQTT_sensor_StateClass(Cache.getTaskVarStateClass(x, v), false);
                    #  else // if FEATURE_MQTT_STATE_CLASS
                    const String stateClass = EMPTY_STRING;
                    #  endif // if FEATURE_MQTT_STATE_CLASS
                    success &= MQTT_DiscoveryPublishWithStatusAndSet(x, v, valuename,
                                                                     ControllerIndex,
                                                                     ControllerSettings,
                                                                     F("sensor"),
                                                                     getValueType2HADeviceClass(Sensor_VType::SENSOR_TYPE_TEMP_ONLY),
                                                                     uom,
                                                                     &TempEvent,
                                                                     deviceElement,
                                                                     stateClass,
                                                                     success,
                                                                     discoveryItems[s].canSet, false,
                                                                     useGroupId ? elementName : EMPTY_STRING, elementIds);
                  }
                }

                // Humidity
                if ((Sensor_VType::SENSOR_TYPE_TEMP_HUM == discoveryItems[s].VType) ||
                    (Sensor_VType::SENSOR_TYPE_TEMP_HUM_BARO == discoveryItems[s].VType) ||
                    (Sensor_VType::SENSOR_TYPE_HUM_ONLY == discoveryItems[s].VType)) {
                  uint8_t fromV = v;
                  uint8_t maxV  = v + 1;

                  if (Sensor_VType::SENSOR_TYPE_HUM_ONLY == discoveryItems[s].VType) {
                    maxV = fromV + discoveryItems[s].valueCount; // SENSOR_TYPE_HUM_ONLY Can have multiple values
                  }

                  for (v = fromV; v < maxV; ++v) {
                    const String valuename = MQTT_DiscoveryHelperGetValueName(x, v, discoveryItems[s]);
                    const String uom       = MQTT_DiscoveryHelperGetValueUoM(x, v, discoveryItems[s],
                                                                             getValueType2DefaultHAUoM(Sensor_VType::SENSOR_TYPE_HUM_ONLY));
                    #  if FEATURE_MQTT_STATE_CLASS
                    const String stateClass = MQTT_sensor_StateClass(Cache.getTaskVarStateClass(x, v), false);
                    #  else // if FEATURE_MQTT_STATE_CLASS
                    const String stateClass = EMPTY_STRING;
                    #  endif // if FEATURE_MQTT_STATE_CLASS
                    success &= MQTT_DiscoveryPublishWithStatusAndSet(x, v, valuename,
                                                                     ControllerIndex,
                                                                     ControllerSettings,
                                                                     F("sensor"),
                                                                     getValueType2HADeviceClass(Sensor_VType::SENSOR_TYPE_HUM_ONLY),
                                                                     uom,
                                                                     &TempEvent,
                                                                     deviceElement,
                                                                     stateClass,
                                                                     success,
                                                                     discoveryItems[s].canSet, false,
                                                                     useGroupId ? elementName : EMPTY_STRING, elementIds);
                  }
                }

                // Barometric pressure
                if ((Sensor_VType::SENSOR_TYPE_TEMP_BARO == discoveryItems[s].VType) ||
                    (Sensor_VType::SENSOR_TYPE_TEMP_HUM_BARO == discoveryItems[s].VType) ||
                    (Sensor_VType::SENSOR_TYPE_TEMP_EMPTY_BARO == discoveryItems[s].VType) ||
                    (Sensor_VType::SENSOR_TYPE_BARO_ONLY == discoveryItems[s].VType)) {
                  if (Sensor_VType::SENSOR_TYPE_TEMP_EMPTY_BARO == discoveryItems[s].VType) {
                    v++; // Skip 2nd value = 'EMPTY'
                  }
                  uint8_t fromV = v;
                  uint8_t maxV  = v + 1;

                  if (Sensor_VType::SENSOR_TYPE_BARO_ONLY == discoveryItems[s].VType) {
                    maxV = fromV + discoveryItems[s].valueCount; // SENSOR_TYPE_BARO_ONLY Can have multiple values
                  }

                  for (v = fromV; v < maxV; ++v) {
                    const String valuename = MQTT_DiscoveryHelperGetValueName(x, v, discoveryItems[s]);
                    const String uom       = MQTT_DiscoveryHelperGetValueUoM(x, v, discoveryItems[s],
                                                                             getValueType2DefaultHAUoM(Sensor_VType::SENSOR_TYPE_BARO_ONLY));
                    #  if FEATURE_MQTT_STATE_CLASS
                    const String stateClass = MQTT_sensor_StateClass(Cache.getTaskVarStateClass(x, v), false);
                    #  else // if FEATURE_MQTT_STATE_CLASS
                    const String stateClass = EMPTY_STRING;
                    #  endif // if FEATURE_MQTT_STATE_CLASS
                    success &= MQTT_DiscoveryPublishWithStatusAndSet(x, v, valuename,
                                                                     ControllerIndex,
                                                                     ControllerSettings,
                                                                     F("sensor"),
                                                                     getValueType2HADeviceClass(Sensor_VType::SENSOR_TYPE_BARO_ONLY),
                                                                     uom,
                                                                     &TempEvent,
                                                                     deviceElement,
                                                                     stateClass,
                                                                     success,
                                                                     discoveryItems[s].canSet, false,
                                                                     useGroupId ? elementName : EMPTY_STRING, elementIds);
                    v++;
                  }
                }
                break;
              }
              case Sensor_VType::SENSOR_TYPE_DISTANCE_ONLY:
              case Sensor_VType::SENSOR_TYPE_DUSTPM2_5_ONLY:
              case Sensor_VType::SENSOR_TYPE_DUSTPM1_0_ONLY:
              case Sensor_VType::SENSOR_TYPE_DUSTPM10_ONLY:
              case Sensor_VType::SENSOR_TYPE_CO2_ONLY:
              case Sensor_VType::SENSOR_TYPE_TVOC_ONLY:
              case Sensor_VType::SENSOR_TYPE_AQI_ONLY:
              case Sensor_VType::SENSOR_TYPE_NOX_ONLY:
              case Sensor_VType::SENSOR_TYPE_WEIGHT_ONLY:
              case Sensor_VType::SENSOR_TYPE_MOISTURE_ONLY:
              case Sensor_VType::SENSOR_TYPE_VOLTAGE_ONLY:
              case Sensor_VType::SENSOR_TYPE_CURRENT_ONLY:
              case Sensor_VType::SENSOR_TYPE_POWER_FACT_ONLY:
              case Sensor_VType::SENSOR_TYPE_POWER_USG_ONLY:
              case Sensor_VType::SENSOR_TYPE_REACTIVE_POWER_ONLY:
              case Sensor_VType::SENSOR_TYPE_APPRNT_POWER_USG_ONLY:
              case Sensor_VType::SENSOR_TYPE_LUX_ONLY:
              case Sensor_VType::SENSOR_TYPE_COLOR_RED_ONLY:
              case Sensor_VType::SENSOR_TYPE_COLOR_GREEN_ONLY:
              case Sensor_VType::SENSOR_TYPE_COLOR_BLUE_ONLY:
              case Sensor_VType::SENSOR_TYPE_COLOR_TEMP_ONLY:
              case Sensor_VType::SENSOR_TYPE_UV_ONLY:
              case Sensor_VType::SENSOR_TYPE_UV_INDEX_ONLY:
              case Sensor_VType::SENSOR_TYPE_IR_ONLY:
              case Sensor_VType::SENSOR_TYPE_WIND:
              case Sensor_VType::SENSOR_TYPE_DIRECTION_ONLY:
              case Sensor_VType::SENSOR_TYPE_WIND_SPEED:
              case Sensor_VType::SENSOR_TYPE_DURATION:
              case Sensor_VType::SENSOR_TYPE_DATE:
              case Sensor_VType::SENSOR_TYPE_TIMESTAMP:
              case Sensor_VType::SENSOR_TYPE_DATA_RATE:
              case Sensor_VType::SENSOR_TYPE_DATA_SIZE:
              case Sensor_VType::SENSOR_TYPE_SOUND_PRESSURE:
              case Sensor_VType::SENSOR_TYPE_SIGNAL_STRENGTH:
              case Sensor_VType::SENSOR_TYPE_REACTIVE_ENERGY:
              case Sensor_VType::SENSOR_TYPE_FREQUENCY:
              case Sensor_VType::SENSOR_TYPE_ENERGY:
              case Sensor_VType::SENSOR_TYPE_ENERGY_STORAGE:
              case Sensor_VType::SENSOR_TYPE_ABS_HUMIDITY:
              case Sensor_VType::SENSOR_TYPE_ATMOS_PRESSURE:
              case Sensor_VType::SENSOR_TYPE_BLOOD_GLUCOSE_C:
              case Sensor_VType::SENSOR_TYPE_CO_ONLY:
              case Sensor_VType::SENSOR_TYPE_ENERGY_DISTANCE:
              case Sensor_VType::SENSOR_TYPE_GAS_ONLY:
              case Sensor_VType::SENSOR_TYPE_NITROUS_OXIDE:
              case Sensor_VType::SENSOR_TYPE_OZONE_ONLY:
              case Sensor_VType::SENSOR_TYPE_PRECIPITATION:
              case Sensor_VType::SENSOR_TYPE_PRECIPITATION_INTEN:
              case Sensor_VType::SENSOR_TYPE_SULPHUR_DIOXIDE:
              case Sensor_VType::SENSOR_TYPE_VOC_PARTS:
              case Sensor_VType::SENSOR_TYPE_VOLUME:
              case Sensor_VType::SENSOR_TYPE_VOLUME_FLOW_RATE:
              case Sensor_VType::SENSOR_TYPE_VOLUME_STORAGE:
              case Sensor_VType::SENSOR_TYPE_WATER:
              {
                const String dev    = getValueType2HADeviceClass(discoveryItems[s].VType);
                const String uomDef = getValueType2DefaultHAUoM(discoveryItems[s].VType);

                for (uint8_t v = discoveryItems[s].varIndex; v < varCount; ++v) {
                  const String valuename = MQTT_DiscoveryHelperGetValueName(x, v, discoveryItems[s]);
                  const String uom       = MQTT_DiscoveryHelperGetValueUoM(x, v, discoveryItems[s], uomDef);
                  #  if FEATURE_MQTT_STATE_CLASS
                  const String stateClass = MQTT_sensor_StateClass(Cache.getTaskVarStateClass(x, v), false);
                  #  else // if FEATURE_MQTT_STATE_CLASS
                  const String stateClass = EMPTY_STRING;
                  #  endif // if FEATURE_MQTT_STATE_CLASS
                  success &= MQTT_DiscoveryPublishWithStatusAndSet(x, v, valuename,
                                                                   ControllerIndex,
                                                                   ControllerSettings,
                                                                   F("sensor"),
                                                                   dev,
                                                                   uom,
                                                                   &TempEvent,
                                                                   deviceElement,
                                                                   stateClass,
                                                                   success,
                                                                   discoveryItems[s].canSet, false,
                                                                   useGroupId ? elementName : EMPTY_STRING, elementIds);
                }
                break;
              }


              case Sensor_VType::SENSOR_TYPE_ANALOG_ONLY:
              case Sensor_VType::SENSOR_TYPE_GPS_ONLY:
              case Sensor_VType::SENSOR_TYPE_DIMMER: // Maybe implement?
                // TODO
                break;

              // VType values to ignore, will/should be mapped into something more explicit
              case Sensor_VType::SENSOR_TYPE_NONE:
              case Sensor_VType::SENSOR_TYPE_SINGLE:
              case Sensor_VType::SENSOR_TYPE_DUAL:
              case Sensor_VType::SENSOR_TYPE_TRIPLE:
              case Sensor_VType::SENSOR_TYPE_QUAD:
              case Sensor_VType::SENSOR_TYPE_STRING:
              case Sensor_VType::SENSOR_TYPE_ULONG:
              #  if FEATURE_EXTENDED_TASK_VALUE_TYPES
              case Sensor_VType::SENSOR_TYPE_UINT32_DUAL:
              case Sensor_VType::SENSOR_TYPE_UINT32_TRIPLE:
              case Sensor_VType::SENSOR_TYPE_UINT32_QUAD:
              case Sensor_VType::SENSOR_TYPE_INT32_SINGLE:
              case Sensor_VType::SENSOR_TYPE_INT32_DUAL:
              case Sensor_VType::SENSOR_TYPE_INT32_TRIPLE:
              case Sensor_VType::SENSOR_TYPE_INT32_QUAD:
              case Sensor_VType::SENSOR_TYPE_UINT64_SINGLE:
              case Sensor_VType::SENSOR_TYPE_UINT64_DUAL:
              case Sensor_VType::SENSOR_TYPE_INT64_SINGLE:
              case Sensor_VType::SENSOR_TYPE_INT64_DUAL:
              case Sensor_VType::SENSOR_TYPE_DOUBLE_SINGLE:
              case Sensor_VType::SENSOR_TYPE_DOUBLE_DUAL:
              #  endif // if FEATURE_EXTENDED_TASK_VALUE_TYPES
              case Sensor_VType::SENSOR_TYPE_NOT_SET:
                break;
            }
          }
        }
      }
    }
  }

  return success;
}

bool MQTT_DiscoveryGetDeviceVType(taskIndex_t                 TaskIndex,
                                  std::vector<DiscoveryItem>& discoveryItems,
                                  int                         valueCount,
                                  String                    & deviceClass) {
  const size_t orgLen = discoveryItems.size();
  struct EventStruct TempEvent(TaskIndex);
  String dummy;

  PluginCall(PLUGIN_GET_DEVICEVTYPE, &TempEvent, dummy);
  Sensor_VType VType = TempEvent.sensorType;

  deviceClass.clear();

  TempEvent.Par5 = valueCount; // Pass in the provided value count, so we don't have to determine that again

  // Get value VTypes and deviceClass from plugin
  if (PluginCall(PLUGIN_GET_DISCOVERY_VTYPES, &TempEvent, deviceClass)) {
    #  ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      addLog(LOG_LEVEL_DEBUG, strformat(F("MQTT : AutoDiscovery using Plugin-defined config (%d)"), discoveryItems.size()));
    }
    #  endif // ifndef BUILD_NO_DEBUG

    uint8_t maxVar = VARS_PER_TASK;

    for (; maxVar > 0; --maxVar) { // Only include minimal used values
      if (TempEvent.ParN[maxVar - 1] != 0) {
        break;
      }
    }

    for (uint8_t v = 0; v < maxVar; ++v) {
      const Sensor_VType VType = static_cast<Sensor_VType>(TempEvent.ParN[v] & 0xff);
      const bool _canSet       = (TempEvent.ParN[v] & Sensor_VType_CAN_SET) == Sensor_VType_CAN_SET;

      if (Sensor_VType::SENSOR_TYPE_NONE != VType) {
        discoveryItems.push_back(DiscoveryItem(VType, 1, v, _canSet));
      }
    }

    #  ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      addLog(LOG_LEVEL_DEBUG, strformat(F("MQTT : AutoDiscovery Plugin specific config added (%d)"), discoveryItems.size()));
    }
    #  endif // ifndef BUILD_NO_DEBUG
  } else {
    // Use Device VType setting
    if (Sensor_VType::SENSOR_TYPE_NONE != VType) {
      // Expand combined VTypes into separate single-value VTypes
      if (Sensor_VType::SENSOR_TYPE_TEMP_HUM == VType) {
        discoveryItems.push_back(DiscoveryItem(Sensor_VType::SENSOR_TYPE_TEMP_ONLY, 1, 0));
        discoveryItems.push_back(DiscoveryItem(Sensor_VType::SENSOR_TYPE_HUM_ONLY, 1, 1));
      } else
      if (Sensor_VType::SENSOR_TYPE_TEMP_BARO == VType) {
        discoveryItems.push_back(DiscoveryItem(Sensor_VType::SENSOR_TYPE_TEMP_ONLY, 1, 0));
        discoveryItems.push_back(DiscoveryItem(Sensor_VType::SENSOR_TYPE_BARO_ONLY, 1, 1));
      } else
      if (Sensor_VType::SENSOR_TYPE_TEMP_HUM_BARO == VType) {
        discoveryItems.push_back(DiscoveryItem(Sensor_VType::SENSOR_TYPE_TEMP_ONLY, 1, 0));
        discoveryItems.push_back(DiscoveryItem(Sensor_VType::SENSOR_TYPE_HUM_ONLY, 1, 1));
        discoveryItems.push_back(DiscoveryItem(Sensor_VType::SENSOR_TYPE_BARO_ONLY, 1, 2));
      } else
      if (Sensor_VType::SENSOR_TYPE_TEMP_EMPTY_BARO == VType) {
        discoveryItems.push_back(DiscoveryItem(Sensor_VType::SENSOR_TYPE_TEMP_ONLY, 1, 0));
        discoveryItems.push_back(DiscoveryItem(Sensor_VType::SENSOR_TYPE_BARO_ONLY, 1, 2));
      } else {
        discoveryItems.push_back(DiscoveryItem(VType, valueCount, 0));
      }

      #  ifndef BUILD_NO_DEBUG

      if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
        addLog(LOG_LEVEL_DEBUG, strformat(F("MQTT : AutoDiscovery Default Plugin config (%d)"), discoveryItems.size()));
      }
      #  endif // ifndef BUILD_NO_DEBUG
    }
  }

  return discoveryItems.size() != orgLen; // Something added?
}

String MQTT_TaskValueUniqueName(const String& taskName,
                                const String& valueName) {
  String uniqueId = strformat(F("%s_%s_%s"), Settings.getHostname().c_str(), taskName.c_str(), valueName.c_str());

  uniqueId.toLowerCase();
  return uniqueId;
}

String MQTT_DiscoveryBuildValueTopic(const String            & topic,
                                     struct EventStruct       *event,
                                     uint8_t                   taskValueIndex,
                                     const __FlashStringHelper*deviceClass,
                                     const String            & uniqueId,
                                     const String            & elementId,
                                     const String            & valueName) {
  String tmpTopic(topic);

  #  if FEATURE_STRING_VARIABLES

  if (taskValueIndex >= VARS_PER_TASK) {
    parseValNameVariable(tmpTopic, valueName, false);
  } else
  #  endif // if FEATURE_STRING_VARIABLES
  {
    parseSingleControllerVariable(tmpTopic, event, taskValueIndex, false);
  }
  parseDeviceClassVariable(tmpTopic, deviceClass, false);
  parseUniqueIdVariable(tmpTopic, uniqueId, false);
  parseElementIdVariable(tmpTopic, elementId, false);
  parseControllerVariables(tmpTopic, event, false); // Replace this last

  while (tmpTopic.endsWith(F("/"))) {
    tmpTopic = tmpTopic.substring(0, tmpTopic.length() - 1);
  }

  return tmpTopic;
}

bool MQTT_DiscoveryPublish(controllerIndex_t ControllerIndex,
                           const String    & topic,
                           const String    & discoveryMessage,
                           taskIndex_t       x,
                           uint8_t           v,
                           bool              retainDsc) {
  bool result = false;

  // Send each discovery message separate
  result = MQTTpublish(ControllerIndex, INVALID_TASK_INDEX, topic.c_str(), discoveryMessage.c_str(), retainDsc);
  #  ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, strformat(F("MQTT : Discovery %d Task %d '%s': disc.topic: %s"),
                                     result, x + 1, getTaskDeviceName(x).c_str(), topic.c_str()));
    addLog(LOG_LEVEL_INFO, strformat(F("MQTT : Discovery payload: %s"),
                                     discoveryMessage.c_str()));
  }
  #  endif // ifndef BUILD_NO_DEBUG
  return result;
}

bool MQTT_DiscoveryPublishWithStatusAndSet(taskIndex_t               taskIndex,
                                           uint8_t                   taskValue,
                                           const String            & valueName,
                                           controllerIndex_t         ControllerIndex,
                                           ControllerSettingsStruct& ControllerSettings,
                                           const __FlashStringHelper*componentClass,
                                           const String            & deviceClass,
                                           const String            & unitOfMeasure,
                                           struct EventStruct       *event,
                                           const String            & deviceElement,
                                           const String            & stateClass,
                                           bool                      success,
                                           bool                      hasSet,
                                           bool                      hasIcon,
                                           const String            & elementName,
                                           const String            & elementId,
                                           bool                      sendTrigger) {
  if (!valueName.isEmpty()) {
    //    const String discoveryTopic(ControllerSettings.MqttAutoDiscoveryTopic);
    //    const String publishTopic(ControllerSettings.Publish);

    const String taskName = makeHomeAssistantCompliantName(getTaskDeviceName(taskIndex));
    const String valName  = makeHomeAssistantCompliantName(valueName);

    const String uniqueId = elementName.isEmpty() ? MQTT_TaskValueUniqueName(taskName, valName)
                                                  : strformat(F("%s_%s"), elementId.c_str(), valName.c_str());
    String message;
    {
      const String publish = MQTT_DiscoveryBuildValueTopic(ControllerSettings.Publish,
                                                           event,
                                                           taskValue,
                                                           componentClass,
                                                           uniqueId,
                                                           elementId,
                                                           valName);

      if (sendTrigger) {
        // triggerMessage
        message = strformat(F("{\"atype\":\"trigger\",\"t\":\"%s\","
                              "\"p\":\"device_automation\","
                              "\"type\":\"button_short_press\"," // FIXME ?
                              "\"stype\":\"switch_1\""           // FIXME ?
                              "%s}"),                            // deviceElement is used to pass in the TriggerState
                            publish.c_str(), deviceElement.c_str());
      } else {
        // discoveryMessage
        const String withSet   = hasSet ? F(",\"cmd_t\":\"~/set\"") : EMPTY_STRING;
        const String schema    = hasSet ? EMPTY_STRING : "\"schema\":\"basic\",";
        const String devOrIcon = hasIcon ? F("ic") : F("dev_cla");
        const String withUoM   = unitOfMeasure.isEmpty() ? EMPTY_STRING :
                                 strformat(F(",\"unit_of_meas\":\"%s\""), unitOfMeasure.c_str());
        const String stateJson = stateClass.isEmpty() ? EMPTY_STRING :
                                 strformat(F(",\"stat_cla\":\"%s\""),     stateClass.c_str());

        message = strformat(F("{\"~\":\"%s\",\"name\":\"%s %s\",\"uniq_id\":\"%s\",%s"
                              "\"%s\":\"%s\"%s%s%s,\"stat_t\":\"~\""
                              "%s}"), // deviceElement last
                            publish.c_str(), taskName.c_str(), valName.c_str(), uniqueId.c_str(), schema.c_str(),
                            devOrIcon.c_str(), deviceClass.c_str(), withUoM.c_str(), stateJson.c_str(), withSet.c_str(),
                            deviceElement.c_str());
      }
    }

    const bool retainDsc = ControllerSettings.mqtt_retainDiscovery();
    String     topic;
    {
      const String discoveryUrl = MQTT_DiscoveryBuildValueTopic(ControllerSettings.MqttAutoDiscoveryTopic,
                                                                event,
                                                                taskValue,
                                                                componentClass,
                                                                uniqueId,
                                                                elementId,
                                                                valName);
      const String discoveryConfig(parseStringKeepCase(ControllerSettings.MqttAutoDiscoveryConfig, 1, '|'));
      topic = discoveryConfig.isEmpty()
        ? concat(discoveryUrl, F("/config"))
        : concat(discoveryUrl, discoveryConfig);
    }

    return MQTT_DiscoveryPublish(ControllerIndex,
                                 topic,
                                 message,
                                 taskIndex,
                                 taskValue,
                                 retainDsc);
  }
  return success;
}

String MQTT_DiscoveryHelperGetValueName(taskIndex_t   taskIndex,
                                        uint8_t       taskVarIndex,
                                        DiscoveryItem discoveryItem) {
  return
    #  if FEATURE_STRING_VARIABLES
    discoveryItem.varIndex >= VARS_PER_TASK ? discoveryItem.varName :
    #  endif // if FEATURE_STRING_VARIABLES
    getTaskValueName(taskIndex, taskVarIndex);
}

String MQTT_DiscoveryHelperGetValueUoM(taskIndex_t   taskIndex,
                                       uint8_t       taskVarIndex,
                                       DiscoveryItem discoveryItem,
                                       const String& defaultUoM) {
  return
  #  if FEATURE_TASKVALUE_UNIT_OF_MEASURE
    #   if FEATURE_STRING_VARIABLES
    discoveryItem.varIndex >= VARS_PER_TASK ? discoveryItem.uom :
    #   endif // if FEATURE_STRING_VARIABLES
    toUnitOfMeasureName(Cache.getTaskVarUnitOfMeasure(taskIndex, taskVarIndex), defaultUoM);
  #  else // FEATURE_TASKVALUE_UNIT_OF_MEASURE
    #   if FEATURE_STRING_VARIABLES
    discoveryItem.varIndex >= VARS_PER_TASK ? discoveryItem.uom :
    #   endif // if FEATURE_STRING_VARIABLES
    defaultUoM;
  #  endif // FEATURE_TASKVALUE_UNIT_OF_MEASURE
}

# endif // if FEATURE_MQTT_DISCOVER

#endif // if FEATURE_MQTT

#include "../Helpers/_Internal_GPIO_pulseHelper.h"


#include "../ESPEasyCore/ESPEasyGPIO.h"
#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../Globals/ESPEasy_Scheduler.h"
#include "../Helpers/ESPEasy_time_calc.h"
#include "../Helpers/StringConverter.h"
#include "../WebServer/Markup_Forms.h"

#include "../../ESPEasy_common.h"


#include <GPIO_Direct_Access.h>


const __FlashStringHelper * Internal_GPIO_pulseHelper::toString(GPIOtriggerMode mode)
{
  switch (mode) {
    case GPIOtriggerMode::None: return F("None");
    case GPIOtriggerMode::Change: return F("Change");
    case GPIOtriggerMode::Rising: return F("Rising");
    case GPIOtriggerMode::Falling: return F("Falling");
    case GPIOtriggerMode::PulseLow: return F("PULSE Low");
    case GPIOtriggerMode::PulseHigh: return F("PULSE High");
    case GPIOtriggerMode::PulseChange: return F("PULSE Change");
  }
  return F("");
}

void Internal_GPIO_pulseHelper::addGPIOtriggerMode(const __FlashStringHelper *label,
                                                   const __FlashStringHelper *id,
                                                   GPIOtriggerMode            currentSelection)
{
  #define NR_TRIGGER_MODES  7
  const __FlashStringHelper *options[NR_TRIGGER_MODES];
  const int optionValues[NR_TRIGGER_MODES] = {
    static_cast<int>(GPIOtriggerMode::None),
    static_cast<int>(GPIOtriggerMode::Change),
    static_cast<int>(GPIOtriggerMode::Rising),
    static_cast<int>(GPIOtriggerMode::Falling),
    static_cast<int>(GPIOtriggerMode::PulseLow),
    static_cast<int>(GPIOtriggerMode::PulseHigh),
    static_cast<int>(GPIOtriggerMode::PulseChange)
  };

  for (int i = 0; i < NR_TRIGGER_MODES; ++i) {
    options[i] = Internal_GPIO_pulseHelper::toString(static_cast<Internal_GPIO_pulseHelper::GPIOtriggerMode>(optionValues[i]));
  }
  const FormSelectorOptions selector( NR_TRIGGER_MODES, options, optionValues);
  selector.addFormSelector(label, id, static_cast<int>(currentSelection));
}

Internal_GPIO_pulseHelper::Internal_GPIO_pulseHelper(Internal_GPIO_pulseHelper::pulseCounterConfig configuration)
  : config(configuration) {}

Internal_GPIO_pulseHelper::~Internal_GPIO_pulseHelper() {
  detachInterrupt(digitalPinToInterrupt(config.gpio));
}

bool Internal_GPIO_pulseHelper::init()
{
  if (checkValidPortRange(PLUGIN_GPIO, config.gpio)) {
    pinMode(config.gpio, config.pullupPinMode);

    pulseModeData.currentStableState = config.interruptPinMode == GPIOtriggerMode::PulseLow ? HIGH : LOW;
    pulseModeData.lastCheckState     = HIGH;

    // initialize internal variables for PULSE mode handling
    #ifdef PULSE_STATISTIC
    resetStatsErrorVars();
    ISRdata.Step0counter         = ISRdata.pulseTotalCounter;
    pulseModeData.Step1counter   = ISRdata.pulseTotalCounter;
    pulseModeData.Step2OKcounter = ISRdata.pulseTotalCounter;
    pulseModeData.Step3OKcounter = ISRdata.pulseTotalCounter;
    #endif // ifdef PULSE_STATISTIC

    const int intPinMode = static_cast<int>(config.interruptPinMode) & MODE_INTERRUPT_MASK;
    attachInterruptArg(
      digitalPinToInterrupt(config.gpio),
      config.useEdgeMode() ?
      reinterpret_cast<void (*)(void *)>(ISR_edgeCheck) :
      reinterpret_cast<void (*)(void *)>(ISR_pulseCheck),
      this,
      intPinMode);
    return true;
  }
  return false;
}

void Internal_GPIO_pulseHelper::getPulseCounters(unsigned long& pulseCounter, unsigned long& pulseTotalCounter, float& pulseTime_msec)
{
  pulseCounter      = ISRdata.pulseCounter;
  pulseTotalCounter = ISRdata.pulseTotalCounter;
  pulseTime_msec    = static_cast<float>(ISRdata.pulseTime) / 1000.0f;
}

void Internal_GPIO_pulseHelper::setPulseCountTotal(unsigned long pulseTotalCounter)
{
  ISRdata.pulseTotalCounter = pulseTotalCounter;
}

void Internal_GPIO_pulseHelper::setPulseCounter(unsigned long pulseCounter, float pulseTime_msec)
{
  ISRdata.pulseCounter = pulseCounter;
  ISRdata.pulseTime    = static_cast<uint64_t>(pulseTime_msec * 1000.0f);
}

void Internal_GPIO_pulseHelper::resetPulseCounter()
{
  ISRdata.pulseCounter = 0;
  ISRdata.pulseTime    = 0;
}

void Internal_GPIO_pulseHelper::doPulseStepProcessing(int pStep)
{
  switch (pStep)
  {
    case GPIO_PULSE_HELPER_PROCESSING_STEP_0:
      // regularily called to check if the trigger has flagged the next signal edge
    {
      if (ISRdata.initStepsFlags)
      {
        // schedule step 1 in remaining milliseconds from debounce time
        long delayTime =
          static_cast<long>(
            (static_cast<int64_t>(config.debounceTime_micros)) -
            static_cast<int64_t>(us_Since_triggerTimestamp())
            ) / 1000L;

        if (delayTime < 0)
        {
          // if debounce time was too short or we were called too late by Scheduler
          #ifdef PULSE_STATISTIC
          pulseModeData.Step0ODcounter++; // count occurences
          pulseModeData.StepOverdueMax[pStep] = max(pulseModeData.StepOverdueMax[pStep], -delayTime);
          #endif // PULSE_STATISTIC
          delayTime = 0;
        }
        Scheduler.setPluginTaskTimer(delayTime, config.taskIndex, GPIO_PULSE_HELPER_PROCESSING_STEP_1);

        #ifdef PULSE_STATISTIC

        // FIXME TD-er: Why only correct this when statistics are collected?
        ISRdata.triggerTimestamp = getMicros64() + delayTime * 1000L;
        #endif // PULSE_STATISTIC

        // initialization done
        ISRdata.initStepsFlags = false;
      }
      break;
    }

    case GPIO_PULSE_HELPER_PROCESSING_STEP_1: // read pin status
    {
      #ifdef PULSE_STATISTIC
      pulseModeData.Step1counter++;
      pulseModeData.setStepOverdueMax(pStep, msec_Since_triggerTimestamp());
      #endif // PULSE_STATISTIC

      //  read current state from this tasks's GPIO
      pulseModeData.lastCheckState = DIRECT_pinRead(config.gpio);

      // after debounceTime/2, do step 2
      Scheduler.setPluginTaskTimer(config.debounceTime >> 1, config.taskIndex, GPIO_PULSE_HELPER_PROCESSING_STEP_2);

      #ifdef PULSE_STATISTIC

      // FIXME TD-er: Why only correct this when statistics are collected?
      ISRdata.triggerTimestamp = getMicros64() + (config.debounceTime >> 1) * 1000L;
      #endif // PULSE_STATISTIC
      break;
    }

    case GPIO_PULSE_HELPER_PROCESSING_STEP_2: // 1st validation of pin status
    {
      #ifdef PULSE_STATISTIC
      pulseModeData.setStepOverdueMax(pStep, msec_Since_triggerTimestamp());
      #endif // PULSE_STATISTIC

      //  read current state from this tasks's GPIO
      const int pinState = DIRECT_pinRead(config.gpio);

      if (pinState == pulseModeData.lastCheckState)

      // we found stable state
      {
        #ifdef PULSE_STATISTIC
        pulseModeData.Step2OKcounter++;
        #endif // PULSE_STATISTIC
        // after debounceTime/2, do step 3
        Scheduler.setPluginTaskTimer(config.debounceTime >> 1, config.taskIndex, GPIO_PULSE_HELPER_PROCESSING_STEP_3);
      }
      else

      // we found unexpected different pin state
      {
        #ifdef PULSE_STATISTIC
        pulseModeData.Step2NOKcounter++;
        #endif // PULSE_STATISTIC
        // lets ignore previous pin status. It might have been a spike. Try to detect stable signal
        pulseModeData.lastCheckState = pinState; // now trust the new state
        // after debounceTime/2, do step 2 again
        Scheduler.setPluginTaskTimer(config.debounceTime >> 1, config.taskIndex, GPIO_PULSE_HELPER_PROCESSING_STEP_2);
      }

      #ifdef PULSE_STATISTIC

      // FIXME TD-er: Why only correct this when statistics are collected?
      ISRdata.triggerTimestamp = getMicros64() + (config.debounceTime >> 1) * 1000L;
      #endif // PULSE_STATISTIC
      break;
    }

    case GPIO_PULSE_HELPER_PROCESSING_STEP_3: // 2nd validation of pin status and counting
    {
      #ifdef PULSE_STATISTIC
      pulseModeData.setStepOverdueMax(pStep, msec_Since_triggerTimestamp());
      #endif // PULSE_STATISTIC

      // determine earliest effective start time of current stable pulse (= NOW - 2 * DebounceTime )
      const uint64_t pulseChangeTime = getMicros64() - (config.debounceTime_micros << 1);

      // determine how long the current stable pulse was lasting
      if (pulseModeData.currentStableState == HIGH) // pulse was HIGH
      {
        pulseModeData.pulseHighTime = pulseChangeTime - ISRdata.currentStableStartTime;
      }
      else // pulse was LOW
      {
        pulseModeData.pulseLowTime = pulseChangeTime - ISRdata.currentStableStartTime;
      }

      //  read current state from this tasks's GPIO
      const int pinState = DIRECT_pinRead(config.gpio);

      if (pinState == pulseModeData.lastCheckState)

      // we found the same state as in step 2. It is stable and valid.
      {
        processStablePulse(pinState, pulseChangeTime);
      }
      else

      // we found unexpected different pin state
      {
        #ifdef PULSE_STATISTIC
        pulseModeData.Step3NOKcounter++;
        #endif // PULSE_STATISTIC

        // ignore spike from previous step. It is regarded as spike within previous=current signal. Again try to detect stable signal
        pulseModeData.lastCheckState = pinState; // now trust the previous=new state
        Scheduler.setPluginTaskTimer(config.debounceTime >> 1, config.taskIndex, GPIO_PULSE_HELPER_PROCESSING_STEP_2);

        #ifdef PULSE_STATISTIC

        // FIXME TD-er: Why only correct this when statistics are collected?
        ISRdata.triggerTimestamp = getMicros64() + (config.debounceTime >> 1) * 1000L;
        #endif // PULSE_STATISTIC
      }
      break;
    }
    default:
    {
      if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
        addLog(LOG_LEVEL_ERROR,
               concat(F("_P003:PLUGIN_TASKTIMER_IN: Invalid processingStep: "), pStep));
      }
      break;
    }
  }
}

uint64_t Internal_GPIO_pulseHelper::us_Since_triggerTimestamp() const {
  return getMicros64() - ISRdata.triggerTimestamp;
}

long Internal_GPIO_pulseHelper::msec_Since_triggerTimestamp() const {
  return static_cast<long>(us_Since_triggerTimestamp()) / 1000L;
}

/*********************************************************************************************\
*  Processing for found stable pulse
\*********************************************************************************************/
void Internal_GPIO_pulseHelper::processStablePulse(int pinState, uint64_t pulseChangeTime)
{
  if (pinState != pulseModeData.currentStableState)

  // The state changed. Previous sable pulse ends, new starts
  {
    #ifdef PULSE_STATISTIC
    pulseModeData.Step3OKcounter++;
    #endif // PULSE_STATISTIC

    // lets terminate the previous pulse and setup start point for new stable one
    pulseModeData.currentStableState = !pulseModeData.currentStableState;
    ISRdata.currentStableStartTime   = pulseChangeTime;

    // now provide the counter result values for the ended pulse ( depending on mode type)
    switch (config.interruptPinMode)
    {
      case GPIOtriggerMode::PulseChange:
      {
        if (pulseModeData.currentStableState == LOW) { // HIGH had ended
          ISRdata.pulseTime = pulseModeData.pulseHighTime;
        }
        else {                                         // LOW has ended
          ISRdata.pulseTime = pulseModeData.pulseLowTime;
        }

        ISRdata.pulseCounter++;
        ISRdata.pulseTotalCounter++;
        break;
      }
      case GPIOtriggerMode::PulseHigh:
      {
        if (pulseModeData.currentStableState == LOW) // HIGH had ended (else do nothing)
        {
          ISRdata.pulseTime = pulseModeData.pulseLowTime + pulseModeData.pulseHighTime;
          ISRdata.pulseCounter++;
          ISRdata.pulseTotalCounter++;
        }
        break;
      }
      case GPIOtriggerMode::PulseLow:
      {
        if (pulseModeData.currentStableState == HIGH) // LOW had ended (else do nothing)
        {
          ISRdata.pulseTime = pulseModeData.pulseLowTime + pulseModeData.pulseHighTime;
          ISRdata.pulseCounter++;
          ISRdata.pulseTotalCounter++;
        }
        break;
      }
      default:
      {
        if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
          addLog(LOG_LEVEL_ERROR,
                 concat(F("_P003:PLUGIN_TASKTIMER_IN: Invalid modeType: "),
                        static_cast<int>(config.interruptPinMode)));
        }
        break;
      }
    }
  }
  else

  // we found the same stable state as before
  {
    #ifdef PULSE_STATISTIC
    pulseModeData.Step3IGNcounter++;
    #endif // PULSE_STATISTIC
    // do nothing. Ignore interupt. previous stable state was confirmed probably after a spike
  }

  #ifdef PULSE_STATISTIC
  doStatisticLogging(pulseModeData.StatsLogLevel);
  #endif // PULSE_STATISTIC

  // allow next pulse check call from interrupt
  ISRdata.processingFlags = false;
}

void IRAM_ATTR Internal_GPIO_pulseHelper::ISR_edgeCheck(Internal_GPIO_pulseHelper *self)
{
  ISR_noInterrupts(); // s0170071: avoid nested interrups due to bouncing.

  // legacy edge Mode types
  // KP: we use here P003_currentStableStartTime[taskID] to persist the PulseTime (pulseTimePrevious)
  const uint64_t currentTime          = getMicros64();
  const uint64_t timeSinceLastTrigger = currentTime -
                                        self->ISRdata.currentStableStartTime;

  if (timeSinceLastTrigger > self->config.debounceTime_micros) // check with debounce time for this task
  {
    self->ISRdata.pulseCounter++;
    self->ISRdata.pulseTotalCounter++;
    self->ISRdata.pulseTime              = timeSinceLastTrigger;
    self->ISRdata.currentStableStartTime = currentTime; // reset when counted to determine interval between counted pulses
  }
  ISR_interrupts();                                     // enable interrupts again.
}

void IRAM_ATTR Internal_GPIO_pulseHelper::ISR_pulseCheck(Internal_GPIO_pulseHelper *self)
{
  ISR_noInterrupts(); // s0170071: avoid nested interrups due to bouncing.

  // processing for new PULSE mode types
  #ifdef PULSE_STATISTIC
  self->ISRdata.Step0counter++;
  #endif // PULSE_STATISTIC

  // check if processing is allowed (not blocked) for this task (taskID)
  if (!self->ISRdata.processingFlags)
  {
    // initiate processing
    self->ISRdata.processingFlags  = true; // block further initiations as long as async processing is taking place
    self->ISRdata.initStepsFlags   = true; // PLUGIN_FIFTY_PER_SECOND is polling for this flag set
    self->ISRdata.triggerTimestamp = getMicros64();
  }
  ISR_interrupts();                        // enable interrupts again.
}

#ifdef PULSE_STATISTIC

void Internal_GPIO_pulseHelper::updateStatisticalCounters(int par1) {
  ISRdata.Step0counter         -= ISRdata.pulseTotalCounter - par1;
  pulseModeData.Step1counter   -= ISRdata.pulseTotalCounter - par1;
  pulseModeData.Step2OKcounter -= ISRdata.pulseTotalCounter - par1;
  pulseModeData.Step3OKcounter -= ISRdata.pulseTotalCounter - par1;
}

void Internal_GPIO_pulseHelper::setStatsLogLevel(uint8_t logLevel) {
  pulseModeData.StatsLogLevel = logLevel;
}

/*********************************************************************************************\
*  reset statistical error cunters and overview variables
\*********************************************************************************************/
void Internal_GPIO_pulseHelper::resetStatsErrorVars() {
  // initialize statistical step counters from TotalCounter and error counters with 0
  pulseModeData.Step2NOKcounter = 0;
  pulseModeData.Step3NOKcounter = 0;
  pulseModeData.Step3IGNcounter = 0;
  pulseModeData.Step0ODcounter  = 0;

  for (int pStep = 0; pStep <= P003_PSTEP_MAX; pStep++) {
    pulseModeData.StepOverdueMax[pStep] = 0;
  }
}

/*********************************************************************************************\
*  write statistic counters to logfile
\*********************************************************************************************/
void Internal_GPIO_pulseHelper::doStatisticLogging(uint8_t logLevel)
{
  if (loglevelActiveFor(logLevel)) {
    // Statistic to logfile. E.g: ... [123/1|111|100/5|80/3/4|40] [12243|3244]
    addLog(logLevel,
           strformat(F("Pulse:Stats (GPIO) [step0|1|2|3|tot(ok/nok/ign)] [lo|hi]= (%d) [%d|%d/%d|%d/%d/%d|%d][%.4f|%.4f]"),
                     static_cast<int>(config.gpio),
                     static_cast<int>(ISRdata.Step0counter),
                     pulseModeData.Step1counter,
                     pulseModeData.Step2OKcounter,
                     pulseModeData.Step2NOKcounter,
                     pulseModeData.Step3OKcounter,
                     pulseModeData.Step3NOKcounter,
                     pulseModeData.Step3IGNcounter,
                     static_cast<int>(ISRdata.pulseTotalCounter),
                     pulseModeData.pulseLowTime / 1000L,
                     pulseModeData.pulseHighTime / 1000L));
  }
}

/*********************************************************************************************\
*  write collected timing values to logfile
\*********************************************************************************************/
void Internal_GPIO_pulseHelper::doTimingLogging(uint8_t logLevel)
{
  if (loglevelActiveFor(logLevel)) {
    // Timer to logfile. E.g: ... [4|12000|13444|12243|3244]
    String log;

    if (reserve_special(log, 120)) {
      log = strformat(F("Pulse:OverDueStats (GPIO) [dbTim] {step0OdCnt} [maxOdTimeStep0|1|2|3]= (%d) [%d] {%d} ["),
                      config.gpio,
                      config.debounceTime,
                      pulseModeData.Step0ODcounter);

      for (int pStep = 0; pStep <= P003_PSTEP_MAX; pStep++) {
        log += pulseModeData.StepOverdueMax[pStep];

        if (pStep < P003_PSTEP_MAX) { log += '|'; }
      }
      log += ']';
      addLogMove(logLevel, log);
    }
  }
}

#endif // PULSE_STATISTIC

#include "../Helpers/Hardware_device_info.h"

#ifdef ESP32S2

// See: https://github.com/espressif/esptool/blob/master/esptool/targets/esp32s2.py

# include <soc/soc.h>
# include <soc/efuse_reg.h>
# include <soc/spi_reg.h>
# include <soc/spi_pins.h>
# include <soc/rtc.h>
# include <esp_chip_info.h>
# include <bootloader_common.h>


// Flash datalines:
// https://github.com/tasmota/esp-idf/blob/206ce4b7f875bf5568ba47aba23f4b28e81b0574/components/efuse/esp32s2/esp_efuse_table.csv#L155


/** EFUSE_FLASH_CAP : R; bitpos: [24:21]; default: 0;
 * register: EFUSE_RD_MAC_SPI_SYS_3_REG
 */
# define EFUSE_FLASH_CAP    0x0000000FU
# define EFUSE_FLASH_CAP_M  (EFUSE_FLASH_CAP_V << EFUSE_FLASH_CAP_S)
# define EFUSE_FLASH_CAP_V  0x0000000FU
# define EFUSE_FLASH_CAP_S  21

bool isFlashInterfacePin_ESPEasy(int gpio) {
  // GPIO-22 ... 25: SPI flash and PSRAM
  // GPIO-26: CS for PSRAM, thus only unuable when PSRAM is present
  // GPIO-27 ... 32: SPI 8 ­line mode (OPI) pins for flash or PSRAM (e.g. ESP32-S2FH2 and ESP32-S2FH4)
//  return (gpio) >= 22 && (gpio) <= 25;
  switch (gpio) {
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 0)
    case SPI_IOMUX_PIN_NUM_HD:
    case SPI_IOMUX_PIN_NUM_CS:
    case SPI_IOMUX_PIN_NUM_MOSI:
    case SPI_IOMUX_PIN_NUM_CLK:
    case SPI_IOMUX_PIN_NUM_MISO:
    case SPI_IOMUX_PIN_NUM_WP:
#else
    case MSPI_IOMUX_PIN_NUM_CS1: return FoundPSRAM(); // SPICS1   Only when PSRAM is present
    case MSPI_IOMUX_PIN_NUM_HD:
    case MSPI_IOMUX_PIN_NUM_WP:
    case MSPI_IOMUX_PIN_NUM_CS0:
    case MSPI_IOMUX_PIN_NUM_CLK:
    case MSPI_IOMUX_PIN_NUM_MISO:
    case MSPI_IOMUX_PIN_NUM_MOSI:
#endif
      return true;
  }
  return false;

}

bool flashVddPinCanBeUsedAsGPIO()
{
  return false;
}

int32_t getEmbeddedFlashSize()
{
  const uint32_t flash_cap = REG_GET_FIELD(EFUSE_RD_MAC_SPI_SYS_3_REG, EFUSE_FLASH_CAP);

  switch (flash_cap) {
    case 0: return 0;
    case 1: return 2;
    case 2: return 4;
  }

  // Unknown value, thus mark as negative value
  return -1 *  static_cast<int32_t>(flash_cap);
}

/** EFUSE_PSRAM_CAP : R; bitpos: [31:28]; default: 0;
 * register: EFUSE_RD_MAC_SPI_SYS_3_REG
 */
# define EFUSE_PSRAM_CAP    0x0000000FU
# define EFUSE_PSRAM_CAP_M  (EFUSE_PSRAM_CAP_V << EFUSE_PSRAM_CAP_S)
# define EFUSE_PSRAM_CAP_V  0x0000000FU
# define EFUSE_PSRAM_CAP_S  28

int32_t getEmbeddedPSRAMSize()
{
  const uint32_t psram_cap = REG_GET_FIELD(EFUSE_RD_MAC_SPI_SYS_3_REG, EFUSE_PSRAM_CAP);

  switch (psram_cap) {
    case 0: return 0;
    case 1: return 2;
    case 2: return 4;
  }

  // Unknown value, thus mark as negative value
  return -1 * static_cast<int32_t>(psram_cap);
}

# ifndef isPSRAMInterfacePin
bool isPSRAMInterfacePin(int gpio) {
  // GPIO-22 ... 25: SPI flash and PSRAM
  // GPIO-26: CS for PSRAM, thus only unuable when PSRAM is present
  // GPIO-27 ... 32: SPI 8 ­line mode (OPI) pins for flash or PSRAM (e.g. ESP32-S2FH2 and ESP32-S2FH4)
  // GPIO-27 ... 32: are never made accessible
  return FoundPSRAM() ? ((gpio) >= 26 && (gpio) <= 32) : false;
}

# endif // ifndef isPSRAMInterfacePin

const __FlashStringHelper* getChipModel(uint32_t chip_model, uint32_t chip_revision, uint32_t pkg_version, bool single_core)
{
  if (CHIP_ESP32S2 == chip_model) { // ESP32-S2
    /* esptool:
        def get_flash_version(self):
            num_word = 3
            block1_addr = self.EFUSE_BASE + 0x044
            word3 = self.read_reg(block1_addr + (4 * num_word))
            pkg_version = (word3 >> 21) & 0x0F
            return pkg_version
        def get_psram_version(self):
            num_word = 3
            block1_addr = self.EFUSE_BASE + 0x044
            word3 = self.read_reg(block1_addr + (4 * num_word))
                pkg_version = (word3 >> 28) & 0x0F
                return pkg_version
     */
    uint32_t chip_ver  = REG_GET_FIELD(EFUSE_RD_MAC_SPI_SYS_3_REG, EFUSE_FLASH_VERSION);
    uint32_t psram_ver = REG_GET_FIELD(EFUSE_RD_MAC_SPI_SYS_3_REG, EFUSE_PSRAM_VERSION);
    pkg_version = (chip_ver & 0xF) + ((psram_ver & 0xF) * 100);

    //    AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("HDW: ESP32 Model %d, Revision %d, Core %d, Package %d"), chip_info.model, chip_revision,
    // chip_info.cores, chip_ver);

    switch (pkg_version) {
      case 0: return F("ESP32-S2");      // Max 240MHz, Single core, QFN 7*7, ESP32-S2-WROOM, ESP32-S2-WROVER, ESP32-S2-Saola-1, ESP32-S2-Kaluga-1
      case 1: return F("ESP32-S2FH2");   // Max 240MHz, Single core, QFN 7*7, 2MB embedded flash, ESP32-S2-MINI-1, ESP32-S2-DevKitM-1
      case 2: return F("ESP32-S2FH4");   // Max 240MHz, Single core, QFN 7*7, 4MB embedded flash
      case 3: return F("ESP32-S2FN4R2"); // Max 240MHz, Single core, QFN 7*7, 4MB embedded flash, 2MB embedded PSRAM, ESP32-S2-MINI-1U, ESP32-S2-DevKitM-1U
      case 100: return F("ESP32-S2R2");
      case 102: return F("ESP32-S2FNR2");  // Max 240MHz, Single core, QFN 7*7, 4MB embedded flash, 2MB embedded PSRAM, Lolin S2 mini
    }

    return F("ESP32-S2");
  }

  return F("Unknown");
}

#endif // ifdef ESP32S2

#include "../Helpers/Scheduler.h"

#include "../DataStructs/Scheduler_SystemEventQueueTimerID.h"
#include "../DataStructs/TimingStats.h"

#include "../Globals/CPlugins.h"
#include "../Globals/Device.h"
#include "../Globals/NPlugins.h"
#include "../Globals/RTC.h"

#include "../Helpers/_Plugin_init.h"
#include "../Helpers/ESPEasyRTC.h"
#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/StringConverter.h"

/*********************************************************************************************\
* System Event Timer
* Handling of these events will be asynchronous and being called from the loop().
* Thus only use these when the result is not needed immediately.
* Proper use case is calling from a callback function, since those cannot use yield() or delay()
\*********************************************************************************************/
void ESPEasy_Scheduler::schedule_plugin_task_event_timer(
  deviceIndex_t        DeviceIndex,
  uint8_t              Function,
  struct EventStruct&& event) {
  if (validDeviceIndex(DeviceIndex)) {
    schedule_event_timer(
      SchedulerPluginPtrType_e::TaskPlugin,
      DeviceIndex.value,
      Function,
      std::move(event));
  }
}

#if FEATURE_MQTT
void ESPEasy_Scheduler::schedule_mqtt_plugin_import_event_timer(
  deviceIndex_t  DeviceIndex,
  taskIndex_t    TaskIndex,
  uint8_t        Function,
  const char    *c_topic,
  const uint8_t *b_payload,
  unsigned int   length) {
  if (validDeviceIndex(DeviceIndex)) {
    EventStruct  event(TaskIndex);
    const size_t topic_length = strlen_P(c_topic);

    if (!(reserve_special(event.String1, topic_length) &&
          reserve_special(event.String2, length))) {
      addLog(LOG_LEVEL_ERROR, F("MQTT : Out of Memory! Cannot process MQTT message"));
      return;
    }
    event.String1.concat(c_topic, topic_length);
    event.String2.concat((const char *)b_payload, length);

    // Emplace using move.
    // This makes sure the relatively large event will not be in memory twice.
    const SystemEventQueueTimerID timerID(
      SchedulerPluginPtrType_e::TaskPlugin,
      DeviceIndex.value,
      static_cast<uint8_t>(Function));

    {
      // Make sure the emplace_back is not using 2nd heap
      # ifdef USE_SECOND_HEAP
      HeapSelectDram ephemeral;
      # endif // ifdef USE_SECOND_HEAP

      ScheduledEventQueue.emplace_back(timerID.mixed_id, std::move(event));
    }
  }
}

#endif // if FEATURE_MQTT

void ESPEasy_Scheduler::schedule_controller_event_timer(
  protocolIndex_t      ProtocolIndex,
  uint8_t              Function,
  struct EventStruct&& event) {
  if (validProtocolIndex(ProtocolIndex)) {
    schedule_event_timer(
      SchedulerPluginPtrType_e::ControllerPlugin,
      ProtocolIndex,
      Function,
      std::move(event));
  }
}

#if FEATURE_MQTT
void ESPEasy_Scheduler::schedule_mqtt_controller_event_timer(
  protocolIndex_t   ProtocolIndex,
  CPlugin::Function Function,
  const char       *c_topic,
  const uint8_t    *b_payload,
  unsigned int      length) {
  if (validProtocolIndex(ProtocolIndex) && c_topic && b_payload) {
    EventStruct  event;
    const size_t topic_length = strlen_P(c_topic);

    // This is being called from a callback function, so do not try to allocate this on the 2nd heap, but rather on the default heap.
    if (!(reserve_special(event.String1, topic_length) &&
          reserve_special(event.String2, length))) {
      addLog(LOG_LEVEL_ERROR, F("MQTT : Out of Memory! Cannot process MQTT message"));
      return;
    }
    event.String1.concat(c_topic, topic_length);
    event.String2.concat((const char *)b_payload, length);

    // Emplace using move.
    // This makes sure the relatively large event will not be in memory twice.
    const SystemEventQueueTimerID timerID(
      SchedulerPluginPtrType_e::ControllerPlugin,
      ProtocolIndex,
      static_cast<uint8_t>(Function));

    {
      # ifdef USE_SECOND_HEAP
      // Make sure emplace_back is not called when on 2nd heap
      HeapSelectDram ephemeral;
      # endif // ifdef USE_SECOND_HEAP
      
      ScheduledEventQueue.emplace_back(timerID.mixed_id, std::move(event));
    }
  }
}

#endif // if FEATURE_MQTT

#if FEATURE_NOTIFIER
void ESPEasy_Scheduler::schedule_notification_event_timer(
  uint8_t              NotificationProtocolIndex,
  NPlugin::Function    Function,
  struct EventStruct&& event)
{
  schedule_event_timer(SchedulerPluginPtrType_e::NotificationPlugin,
                       NotificationProtocolIndex,
                       static_cast<uint8_t>(Function),
                       std::move(event));
}

#endif // if FEATURE_NOTIFIER

void ESPEasy_Scheduler::schedule_event_timer(
  SchedulerPluginPtrType_e ptr_type,
  uint8_t                  Index,
  uint8_t                  Function,
  struct EventStruct    && event) {
  const SystemEventQueueTimerID timerID(ptr_type, Index, Function);

  //  EventStructCommandWrapper eventWrapper(mixedId, *event);
  //  ScheduledEventQueue.push_back(eventWrapper);

  // Make sure emplace_back is not done on the 2nd heap
  # ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  # endif // ifdef USE_SECOND_HEAP
  ScheduledEventQueue.emplace_back(timerID.mixed_id, std::move(event));
}

void ESPEasy_Scheduler::process_system_event_queue() {
  #ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  #endif // ifdef USE_SECOND_HEAP

  if (ScheduledEventQueue.size() == 0) { return; }

  START_TIMER

  const SchedulerTimerID timerID(ScheduledEventQueue.front().id);
  const SystemEventQueueTimerID *sysEventTimerID =
    reinterpret_cast<const SystemEventQueueTimerID *>(&timerID);


  if (RTC.lastMixedSchedulerId != timerID.mixed_id) {
    RTC.lastMixedSchedulerId = timerID.mixed_id;
    saveToRTC();
  }

  uint8_t Function                  = sysEventTimerID->getFunction();
  uint8_t Index                     = sysEventTimerID->getIndex();
  SchedulerPluginPtrType_e ptr_type = sysEventTimerID->getPtrType();

  // At this moment, the String is not being used in the plugin calls, so just supply a dummy String.
  // Also since these events will be processed asynchronous, the resulting
  //   output in the String is probably of no use elsewhere.
  // Else the line string could be used.
  String tmpString;

  switch (ptr_type) {
    case SchedulerPluginPtrType_e::TaskPlugin:
    {
      const deviceIndex_t deviceIndex = deviceIndex_t::toDeviceIndex(Index);

      if (validDeviceIndex(deviceIndex)) {
        if (((Function != PLUGIN_READ) &&
             (Function != PLUGIN_MQTT_CONNECTION_STATE) &&
             (Function != PLUGIN_MQTT_IMPORT))
            || Device[deviceIndex].ErrorStateValues) {
          // FIXME TD-er: LoadTaskSettings should only be called when needed, not pre-emptive.
          LoadTaskSettings(ScheduledEventQueue.front().event.TaskIndex);
        }
        PluginCall(deviceIndex,
                   Function,
                   &ScheduledEventQueue.front().event,
                   tmpString);
      }
      break;
    }
    case SchedulerPluginPtrType_e::ControllerPlugin:
      CPluginCall(Index,
                  static_cast<CPlugin::Function>(Function),
                  &ScheduledEventQueue.front().event,
                  tmpString);
      break;
#if FEATURE_NOTIFIER
    case SchedulerPluginPtrType_e::NotificationPlugin:
      NPlugin_ptr[Index](static_cast<NPlugin::Function>(Function),
                         &ScheduledEventQueue.front().event,
                         tmpString);
      break;
#endif // if FEATURE_NOTIFIER
  }
  ScheduledEventQueue.pop_front();
  STOP_TIMER(PROCESS_SYSTEM_EVENT_QUEUE);
}

#include "../Helpers/TarStream.h"

#if FEATURE_TARSTREAM_SUPPORT
# include "../Globals/ESPEasy_time.h"
# include "../Helpers/ESPEasy_Storage.h"
# include "../Helpers/StringConverter.h"

/**
 * TarStream : Create/receive a .tar file while streaming via http webserver
 * Copyright (c) 2023.. Ton Huisman for ESPEasy
 *
 * Changelog: See TarStream.h
 */

/**
 * TarFileInfo_struct implementation
 */
TarFileInfo_struct::TarFileInfo_struct(const String fname,
                                       size_t       fsize) :
  fileName(fname), fileSize(fsize) {
  tarSize = ((fsize % TAR_BLOCK_SIZE == 0 ? 0 : 1) + (fsize / TAR_BLOCK_SIZE)) * TAR_BLOCK_SIZE; // Multiple of block size
  # if TAR_STREAM_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, strformat(F("Added file: %s, size: %d, tarSize: %d"), fileName.c_str(), fileSize, tarSize));
  }
  # endif // if TAR_STREAM_DEBUG
}

/**
 * TarStream implementation
 */
TarStream::TarStream() {}

TarStream::TarStream(const String& fileName)
  : _fileName(fileName) {}

TarStream::TarStream(const String& fileName, FileDestination_e destination)
  : _fileName(fileName), _destination(destination) {}

TarStream::~TarStream() {
  _filesList.clear();
}

size_t TarStream::write(uint8_t ch) {
  // TODO implement
  addLogMove(LOG_LEVEL_ERROR, F("TarStream: write(ch) NOT IMPLEMENTED YET."));
  return 1u;
}

size_t TarStream::write(const uint8_t *buf,
                        size_t         size) {
  size_t bufOffset  = 0u;   // Offset into the buffer
  bool   stayInLoop = true; // To allow processing the rest of the data

  # if TAR_STREAM_DEBUG
  const bool logInfo = loglevelActiveFor(LOG_LEVEL_INFO);
  # endif // if TAR_STREAM_DEBUG

  while (stayInLoop) {
    stayInLoop = false;

    switch (_streamState) {
      case TarStreamState_e::Initial: // Initial behaves like WritingHeader
      case TarStreamState_e::WritingHeader:
      {
        if (_headerPosition == 0) {
          clearHeader();
        }
        const size_t toMove = std::min(size - bufOffset, TAR_HEADER_SIZE - _headerPosition);

        memcpy(&_tarData[_headerPosition], &buf[bufOffset], toMove);
        _headerPosition += toMove;
        bufOffset       += toMove;

        if (_headerPosition == TAR_HEADER_SIZE) {
          bool allZeros = true;

          for (size_t n = 0; n < TAR_HEADER_SIZE && allZeros; ++n) {
            allZeros &= (_tarData[n] == 0u);
          }

          if (allZeros) {
            _headerPosition = 0;
            _streamState    = TarStreamState_e::WritingFinal;
            # if TAR_STREAM_DEBUG
            addLog(LOG_LEVEL_INFO, F("TarStream: Switch from Initial/WritingHeader to WritingFinal"));
            # endif // if TAR_STREAM_DEBUG
          } else {
            const String fname(_tarHeader.name);
            const size_t fsize   = strtoul(_tarHeader.size, nullptr, 8);         // Octal
            const bool   isValid = validateHeader() && fname.indexOf('/') == -1; // Don't _allow_ subdirectories

            if (loglevelActiveFor(LOG_LEVEL_INFO)) {
              # if TAR_STREAM_DEBUG
              addLog(LOG_LEVEL_INFO, strformat(F("TarStream: Write Receiving file %s size: %d"),
                                               fname.c_str(), fsize));
              # else // if TAR_STREAM_DEBUG
              addLog(LOG_LEVEL_INFO, concat(F("Tar   : Load file: "), fname));
              # endif // if TAR_STREAM_DEBUG
            }

            if (((_tarHeader.typeflag == REGTYPE) || (_tarHeader.typeflag == AREGTYPE)) &&
                isValid) {           // Checked: typeflag, magic & checksum
              addFile(fname, fsize); // Add to list
              _fileIndex++;
              _filesSizes += fsize;
              bool validConfig = true;

              bufOffset += TAR_BLOCK_SIZE - TAR_HEADER_SIZE; // Skip remaining bytes to start of next block

              if (matchFileType(fname, FileType::CONFIG_DAT)) {
                validConfig = validateUploadConfigDat(&buf[bufOffset]);
              }

              if (validConfig) {
                _streamState = TarStreamState_e::WritingFile;

                size_t available = UINT32_MAX;

                if (FileDestination_e::SD != _destination) { // Check flash storage only
                  available = SpiffsFreeSpace();             // Leave(s) at least 2 blocks free
                  fs::File tmpfile = tryOpenFile(_filesList[_fileIndex].fileName, F("r"), _destination);

                  if (tmpfile) {
                    available += tmpfile.size(); // Existing file will be deleted
                    tmpfile.close();
                  }
                }

                if (available > _filesList[_fileIndex].tarSize) { // Use rounded-up size
                  // delete and create file for write mode
                  if (fileExists(_filesList[_fileIndex].fileName) &&
                      !tryDeleteFile(_filesList[_fileIndex].fileName, _destination) &&
                      loglevelActiveFor(LOG_LEVEL_ERROR)) {
                    addLog(LOG_LEVEL_ERROR, concat(F("TarStream: Can't delete file: "), _filesList[_fileIndex].fileName));
                  }
                  _currentFile = tryOpenFile(_filesList[_fileIndex].fileName, F("w"), _destination);

                  if (!_currentFile && loglevelActiveFor(LOG_LEVEL_ERROR)) {
                    addLog(LOG_LEVEL_ERROR, concat(F("TarStream: Can't create file: "), _filesList[_fileIndex].fileName));
                  }
                } else {
                  if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
                    addLog(LOG_LEVEL_ERROR, concat(F("TarStream: Not enough space to save file: "),
                                                   _filesList[_fileIndex].fileName));
                  }
                }

                # if TAR_STREAM_DEBUG
                addLog(LOG_LEVEL_INFO, F("TarStream: Switch from Initial/WritingHeader to WritingFile"));
                # endif // if TAR_STREAM_DEBUG
              } else {
                _streamState = TarStreamState_e::WritingSlack;

                if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
                  addLog(LOG_LEVEL_ERROR, F("TarStream: Received invalid config.dat, ignored."));
                }
                _filesList[_fileIndex].fileName = F("(ignored)"); // Won't be recognized
                # if TAR_STREAM_DEBUG
                addLog(LOG_LEVEL_INFO, F("TarStream: Switch from Initial/WritingHeader to WritingSlack"));
                # endif // if TAR_STREAM_DEBUG
              }
              _writePosition = 0u; // Start at file-position 0
            } else {
              _streamState = TarStreamState_e::Error;

              if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
                addLog(LOG_LEVEL_ERROR, strformat(F("TarStream: Unsupported file: %s, type: %c"),
                                                  fname.c_str(), _tarHeader.typeflag));
              }
              # if TAR_STREAM_DEBUG
              addLog(LOG_LEVEL_INFO, F("TarStream: Switch from Initial/WritingHeader to Error"));
              # endif // if TAR_STREAM_DEBUG
            }
          }
        }

        # if !defined(BUILD_NO_DEBUG) && TAR_STREAM_DEBUG

        if (loglevelActiveFor(TAR_LOG_LEVEL_DEBUG)) {
          addLog(TAR_LOG_LEVEL_DEBUG, strformat(F("TarStream: DEBUG WritingHeader size: %d, offset: %d, stay: %d"),
                                                size, bufOffset, stayInLoop));
        }
        # endif // if !defined(BUILD_NO_DEBUG) && TAR_STREAM_DEBUG

        break;
      }
      case TarStreamState_e::WritingFile:
      {
        const size_t toWrite = std::min(size - bufOffset, _filesList[_fileIndex].fileSize - _writePosition);

        // write to file
        if (_currentFile) {
          _currentFile.write(&buf[bufOffset], toWrite);
        }

        _writePosition += toWrite;
        bufOffset      += toWrite;
        # if !defined(BUILD_NO_DEBUG) && TAR_STREAM_DEBUG

        if (loglevelActiveFor(TAR_LOG_LEVEL_DEBUG)) {
          addLog(TAR_LOG_LEVEL_DEBUG,
                 strformat(F("TarStream: DEBUG WritingFile %d bytes of %d to file, pos: %d, size: %d, bufoff: %d, stay: %d"),
                           toWrite, _filesList[_fileIndex].fileSize,
                           _writePosition, size, bufOffset, stayInLoop));
        }
        # endif // if !defined(BUILD_NO_DEBUG) && TAR_STREAM_DEBUG

        if (_writePosition == _filesList[_fileIndex].fileSize) { // Done with this file?
          // close the file
          if (_currentFile) {
            _currentFile.close();
          }

          if (_writePosition < _filesList[_fileIndex].tarSize) {
            _streamState = TarStreamState_e::WritingSlack;
            # if TAR_STREAM_DEBUG

            if (logInfo) {
              addLog(LOG_LEVEL_INFO, strformat(F("TarStream: Switch from WritingFile to WritingSlack, bytes: %d"),
                                               _writePosition));
            }
            # endif // if TAR_STREAM_DEBUG
          } else {
            _streamState = TarStreamState_e::WritingHeader;
            # if TAR_STREAM_DEBUG

            if (logInfo) {
              addLog(LOG_LEVEL_INFO, strformat(F("TarStream: Switch from WritingFile to WritingHeader, bytes: %d"),
                                               _writePosition));
            }
            # endif // if TAR_STREAM_DEBUG
            _headerPosition = 0;
          }
        }

        break;
      }
      case TarStreamState_e::WritingSlack:
      {
        const size_t toSkip = std::min(size - bufOffset, _filesList[_fileIndex].tarSize - _writePosition);

        _writePosition += toSkip;
        bufOffset      += toSkip;
        # if !defined(BUILD_NO_DEBUG) && TAR_STREAM_DEBUG

        if (loglevelActiveFor(TAR_LOG_LEVEL_DEBUG)) {
          addLog(TAR_LOG_LEVEL_DEBUG, strformat(F("%sDEBUG WritingSlack %d bytes of %d to file"),
                                                F("TarStream: "), toSkip, _filesList[_fileIndex].tarSize));
        }
        # endif // if !defined(BUILD_NO_DEBUG) && TAR_STREAM_DEBUG

        if (_writePosition == _filesList[_fileIndex].tarSize) {
          _streamState = TarStreamState_e::WritingHeader;
          # if TAR_STREAM_DEBUG
          addLog(LOG_LEVEL_INFO, F("TarStream: Switch from WritingSlack to WritingHeader"));
          # endif // if TAR_STREAM_DEBUG
          _headerPosition = 0;
        }
        break;
      }
      case TarStreamState_e::WritingFinal:
      {
        // FIXME check entire 512 byte block or just ignore?
        const size_t toMove = std::min(size - bufOffset, TAR_HEADER_SIZE - _headerPosition);
        memcpy(&_tarData[_headerPosition], &buf[bufOffset], toMove);
        _headerPosition += toMove;
        bufOffset       += toMove;
        bool allZeros = true;

        if (_headerPosition == TAR_HEADER_SIZE) {
          for (size_t n = 0; n < TAR_HEADER_SIZE && allZeros; ++n) {
            allZeros &= (_tarData[n] == 0u);
          }
          _streamState = TarStreamState_e::WritingDone;
          # if TAR_STREAM_DEBUG

          if (logInfo) {
            addLog(LOG_LEVEL_INFO, strformat(F("%sWritingFinal to WritingDone, allZeros: %d"),
                                             F("TarStream: Switch from "), allZeros));
          }
          # endif // if TAR_STREAM_DEBUG
        }
        break;
      }
      case TarStreamState_e::WritingDone:
      {
        const size_t toSkip = size - bufOffset;
        bufOffset += toSkip;
        # if TAR_STREAM_DEBUG

        if (logInfo) {
          addLog(LOG_LEVEL_INFO, strformat(F("TarStream: WritingDone, skipping: %d"), toSkip));
        }
        # endif // if TAR_STREAM_DEBUG
        break;
      }
      case TarStreamState_e::ReadingHeader: // Not here
      case TarStreamState_e::ReadingFile:
      case TarStreamState_e::ReadingSlack:
      case TarStreamState_e::ReadingFinal:
        break;
      case TarStreamState_e::Error: // No real error state
        break;
    }

    if (bufOffset < size) { // We got leftover bytes
      stayInLoop = true;
    }
  }
  delay(0);
  _tarSize += size;
  return size;
}

int TarStream::available() {
  return _tarRemaining;
}

void TarStream::clearHeader() {
  memset(&_tarHeader, 0, TAR_HEADER_SIZE);
}

void TarStream::setupHeader() {
  constexpr size_t tarHeader_name_size = NR_ELEMENTS(_tarHeader.name);

  clearHeader();
  safe_strncpy(_tarHeader.name, _currentIndex.fileName.c_str(), tarHeader_name_size);
  sprintf(_tarHeader.mode,  PSTR("%07o"),  TUREAD + TUWRITE + TUEXEC + TGREAD + TGWRITE + TGEXEC + TOREAD + TOWRITE + TOEXEC);
  sprintf(_tarHeader.uid,   PSTR("%07o"),  0);
  sprintf(_tarHeader.gid,   PSTR("%07o"),  0);
  sprintf(_tarHeader.size,  PSTR("%011o"), _currentIndex.fileSize);
  sprintf(_tarHeader.mtime, PSTR("%011o"), node_time.getUnixTime()); // We don't have file-date/times, use current date/time
  _tarHeader.typeflag = REGTYPE;
  sprintf(_tarHeader.magic, PSTR("%s"),    TMAGIC);
  _tarHeader.version[0] = TVERSION[0]; _tarHeader.version[1] = TVERSION[1];

  const uint32_t chksum = clearAndCalculateHeaderChecksum();

  sprintf(_tarHeader.chksum, "%06o", chksum); // FIXME Compatible with 7-zip: 6 octal digits + 0x0 + space?
}

bool TarStream::validateHeader() {
  String magic(_tarHeader.magic);

  magic = magic.substring(0, 6);                                    // Only get magic part
  magic.trim();
  const size_t   expected = strtoul(_tarHeader.chksum, nullptr, 8); // Octal
  const uint32_t chksum   = clearAndCalculateHeaderChecksum();

  # if TAR_STREAM_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, strformat(F("TarStream: Validate Header, magic: %s checksum: %06o expected: %06o (octal)"),
                                     magic.c_str(), chksum, expected));
  }
  # endif // if TAR_STREAM_DEBUG
  return magic.equals(F(TMAGIC)) && chksum == expected;
}

uint32_t TarStream::clearAndCalculateHeaderChecksum() {
  constexpr size_t tarHeader_chksum_size = NR_ELEMENTS(_tarHeader.chksum);
  uint32_t chksum                        = 0u;

  for (size_t c = 0; c < tarHeader_chksum_size; ++c) { // note: chksum content during calculation is spaces
    _tarHeader.chksum[c] = ' ';
  }

  for (uint16_t hdr = 0; hdr < TAR_HEADER_SIZE; ++hdr) {
    chksum += _tarData[hdr];
  }
  return chksum;
}

int TarStream::read() {
  int result = EOF;

  switch (_streamState) {
    case TarStreamState_e::Initial:
    {
      _currentIterator = _filesList.begin();
      _currentIndex    = *_currentIterator;
      _currentFile     = tryOpenFile(_currentIndex.fileName, F("r"));

      if (_currentFile) {
        # ifndef BUILD_NO_DEBUG

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          addLog(LOG_LEVEL_INFO, concat(F("Tar   : Save file: "), _currentIndex.fileName));
        }
        # endif // ifndef BUILD_NO_DEBUG

        // Set up header
        setupHeader();

        // Header done
        _streamState    = TarStreamState_e::ReadingHeader;
        _headerPosition = 0;
        result          = _tarData[_headerPosition];
        # if TAR_STREAM_DEBUG
        addLog(LOG_LEVEL_INFO, F("TarStream: Switch from Initial to ReadingHeader"));
        # endif // if TAR_STREAM_DEBUG
      } else {
        _streamState = TarStreamState_e::Error;

        # if TAR_STREAM_DEBUG
        addLog(LOG_LEVEL_INFO, F("TarStream: Switch from Initial to Error"));
        # endif // if TAR_STREAM_DEBUG
      }
      break;
    }
    case TarStreamState_e::ReadingHeader:
    {
      _headerPosition++;

      if (_headerPosition < TAR_HEADER_SIZE) {
        result = _tarData[_headerPosition];
      } else if (_headerPosition < TAR_BLOCK_SIZE) {
        result = 0;
      } else {
        _tarPosition = 0;

        if (_currentFile) {
          if (_tarPosition < _currentFile.size()) {
            result       = _currentFile.read();
            _streamState = TarStreamState_e::ReadingFile;
            # if TAR_STREAM_DEBUG
            addLog(LOG_LEVEL_INFO, F("TarStream: Switch from ReadingHeader to ReadingFile"));
            # endif // if TAR_STREAM_DEBUG
          } else {
            _streamState = TarStreamState_e::ReadingSlack;
            # if TAR_STREAM_DEBUG
            addLog(LOG_LEVEL_INFO, F("TarStream: Switch from ReadingHeader to ReadingSlack 0"));
            # endif // if TAR_STREAM_DEBUG
            result = 0;
          }
        } else {
          _streamState = TarStreamState_e::Error;
          # if TAR_STREAM_DEBUG
          addLog(LOG_LEVEL_INFO, F("TarStream: Switch from ReadingHeader to Error"));
          # endif // if TAR_STREAM_DEBUG
        }
      }

      if (!((TarStreamState_e::ReadingSlack == _streamState) && (_currentIndex.fileSize == 0))) {
        break; // Special 0-file case
      }

      // Fall through
    }
    case TarStreamState_e::ReadingFile:
    {
      // FIXME tonhuisman: This looks horrible... but we can't return a single byte for a 0-size file :-(
      if (!((TarStreamState_e::ReadingSlack == _streamState) && (_currentIndex.fileSize == 0))) {
        _tarPosition++;

        if (_tarPosition < _currentFile.size()) {
          result = _currentFile.read();
          break;
        } else if (_tarPosition < _currentIndex.tarSize) {
          _currentFile.close();
          result       = 0;
          _streamState = TarStreamState_e::ReadingSlack;
          # if TAR_STREAM_DEBUG
          addLog(LOG_LEVEL_INFO, F("TarStream: Switch from ReadingFile to ReadingSlack 1"));
          # endif // if TAR_STREAM_DEBUG
          break;
        }
        _currentFile.close();
        _tarPosition--; // revert 1 position
        _streamState = TarStreamState_e::ReadingSlack;
        # if TAR_STREAM_DEBUG
        addLog(LOG_LEVEL_INFO, F("TarStream: Switch from ReadingFile to ReadingSlack 2"));
        # endif // if TAR_STREAM_DEBUG
      }

      // Fall through
    }
    case TarStreamState_e::ReadingSlack:
    {
      _tarPosition++;

      if ((_tarPosition < _currentIndex.tarSize) && (_currentIndex.fileSize != 0)) {
        result = 0;
      } else {
        if (_currentIndex.fileSize == 0) {
          _tarPosition--; // Revert 1 position for 0 byte file
        }
        _currentIterator++;

        if (_currentIterator != _filesList.end()) {
          _currentIndex = *_currentIterator;
          _currentFile  = tryOpenFile(_currentIndex.fileName, F("r"));

          if (_currentFile) {
            if (loglevelActiveFor(LOG_LEVEL_INFO)) {
              addLog(LOG_LEVEL_INFO, concat(F("Tar   : Save file: "), _currentIndex.fileName));
            }

            // Set up header
            setupHeader();

            // Header done
            _streamState = TarStreamState_e::ReadingHeader;
            # if TAR_STREAM_DEBUG
            addLog(LOG_LEVEL_INFO, F("TarStream: Switch from ReadingSlack to ReadingHeader"));
            # endif // if TAR_STREAM_DEBUG
          } else {
            _streamState = TarStreamState_e::Error;
            # if TAR_STREAM_DEBUG
            addLog(LOG_LEVEL_INFO, F("TarStream: Switch from ReadingSlack to Error"));
            # endif // if TAR_STREAM_DEBUG
          }
        } else {
          clearHeader();
          _streamState = TarStreamState_e::ReadingFinal;
          # if TAR_STREAM_DEBUG
          addLog(LOG_LEVEL_INFO, F("TarStream: Switch from ReadingSlack to ReadingFinal"));
          # endif // if TAR_STREAM_DEBUG
        }
        _headerPosition = 0;
        result          = _tarData[_headerPosition];
      }
      break;
    }
    case TarStreamState_e::ReadingFinal:
    {
      _headerPosition++;
      result = 0;

      if (_headerPosition == (TAR_BLOCK_SIZE * 2)) {
        result = EOF;
        # if TAR_STREAM_DEBUG
        addLog(LOG_LEVEL_INFO, F("TarStream: Reached ReadingFinal EOF"));
        # endif // if TAR_STREAM_DEBUG
      } else if (_headerPosition > (TAR_BLOCK_SIZE * 2)) {
        result       = EOF;
        _streamState = TarStreamState_e::Error;
        # if TAR_STREAM_DEBUG
        addLog(LOG_LEVEL_INFO, F("TarStream: Switch from ReadingFinal to Error"));
        # endif // if TAR_STREAM_DEBUG
      }
      break;
    }
    case TarStreamState_e::WritingHeader: // Not here
    case TarStreamState_e::WritingFile:
    case TarStreamState_e::WritingSlack:
    case TarStreamState_e::WritingFinal:
    case TarStreamState_e::WritingDone:
      break;
    case TarStreamState_e::Error: // Endstate, something went wrong
      break;
  }

  if (_tarRemaining > 0u) {
    _tarRemaining--;
  }

  return result;
}

int TarStream::peek() {
  int result = 0;

  # if TAR_STREAM_PEEK

  switch (_streamState) {
    case TarStreamState_e::Initial:
      break;
    case TarStreamState_e::ReadingHeader:
    {
      if (_headerPosition < TAR_HEADER_SIZE - 1) {
        result = _tarData[_headerPosition];
      } else if (_headerPosition < TAR_BLOCK_SIZE - 1) {
        result = 0;
      } else if (_currentFile) {
        result = _currentFile.peek();
      }
      break;
    }
    case TarStreamState_e::ReadingFile:
    {
      if (_tarPosition < _currentIndex.fileSize + 1) {
        result = _currentFile.peek();
      }
      break;
    }
    case TarStreamState_e::ReadingFinal:
    {
      if (_headerPosition == (TAR_BLOCK_SIZE * 2) - 1) {
        result = EOF;
      }
      break;
    }
    case TarStreamState_e::ReadingSlack:
    case TarStreamState_e::WritingHeader: // Ignore
    case TarStreamState_e::WritingFile:
    case TarStreamState_e::WritingSlack:
    case TarStreamState_e::WritingFinal:
    case TarStreamState_e::WritingDone:
      break;
    case TarStreamState_e::Error:
      result = EOF; // Endstate, something went wrong
      break;
  }
  # endif // if TAR_STREAM_PEEK

  return result;
}

void TarStream::flush() {
  if (_currentFile) {
    _currentFile.flush();
  }
  _tarRemaining = 0u;
}

size_t TarStream::size() {
  return _tarSize;
}

const char * TarStream::name() {
  return _fileName.c_str();
}

bool TarStream::addFileIfExists(const String& fileName) {
  fs::File tryFile = tryOpenFile(fileName, F("r"));

  if (tryFile) {
    addFile(tryFile.name(), tryFile.size());

    tryFile.close();

    return true;
  }
  return false;
}

bool TarStream::addFile(const String& fileName,
                        size_t        fileSize) {
  const TarFileInfo_struct tarFileInfo(fileName, fileSize);

  _filesList.push_back(tarFileInfo);

  if (_tarSize == 0) {
    _tarSize = TAR_BLOCK_SIZE * 2;                      // Closing 2 blocks of 0s
  }
  _tarSize     += TAR_BLOCK_SIZE + tarFileInfo.tarSize; // Header block + rounded-up file-size
  _filesSizes  += tarFileInfo.fileSize;                 // Actual file-size
  _tarRemaining = _tarSize;

  return true;
}

bool TarStream::isFileIncluded(const String& filename) {
  if (!filename.isEmpty()) {
    for (auto it = _filesList.begin(); it != _filesList.end(); ++it) {
      if (it->fileName.equalsIgnoreCase(filename)) {
        return true;
      }
    }
  }
  return false;
}

size_t TarStream::getFileCount() const {
  return _filesList.size();
}

#endif // if FEATURE_TARSTREAM_SUPPORT

#include "../Helpers/Networking.h"

#include "../Commands/ExecuteCommand.h"
#include "../CustomBuild/CompiletimeDefines.h"
#include "../DataStructs/NodeStruct.h"
#include "../DataStructs/TimingStats.h"
#include "../DataTypes/EventValueSource.h"
#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../ESPEasyCore/ESPEasy_backgroundtasks.h"
#include "../ESPEasyCore/ESPEasyEth.h"
#include "../ESPEasyCore/ESPEasyNetwork.h"
#include "../ESPEasyCore/ESPEasyWifi.h"
#include "../ESPEasyCore/Serial.h"
#include "../Globals/ESPEasyEthEvent.h"
#include "../Globals/ESPEasyWiFiEvent.h"
#include "../Globals/ESPEasy_Scheduler.h"
#include "../Globals/ESPEasy_time.h"

#ifdef USES_ESPEASY_NOW
#include "../Globals/ESPEasy_now_handler.h"
#endif

#include "../Globals/EventQueue.h"
#include "../Globals/NetworkState.h"
#include "../Globals/Nodes.h"
#include "../Globals/ResetFactoryDefaultPref.h"
#include "../Globals/Settings.h"
#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/ESPEasy_time_calc.h"
#include "../Helpers/Hardware.h"
#include "../Helpers/Misc.h"
#include "../Helpers/Network.h"
#include "../Helpers/Numerical.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringProvider.h"

#if RESPONSE_PARSER_SUPPORT
#include "../Helpers/HTTPResponseParser.h"
#endif

#include "../../ESPEasy-Globals.h"

#include <IPAddress.h>
#include <base64.h>
#include <MD5Builder.h> // for getDigestAuth

#include <WiFiUdp.h>

#include <lwip/dns.h>


// Generic Networking routines

// Syslog
// UDP system messaging
// SSDP
//  #if LWIP_VERSION_MAJOR == 2
#define IPADDR2STR(addr) (uint8_t)((uint32_t)addr &  0xFF), (uint8_t)(((uint32_t)addr >> 8) &  0xFF), \
  (uint8_t)(((uint32_t)addr >> 16) &  0xFF), (uint8_t)(((uint32_t)addr >> 24) &  0xFF)

//  #endif

#include <lwip/netif.h>

#ifdef ESP8266
#include <lwip/opt.h>
#include <lwip/udp.h>
#include <lwip/igmp.h>
#include <include/UdpContext.h>
#endif

#ifdef SUPPORT_ARP
# include <lwip/etharp.h>

# ifdef ESP32
#  include <lwip/etharp.h>
#  include <lwip/tcpip.h>

void _etharp_gratuitous_func(struct netif *netif) {
  etharp_gratuitous(netif);
}

void etharp_gratuitous_r(struct netif *netif) {
  tcpip_callback_with_block((tcpip_callback_fn)_etharp_gratuitous_func, netif, 0);
}

# endif // ifdef ESP32

#endif  // ifdef SUPPORT_ARP

#if FEATURE_DOWNLOAD
# ifdef ESP8266
#  include <ESP8266HTTPClient.h>
# endif // ifdef ESP8266
# ifdef ESP32
#  include <HTTPClient.h>
#  include <Update.h>
# endif // ifdef ESP32
#endif  // if FEATURE_DOWNLOAD

#include <vector>

/*********************************************************************************************\
   Syslog client
\*********************************************************************************************/
void sendSyslog(uint8_t logLevel, const String& message)
{
  if ((Settings.Syslog_IP[0] != 0) && NetworkConnected())
  {
    IPAddress broadcastIP(Settings.Syslog_IP[0], Settings.Syslog_IP[1], Settings.Syslog_IP[2], Settings.Syslog_IP[3]);

    FeedSW_watchdog();

    if (portUDP.beginPacket(broadcastIP, Settings.SyslogPort) == 0) {
      // problem resolving the hostname or port
      return;
    }
    unsigned int prio = Settings.SyslogFacility * 8;

    if (logLevel == LOG_LEVEL_ERROR) {
      prio += 3; // syslog error
    }
    else if (logLevel == LOG_LEVEL_INFO) {
      prio += 5; // syslog notice
    }
    else {
      prio += 7;
    }

    // An RFC3164 compliant message must be formated like :  "<PRIO>[TimeStamp ]Hostname TaskName: Message"

    // Using Settings.Name as the Hostname (Hostname must NOT content space)
    {
      String header;
      header += '<';
      header += prio;
      header += '>';
      header += NetworkCreateRFCCompliantHostname(true);
      header += F(" EspEasy: ");
      header.trim();
      header.replace(' ', '_');
      
      #ifdef ESP8266
      portUDP.write(header.c_str(),                                    header.length());
      #endif // ifdef ESP8266
      #ifdef ESP32
      portUDP.write(reinterpret_cast<const uint8_t *>(header.c_str()), header.length());
      #endif // ifdef ESP32
    }

    #ifdef ESP8266
    portUDP.write(message.c_str(), message.length());
    #endif // ifdef ESP8266
    #ifdef ESP32
    portUDP.write(reinterpret_cast<const uint8_t *>(message.c_str()), message.length());
    #endif // ifdef ESP32

    portUDP.endPacket();
    FeedSW_watchdog();
    delay(0);
  }
}

#if FEATURE_ESPEASY_P2P

/*********************************************************************************************\
   Send event using UDP message
\*********************************************************************************************/
void SendUDPCommand(uint8_t destUnit, const char *data, uint8_t dataLength)
{
  if (!NetworkConnected(10)) {
    return;
  }

  if (destUnit != 0)
  {
    sendUDP(destUnit, (const uint8_t *)data, dataLength);
    delay(10);
  } else {
    for (auto it = Nodes.begin(); it != Nodes.end(); ++it) {
      if (it->first != Settings.Unit) {
        sendUDP(it->first, (const uint8_t *)data, dataLength);
        delay(10);
      }
    }
  }
  delay(50);
}

/*********************************************************************************************\
   Send UDP message to specific unit (unit 255=broadcast)
\*********************************************************************************************/
void sendUDP(uint8_t unit, const uint8_t *data, uint8_t size)
{
  if (!NetworkConnected(10)) {
    return;
  }

  IPAddress remoteNodeIP = getIPAddressForUnit(unit);

  if (remoteNodeIP[0] == 0) {
    return;
  }

# ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG_MORE)) {
    addLogMove(LOG_LEVEL_DEBUG_MORE,  strformat(
      F("UDP  : Send UDP message to %d (%s)"), 
      unit,
      remoteNodeIP.toString().c_str()
      ));
  }
# endif // ifndef BUILD_NO_DEBUG

  statusLED(true);
  FeedSW_watchdog();
  portUDP.beginPacket(remoteNodeIP, Settings.UDPPort);
  portUDP.write(data, size);
  portUDP.endPacket();
  FeedSW_watchdog();
  delay(0);
}

/*********************************************************************************************\
   Update UDP port (ESPEasy propiertary protocol)
\*********************************************************************************************/
void updateUDPport(bool force)
{
  static uint16_t lastUsedUDPPort = 0;

  if (!force && Settings.UDPPort == lastUsedUDPPort) {
    return;
  }

  if (lastUsedUDPPort != 0) {
    portUDP.stop();
    lastUsedUDPPort = 0;
  }

  if (!NetworkConnected()) {
    return;
  }

  if (Settings.UDPPort != 0) {
    if (portUDP.begin(Settings.UDPPort) == 0) {
      if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
        addLogMove(LOG_LEVEL_ERROR, concat(F("UDP : Cannot bind to ESPEasy p2p UDP port "), Settings.UDPPort));
      }
    } else {
      lastUsedUDPPort = Settings.UDPPort;

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLogMove(LOG_LEVEL_INFO, concat(F("UDP : Start listening on port "), Settings.UDPPort));
      }
    }
  }
}

/*********************************************************************************************\
   Check UDP messages (ESPEasy propiertary protocol)
\*********************************************************************************************/
boolean runningUPDCheck = false;
void checkUDP()
{
  if (!NetworkConnected())
    return;
  if (Settings.UDPPort == 0) {
    return;
  }

  if (runningUPDCheck) {
    return;
  }
  START_TIMER

  runningUPDCheck = true;

  // UDP events
  int packetSize = portUDP.parsePacket();

  if (packetSize > 0 /*&& portUDP.remotePort() == Settings.UDPPort*/)
  {
    statusLED(true);

    IPAddress remoteIP = portUDP.remoteIP();

    if (portUDP.remotePort() == 123)
    {
      // unexpected NTP reply, drop for now...
      while (portUDP.available()) {
        // Do not call portUDP.flush() as that's meant to sending the packet (on ESP8266)
        portUDP.read();
      }

      runningUPDCheck = false;
      return;
    }

    // UDP_PACKETSIZE_MAX should be as small as possible but still enough to hold all
    // data for PLUGIN_UDP_IN or CPLUGIN_UDP_IN calls
    // This node may also receive other UDP packets which may be quite large
    // and then crash due to memory allocation failures
    if ((packetSize >= 2) && (packetSize < UDP_PACKETSIZE_MAX)) {
      // Allocate buffer to process packet.
      // Resize it to be 1 byte larger so we can 0-terminate it 
      // in case it is some plain text string
      std::vector<char> packetBuffer;
      packetBuffer.resize(packetSize + 1);

      if (packetBuffer.size() >= static_cast<size_t>(packetSize)) {
        memset(&packetBuffer[0], 0, packetSize + 1);
        int len = portUDP.read(&packetBuffer[0], packetSize);

        if (len >= 2) {
          if (static_cast<uint8_t>(packetBuffer[0]) != 255)
          {
            packetBuffer[len] = 0;
            # ifndef BUILD_NO_DEBUG

            if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
              addLogMove(LOG_LEVEL_DEBUG,  
                strformat(F("UDP  : %s  Command: %s"), 
                  formatIP(remoteIP, true).c_str(), 
                  wrapWithQuotesIfContainsParameterSeparatorChar(String(&packetBuffer[0])).c_str()
                  ));
            }
            #endif
            ExecuteCommand_all({EventValueSource::Enum::VALUE_SOURCE_SYSTEM, &packetBuffer[0]}, true);
          }
          else
          {
            // binary data!
            switch (packetBuffer[1])
            {
              case 1: // sysinfo message
              {
                if (len < 13) {
                  break;
                }
                int copy_length = sizeof(NodeStruct);
                // Older versions sent 80 bytes, regardless of the size of NodeStruct
                // Make sure the extra data received is ignored as it was also not initialized
                if (len == 80) {
                  copy_length = 56;
                }

                if (copy_length > (len - 2)) {
                  copy_length = (len - 2);
                }
                NodeStruct received;
                memcpy(&received, &packetBuffer[2], copy_length);

                if (received.validate(remoteIP)) {
                  Nodes.addNode(received); // Create a new element when not present

# ifndef BUILD_NO_DEBUG

                  if (loglevelActiveFor(LOG_LEVEL_DEBUG_MORE)) {
                    addLogMove(LOG_LEVEL_DEBUG_MORE,  
                      strformat(F("UDP  : %s (%d) %s,%s,%d"), 
                        formatIP(remoteIP).c_str(), 
                        received.unit,
                        received.STA_MAC().toString().c_str(), 
                        formatIP(received.IP(), true).c_str(), 
                        received.unit));
                  }

#endif // ifndef BUILD_NO_DEBUG
                }
                break;
              }

              default:
              {
                struct EventStruct TempEvent;
                TempEvent.Data = reinterpret_cast<uint8_t *>(&packetBuffer[0]);
                TempEvent.Par1 = remoteIP[3];
                TempEvent.Par2 = len;
                String dummy;
                // TD-er: Disabled the PLUGIN_UDP_IN call as we don't have any plugin using this.
                //PluginCall(PLUGIN_UDP_IN, &TempEvent, dummy);
                CPluginCall(CPlugin::Function::CPLUGIN_UDP_IN, &TempEvent);
                break;
              }
            }
          }
        }
      }
    }
  }

  // Flush any remaining content of the packet.
  while (portUDP.available()) {
    // Do not call portUDP.flush() as that's meant to sending the packet (on ESP8266)
    portUDP.read();
  }
  runningUPDCheck = false;
  STOP_TIMER(CHECK_UDP);
}

/*********************************************************************************************\
   Get formatted IP address for unit
   formatcodes: 0 = default toString(), 1 = empty string when invalid, 2 = 0 when invalid
\*********************************************************************************************/
String formatUnitToIPAddress(uint8_t unit, uint8_t formatCode) {
  IPAddress unitIPAddress = getIPAddressForUnit(unit);

  if (unitIPAddress[0] == 0) { // Invalid?
    switch (formatCode) {
      case 1:                  // Return empty string
      {
        return EMPTY_STRING;
      }
      case 2: // Return "0"
      {
        return String('0');
      }
    }
  }
  return formatIP(unitIPAddress);
}

/*********************************************************************************************\
   Get IP address for unit
\*********************************************************************************************/
IPAddress getIPAddressForUnit(uint8_t unit) {
  if (unit == 255) {
    const IPAddress ip(255, 255, 255, 255);
    return ip;
  }
  auto it = Nodes.find(unit);

  if (it == Nodes.end() || it->second.ip[0] == 0) {
    IPAddress ip;
    return ip;
  }
#if FEATURE_USE_IPV6
/*
  // FIXME TD-er: for now do not try to send to IPv6
  if (it->second.hasIPv6_mac_based_link_local) {
    return it->second.IPv6_link_local();
  }
  if (it->second.hasIPv6_mac_based_link_global) {
    return it->second.IPv6_global();
  }
*/
#endif
  return it->second.IP();
}


String getNameForUnit(uint8_t unit) {
  auto it = Nodes.find(unit);

  if (it == Nodes.end() || it->second.getNodeName().isEmpty()) {
    return EMPTY_STRING;
  }
  return it->second.getNodeName();
}

long getAgeForUnit(uint8_t unit) {
  auto it = Nodes.find(unit);

  if (it == Nodes.end()) {
    return -1000; // milliseconds, negative == unknown
  }
  return static_cast<long>(it->second.getAge());
}

uint16_t getBuildnrForUnit(uint8_t unit) {
  auto it = Nodes.find(unit);

  if (it == Nodes.end() || it->second.build == 0) {
    return 0;
  }
  return it->second.build;
}

float getLoadForUnit(uint8_t unit) {
  auto it = Nodes.find(unit);

  if (it == Nodes.end()) {
    return 0.0f;
  }
  return it->second.getLoad();
}

uint8_t getTypeForUnit(uint8_t unit) {
  auto it = Nodes.find(unit);

  if (it == Nodes.end()) {
    return 0;
  }
  return it->second.nodeType;
}

const __FlashStringHelper* getTypeStringForUnit(uint8_t unit) {
  auto it = Nodes.find(unit);

  if (it == Nodes.end()) {
    return F("");
  }
  return it->second.getNodeTypeDisplayString();
}

/*********************************************************************************************\
   Refresh aging for remote units, drop if too old...
\*********************************************************************************************/
void refreshNodeList()
{
  unsigned long max_age;
  const unsigned long max_age_allowed = 10 * 60 * 1000; // 10 minutes

  Nodes.refreshNodeList(max_age_allowed, max_age);

  #ifdef USES_ESPEASY_NOW
  #ifdef ESP8266
  // FIXME TD-er: Do not perform regular scans on ESP32 as long as we cannot scan per channel
  if (!Nodes.isEndpoint()) {
    WifiScan(true, Nodes.getESPEasyNOW_channel());
  }
  #endif
  #endif

  if (max_age > (0.75 * max_age_allowed)) {
    Scheduler.sendGratuitousARP_now();
  }
  sendSysInfoUDP(1);
  #ifdef USES_ESPEASY_NOW
  if (Nodes.recentlyBecameDistanceZero()) {
    // Send to all channels
    ESPEasy_now_handler.sendDiscoveryAnnounce(-1);
  } else {
    ESPEasy_now_handler.sendDiscoveryAnnounce();
  }
  ESPEasy_now_handler.sendNTPquery();
  ESPEasy_now_handler.sendTraceRoute();
  #endif // ifdef USES_ESPEASY_NOW
}

/*********************************************************************************************\
   Broadcast system info to other nodes. (to update node lists)
\*********************************************************************************************/
void sendSysInfoUDP(uint8_t repeats)
{
  if ((Settings.UDPPort == 0) || !NetworkConnected(10)) {
    return;
  }

  // 1 byte 'binary token 255'
  // 1 byte id '1'
  // NodeStruct object (packed data struct)

  // send my info to the world...
# ifndef BUILD_NO_DEBUG
  addLog(LOG_LEVEL_DEBUG_MORE, F("UDP  : Send Sysinfo message"));
# endif // ifndef BUILD_NO_DEBUG

  const NodeStruct *thisNode = Nodes.getThisNode();

  if (thisNode == nullptr) {
    // Should not happen
    return;
  }

  // Prepare UDP packet to send
  constexpr size_t data_size = sizeof(NodeStruct) + 2;
  uint8_t data[data_size] = {0};
  data[0] = 255;
  data[1] = 1;
  memcpy(&data[2], thisNode, sizeof(NodeStruct));

  for (uint8_t counter = 0; counter < repeats; counter++)
  {
    statusLED(true);

    IPAddress broadcastIP(255, 255, 255, 255);
    FeedSW_watchdog();
    portUDP.beginPacket(broadcastIP, Settings.UDPPort);
    portUDP.write(data, data_size);
    portUDP.endPacket();

    if (counter < (repeats - 1)) {
      // FIXME TD-er: Must use scheduler to send out messages, not using delay
      delay(100);
    }
  }
}

#endif // FEATURE_ESPEASY_P2P

#if defined(ESP8266)

# if FEATURE_SSDP

/********************************************************************************************\
   Respond to HTTP XML requests for SSDP information
 \*********************************************************************************************/
void SSDP_schema() {
  if (!NetworkConnected(10)) {
    return;
  }

  const IPAddress ip     = NetworkLocalIP();
  const uint32_t  chipId = ESP.getChipId();
  char uuid[64];

  sprintf_P(uuid, PSTR("38323636-4558-4dda-9188-cda0e6%02x%02x%02x"),
            (uint16_t)((chipId >> 16) & 0xff),
            (uint16_t)((chipId >>  8) & 0xff),
            (uint16_t)chipId        & 0xff);

  web_server.client().print(F(
                 "HTTP/1.1 200 OK\r\n"
                 "Content-Type: text/xml\r\n"
                 "Connection: close\r\n"
                 "Access-Control-Allow-Origin: *\r\n"
                 "\r\n"
                 "<?xml version=\"1.0\"?>"
                 "<root xmlns=\"urn:schemas-upnp-org:device-1-0\">"
                 "<specVersion>"
                 "<major>1</major>"
                 "<minor>0</minor>"
                 "</specVersion>"
                 "<URLBase>http://"));

  web_server.client().print(formatIP(ip));
  web_server.client().print(F(":80/</URLBase>"
                 "<device>"
                 "<deviceType>urn:schemas-upnp-org:device:BinaryLight:1</deviceType>"
                 "<friendlyName>"));
  web_server.client().print(Settings.getName());
  web_server.client().print(F("</friendlyName>"
                 "<presentationURL>/</presentationURL>"
                 "<serialNumber>"));
  web_server.client().print(String(ESP.getChipId()));
  web_server.client().print(F("</serialNumber>"
                 "<modelName>ESP Easy</modelName>"
                 "<modelNumber>"));
  web_server.client().print(getValue(LabelType::GIT_BUILD));
  web_server.client().print(F("</modelNumber>"
                 "<modelURL>http://www.letscontrolit.com</modelURL>"
                 "<manufacturer>http://www.letscontrolit.com</manufacturer>"
                 "<manufacturerURL>http://www.letscontrolit.com</manufacturerURL>"
                 "<UDN>uuid:"));
  web_server.client().print(String(uuid));
  web_server.client().print(F("</UDN></device>"
                 "</root>\r\n"
                 "\r\n"));
}

/********************************************************************************************\
   Global SSDP stuff
 \*********************************************************************************************/

UdpContext *_server;

IPAddress _respondToAddr;
uint16_t  _respondToPort;

bool _pending;
unsigned short _delay;
unsigned long  _process_time;
unsigned long  _notify_time;

#  define SSDP_INTERVAL     1200
#  define SSDP_PORT         1900
#  define SSDP_METHOD_SIZE  10
#  define SSDP_URI_SIZE     2
#  define SSDP_BUFFER_SIZE  64
#  define SSDP_MULTICAST_TTL 2

static const IPAddress SSDP_MULTICAST_ADDR(239, 255, 255, 250);


/********************************************************************************************\
   Launch SSDP listener and send initial notify
 \*********************************************************************************************/
bool SSDP_begin() {
  _pending = false;

  if (_server != nullptr) {
    _server->unref();

    // FIXME TD-er: Shouldn't this also call delete _server ?

    _server = nullptr;
  }

  constexpr unsigned size = sizeof(UdpContext);
  void *ptr               = special_calloc(1, size);

  if (ptr != nullptr) {
    _server = new (ptr) UdpContext;
  }

  if (_server == nullptr) {
    return false;
  }
  _server->ref();

  ip_addr_t ifaddr;

  ifaddr.addr = NetworkLocalIP();
  ip_addr_t multicast_addr;

  multicast_addr.addr = (uint32_t)SSDP_MULTICAST_ADDR;

  if (igmp_joingroup(&ifaddr, &multicast_addr) != ERR_OK) {
    return false;
  }

#  ifdef CORE_POST_2_5_0

  // Core 2.5.0 changed the signature of some UdpContext function.
  if (!_server->listen(IP_ADDR_ANY, SSDP_PORT)) {
    return false;
  }

  _server->setMulticastInterface(&ifaddr);
  _server->setMulticastTTL(SSDP_MULTICAST_TTL);
  _server->onRx(&SSDP_update);

  if (!_server->connect(&multicast_addr, SSDP_PORT)) {
    return false;
  }
#  else // ifdef CORE_POST_2_5_0

  if (!_server->listen(*IP_ADDR_ANY, SSDP_PORT)) {
    return false;
  }

  _server->setMulticastInterface(ifaddr);
  _server->setMulticastTTL(SSDP_MULTICAST_TTL);
  _server->onRx(&SSDP_update);

  if (!_server->connect(multicast_addr, SSDP_PORT)) {
    return false;
  }
#  endif // ifdef CORE_POST_2_5_0

  SSDP_update();

  return true;
}

/********************************************************************************************\
   Send SSDP messages (notify & responses)
 \*********************************************************************************************/
void SSDP_send(uint8_t method) {
  uint32_t ip = NetworkLocalIP();

  // FIXME TD-er: Why create String objects of these flashstrings?
  String _ssdp_response_template = F(
    "HTTP/1.1 200 OK\r\n"
    "EXT:\r\n"
    "ST: upnp:rootdevice\r\n");

  String _ssdp_notify_template = F(
    "NOTIFY * HTTP/1.1\r\n"
    "HOST: 239.255.255.250:1900\r\n"
    "NT: upnp:rootdevice\r\n"
    "NTS: ssdp:alive\r\n");

  String _ssdp_packet_template = F(
    "%s"                                           // _ssdp_response_template / _ssdp_notify_template
    "CACHE-CONTROL: max-age=%u\r\n"                // SSDP_INTERVAL
    "SERVER: Arduino/1.0 UPNP/1.1 ESPEasy/%u\r\n"  // _modelNumber
    "USN: uuid:%s\r\n"                             // _uuid
    "LOCATION: http://%u.%u.%u.%u:80/ssdp.xml\r\n" // NetworkLocalIP(),
    "\r\n");
  {
    char uuid[64]   = { 0 };
    uint32_t chipId = ESP.getChipId();
    sprintf_P(uuid, PSTR("38323636-4558-4dda-9188-cda0e6%02x%02x%02x"),
              (uint16_t)((chipId >> 16) & 0xff),
              (uint16_t)((chipId >>  8) & 0xff),
              (uint16_t)chipId        & 0xff);

    char *buffer = nullptr;
    # ifdef USE_SECOND_HEAP
    {
      HeapSelectIram ephemeral;
      buffer = new (std::nothrow) char[1460]();
    }
    # endif // ifdef USE_SECOND_HEAP
    if (buffer == nullptr) {
      buffer = new (std::nothrow) char[1460]();
    }

    if (buffer == nullptr) { return; }
    int len = snprintf(buffer, 1460,
                       _ssdp_packet_template.c_str(),
                       (method == 0) ? _ssdp_response_template.c_str() : _ssdp_notify_template.c_str(),
                       SSDP_INTERVAL,
                       Settings.Build,
                       uuid,
                       IPADDR2STR(&ip)
                       );

    _server->append(buffer, len);
    delete[] buffer;
  }

  ip_addr_t remoteAddr;
  uint16_t  remotePort;

  if (method == 0) {
    remoteAddr.addr = _respondToAddr;
    remotePort      = _respondToPort;
  } else {
    remoteAddr.addr = SSDP_MULTICAST_ADDR;
    remotePort      = SSDP_PORT;
  }
  _server->send(&remoteAddr, remotePort);
  statusLED(true);
}

/********************************************************************************************\
   SSDP message processing
 \*********************************************************************************************/
void SSDP_update() {
  if (!_pending && _server->next()) {
    ssdp_method_t method = NONE;

    _respondToAddr = _server->getRemoteAddress();
    _respondToPort = _server->getRemotePort();

    typedef enum { METHOD, URI, PROTO, KEY, VALUE, ABORT } states;
    states state = METHOD;

    typedef enum { START, MAN, ST, MX } headers;
    headers header = START;

    uint8_t cursor = 0;
    uint8_t cr     = 0;

    char buffer[SSDP_BUFFER_SIZE] = { 0 };

    while (_server->getSize() > 0) {
      char c = _server->read();

      (c == '\r' || c == '\n') ? cr++ : cr = 0;

      switch (state) {
        case METHOD:

          if (c == ' ') {
            if (strcmp_P(buffer, PSTR("M-SEARCH")) == 0) { method = SEARCH; }
            else if (strcmp_P(buffer, PSTR("NOTIFY")) == 0) { method = NOTIFY; }

            if (method == NONE) { state = ABORT; }
            else { state = URI; }
            cursor = 0;
          } else if (cursor < SSDP_METHOD_SIZE - 1) {
            buffer[cursor++] = c;
            buffer[cursor]   = '\0';
          }
          break;
        case URI:

          if (c == ' ') {
            if (strcmp(buffer, "*")) { state = ABORT; }
            else { state = PROTO; }
            cursor = 0;
          } else if (cursor < SSDP_URI_SIZE - 1) {
            buffer[cursor++] = c;
            buffer[cursor]   = '\0';
          }
          break;
        case PROTO:

          if (cr == 2) {
            state  = KEY;
            cursor = 0;
          }
          break;
        case KEY:

          if (cr == 4) {
            _pending      = true;
            _process_time = millis();
          }
          else if (c == ' ') {
            cursor = 0;
            state  = VALUE;
          }
          else if ((c != '\r') && (c != '\n') && (c != ':') && (cursor < SSDP_BUFFER_SIZE - 1)) {
            buffer[cursor++] = c;
            buffer[cursor]   = '\0';
          }
          break;
        case VALUE:

          if (cr == 2) {
            switch (header) {
              case START:
                break;
              case MAN:
                break;
              case ST:

                if (strcmp_P(buffer, PSTR("ssdp:all"))) {
                  state = ABORT;
                }

                // if the search type matches our type, we should respond instead of ABORT
                if (strcmp_P(buffer, PSTR("urn:schemas-upnp-org:device:BinaryLight:1")) == 0) {
                  _pending      = true;
                  _process_time = millis();
                  state         = KEY;
                }
                break;
              case MX:
                _delay = HwRandom(0, atoi(buffer)) * 1000L;
                break;
            }

            if (state != ABORT) {
              state  = KEY;
              header = START;
              cursor = 0;
            }
          } else if ((c != '\r') && (c != '\n')) {
            if (header == START) {
              if (strncmp(buffer, "MA", 2) == 0) { header = MAN; }
              else if (strcmp(buffer, "ST") == 0) { header = ST; }
              else if (strcmp(buffer, "MX") == 0) { header = MX; }
            }

            if (cursor < SSDP_BUFFER_SIZE - 1) {
              buffer[cursor++] = c;
              buffer[cursor]   = '\0';
            }
          }
          break;
        case ABORT:
          _pending = false; _delay = 0;
          break;
      }
    }
  }

  if (_pending && timeOutReached(_process_time + _delay)) {
    _pending = false; _delay = 0;
    SSDP_send(NONE);
  } else if ((_notify_time == 0) || timeOutReached(_notify_time + (SSDP_INTERVAL * 1000L))) {
    _notify_time = millis();
    SSDP_send(NOTIFY);
  }

  if (_pending) {
    while (_server->next()) {
      _server->flush();
    }
  }
}

# endif // if FEATURE_SSDP
#endif  // if defined(ESP8266)


// ********************************************************************************
// Return subnet range of WiFi.
// ********************************************************************************
bool getSubnetRange(IPAddress& low, IPAddress& high)
{
  if (!WiFiEventData.WiFiGotIP()) {
    return false;
  }

  const IPAddress ip     = NetworkLocalIP();
  const IPAddress subnet = NetworkSubnetMask();

  low  = ip;
  high = ip;

  // Compute subnet range.
  for (uint8_t i = 0; i < 4; ++i) {
    if (subnet[i] != 255) {
      low[i]  = low[i] & subnet[i];
      high[i] = high[i] | ~subnet[i];
    }
  }
  return true;
}

// ********************************************************************************
// Functions to test and handle network/client connectivity.
// ********************************************************************************

#ifdef CORE_POST_2_5_0
# include <AddrList.h>
#endif // ifdef CORE_POST_2_5_0


bool hasIPaddr() {
  if (useStaticIP()) { return true; }

#ifdef CORE_POST_2_5_0
  bool configured = false;

  for (auto addr : addrList) {
    if ((configured = (!addr.isLocal() && (addr.ifnumber() == STATION_IF)))) {
      /*
         ESPEASY_SERIAL_CONSOLE_PORT.printf("STA: IF='%s' hostname='%s' addr= %s\n",
                    addr.ifname().c_str(),
                    addr.ifhostname(),
                    addr.toString().c_str());
       */
      break;
    }
  }
  return configured;
#else // ifdef CORE_POST_2_5_0
  return WiFi.isConnected();
#endif // ifdef CORE_POST_2_5_0
}

bool useStaticIP() {
  #if FEATURE_ETHERNET
  if (active_network_medium == NetworkMedium_t::Ethernet) {
    return ethUseStaticIP();
  }
  #endif
  return WiFiUseStaticIP();
}

// Check connection. Maximum timeout 500 msec.
bool NetworkConnected(uint32_t timeout_ms) {
  if (NetworkConnected()) return true;

#ifdef USES_ESPEASY_NOW
  if (isESPEasy_now_only()) {
    return false;
  }
#endif

  if (timeout_ms > 500) {
    timeout_ms = 500;
  }

  uint32_t timer     = millis() + timeout_ms;
  uint32_t min_delay = timeout_ms / 20;

  if (min_delay < 10) {
    delay(0); // Allow at least once time for backgroundtasks
    min_delay = 10;
  }

  // Apparently something needs network, perform check to see if it is ready now.
  while (!NetworkConnected()) {
    if (timeOutReached(timer)) {
      return false;
    }
    delay(min_delay); // Allow the backgroundtasks to continue procesing.
  }
  return true;
}

bool hostReachable(const IPAddress& ip) {
  if (!NetworkConnected()) { return false; }

  return true; // Disabled ping as requested here:
  // https://github.com/letscontrolit/ESPEasy/issues/1494#issuecomment-397872538

  /*
     // Only do 1 ping at a time to return early
     uint8_t retry = 3;
     while (retry > 0) {
   #if defined(ESP8266)
      if (Ping.ping(ip, 1)) return true;
   #endif
   #if defined(ESP32)
     if (ping_start(ip, 4, 0, 0, 5)) return true;
   #endif
      delay(50);
      --retry;
     }
     if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
      String log = F("Host unreachable: ");
      log += formatIP(ip);
      addLog(LOG_LEVEL_ERROR, log);
     }
     if (ip[1] == 0 && ip[2] == 0 && ip[3] == 0) {
      // Work-around to fix connected but not able to communicate.
      addLog(LOG_LEVEL_ERROR, F("WiFi : Detected strange behavior, reconnect wifi."));
      WifiDisconnect();
     }
     logConnectionStatus();
     return false;
   */
}

#if FEATURE_HTTP_CLIENT
bool connectClient(WiFiClient& client, const char *hostname, uint16_t port, uint32_t timeout_ms) {
  IPAddress ip;

  if (resolveHostByName(hostname, ip, timeout_ms)) {
    return connectClient(client, ip, port, timeout_ms);
  }
  return false;
}

bool connectClient(WiFiClient& client, IPAddress ip, uint16_t port, uint32_t timeout_ms)
{
  START_TIMER;

  if (!NetworkConnected()) {
    client.stop();
    return false;
  }
#ifndef BUILD_NO_DEBUG
  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLog(LOG_LEVEL_DEBUG, strformat(
      F("connectClient: '%s' port: %u"),
      ip.toString().c_str(),
      port));
  }
#endif

  // In case of domain name resolution error result can be negative.
  // https://github.com/esp8266/Arduino/blob/18f643c7e2d6a0da9d26ff2b14c94e6536ab78c1/libraries/Ethernet/src/Dns.cpp#L44
  // Thus must match the result with 1.
  bool connected = (client.connect(ip, port) == 1);

  delay(0);

  if (!connected) {
#ifndef BUILD_NO_DEBUG
  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLog(LOG_LEVEL_ERROR, strformat(
      F("connectClient: connect failed to '%s' port: %u"),
      ip.toString().c_str(),
      port));
  }
#endif

    Scheduler.sendGratuitousARP_now();
    client.stop(); // Make sure to start over without some stale connection
  }
  STOP_TIMER(CONNECT_CLIENT_STATS);
#if defined(ESP32) || defined(ARDUINO_ESP8266_RELEASE_2_3_0) || defined(ARDUINO_ESP8266_RELEASE_2_4_0)
#else

  if (connected) {
    client.keepAlive(); // Use default keep alive values
  }
#endif // if defined(ESP32) || defined(ARDUINO_ESP8266_RELEASE_2_3_0) || defined(ARDUINO_ESP8266_RELEASE_2_4_0)
  return connected;
}
#endif // FEATURE_HTTP_CLIENT

void scrubDNS() {
  #if FEATURE_ETHERNET
  if (active_network_medium == NetworkMedium_t::Ethernet) {
    if (EthEventData.EthServicesInitialized()) {
      setDNS(0, EthEventData.dns0_cache);
      setDNS(1, EthEventData.dns1_cache);
    }
    return;
  }
  #endif
  if (WiFiEventData.WiFiServicesInitialized()) {
    setDNS(0, WiFiEventData.dns0_cache);
    setDNS(1, WiFiEventData.dns1_cache);
  }
}

bool valid_DNS_address(const IPAddress& dns) {
  return (/*dns.v4() != (uint32_t)0x00000000 && */
          dns != IPAddress((uint32_t)0xFD000000) && 
#ifdef ESP32
          // Bug where IPv6 global prefix is set as DNS
          // Global IPv6 prefixes currently start with 2xxx::
          (dns[0] & 0xF0) != 0x20 && 
#endif
          dns != INADDR_NONE);
}

bool setDNS(int index, const IPAddress& dns) {
  if (index >= 2) return false;
  #ifdef ESP8266
  if(dns.isSet() && dns != WiFi.dnsIP(index)) {
    dns_setserver(index, dns);
    #ifndef BUILD_NO_DEBUG
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, concat(F("IP   : Set DNS: "),  formatIP(dns)));
    }
    #endif
    return true;
  }
  #endif
  #ifdef ESP32
  ip_addr_t d;
  d.type = IPADDR_TYPE_V4;

  if (valid_DNS_address(dns) || dns == INADDR_NONE) {
    // Set DNS0-Server
    d.u_addr.ip4.addr = static_cast<uint32_t>(dns);
    const ip_addr_t* cur_dns = dns_getserver(index);
    if (cur_dns != nullptr && cur_dns->u_addr.ip4.addr == d.u_addr.ip4.addr) {
      // Still the same as before
      return false;
    }
    dns_setserver(index, &d);
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, concat(F("IP   : Set DNS: "),  formatIP(dns)));
    }
    return true;
  }
  #endif
  return false;
}

bool resolveHostByName(const char *aHostname, IPAddress& aResult, uint32_t timeout_ms) {
  START_TIMER;

  if (!NetworkConnected()) {
    return false;
  }

  FeedSW_watchdog();

  // FIXME TD-er: Must try to restore DNS server entries.
  scrubDNS();

#if defined(ARDUINO_ESP8266_RELEASE_2_3_0) || defined(ESP32)
  bool resolvedIP = WiFi.hostByName(aHostname, aResult) == 1;
#else // if defined(ARDUINO_ESP8266_RELEASE_2_3_0) || defined(ESP32)
  bool resolvedIP = WiFi.hostByName(aHostname, aResult, timeout_ms) == 1;
#endif // if defined(ARDUINO_ESP8266_RELEASE_2_3_0) || defined(ESP32)
  delay(0);
  FeedSW_watchdog();

  if (!resolvedIP) {
    Scheduler.sendGratuitousARP_now();
  }
  STOP_TIMER(HOST_BY_NAME_STATS);
  return resolvedIP;
}

bool hostReachable(const String& hostname) {
  IPAddress remote_addr;

  if (resolveHostByName(hostname.c_str(), remote_addr)) {
    return hostReachable(remote_addr);
  }

  if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
    addLogMove(LOG_LEVEL_ERROR,  concat(F("Hostname cannot be resolved: "), hostname));
  }
  return false;
}

// Create a random port for the UDP connection.
// Return true when successful.
bool beginWiFiUDP_randomPort(WiFiUDP& udp) {
  if (!NetworkConnected()) {
    return false;
  }
  unsigned int attempts = 3;

  while (attempts > 0) {
    --attempts;
    long port = HwRandom(1025, 65535);

    if (udp.begin(port) != 0) {
      return true;
    }
  }
  return false;
}

void sendGratuitousARP() {
  if (!NetworkConnected()) {
    return;
  }
#ifdef SUPPORT_ARP

  // See https://github.com/letscontrolit/ESPEasy/issues/2374
  START_TIMER;
  netif *n = netif_list;

  while (n) {
    if ((n->hwaddr_len == ETH_HWADDR_LEN) &&
        (n->flags & NETIF_FLAG_ETHARP) &&
        ((n->flags & NETIF_FLAG_LINK_UP) && (n->flags & NETIF_FLAG_UP))) {
      # ifdef ESP32
      etharp_gratuitous_r(n);
      # else // ifdef ESP32
      etharp_gratuitous(n);
      # endif // ifdef ESP32
    }
    n = n->next;
  }
  STOP_TIMER(GRAT_ARP_STATS);
#endif // ifdef SUPPORT_ARP
}

bool splitHostPortString(const String& hostPortString, String& host, uint16_t& port) {
  port = 80; // Some default
  int index_colon = hostPortString.indexOf(':');

  if (index_colon >= 0) {
    int32_t port_tmp;

    if (!validIntFromString(hostPortString.substring(index_colon + 1), port_tmp)) {
      return false;
    }

    if ((port_tmp < 0) || (port_tmp > 65535)) { return false; }
    port = port_tmp;
    host = hostPortString.substring(0, index_colon);
  } else {
    // No port nr defined.
    host = hostPortString;
  }
  return true;
}

bool splitUserPass_HostPortString(const String& hostPortString, String& user, String& pass, String& host, uint16_t& port)
{
  const int pos_at = hostPortString.indexOf('@');

  if (pos_at != -1) {
    user = hostPortString.substring(0, pos_at);
    const int pos_colon = user.indexOf(':');

    if (pos_colon != -1) {
      pass = user.substring(pos_colon + 1);
      user = user.substring(0, pos_colon);
    }
    return splitHostPortString(hostPortString.substring(pos_at + 1), host, port);
  }
  return splitHostPortString(hostPortString, host, port);
}

// Split a full URL like "http://hostname:port/path/file.htm"
// Return value is everything after the hostname:port section (including /)
String splitURL(const String& fullURL, String& user, String& pass, String& host, uint16_t& port, String& file) {
  int starthost = fullURL.indexOf(F("://"));

  if (starthost == -1) {
    starthost = 0;
  } else {
    starthost += 3;
  }
  const int endhost = fullURL.indexOf('/', starthost);
  splitUserPass_HostPortString(fullURL.substring(starthost, endhost), user, pass, host, port);

  if (endhost == -1) {
    return EMPTY_STRING;
  }

  #if FEATURE_HTTP_TLS
  if ((starthost > 3) && (port == 80)) { // 'Upgrade' from port 80 to port 443 for HTTPS url
    String proto = fullURL.substring(0, starthost - 3);
    proto.toLowerCase();

    if (equals(proto, F("https"))) {
      port = 443;
    }
  }
  #endif // if FEATURE_HTTP_TLS

  int startfile = fullURL.lastIndexOf('/');

  if (startfile >= 0) {
    file = fullURL.substring(startfile);
  }
  return fullURL.substring(endhost);
}

String get_user_agent_string() {
  static unsigned int agent_size = 20;
  String userAgent;

  userAgent.reserve(agent_size);
  userAgent += F("ESP Easy/");
  userAgent += get_build_nr();
  userAgent += '/';
  userAgent += get_build_date();
  userAgent += ' ';
  userAgent += get_build_time();
  agent_size = userAgent.length();
  return userAgent;
}

bool splitHeaders(int& strpos, const String& multiHeaders, String& name, String& value) {
  if (strpos < 0) {
    return false;
  }
  int colonPos = multiHeaders.indexOf(':', strpos);

  if (colonPos < 0) {
    return false;
  }
  name = multiHeaders.substring(strpos, colonPos);
  int valueEndPos = multiHeaders.indexOf('\n', colonPos + 1);

  if (valueEndPos < 0) {
    value  = multiHeaders.substring(colonPos + 1);
    strpos = -1;
  } else {
    value  = multiHeaders.substring(colonPos + 1, valueEndPos);
    strpos = valueEndPos + 1;
  }
  value.replace('\r', ' ');
  value.trim();
  return true;
}

String extractParam(const String& authReq, const String& param, const char delimit) {
  int _begin = authReq.indexOf(param);

  if (_begin == -1) { return EMPTY_STRING; }
  return authReq.substring(_begin + param.length(), authReq.indexOf(delimit, _begin + param.length()));
}

#if FEATURE_HTTP_CLIENT
String getCNonce(const int len) {
  static const char alphanum[] = "0123456789"
                                 "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
                                 "abcdefghijklmnopqrstuvwxyz";
  String s;

  for (int i = 0; i < len; ++i) {
    // FIXME TD-er: Is this "-1" correct? The mod operator makes sure we never reach the sizeof index
    s += alphanum[rand() % (sizeof(alphanum) - 1)];
  }

  return s;
}

String getDigestAuth(const String& authReq,
                     const String& username,
                     const String& password,
                     const String& method,
                     const String& uri,
                     unsigned int  counter) {
  // extracting required parameters for RFC 2069 simpler Digest
  const String realm  = extractParam(authReq, F("realm=\""), '"');
  const String nonce  = extractParam(authReq, F("nonce=\""), '"');
  const String cNonce = getCNonce(8);

  char nc[9];

  snprintf(nc, sizeof(nc), "%08x", counter);

  // parameters for the RFC 2617 newer Digest
  MD5Builder md5;

  md5.begin();
  md5.add(username + ':' + realm + ':' + password); // md5 of the user:realm:user
  md5.calculate();
  const String h1 = md5.toString();

  md5.begin();
  md5.add(method + ':' + uri);
  md5.calculate();
  const String h2 = md5.toString();

  md5.begin();
  md5.add(h1 + ':' + nonce + ':' + String(nc) + ':' + cNonce + F(":auth:") + h2);
  md5.calculate();

  // return authorization
  return strformat(
    F("Digest username=\"%s\""
    ", realm=\"%s\""
    ", nonce=\"%s\""
    ", uri=\"%s\""
    ", algorithm=\"MD5\", qop=auth, nc=%s, cnonce=\"%s\""
    ", response=\"%s\""),
    username.c_str(),
    realm.c_str(),
    nonce.c_str(),
    uri.c_str(),
    nc,
    cNonce.c_str(),
    md5.toString().c_str()); // response
}

#ifndef BUILD_NO_DEBUG
void log_http_result(const HTTPClient& http,
                     const String    & logIdentifier,
                     const String    & host,
                     const String    & HttpMethod,
                     int               httpCode,
                     const String    & response)
{
  uint8_t loglevel = LOG_LEVEL_ERROR;
  bool    success  = false;

  // HTTP codes:
  // 1xx Informational response
  // 2xx Success
  if ((httpCode >= 100) && (httpCode < 300)) {
    loglevel = LOG_LEVEL_INFO;
    success  = true;
  }

  if (loglevelActiveFor(loglevel)) {
    String log = strformat(F("HTTP : %s %s %s"), 
                          logIdentifier.c_str(), host.c_str(), HttpMethod.c_str());

    if (!success) {
      log += F("failed ");
    }
    log += concat(F("HTTP code: "), httpCode);

    if (!success) {
      log += ' ';
      log += http.errorToString(httpCode);
    }

    if (response.length() > 0) {
      log += concat(F(" Received reply: "), response.substring(0, 100)); // Returned string may be huge, so only log the first part.
    }
    addLogMove(loglevel, log);
  }
}
#endif

int http_authenticate(const String& logIdentifier,
                      WiFiClient  & client,
                      HTTPClient  & http,
                      uint16_t      timeout,
                      const String& user,
                      const String& pass,
                      const String& host,
                      uint16_t      port,
                      const String& uri,
                      const String& HttpMethod,
                      const String& header,
                      const String& postStr,
                      bool          must_check_reply)
{
  if (!uri.startsWith(F("/"))) {
    return http_authenticate(
      logIdentifier,
      client,
      http,
      timeout,
      user,
      pass,
      host,
      port,
      concat(F("/"), uri),
      HttpMethod,
      header,
      postStr,
      must_check_reply);
  }
  int httpCode = 0;
  const bool hasCredentials = !user.isEmpty() && !pass.isEmpty();

  if (hasCredentials) {
    must_check_reply = true;
    http.setAuthorization(user.c_str(), pass.c_str());
  } else {
    http.setAuthorization(""); // Clear Basic authorization
#ifdef ESP32
    http.setAuthorizationType(""); // Default type is "Basic"
#endif
  }
  http.setTimeout(timeout);
  http.setUserAgent(get_user_agent_string());

  if (Settings.SendToHTTP_follow_redirects()) {
    http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
    http.setRedirectLimit(2);
  }

  #ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS

  // See: https://github.com/espressif/arduino-esp32/pull/6676
  client.setTimeout((timeout + 500) / 1000); // in seconds!!!!
  Client *pClient = &client;
  pClient->setTimeout(timeout);
  #else // ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS
  client.setTimeout(timeout);                // in msec as it should be!
  #endif // ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS

  // Add request header as fall back.
  // When adding another "accept" header, it may be interpreted as:
  // "if you have XXX, send it; or failing that, just give me what you've got."
  http.addHeader(F("Accept"), F("*/*;q=0.1"));

  // Add client IP
  http.addHeader(F("X-Forwarded-For"), formatIP(NetworkLocalIP()));

  delay(0);
  scrubDNS();
#if defined(CORE_POST_2_6_0) || defined(ESP32)
  http.begin(client, host, port, uri, false); // HTTP
#else // if defined(CORE_POST_2_6_0) || defined(ESP32)
  http.begin(client, host, port, uri);
#endif // if defined(CORE_POST_2_6_0) || defined(ESP32)

  const char *keys[] = { "WWW-Authenticate" };
  http.collectHeaders(keys, 1);

  {
    int headerpos = 0;
    String name, value;

    while (splitHeaders(headerpos, header, name, value)) {
      // Disabled the check to exclude "Authorization", due to: 
      //   https://github.com/letscontrolit/ESPEasy/issues/4364
      // Check was added for: https://github.com/letscontrolit/ESPEasy/issues/4355
      // However, I doubt this was the actual bug. More likely the supplied credential strings were not entirely empty for whatever reason.
      //
      // Work-around to not add Authorization header since the HTTPClient code
      // only ignores this when base64Authorication is set.
      
//      if (!name.equalsIgnoreCase(F("Authorization"))) {
        http.addHeader(name, value);
//      }
    }
  }

  // start connection and send HTTP header (and body)
  if (equals(HttpMethod, F("HEAD")) || equals(HttpMethod, F("GET"))) {
    httpCode = http.sendRequest(HttpMethod.c_str());
  } else {
    httpCode = http.sendRequest(HttpMethod.c_str(), postStr);
  }

  // Check to see if we need to try digest auth
  if ((httpCode == 401) && must_check_reply) {
    const String authReq = http.header(String(F("WWW-Authenticate")).c_str());

    if (authReq.indexOf(F("Digest")) != -1) {
      // Use Digest authorization
      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLogMove(LOG_LEVEL_INFO, concat(F("HTTP : Start Digest Authorization for "), host));
      }

      http.setAuthorization(""); // Clear Basic authorization
#ifdef ESP32
      http.setAuthorizationType(""); // Default type is "Basic" and "Digest" is already part of the string generated by getDigestAuth()
#endif
      const String authorization = getDigestAuth(authReq, user, pass, F("GET"), uri, 1);

      http.end();
#if defined(CORE_POST_2_6_0) || defined(ESP32)
      http.begin(client, host, port, uri, false); // HTTP, not HTTPS
#else // if defined(CORE_POST_2_6_0) || defined(ESP32)
      http.begin(client, host, port, uri);
#endif // if defined(CORE_POST_2_6_0) || defined(ESP32)

      http.addHeader(F("Authorization"), authorization);

      // start connection and send HTTP header (and body)
      if (equals(HttpMethod, F("HEAD")) || equals(HttpMethod, F("GET"))) {
        httpCode = http.sendRequest(HttpMethod.c_str());
      } else {
        httpCode = http.sendRequest(HttpMethod.c_str(), postStr);
      }
    }
  }

  if (!must_check_reply) {
    // There are services which do not send an ack.
    // So if the return code matches a read timeout, we change it into HTTP code 200
    if (httpCode == HTTPC_ERROR_READ_TIMEOUT) {
      httpCode = 200;
    }
  }

  if (Settings.UseRules) {
    // Generate event with the HTTP return code
    // e.g. http#hostname=401
    eventQueue.addMove(strformat(F("http#%s=%d"), host.c_str(), httpCode));

// ----This way to the custom response parser----------------
#if RESPONSE_PARSER_SUPPORT
    eventFromResponse(host, httpCode, uri, http);
#endif
  }
// -----------------------------------------------------------

#ifndef BUILD_NO_DEBUG
  log_http_result(http, logIdentifier, host + ':' + port, HttpMethod, httpCode, EMPTY_STRING);
#endif
  return httpCode;
}

# if FEATURE_HTTP_TLS
#  include <WiFiClientSecureLightBearSSL.h>
#  include "../CustomBuild/Certificate_CA.h"
# endif // if FEATURE_HTTP_TLS

String send_via_http(const String& logIdentifier,
                     uint16_t      timeout,
                     const String& user,
                     const String& pass,
                     const String& host,
                     uint16_t      port,
                     const String& uri,
                     const String& HttpMethod,
                     const String& header,
                     const String& postStr,
                     int         & httpCode,
                     bool          must_check_reply
                     #if FEATURE_HTTP_TLS
                     , TLS_types   tlsType
                     #endif // if FEATURE_HTTP_TLS
                    ) {
  WiFiClient client;
  #if FEATURE_HTTP_TLS
  BearSSL::WiFiClientSecure_light *client_secure = nullptr;
  if (tlsType != TLS_types::NoTLS) {
    client_secure = new (std::nothrow) BearSSL::WiFiClientSecure_light(2048, 2048);
    if (nullptr == client_secure) {
      return F("HTTPS: Could not create TLS client, out of memory");
    }
    client_secure->setUtcTime_fcn(getUnixTime);
    client_secure->setCfgTime_fcn(get_build_unixtime);
    switch (tlsType) {
      case TLS_types::NoTLS:
        break;
      // TODO: Implement?
      // case TLS_types::TLS_PSK:
      // case TLS_types::TLS_CA_CLI_CERT:
      //   break;
      case TLS_types::TLS_FINGERPRINT:
        // TODO: Implement
        addLog(LOG_LEVEL_ERROR, F("HTTPS: TLS_FINGERPRINT Not implemented yet."));
        client_secure->setInsecure();
        break;
      case TLS_types::TLS_CA_CERT:
        // TODO: Read from filesystem?
        client_secure->setTrustAnchor(Tasmota_TA, Tasmota_TA_size);
        break;
      case TLS_types::TLS_insecure:
        client_secure->setInsecure();
        break;
    }
    # ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS

    // See: https://github.com/espressif/arduino-esp32/pull/6676
    client_secure->setTimeout((timeout + 500) / 1000); // in seconds!!!!
    Client *pClient = client_secure;
    pClient->setTimeout(timeout);
    # else // ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS
    client_secure->setTimeout(timeout); // in msec as it should be!
    # endif // ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS
  }
  #endif // if FEATURE_HTTP_TLS
  HTTPClient http;
  http.setReuse(false);

  httpCode = http_authenticate(
    logIdentifier,
    #if FEATURE_HTTP_TLS
    tlsType != TLS_types::NoTLS ? *client_secure :
    #endif // if FEATURE_HTTP_TLS
    client,
    http,
    timeout,
    user,
    pass,
    host,
    port,
    uri,
    HttpMethod,
    header,
    postStr,
    must_check_reply);

  String response;

  if ((httpCode > 0) && must_check_reply) {
    response = http.getString();
#ifndef BUILD_NO_DEBUG
    if (!response.isEmpty()) {
      log_http_result(http, logIdentifier, host, HttpMethod, httpCode, response);
    }
#endif
  }

  http.end();
  // http.end() does not call client.stop() if it is no longer connected.
  // However the client may still keep its internal state which may prevent 
  // future connections to the same host until there has been a connection to another host inbetween.
  client.stop();

  #if FEATURE_HTTP_TLS
  if (nullptr != client_secure) {
    client_secure->stop();    
  }
  #endif // if FEATURE_HTTP_TLS

  return response;
}
#endif // FEATURE_HTTP_CLIENT

#if FEATURE_DOWNLOAD

// FIXME TD-er: Must set the timeout somewhere
# ifndef DOWNLOAD_FILE_TIMEOUT
  #  define DOWNLOAD_FILE_TIMEOUT 2000
# endif // ifndef DOWNLOAD_FILE_TIMEOUT

// Download a file from a given URL and save to a local file named "file_save"
// If the URL ends with a /, the file part will be assumed the same as file_save.
// If file_save is empty, the file part from the URL will be used as local file name.
// Return true when successful.
bool downloadFile(const String& url, String file_save) {
  String error;

  return downloadFile(url, file_save, EMPTY_STRING, EMPTY_STRING, error);
}

// User and Pass may be updated if they occur in the hostname part.
// Thus have to be copied instead of const reference.
bool start_downloadFile(WiFiClient  & client,
                        HTTPClient  & http,
                        const String& url,
                        String      & file_save,
                        String        user,
                        String        pass,
                        String      & error) {
  String   host, file;
  uint16_t port;
  String   uri = splitURL(url, user, pass, host, port, file);

  if (file_save.isEmpty()) {
    file_save = file;
  } else if ((file.isEmpty()) && uri.endsWith("/")) {
    // file = file_save;
    uri += file_save;
  }
# ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLogMove(LOG_LEVEL_DEBUG, strformat(F("downloadFile: URL: %s decoded: %s:%d%s"), 
                                          url.c_str(), host.c_str(), port, uri.c_str()));
  }
# endif // ifndef BUILD_NO_DEBUG

  if (file_save.isEmpty()) {
    error = F("Empty filename");
    addLog(LOG_LEVEL_ERROR, error);
    return false;
  }

  const int httpCode = http_authenticate(
    F("DownloadFile"),
    client,
    http,
    DOWNLOAD_FILE_TIMEOUT,
    user,
    pass,
    host,
    port,
    uri,
    F("GET"),
    EMPTY_STRING, // header
    EMPTY_STRING, // postStr
    true          // must_check_reply
    );

  if (httpCode != HTTP_CODE_OK) {
    error  = strformat(F("HTTP code: %d %s"), httpCode, url.c_str());

    addLog(LOG_LEVEL_ERROR, error);
    http.end();
    client.stop();
    return false;
  }
  return true;
}

bool downloadFile(const String& url, String file_save, const String& user, const String& pass, String& error) {
  WiFiClient client;
  HTTPClient http;
  http.setReuse(false);

  if (!start_downloadFile(client, http, url, file_save, user, pass, error)) {
    return false;
  }

  if (fileExists(file_save)) {
    error  = concat(F("File exists: "), file_save);
    addLog(LOG_LEVEL_ERROR, error);
    http.end();
    client.stop();
    return false;
  }

  long len   = http.getSize();
  fs::File f = tryOpenFile(file_save, "w");

  if (f) {
    const size_t downloadBuffSize = 256;
    uint8_t buff[downloadBuffSize];
    size_t  bytesWritten  = 0;
    unsigned long timeout = millis() + DOWNLOAD_FILE_TIMEOUT;

    // get tcp stream
    WiFiClient *stream = &client;

    // read all data from server
    while (http.connected() && (len > 0 || len == -1)) {
      // read up to downloadBuffSize at a time.
      size_t bytes_to_read = downloadBuffSize;

      if ((len > 0) && (len < static_cast<int>(bytes_to_read))) {
        bytes_to_read = len;
      }
      const size_t c = stream->readBytes(buff, bytes_to_read);

      if (c > 0) {
        timeout = millis() + DOWNLOAD_FILE_TIMEOUT;

        if (f.write(buff, c) != c) {
          error  = strformat(F("Error saving file: %s %d Bytes written"), file_save.c_str(), bytesWritten);
          addLog(LOG_LEVEL_ERROR, error);
          http.end();
          client.stop();
          return false;
        }
        bytesWritten += c;

        if (len > 0) { len -= c; }
      }

      if (timeOutReached(timeout)) {
        error  = concat(F("Timeout: "), file_save);
        addLog(LOG_LEVEL_ERROR, error);
        delay(0);
        http.end();
        client.stop();
        return false;
      }
      delay(0);
    }
    f.close();
    http.end();
    client.stop();

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO, strformat(F("downloadFile: %s Success"), file_save.c_str()));
    }
    return true;
  }
  http.end();
  client.stop();
  error  = concat(F("Failed to open file for writing: "), file_save);
  addLog(LOG_LEVEL_ERROR, error);
  return false;
}

bool downloadFirmware(String filename, String& error)
{
  String baseurl, user, pass;
# if FEATURE_CUSTOM_PROVISIONING
  MakeProvisioningSettings(ProvisioningSettings);

  if (ProvisioningSettings.get()) {
    loadProvisioningSettings(*ProvisioningSettings);
    if (!ProvisioningSettings->allowedFlags.allowFetchFirmware) {
      error = F("Not Allowed");
      return false;
    }
    baseurl = ProvisioningSettings->url;
    user = ProvisioningSettings->user;
    pass = ProvisioningSettings->pass;
  }
# endif // if FEATURE_CUSTOM_PROVISIONING

  const String fullUrl = joinUrlFilename(baseurl, filename);

  return downloadFirmware(fullUrl, filename, user, pass, error);
}

bool downloadFirmware(const String& url, String& file_save, String& user, String& pass, String& error)
{
  WiFiClient client;
  HTTPClient http;
  error.clear();

  if (!start_downloadFile(client, http, url, file_save, user, pass, error)) {
    return false;
  }

  int len = http.getSize();

  if (Update.begin(len, U_FLASH, Settings.Pin_status_led, Settings.Pin_status_led_Inversed ? LOW : HIGH)) {
    const size_t downloadBuffSize = 256;
    uint8_t buff[downloadBuffSize];
    size_t  bytesWritten  = 0;
    unsigned long timeout = millis() + DOWNLOAD_FILE_TIMEOUT;

    // get tcp stream
    WiFiClient *stream = &client;

    while (error.isEmpty() && http.connected() && (len > 0 || len == -1)) {
      // read up to downloadBuffSize at a time.
      size_t bytes_to_read = downloadBuffSize;

      if ((len > 0) && (len < static_cast<int>(bytes_to_read))) {
        bytes_to_read = len;
      }
      const size_t c = stream->readBytes(buff, bytes_to_read);

      if (c > 0) {
        timeout = millis() + DOWNLOAD_FILE_TIMEOUT;

        if (Update.write(buff, c) != c) {
          error  = strformat(F("Error saving firmware update: %s %d Bytes written"),
                             file_save.c_str(), bytesWritten);
          break;
        }
        bytesWritten += c;

        if (len > 0) { len -= c; }
      }

      if (timeOutReached(timeout)) {
        error  = concat(F("Timeout: "), file_save);
        break;
      }

      if (!UseRTOSMultitasking) {
        // On ESP32 the schedule is executed on the 2nd core.
        Scheduler.handle_schedule();
      }
      backgroundtasks();
    }
  }
  http.end();
  client.stop();

  if (error.isEmpty() && loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, strformat(F("downloadFile: %s Success"), file_save.c_str()));
  }

  uint8_t errorcode = 0;
  if (!Update.end()) {
    errorcode = Update.getError();
#ifdef ESP32
    const __FlashStringHelper * err_fstr = F("Unknown");
    switch (errorcode) {
      case UPDATE_ERROR_OK:                  err_fstr = F("OK");           break;
      case UPDATE_ERROR_WRITE:               err_fstr = F("WRITE");        break;
      case UPDATE_ERROR_ERASE:               err_fstr = F("ERASE");        break;
      case UPDATE_ERROR_READ:                err_fstr = F("READ");         break;
      case UPDATE_ERROR_SPACE:               err_fstr = F("SPACE");        break;
      case UPDATE_ERROR_SIZE:                err_fstr = F("SIZE");         break;
      case UPDATE_ERROR_STREAM:              err_fstr = F("STREAM");       break;
      case UPDATE_ERROR_MD5:                 err_fstr = F("MD5");          break;
      case UPDATE_ERROR_MAGIC_BYTE:          err_fstr = F("MAGIC_BYTE");   break;
      case UPDATE_ERROR_ACTIVATE:            err_fstr = F("ACTIVATE");     break;
      case UPDATE_ERROR_NO_PARTITION:        err_fstr = F("NO_PARTITION"); break;
      case UPDATE_ERROR_BAD_ARGUMENT:        err_fstr = F("BAD_ARGUMENT"); break;
      case UPDATE_ERROR_ABORT:               err_fstr = F("ABORT");        break;
    }
    error += concat(F(" Error: "), err_fstr);
#else
    error += concat(F(" Error: "), errorcode);
#endif
  } else {
    if (Settings.UseRules) {
      eventQueue.addMove(concat(F("ProvisionFirmware#success="), file_save));
    }
    return true;
  }

  backgroundtasks();
  if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
    addLog(LOG_LEVEL_ERROR, concat(F("Failed update firmware: "), error));
  }

  if (Settings.UseRules) {
    eventQueue.addMove(concat(F("ProvisionFirmware#failed="), file_save));
  }
  return false;
}

String joinUrlFilename(const String& url, String& filename)
{
  String fullUrl;

  fullUrl.reserve(url.length() + filename.length() + 1); // May need to add an extra slash
  fullUrl = url;
  fullUrl = parseTemplate(fullUrl, true);                // URL encode

  // URLEncode may also encode the '/' into "%2f"
  // FIXME TD-er: Can this really occur?
  fullUrl.replace(F("%2f"), F("/"));

  while (filename.startsWith(F("/"))) {
    filename = filename.substring(1);
  }

  if (!fullUrl.endsWith(F("/"))) {
    fullUrl += F("/");
  }
  fullUrl += filename;
  return fullUrl;
}

#endif // if FEATURE_DOWNLOAD


#include "../Helpers/ESPEasy_Storage.h"

#include "../../ESPEasy_common.h"

#include "../CustomBuild/CompiletimeDefines.h"
#include "../CustomBuild/StorageLayout.h"

#include "../DataStructs/TimingStats.h"

#include "../DataTypes/ESPEasyFileType.h"
#include "../DataTypes/ESPEasyTimeSource.h"
#include "../DataTypes/SPI_options.h"

#if FEATURE_MQTT
# include "../ESPEasyCore/Controller.h"
#endif // if FEATURE_MQTT
#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../ESPEasyCore/ESPEasyNetwork.h"
#include "../ESPEasyCore/ESPEasyWifi.h"
#include "../ESPEasyCore/Serial.h"

#include "../Globals/CRCValues.h"
#include "../Globals/Cache.h"
#include "../Globals/Device.h"
#include "../Globals/ESPEasyWiFiEvent.h"
#include "../Globals/ESPEasy_Scheduler.h"
#include "../Globals/ESPEasy_time.h"
#include "../Globals/EventQueue.h"
#include "../Globals/ExtraTaskSettings.h"
#include "../Globals/NetworkState.h"
#include "../Globals/Plugins.h"
#include "../Globals/RTC.h"
#include "../Globals/ResetFactoryDefaultPref.h"
#include "../Globals/RuntimeData.h"
#include "../Globals/SecuritySettings.h"
#include "../Globals/Settings.h"
#include "../Globals/WiFi_AP_Candidates.h"

#include "../Helpers/ESPEasyRTC.h"
#include "../Helpers/ESPEasy_checks.h"
#include "../Helpers/ESPEasy_FactoryDefault.h"
#include "../Helpers/ESPEasy_NVS_Helper.h"
#include "../Helpers/ESPEasy_time_calc.h"
#include "../Helpers/FS_Helper.h"
#include "../Helpers/Hardware.h"
#include "../Helpers/Hardware_device_info.h"
#include "../Helpers/Memory.h"
#include "../Helpers/Misc.h"
#include "../Helpers/Networking.h"
#include "../Helpers/Numerical.h"
#include "../Helpers/PeriodicalActions.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringParser.h"
#if FEATURE_MQTT
#include "../Helpers/_CPlugin_Helper.h"
#endif

#if FEATURE_RTC_CACHE_STORAGE
# include "../Globals/C016_ControllerCache.h"
#endif // if FEATURE_RTC_CACHE_STORAGE

#ifdef ESP32
# include <esp_partition.h>
# include <esp_phy_init.h>

String patch_fname(const String& fname) {
  if (fname.startsWith(F("/"))) {
    return fname;
  }
  return String('/') + fname;
}

#endif // ifdef ESP32

/********************************************************************************************\
   file system error handling
   Look here for error # reference: https://github.com/pellepl/spiffs/blob/master/src/spiffs.h
 \*********************************************************************************************/
String FileError(int line, const char *fname)
{
  String log = strformat(F("FS   : Error while reading/writing %s in %d"), fname, line);

  addLog(LOG_LEVEL_ERROR, log);
  return log;
}

/********************************************************************************************\
   Keep track of number of flash writes.
 \*********************************************************************************************/
void flashCount()
{
  if (RTC.flashDayCounter <= MAX_FLASHWRITES_PER_DAY) {
    RTC.flashDayCounter++;
  }
  RTC.flashCounter++;
  saveToRTC();
}

String flashGuard()
{
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("flashGuard"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  if (RTC.flashDayCounter > MAX_FLASHWRITES_PER_DAY)
  {
    String log = F("FS   : Daily flash write rate exceeded! (powercycle or send command 'resetFlashWriteCounter' to reset this)");
    addLog(LOG_LEVEL_ERROR, log);
    return log;
  }
  flashCount();
  return EMPTY_STRING;
}

// use this in function that can return an error string. it automaticly returns with an error string if there where too many flash writes.
#define FLASH_GUARD() { String flashErr = flashGuard(); \
                        if (flashErr.length()) return flashErr; }


String appendLineToFile(const String& fname, const String& line) {
  return appendToFile(fname, reinterpret_cast<const uint8_t *>(line.c_str()), line.length());
}

String appendToFile(const String& fname, const uint8_t *data, unsigned int size) {
  fs::File f = tryOpenFile(fname, "a+");

  SPIFFS_CHECK(f,                   fname.c_str());
  SPIFFS_CHECK(f.write(data, size), fname.c_str());
  f.close();
  return EMPTY_STRING;
}

bool fileExists(const __FlashStringHelper *fname)
{
  return fileExists(String(fname));
}

bool fileExists(const __FlashStringHelper *fname, FileDestination_e& destination)
{
  return fileExists(String(fname), destination);
}

bool fileExists(const String& fname) {
  FileDestination_e dummy;
  return fileExists(fname, dummy);
}

bool fileExists(const String& fname, FileDestination_e& destination) {
  #ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  #endif // ifdef USE_SECOND_HEAP

  const String patched_fname = patch_fname(fname);
  auto search                = Cache.fileExistsMap.find(patched_fname);
  destination                = FileDestination_e::ANY;

  if (search != Cache.fileExistsMap.end()) {
    destination = static_cast<FileDestination_e>(search->second - 1);
    return search->second != 0;
  }
  bool res = ESPEASY_FS.exists(patched_fname);
  if (res) {
    destination = FileDestination_e::FLASH;
  }

  #if FEATURE_SD

  if (!res) {
    res = SD.exists(patched_fname);
    if (res) {
      destination = FileDestination_e::SD;
    }
  }
  #endif // if FEATURE_SD

  // Only keep track of existing files or non-existing filenames that may be requested several times.
  // Not the non-existing files from the cache controller
  #if FEATURE_RTC_CACHE_STORAGE

  if (res || !isCacheFile(patched_fname))
  #endif // if FEATURE_RTC_CACHE_STORAGE
  {
    Cache.fileExistsMap.emplace(
      std::make_pair(
        patched_fname,
        res ? (static_cast<uint8_t>(destination) + 1) : 0));
  }

  if (Cache.fileCacheClearMoment == 0) {
    if (node_time.getTimeSource() == timeSource_t::No_time_source) {
      // use some random value as we don't have a time yet
      Cache.fileCacheClearMoment = HwRandom();
    } else {
      Cache.fileCacheClearMoment = node_time.getLocalUnixTime();
    }
  }
  return res;
}

fs::File tryOpenFile(const String& fname, const String& mode, FileDestination_e destination) {
  START_TIMER;
  fs::File f;

  if (fname.isEmpty() || equals(fname, '/')) {
    return f;
  }

  FileDestination_e where = FileDestination_e::ANY;
  const bool exists = fileExists(fname, where);

  if (!exists) {
    if (equals(mode, 'r')) {
      return f;
    }
    clearFileCaches();
  }

  if (where != FileDestination_e::SD && ((destination == FileDestination_e::ANY) || (destination == FileDestination_e::FLASH))) {
    f = ESPEASY_FS.open(patch_fname(fname), mode.c_str());
  }
  #if FEATURE_SD

  if ((!f || where == FileDestination_e::SD) && ((destination == FileDestination_e::ANY) || (destination == FileDestination_e::SD))) {
    f = SD.open(patch_fname(fname).c_str(), mode.c_str());
  }
  #endif // if FEATURE_SD


  STOP_TIMER(TRY_OPEN_FILE);
  return f;
}

bool fileMatchesTaskSettingsType(const String& fname) {
  const String config_dat_file = patch_fname(getFileName(FileType::CONFIG_DAT));

  return config_dat_file.equalsIgnoreCase(patch_fname(fname));
}

bool tryRenameFile(const String& fname_old, const String& fname_new, FileDestination_e destination) {
  clearFileCaches();

  if (fileExists(fname_old) && !fileExists(fname_new)) {
    if (fileMatchesTaskSettingsType(fname_old)) {
      clearAllCaches();
    } else {
      clearAllButTaskCaches();
    }
    bool res = false;

    if ((destination == FileDestination_e::ANY) || (destination == FileDestination_e::FLASH)) {
      res = ESPEASY_FS.rename(patch_fname(fname_old), patch_fname(fname_new));
    }
    #if FEATURE_SD && defined(ESP32) // FIXME ESP8266 SDClass doesn't support rename

    if (!res && ((destination == FileDestination_e::ANY) || (destination == FileDestination_e::SD))) {
      res = SD.rename(patch_fname(fname_old), patch_fname(fname_new));
    }
    #endif // if FEATURE_SD && defined(ESP32)
    return res;
  }
  return false;
}

bool tryDeleteFile(const String& fname, FileDestination_e destination) {
  if (fname.length() > 0)
  {
    #if FEATURE_RTC_CACHE_STORAGE

    if (isCacheFile(fname)) {
      ControllerCache.closeOpenFiles();
    }
    #endif // if FEATURE_RTC_CACHE_STORAGE

    if (fileMatchesTaskSettingsType(fname)) {
      clearAllCaches();
    } else {
      clearAllButTaskCaches();
    }
    bool res = false;

    if ((destination == FileDestination_e::ANY) || (destination == FileDestination_e::FLASH)) {
      res = ESPEASY_FS.remove(patch_fname(fname));
    }
    #if FEATURE_SD

    if (!res && ((destination == FileDestination_e::ANY) || (destination == FileDestination_e::SD))) {
      res = SD.remove(patch_fname(fname));
    }
    #endif // if FEATURE_SD

    // A call to GarbageCollection() will at most erase a single block. (e.g. 8k block size)
    // A deleted file may have covered more than a single block, so try to clear multiple blocks.
    uint8_t retries = 3;

    while (retries > 0 && GarbageCollection()) {
      --retries;
    }
    return res;
  }
  return false;
}

/********************************************************************************************\
   Fix stuff to clear out differences between releases
 \*********************************************************************************************/
bool BuildFixes()
{
  if (Settings.Build == get_build_nr()) {
    // Not changed
    return false;
  }
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("BuildFixes"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  serialPrintln(F("\nBuild changed!"));

  if (Settings.Build < 145)
  {
    InitFile(SettingsType::SettingsFileEnum::FILE_NOTIFICATION_type);
  }

  if (Settings.Build < 20101)
  {
    #ifdef LIMIT_BUILD_SIZE
    serialPrintln(F("Fix reset Pin"));
    #endif // ifdef LIMIT_BUILD_SIZE
    Settings.Pin_Reset = -1;
  }

  if (Settings.Build < 20102) {
    // Settings were 'mangled' by using older version
    // Have to patch settings to make sure no bogus data is being used.
    #ifdef LIMIT_BUILD_SIZE
    serialPrintln(F("Fix settings with uninitalized data or corrupted by switching between versions"));
    #endif // ifdef LIMIT_BUILD_SIZE
    Settings.UseRTOSMultitasking              = false;
    Settings.Pin_Reset                        = -1;
    Settings.SyslogFacility                   = DEFAULT_SYSLOG_FACILITY;
    Settings.MQTTUseUnitNameAsClientId_unused = DEFAULT_MQTT_USE_UNITNAME_AS_CLIENTID;
  }

  if (Settings.Build < 20103) {
    Settings.ResetFactoryDefaultPreference = 0;
    Settings.OldRulesEngine(DEFAULT_RULES_OLDENGINE);
  }

  if (Settings.Build < 20105) {
    Settings.I2C_clockSpeed = DEFAULT_I2C_CLOCK_SPEED;
  }

  if (Settings.Build <= 20106) {
    // ClientID is now defined in the controller settings.
    #if FEATURE_MQTT
    controllerIndex_t controller_idx = firstEnabledMQTT_ControllerIndex();

    if (validControllerIndex(controller_idx)) {
      MakeControllerSettings(ControllerSettings); // -V522

      if (AllocatedControllerSettings()) {
        LoadControllerSettings(controller_idx, *ControllerSettings);

        String clientid;

        if (Settings.MQTTUseUnitNameAsClientId_unused) {
          clientid = F("%sysname%");

          if (Settings.appendUnitToHostname()) {
            clientid += F("_%unit%");
          }
        }
        else {
          clientid = F("ESPClient_%mac%");
        }
        safe_strncpy(ControllerSettings->ClientID, clientid, sizeof(ControllerSettings->ClientID));

        ControllerSettings->mqtt_uniqueMQTTclientIdReconnect(Settings.uniqueMQTTclientIdReconnect_unused());
        ControllerSettings->mqtt_retainFlag(Settings.MQTTRetainFlag_unused);
        SaveControllerSettings(controller_idx, *ControllerSettings);
      }
    }
    #endif // if FEATURE_MQTT
  }

  if (Settings.Build < 20107) {
    Settings.WebserverPort = 80;
  }

  if (Settings.Build < 20108) {
#ifdef ESP32

    // Ethernet related settings are never used on ESP8266
    Settings.ETH_Phy_Addr      = DEFAULT_ETH_PHY_ADDR;
    Settings.ETH_Pin_mdc_cs    = DEFAULT_ETH_PIN_MDC;
    Settings.ETH_Pin_mdio_irq  = DEFAULT_ETH_PIN_MDIO;
    Settings.ETH_Pin_power_rst = DEFAULT_ETH_PIN_POWER;
    Settings.ETH_Phy_Type      = DEFAULT_ETH_PHY_TYPE;
    Settings.ETH_Clock_Mode    = DEFAULT_ETH_CLOCK_MODE;
#endif // ifdef ESP32
    Settings.NetworkMedium = DEFAULT_NETWORK_MEDIUM;
  }

  if (Settings.Build < 20109) {
    Settings.SyslogPort = 514;
  }

  if (Settings.Build < 20110) {
    Settings.I2C_clockSpeed_Slow  = DEFAULT_I2C_CLOCK_SPEED_SLOW;
    Settings.I2C_Multiplexer_Type = I2C_MULTIPLEXER_NONE;
    Settings.I2C_Multiplexer_Addr = -1;

    for (taskIndex_t x = 0; x < TASKS_MAX; x++) {
      Settings.I2C_Multiplexer_Channel[x] = -1;
    }
    Settings.I2C_Multiplexer_ResetPin = -1;
  }

  if (Settings.Build < 20111) {
    #ifdef ESP32
    constexpr uint8_t maxStatesesp32 = NR_ELEMENTS(Settings.PinBootStates_ESP32);

    for (uint8_t i = 0; i < maxStatesesp32; ++i) {
      Settings.PinBootStates_ESP32[i] = 0;
    }
    #endif // ifdef ESP32
  }

  if (Settings.Build < 20112) {
    Settings.WiFi_TX_power           = 70; // 70 = 17.5dBm. unit: 0.25 dBm
    Settings.WiFi_sensitivity_margin = 3;  // Margin in dBm on top of sensitivity.
  }

  if (Settings.Build < 20113) {
    Settings.NumberExtraWiFiScans = 0;
  }

  if (Settings.Build < 20114) {
    #ifdef USES_P003

    // P003_Pulse was always using the pull-up, now it is a setting.
    constexpr pluginID_t PLUGIN_ID_P003_PULSE(3);

    for (taskIndex_t taskIndex = 0; taskIndex < TASKS_MAX; ++taskIndex) {
      if (Settings.getPluginID_for_task(taskIndex) == PLUGIN_ID_P003_PULSE) {
        Settings.TaskDevicePin1PullUp[taskIndex] = true;
      }
    }
    #endif // ifdef USES_P003
  }

  if (Settings.Build < 20115) {
    if (Settings.InitSPI != static_cast<int>(SPI_Options_e::UserDefined)) { // User-defined SPI pins set to None
      Settings.SPI_SCLK_pin = -1;
      Settings.SPI_MISO_pin = -1;
      Settings.SPI_MOSI_pin = -1;
    }
  }
  #ifdef USES_P053

  if (Settings.Build < 20116) {
    // Added PWR button, init to "-none-"
    constexpr pluginID_t PLUGIN_ID_P053_PMSx003(53);

    for (taskIndex_t taskIndex = 0; taskIndex < TASKS_MAX; ++taskIndex) {
      if (Settings.getPluginID_for_task(taskIndex) == PLUGIN_ID_P053_PMSx003) {
        Settings.TaskDevicePluginConfig[taskIndex][3] = -1;
      }
    }

    // Remove PeriodicalScanWiFi
    // Reset to default 0 for future use.
    Settings.VariousBits_1.unused_15 = 0;
  }
  #endif // ifdef USES_P053

  if (Settings.Build <= 21066) { // 2024-12-31
    // (MQTT) Keep Alive time-out is now defined in the controller settings.
    #if FEATURE_MQTT
    for (controllerIndex_t controller_idx = 0; controller_idx < CONTROLLER_MAX; ++controller_idx) {
    // controllerIndex_t controller_idx = firstEnabledMQTT_ControllerIndex();
      const protocolIndex_t protocol_idx = getProtocolIndex_from_ControllerIndex(controller_idx);

    if (validProtocolIndex(protocol_idx) && getProtocolStruct(protocol_idx).usesMQTT) {
      MakeControllerSettings(ControllerSettings); // -V522

      if (AllocatedControllerSettings()) {
        LoadControllerSettings(controller_idx, *ControllerSettings);

        // Used to be hardcoded at 10 sec., now default 60 sec. and configurable
        ControllerSettings->KeepAliveTime = CONTROLLER_KEEP_ALIVE_TIME_DFLT;
        SaveControllerSettings(controller_idx, *ControllerSettings);
      }
    }
    }
    #endif // if FEATURE_MQTT
  }

  if (Settings.Build <= 21156) { // 2025-03-31
    // PR #5235 Add 2nd and 3rd I2C Bus
    if ((Settings.Pin_i2c2_sda == 0) &&
        (Settings.Pin_i2c2_scl == 0)) {
      Settings.Pin_i2c2_sda = DEFAULT_PIN_I2C2_SDA;
      Settings.Pin_i2c2_scl = DEFAULT_PIN_I2C2_SCL;
    }
    if ((Settings.Pin_i2c3_sda == 0) &&
        (Settings.Pin_i2c3_scl == 0)) {
      Settings.Pin_i2c3_sda = DEFAULT_PIN_I2C3_SDA;
      Settings.Pin_i2c3_scl = DEFAULT_PIN_I2C3_SCL;
    }
    if ((Settings.I2C2_clockSpeed == 0) &&
        (Settings.I2C2_clockSpeed_Slow == 0)) {
      Settings.I2C2_clockSpeed = DEFAULT_I2C_CLOCK_SPEED;
      Settings.I2C2_clockSpeed_Slow = DEFAULT_I2C_CLOCK_SPEED_SLOW;
    }
    if ((Settings.I2C3_clockSpeed == 0) &&
        (Settings.I2C3_clockSpeed_Slow == 0)) {
      Settings.I2C3_clockSpeed = DEFAULT_I2C_CLOCK_SPEED;
      Settings.I2C3_clockSpeed_Slow = DEFAULT_I2C_CLOCK_SPEED_SLOW;
    }
    if ((Settings.I2C2_Multiplexer_Type == 0) &&
        (Settings.I2C2_Multiplexer_Addr == 0) &&
        (Settings.I2C2_Multiplexer_ResetPin == 0)) {
      Settings.I2C2_Multiplexer_Type = I2C_MULTIPLEXER_NONE;
      Settings.I2C2_Multiplexer_Addr = -1;
      Settings.I2C2_Multiplexer_ResetPin = -1;
    }
    if ((Settings.I2C3_Multiplexer_Type == 0) &&
        (Settings.I2C3_Multiplexer_Addr == 0) &&
        (Settings.I2C3_Multiplexer_ResetPin == 0)) {
      Settings.I2C3_Multiplexer_Type = I2C_MULTIPLEXER_NONE;
      Settings.I2C3_Multiplexer_Addr = -1;
      Settings.I2C3_Multiplexer_ResetPin = -1;
    }
  }

  // Starting 2022/08/18
  // Use get_build_nr() value for settings transitions.
  // This value will also be shown when building using PlatformIO, when showing the  Compile time defines
  Settings.Build      = get_build_nr();
  Settings.StructSize = sizeof(Settings);

  // We may have changed the settings, so update checksum.
  // This way we save settings less often as these changes are always reproducible via this
  // settings transitions function.

  return !COMPUTE_STRUCT_CHECKSUM_UPDATE(SettingsStruct, Settings);
}

/********************************************************************************************\
   Mount FS and check config.dat
 \*********************************************************************************************/
void fileSystemCheck()
{
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("fileSystemCheck"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  addLog(LOG_LEVEL_INFO, F("FS   : Mounting..."));
#if defined(ESP32) && defined(USE_LITTLEFS)

  if ((getPartionCount(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS) != 0)
      && ESPEASY_FS.begin())
#else // if defined(ESP32) && defined(USE_LITTLEFS)

  if (ESPEASY_FS.begin())
#endif // if defined(ESP32) && defined(USE_LITTLEFS)
  {
    clearAllCaches();

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, strformat(
                   F("FS   : "
#ifdef USE_LITTLEFS
                     "LittleFS"
#else // ifdef USE_LITTLEFS
                     "SPIFFS"
#endif // ifdef USE_LITTLEFS
                     " mount successful, used %u bytes of %u"),
                   SpiffsUsedBytes(), SpiffsTotalBytes()));
    }

    // Run garbage collection before any file is open.
    uint8_t retries = 3;

    while (retries > 0 && GarbageCollection()) {
      --retries;
    }

    fs::File f = tryOpenFile(SettingsType::getSettingsFileName(SettingsType::Enum::BasicSettings_Type), "r");

    if (f) {
      f.close();
    } else {
      ResetFactory(false);
    }
  }
  else
  {
    const __FlashStringHelper *log = F("FS   : Mount failed");
    serialPrintln(log);
    addLog(LOG_LEVEL_ERROR, log);
    ResetFactory();
  }
}

bool FS_format() {
#ifdef USE_LITTLEFS
# ifdef ESP32
  const bool res = ESPEASY_FS.begin(true);
  ESPEASY_FS.end();
  return res;
# else // ifdef ESP32
  return ESPEASY_FS.format();
# endif // ifdef ESP32
#else // ifdef USE_LITTLEFS
  return ESPEASY_FS.format();

#endif // ifdef USE_LITTLEFS
}

#ifdef ESP32
// Max. 15 char keys for RF calibration marked keys
# define ESPEASY_RF_CAL_NVS_NAMESPACE  "ESPEasyRFcal"
# define RF_CAL_NVS_PREF_KEY       "RfCalIDFver"
uint32_t getWiFi_CalibrationVersion()
{
  ESPEasy_NVS_Helper preferences;
  if (preferences.begin(F(ESPEASY_RF_CAL_NVS_NAMESPACE))) {
    uint32_t res{};
    if (preferences.getPreference(F(RF_CAL_NVS_PREF_KEY), res)) {
      return res;
    }
  }
  return 0;
}

bool setWiFi_CalibrationVersion() {
  // Store used IDF version in NVS
  const uint32_t idfVersion = ESP_IDF_VERSION;
  ESPEasy_NVS_Helper preferences;
  if (preferences.begin(F(ESPEASY_RF_CAL_NVS_NAMESPACE))) {
    preferences.setPreference(F(RF_CAL_NVS_PREF_KEY), idfVersion);
    return true;
  }
  return false;
}

String formatIdfVersion(uint32_t idfVer) {
  return strformat(
    F("%u.%u.%u"),
    (idfVer >> 16) & 0xFF,
    (idfVer >> 8) & 0xFF,
    (idfVer) & 0xFF);
}

bool check_and_update_WiFi_Calibration() {
  const uint32_t RFcal_idfVersion = getWiFi_CalibrationVersion();
  const uint32_t idfVersion = ESP_IDF_VERSION;

  if (RFcal_idfVersion == idfVersion) {
    // No need to wipe RF calibration
    addLog(LOG_LEVEL_INFO, strformat(
      F("RF Cal: last calibration done with ESP-IDF %s"),
      formatIdfVersion(idfVersion).c_str()));
    return false;
  }
  if (RFcal_idfVersion) {
    addLog(LOG_LEVEL_INFO, strformat(
      F("RF Cal: last calibration with ESP-IDF %s, re-calibrating with ESP-IDF %s"),
      formatIdfVersion(RFcal_idfVersion).c_str(),
      formatIdfVersion(idfVersion).c_str()));
  } else {
    addLog(LOG_LEVEL_INFO, strformat(
      F("RF Cal: last calibration unknown, (re-)calibrating with ESP-IDF %s"),
      formatIdfVersion(idfVersion).c_str()));
  }
  return Erase_WiFi_Calibration();
}
#endif

bool Erase_WiFi_Calibration() {
  #ifdef ESP8266
  WifiDisconnect();
  setWifiMode(WIFI_OFF);
  if (!ESP.eraseConfig())
    return false;
  #ifndef BUILD_MINIMAL_OTA
  addLog(LOG_LEVEL_INFO, F("WiFi : Erased WiFi calibration data"));
  #endif
  #endif

  #ifdef ESP32
  WifiDisconnect();
  setWifiMode(WIFI_OFF);
  // Make sure power is stable, so wait a bit longer
  delay(1000);
  esp_phy_erase_cal_data_in_nvs();
  addLog(LOG_LEVEL_INFO, F("WiFi : Erased WiFi calibration data"));
  delay(200);
  esp_phy_load_cal_and_init();
  addLog(LOG_LEVEL_INFO, F("WiFi : Performed WiFi RF calibration"));
  delay(200);  
  setWiFi_CalibrationVersion();
  #endif
  return true;
}

#ifdef ESP32

# include <esp_partition.h>

int getPartionCount(uint8_t pType, uint8_t pSubType) {
  esp_partition_type_t partitionType       = static_cast<esp_partition_type_t>(pType);
  esp_partition_subtype_t  subtype         = static_cast<esp_partition_subtype_t>(pSubType);
  esp_partition_iterator_t _mypartiterator = esp_partition_find(partitionType, subtype, NULL);
  int nrPartitions                         = 0;

  if (_mypartiterator) {
    do {
      ++nrPartitions;
    } while ((_mypartiterator = esp_partition_next(_mypartiterator)) != NULL);
  }
  esp_partition_iterator_release(_mypartiterator);
  return nrPartitions;
}

#endif // ifdef ESP32

 #ifdef ESP8266
bool clearPartition(ESP8266_partition_type ptype) {
  uint32_t address;
  int32_t  size;
  int32_t  sector = getPartitionInfo(ESP8266_partition_type::rf_cal, address, size);

  while (size > 0) {
    if (!ESP.flashEraseSector(sector)) { return false; }
    ++sector;
    size -= SPI_FLASH_SEC_SIZE;
  }
  return true;
}

bool clearRFcalPartition() {
  return clearPartition(ESP8266_partition_type::rf_cal);
}

bool clearWiFiSDKpartition() {
  return clearPartition(ESP8266_partition_type::wifi);
}

#endif // ifdef ESP8266


/********************************************************************************************\
   Garbage collection
 \*********************************************************************************************/
bool GarbageCollection() {
  #ifdef CORE_POST_2_6_0

  // Perform garbage collection
  START_TIMER;

  if (ESPEASY_FS.gc()) {
# ifndef BUILD_NO_DEBUG
    addLog(LOG_LEVEL_INFO, F("FS   : Success garbage collection"));
# endif // ifndef BUILD_NO_DEBUG
    STOP_TIMER(FS_GC_SUCCESS);
    return true;
  }
  STOP_TIMER(FS_GC_FAIL);
  return false;
  #else // ifdef CORE_POST_2_6_0

  // Not supported, so nothing was removed.
  return false;
  #endif // ifdef CORE_POST_2_6_0
}

/********************************************************************************************\
   Save settings to file system
 \*********************************************************************************************/
String SaveSettings(bool forFactoryReset)
{
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("SaveSettings"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  String err;
  {
    Settings.StructSize = sizeof(Settings);

    // FIXME @TD-er: As discussed in #1292, the CRC for the settings is now disabled.

    Settings.validate();
    initSerial();

    if (forFactoryReset) {
      Settings.forceSave();
    }

    if (!COMPUTE_STRUCT_CHECKSUM_UPDATE(SettingsStruct, Settings)

        /*
           computeChecksum(
            Settings.md5,
            reinterpret_cast<uint8_t *>(&Settings),
            sizeof(SettingsStruct),
            offsetof(SettingsStruct, md5))
         */
        ) {
      err = SaveToFile(SettingsType::getSettingsFileName(SettingsType::Enum::BasicSettings_Type).c_str(),
                       0,
                       reinterpret_cast<const uint8_t *>(&Settings),
                       sizeof(Settings));
    }
#ifndef BUILD_NO_DEBUG
    else {
      addLog(LOG_LEVEL_INFO, F("Skip saving settings, not changed"));
    }
#endif // ifndef BUILD_NO_DEBUG
  }

  if (err.length()) {
    return err;
  }

#ifndef BUILD_MINIMAL_OTA

  // Must check this after saving, or else it is not possible to fix multiple
  // issues which can only corrected on different pages.
  if (!SettingsCheck(err)) { return err; }
#endif // ifndef BUILD_MINIMAL_OTA

  //  }

  err = SaveSecuritySettings(forFactoryReset);

  return err;
}

String SaveSecuritySettings(bool forFactoryReset) {
  String err;

  SecuritySettings.validate();
  memcpy(SecuritySettings.ProgmemMd5, CRCValues.runTimeMD5, 16);

  if (forFactoryReset) {
    SecuritySettings.forceSave();
  }

  if (SecuritySettings.updateChecksum()) {
    // Settings have changed, save to file.
    err = SaveToFile(SettingsType::getSettingsFileName(SettingsType::Enum::SecuritySettings_Type).c_str(),
                     0,
                     reinterpret_cast<const uint8_t *>(&SecuritySettings),
                     sizeof(SecuritySettings));

    // Security settings are saved, may be update of WiFi settings or hostname.
    if (!forFactoryReset && !NetworkConnected()) {
      if (SecuritySettings.hasWiFiCredentials() && (active_network_medium == NetworkMedium_t::WIFI)) {
        WiFiEventData.wifiConnectAttemptNeeded = true;
        WiFi_AP_Candidates.force_reload(); // Force reload of the credentials and found APs from the last scan
        resetWiFi();
        AttemptWiFiConnect();
      }
    }
  }
#ifndef BUILD_NO_DEBUG
  else {
    addLog(LOG_LEVEL_INFO, F("Skip saving SecuritySettings, not changed"));
  }
#endif // ifndef BUILD_NO_DEBUG

  // FIXME TD-er: How to check if these have changed?
  if (forFactoryReset) {
    ExtendedControllerCredentials.clear();
  }

  ExtendedControllerCredentials.save();

  if (!forFactoryReset) {
    afterloadSettings();
  }
  return err;
}

void afterloadSettings() {
  ExtraTaskSettings.clear(); // make sure these will not contain old settings.

  if ((Settings.Version != VERSION) || (Settings.PID != ESP_PROJECT_PID)) {
    // Not valid settings, so do not continue
    return;
  }

  // Load ResetFactoryDefaultPreference from provisioning.dat if available.
  // FIXME TD-er: Must actually move content of Provisioning.dat to NVS and then delete file
  uint32_t pref_temp = Settings.ResetFactoryDefaultPreference;

  #ifdef ESP32

  if (pref_temp == 0) {
    if (ResetFactoryDefaultPreference.getPreference() == 0) {
      // Try loading from NVS
      ESPEasy_NVS_Helper preferences;
      ResetFactoryDefaultPreference.init(preferences);
      pref_temp = ResetFactoryDefaultPreference.getPreference();
    }
  }
  #endif // ifdef ESP32
  #if FEATURE_CUSTOM_PROVISIONING

  if (fileExists(getFileName(FileType::PROVISIONING_DAT))) {
    MakeProvisioningSettings(ProvisioningSettings);

    if (ProvisioningSettings.get()) {
      loadProvisioningSettings(*ProvisioningSettings);

      if (ProvisioningSettings->matchingFlashSize()) {
        if ((pref_temp == 0) && (ProvisioningSettings->ResetFactoryDefaultPreference.getPreference() != 0)) {
          pref_temp = ProvisioningSettings->ResetFactoryDefaultPreference.getPreference();
        }
      }
    }
  }
  #endif // if FEATURE_CUSTOM_PROVISIONING

  // TODO TD-er: Try to get the information from more locations to make it more persistent
  // Maybe EEPROM location?


  ResetFactoryDefaultPreference_struct pref(pref_temp);

  if (modelMatchingFlashSize(pref.getDeviceModel())) {
    ResetFactoryDefaultPreference = pref_temp;
  }
  applyFactoryDefaultPref();
  Scheduler.setEcoMode(Settings.EcoPowerMode());
  #ifdef ESP32
  setCpuFrequencyMhz(Settings.EcoPowerMode() ? getCPU_MinFreqMHz() : getCPU_MaxFreqMHz());
  #endif // ifdef ESP32

  if (!Settings.UseRules) {
    eventQueue.clear();
  }
  node_time.applyTimeZone();
  CheckRunningServices(); // To update changes in hostname.
}

/********************************************************************************************\
   Load settings from file system
 \*********************************************************************************************/
String LoadSettings()
{
  clearAllButTaskCaches();
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("LoadSettings"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  uint8_t oldSettingsChecksum[16] = { 0 };
  memcpy(oldSettingsChecksum, Settings.md5, 16);


  String err;

  err =
    LoadFromFile(SettingsType::getSettingsFileName(SettingsType::Enum::BasicSettings_Type).c_str(),
                 0,
                 reinterpret_cast<uint8_t *>(&Settings),
                 sizeof(SettingsStruct));

  if (memcmp(oldSettingsChecksum, Settings.md5, 16) != 0) {
    // File has changed, so need to flush all task caches.
    Cache.clearAllTaskCaches();
  }

  if (err.length()) {
    return err;
  }

  if (!BuildFixes()) {
    #ifndef BUILD_NO_DEBUG

    if (COMPUTE_STRUCT_CHECKSUM(SettingsStruct, Settings)) {
      addLog(LOG_LEVEL_INFO, concat(F("CRC  : Settings CRC"), F("...OK")));
    } else {
      addLog(LOG_LEVEL_ERROR, concat(F("CRC  : Settings CRC"), F("...FAIL")));
    }
    #endif // ifndef BUILD_NO_DEBUG
  }

  Settings.validate();
  initSerial();

  err =
    LoadFromFile(SettingsType::getSettingsFileName(SettingsType::Enum::SecuritySettings_Type).c_str(),
                 0,
                 reinterpret_cast<uint8_t *>(&SecuritySettings),
                 sizeof(SecurityStruct));

#ifndef BUILD_NO_DEBUG

  if (SecuritySettings.checksumMatch()) {
    addLog(LOG_LEVEL_INFO, concat(F("CRC  : SecuritySettings CRC"), F("...OK ")));

    if (memcmp(SecuritySettings.ProgmemMd5, CRCValues.runTimeMD5, 16) != 0) {
      addLog(LOG_LEVEL_INFO, F("CRC  : binary has changed since last save of Settings"));
    }
  }
  else {
    addLog(LOG_LEVEL_ERROR, concat(F("CRC  : SecuritySettings CRC"), F("...FAIL")));
  }
#endif // ifndef BUILD_NO_DEBUG

  ExtendedControllerCredentials.load();

  //  setupStaticIPconfig();
  // FIXME TD-er: Must check if static/dynamic IP was changed and trigger a reconnect? Or is a reboot better when changing those settings?
  afterloadSettings();
  SecuritySettings.validate();
  return err;
}

/********************************************************************************************\
   Disable Plugin, based on bootFailedCount
 \*********************************************************************************************/
uint8_t disablePlugin(uint8_t bootFailedCount) {
  for (taskIndex_t i = 0; i < TASKS_MAX && bootFailedCount > 0; ++i) {
    if (Settings.TaskDeviceEnabled[i]) {
      --bootFailedCount;

      if (bootFailedCount == 0) {
        // Disable temporarily as unit crashed
        // FIXME TD-er: Should this be stored?
        Settings.TaskDeviceEnabled[i] = false;
      }
    }
  }
  return bootFailedCount;
}

uint8_t disableAllPlugins(uint8_t bootFailedCount) {
  if (bootFailedCount > 0) {
    --bootFailedCount;

    for (taskIndex_t i = 0; i < TASKS_MAX; ++i) {
      // Disable temporarily as unit crashed
      // FIXME TD-er: Should this be stored?
      Settings.TaskDeviceEnabled[i] = false;
    }
  }
  return bootFailedCount;
}

/********************************************************************************************\
   Disable Controller, based on bootFailedCount
 \*********************************************************************************************/
uint8_t disableController(uint8_t bootFailedCount) {
  for (controllerIndex_t i = 0; i < CONTROLLER_MAX && bootFailedCount > 0; ++i) {
    if (Settings.ControllerEnabled[i]) {
      --bootFailedCount;

      if (bootFailedCount == 0) {
        Settings.ControllerEnabled[i] = false;
      }
    }
  }
  return bootFailedCount;
}

uint8_t disableAllControllers(uint8_t bootFailedCount) {
  if (bootFailedCount > 0) {
    --bootFailedCount;

    for (controllerIndex_t i = 0; i < CONTROLLER_MAX; ++i) {
      Settings.ControllerEnabled[i] = false;
    }
  }
  return bootFailedCount;
}

/********************************************************************************************\
   Disable Notification, based on bootFailedCount
 \*********************************************************************************************/
#if FEATURE_NOTIFIER
uint8_t disableNotification(uint8_t bootFailedCount) {
  for (uint8_t i = 0; i < NOTIFICATION_MAX && bootFailedCount > 0; ++i) {
    if (Settings.NotificationEnabled[i]) {
      --bootFailedCount;

      if (bootFailedCount == 0) {
        Settings.NotificationEnabled[i] = false;
      }
    }
  }
  return bootFailedCount;
}

uint8_t disableAllNotifications(uint8_t bootFailedCount) {
  if (bootFailedCount > 0) {
    --bootFailedCount;

    for (uint8_t i = 0; i < NOTIFICATION_MAX; ++i) {
      Settings.NotificationEnabled[i] = false;
    }
  }
  return bootFailedCount;
}

#endif // if FEATURE_NOTIFIER

/********************************************************************************************\
   Disable Rules, based on bootFailedCount
 \*********************************************************************************************/
uint8_t disableRules(uint8_t bootFailedCount) {
  if (bootFailedCount > 0) {
    --bootFailedCount;
    Settings.UseRules = false;
  }
  return bootFailedCount;
}

bool getAndLogSettingsParameters(bool read, SettingsType::Enum settingsType, int index, int& offset, int& max_size) {
#ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG_DEV)) {
    String log = read ? F("Read") : F("Write");
    log += concat(F(" settings: "), SettingsType::getSettingsTypeString(settingsType));
    log += concat(F(" index: "), index);
    addLogMove(LOG_LEVEL_DEBUG_DEV, log);
  }
#endif // ifndef BUILD_NO_DEBUG
  return SettingsType::getSettingsParameters(settingsType, index, offset, max_size);
}

/********************************************************************************************\
   Load array of Strings from Custom settings
   Use maxStringLength = 0 to optimize for size (strings will be concatenated)
 \*********************************************************************************************/
String LoadStringArray(SettingsType::Enum settingsType,
                       int                index,
                       String             strings[],
                       uint16_t           nrStrings,
                       uint16_t           maxStringLength,
                       uint32_t           offset_in_block)
{
  int offset, max_size;

  if (!SettingsType::getSettingsParameters(settingsType, index, offset, max_size))
  {
    #ifndef BUILD_NO_DEBUG
    return F("Invalid index for custom settings");
    #else // ifndef BUILD_NO_DEBUG
    return F("Load error");
    #endif // ifndef BUILD_NO_DEBUG
  }

  const uint32_t bufferSize = 128;

  // FIXME TD-er: For now stack allocated, may need to be heap allocated?
  if (maxStringLength >= bufferSize) { return F("Max 128 chars allowed"); }

  char buffer[bufferSize] = { 0 };

  String   result;
  uint32_t readPos       = offset_in_block;
  uint32_t nextStringPos = readPos;
  uint32_t stringCount   = 0;

  const uint16_t estimatedStringSize = maxStringLength > 0 ? maxStringLength : bufferSize;
  String tmpString;
  tmpString.reserve(estimatedStringSize);
  {
    while (stringCount < nrStrings && static_cast<int>(readPos) < max_size) {
      const uint32_t readSize = std::min(bufferSize, max_size - readPos);
      result += LoadFromFile(settingsType,
                             index,
                             reinterpret_cast<uint8_t *>(&buffer),
                             readSize,
                             readPos);

      for (uint32_t i = 0; i < readSize && stringCount < nrStrings; ++i) {
        const uint32_t curPos = readPos + i;

        if (curPos >= nextStringPos) {
          if (buffer[i] == 0) {
            if (maxStringLength != 0) {
              // Specific string length, so we have to set the next string position.
              nextStringPos += maxStringLength;
            }
            if (!tmpString.isEmpty()) {
              move_special(strings[stringCount], std::move(tmpString));
            } else {
              free_string(strings[stringCount]);
            }

            // Do not allocate tmpString on 2nd heap as byte access on 2nd heap is much slower
            // We're appending per byte, so better prefer speed for short lived objects
            tmpString.reserve(estimatedStringSize);
            ++stringCount;
          } else {
            tmpString += buffer[i];
          }
        }
      }
      readPos += bufferSize;
    }
  }

  if ((!tmpString.isEmpty()) && (stringCount < nrStrings)) {
    result += concat(F("Incomplete custom settings for index "), index + 1);
    move_special(strings[stringCount], std::move(tmpString));
  }
  return result;
}

/********************************************************************************************\
   Save array of Strings from Custom settings
   Use maxStringLength = 0 to optimize for size (strings will be concatenated)
 \*********************************************************************************************/
String SaveStringArray(SettingsType::Enum settingsType,
                       int                index,
                       const String       strings[],
                       uint16_t           nrStrings,
                       uint16_t           maxStringLength,
                       uint32_t           posInBlock)
{
  // FIXME TD-er: Must add some check to see if the existing data has changed before saving.
  int offset, max_size;

  if (!SettingsType::getSettingsParameters(settingsType, index, offset, max_size))
  {
    #ifndef BUILD_NO_DEBUG
    return F("Invalid index for custom settings");
    #else // ifndef BUILD_NO_DEBUG
    return F("Save error");
    #endif // ifndef BUILD_NO_DEBUG
  }

  #ifdef ESP8266
  uint16_t bufferSize = 256;
  #endif // ifdef ESP8266
  #ifdef ESP32
  uint16_t bufferSize = 1024;
  #endif // ifdef ESP32

  if (bufferSize > max_size) {
    bufferSize = max_size;
  }

  std::vector<uint8_t> buffer;
  buffer.resize(bufferSize);

  String   result;
  int      writePos        = posInBlock;
  uint16_t stringCount     = 0;
  uint16_t stringReadPos   = 0;
  uint16_t nextStringPos   = writePos;
  uint16_t curStringLength = 0;

  if (maxStringLength != 0) {
    // Specified string length, check given strings
    for (uint16_t i = 0; i < nrStrings; ++i) {
      if (strings[i].length() >= maxStringLength) {
        result += getCustomTaskSettingsError(i);
      }
    }
  }

  while (stringCount < nrStrings && writePos < max_size) {
    for (size_t i = 0; i < buffer.size(); ++i) {
      buffer[i] = 0;
    }

    int bufpos = 0;

    for (; bufpos < bufferSize && stringCount < nrStrings; ++bufpos) {
      if (stringReadPos == 0) {
        // We're at the start of a string
        curStringLength = strings[stringCount].length();

        if (maxStringLength != 0) {
          if (curStringLength >= maxStringLength) {
            curStringLength = maxStringLength - 1;
          }
        }
      }

      const uint16_t curPos = writePos + bufpos;

      if (curPos >= nextStringPos) {
        if (stringReadPos < curStringLength) {
          buffer[bufpos] = strings[stringCount][stringReadPos];
          ++stringReadPos;
        } else {
          buffer[bufpos] = 0;
          stringReadPos  = 0;
          ++stringCount;

          if (maxStringLength == 0) {
            nextStringPos += curStringLength + 1;
          } else {
            nextStringPos += maxStringLength;
          }
        }
      }
    }

    // Buffer is filled, now write to flash
    // As we write in parts, only count as single write.
    if (RTC.flashDayCounter > 0) {
      RTC.flashDayCounter--;
    }
    result   += SaveToFile(settingsType, index, &(buffer[0]), bufpos, writePos);
    writePos += bufpos;
  }

  #if FEATURE_EXTENDED_CUSTOM_SETTINGS

  if ((SettingsType::Enum::CustomTaskSettings_Type == settingsType) &&
      ((writePos - posInBlock) <= DAT_TASKS_CUSTOM_SIZE)) { // Not needed, so can be deleted
    DeleteExtendedCustomTaskSettingsFile(settingsType, index);
  }
  #endif // if FEATURE_EXTENDED_CUSTOM_SETTINGS

  if ((writePos >= max_size) && (stringCount < nrStrings)) {
    result += F("Error: Not all strings fit in custom settings.");
  }
  return result;
}

/********************************************************************************************\
   Save Task settings to file system
 \*********************************************************************************************/
String SaveTaskSettings(taskIndex_t TaskIndex)
{
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("SaveTaskSettings"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  if (ExtraTaskSettings.TaskIndex != TaskIndex) {
    #ifndef BUILD_NO_DEBUG
    return F("SaveTaskSettings taskIndex does not match");
    #else // ifndef BUILD_NO_DEBUG
    return F("Save error");
    #endif // ifndef BUILD_NO_DEBUG
  }

  START_TIMER
  String err;

  if (!Cache.matchChecksumExtraTaskSettings(TaskIndex, ExtraTaskSettings.computeChecksum())) {
    // Clear task device value names before saving, will generate again when loading them later.
    ExtraTaskSettings.clearDefaultTaskDeviceValueNames();
    ExtraTaskSettings.validate(); // Validate before saving will reduce nr of saves as it is more likely to not have changed the next time
                                  // it will be saved.

    // Call to validate() may have changed the content, so re-compute the checksum.
    // This is how it is now stored, so we can now also update the
    // ExtraTaskSettings cache. This may prevent a reload.
    Cache.updateExtraTaskSettingsCache_afterLoad_Save();

    err = SaveToFile(SettingsType::Enum::TaskSettings_Type,
                     TaskIndex,
                     reinterpret_cast<const uint8_t *>(&ExtraTaskSettings),
                     sizeof(struct ExtraTaskSettingsStruct));

#if !defined(PLUGIN_BUILD_MINIMAL_OTA) && !defined(ESP8266_1M)

    if (err.isEmpty()) {
      err = checkTaskSettings(TaskIndex);
    }
#endif // if !defined(PLUGIN_BUILD_MINIMAL_OTA) && !defined(ESP8266_1M)

    // FIXME TD-er: Is this still needed as it is also cleared on PLUGIN_INIT and PLUGIN_EXIT?
    UserVar.clear_computed(ExtraTaskSettings.TaskIndex);
  }
#ifndef LIMIT_BUILD_SIZE
  else {
    addLog(LOG_LEVEL_INFO, F("Skip saving task settings, not changed"));
  }
#endif // ifndef LIMIT_BUILD_SIZE
  STOP_TIMER(SAVE_TASK_SETTINGS);
  return err;
}

/********************************************************************************************\
   Load Task settings from file system
 \*********************************************************************************************/
String LoadTaskSettings(taskIndex_t TaskIndex)
{
  if (ExtraTaskSettings.TaskIndex == TaskIndex) {
    return EMPTY_STRING; // already loaded
  }

  if (!validTaskIndex(TaskIndex)) {
    return EMPTY_STRING; // Un-initialized task index.
  }
  START_TIMER

  ExtraTaskSettings.clear();
  const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(TaskIndex);

  if (!validDeviceIndex(DeviceIndex)) {
    // No need to load from storage, as there is no plugin assigned to this task.
    ExtraTaskSettings.TaskIndex = TaskIndex; // Needed when an empty task was requested

    // FIXME TD-er: Do we need to keep a cache of an empty task?
    // Maybe better to do this?
    Cache.clearTaskCache(TaskIndex);

    //    Cache.updateExtraTaskSettingsCache_afterLoad_Save();
    return EMPTY_STRING;
  }
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("LoadTaskSettings"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  const String result = LoadFromFile(
    SettingsType::Enum::TaskSettings_Type,
    TaskIndex,
    reinterpret_cast<uint8_t *>(&ExtraTaskSettings),
    sizeof(struct ExtraTaskSettingsStruct));

  // After loading, some settings may need patching.
  ExtraTaskSettings.TaskIndex = TaskIndex; // Needed when an empty task was requested

  if (!Device[DeviceIndex].configurableDecimals()) {
    // Nr of decimals cannot be configured, so set them to 0 just to be sure.
    for (uint8_t i = 0; i < VARS_PER_TASK; ++i) {
      ExtraTaskSettings.TaskDeviceValueDecimals[i] = 0;
    }
  }
  loadDefaultTaskValueNames_ifEmpty(TaskIndex);

  ExtraTaskSettings.validate();
  Cache.updateExtraTaskSettingsCache_afterLoad_Save();
  STOP_TIMER(LOAD_TASK_SETTINGS);

  return result;
}

#if FEATURE_ALTERNATIVE_CDN_URL
String _CDN_url_cache;
bool   _CDN_url_loaded = false;

String get_CDN_url_custom() {
  if (!_CDN_url_loaded) {
    String strings[] = { EMPTY_STRING };

    LoadStringArray(
      SettingsType::Enum::CdnSettings_Type, 0,
      strings, NR_ELEMENTS(strings), 255, 0);
    _CDN_url_cache  = strings[0];
    _CDN_url_loaded = true;
  }
  return _CDN_url_cache;
}

void set_CDN_url_custom(const String& url) {
  _CDN_url_cache = url;
  _CDN_url_cache.trim();

  if (!_CDN_url_cache.isEmpty() && !_CDN_url_cache.endsWith(F("/"))) {
    _CDN_url_cache.concat('/');
  }
  _CDN_url_loaded = true;

  String strings[] = { EMPTY_STRING };

  LoadStringArray(
    SettingsType::Enum::CdnSettings_Type, 0,
    strings, NR_ELEMENTS(strings), 255, 0);

  if (url.equals(strings[0])) {
    // No need to save, is already the same
    return;
  }

  strings[0] = url;

  SaveStringArray(
    SettingsType::Enum::CdnSettings_Type, 0,
    strings, NR_ELEMENTS(strings), 255, 0);
}

#endif // if FEATURE_ALTERNATIVE_CDN_URL

/********************************************************************************************\
   Save Custom Task settings to file system
 \*********************************************************************************************/
String SaveCustomTaskSettings(taskIndex_t TaskIndex, const uint8_t *memAddress, int datasize, uint32_t posInBlock)
{
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("SaveCustomTaskSettings"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  return SaveToFile(SettingsType::Enum::CustomTaskSettings_Type, TaskIndex, memAddress, datasize, posInBlock);
}

/********************************************************************************************\
   Save array of Strings to Custom Task settings
   Use maxStringLength = 0 to optimize for size (strings will be concatenated)
 \*********************************************************************************************/
String SaveCustomTaskSettings(taskIndex_t TaskIndex, String strings[], uint16_t nrStrings, uint16_t maxStringLength, uint32_t posInBlock)
{
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("SaveCustomTaskSettings"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  return SaveStringArray(
    SettingsType::Enum::CustomTaskSettings_Type, TaskIndex,
    strings, nrStrings, maxStringLength, posInBlock);
}

String getCustomTaskSettingsError(uint8_t varNr) {
  return strformat(F("Error: Text too long for line %d\n"), varNr + 1);
}

/********************************************************************************************\
   Clear custom task settings
 \*********************************************************************************************/
String ClearCustomTaskSettings(taskIndex_t TaskIndex)
{
  // addLog(LOG_LEVEL_DEBUG, F("Clearing custom task settings"));
  return ClearInFile(SettingsType::Enum::CustomTaskSettings_Type, TaskIndex);
}

/********************************************************************************************\
   Load Custom Task settings from file system
 \*********************************************************************************************/
String LoadCustomTaskSettings(taskIndex_t TaskIndex, uint8_t *memAddress, int datasize, int offset_in_block)
{
  START_TIMER;
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("LoadCustomTaskSettings"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  String result = LoadFromFile(SettingsType::Enum::CustomTaskSettings_Type, TaskIndex, memAddress, datasize, offset_in_block);
  STOP_TIMER(LOAD_CUSTOM_TASK_STATS);
  return result;
}

/********************************************************************************************\
   Load array of Strings from Custom Task settings
   Use maxStringLength = 0 to optimize for size (strings will be concatenated)
 \*********************************************************************************************/
String LoadCustomTaskSettings(taskIndex_t TaskIndex, String strings[], uint16_t nrStrings, uint16_t maxStringLength, uint32_t offset_in_block)
{
  START_TIMER;
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("LoadCustomTaskSettings"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  String result = LoadStringArray(SettingsType::Enum::CustomTaskSettings_Type,
                                  TaskIndex,
                                  strings, nrStrings, maxStringLength, offset_in_block);
  STOP_TIMER(LOAD_CUSTOM_TASK_STATS);
  return result;
}

/********************************************************************************************\
   Save Controller settings to file system
 \*********************************************************************************************/
String SaveControllerSettings(controllerIndex_t ControllerIndex, ControllerSettingsStruct& controller_settings)
{
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("SaveControllerSettings"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  START_TIMER;

  controller_settings.validate(); // Make sure the saved controller settings have proper values.

  const ChecksumType checksum(reinterpret_cast<const uint8_t *>(&controller_settings), sizeof(ControllerSettingsStruct));

  if (checksum == (Cache.controllerSettings_checksums[ControllerIndex])) {
#ifndef BUILD_NO_DEBUG
    addLog(LOG_LEVEL_INFO, concat(F("Skip saving ControllerSettings: "), checksum.toString()));
#endif // ifndef BUILD_NO_DEBUG
    return EMPTY_STRING;
  }
  const String res = SaveToFile(SettingsType::Enum::ControllerSettings_Type, ControllerIndex,
                                reinterpret_cast<const uint8_t *>(&controller_settings), sizeof(controller_settings));

  Cache.controllerSettings_checksums[ControllerIndex] = checksum;
  #ifdef ESP32
  Cache.setControllerSettings(ControllerIndex, controller_settings);
  #endif // ifdef ESP32
  STOP_TIMER(SAVE_CONTROLLER_SETTINGS);

  return res;
}

/********************************************************************************************\
   Load Controller settings to file system
 \*********************************************************************************************/
String LoadControllerSettings(controllerIndex_t ControllerIndex, ControllerSettingsStruct& controller_settings) {
  if (!validControllerIndex(ControllerIndex)) return EMPTY_STRING;
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("LoadControllerSettings"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  START_TIMER
  #ifdef ESP32

  if (Cache.getControllerSettings(ControllerIndex, controller_settings)) {
    STOP_TIMER(LOAD_CONTROLLER_SETTINGS_C);
    return EMPTY_STRING;
  }
  #endif // ifdef ESP32
  String result =
    LoadFromFile(SettingsType::Enum::ControllerSettings_Type, ControllerIndex,
                 reinterpret_cast<uint8_t *>(&controller_settings), sizeof(controller_settings));

  controller_settings.validate(); // Make sure the loaded controller settings have proper values.

  Cache.controllerSettings_checksums[ControllerIndex] = controller_settings.computeChecksum();
  #ifdef ESP32
  Cache.setControllerSettings(ControllerIndex, controller_settings);
  #endif // ifdef ESP32
  STOP_TIMER(LOAD_CONTROLLER_SETTINGS);
  return result;
}

/********************************************************************************************\
   Clear Custom Controller settings
 \*********************************************************************************************/
String ClearCustomControllerSettings(controllerIndex_t ControllerIndex)
{
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("ClearCustomControllerSettings"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  // addLog(LOG_LEVEL_DEBUG, F("Clearing custom controller settings"));
  return ClearInFile(SettingsType::Enum::CustomControllerSettings_Type, ControllerIndex);
}

/********************************************************************************************\
   Save Custom Controller settings to file system
 \*********************************************************************************************/
String SaveCustomControllerSettings(controllerIndex_t ControllerIndex, const uint8_t *memAddress, int datasize)
{
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("SaveCustomControllerSettings"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  return SaveToFile(SettingsType::Enum::CustomControllerSettings_Type, ControllerIndex, memAddress, datasize);
}

/********************************************************************************************\
   Load Custom Controller settings to file system
 \*********************************************************************************************/
String LoadCustomControllerSettings(controllerIndex_t ControllerIndex, uint8_t *memAddress, int datasize)
{
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("LoadCustomControllerSettings"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  return LoadFromFile(SettingsType::Enum::CustomControllerSettings_Type, ControllerIndex, memAddress, datasize);
}

#if FEATURE_CUSTOM_PROVISIONING

/********************************************************************************************\
   Save Provisioning Settings
 \*********************************************************************************************/
String saveProvisioningSettings(ProvisioningStruct& ProvisioningSettings)
{
  String err;

  ProvisioningSettings.validate();
  memcpy(ProvisioningSettings.ProgmemMd5, CRCValues.runTimeMD5, 16);

  if (!COMPUTE_STRUCT_CHECKSUM_UPDATE(ProvisioningStruct, ProvisioningSettings))
  {
    // Settings have changed, save to file.
    err =
      SaveToFile_trunc(getFileName(FileType::PROVISIONING_DAT, 0).c_str(), 0, (uint8_t *)&ProvisioningSettings, sizeof(ProvisioningStruct));
  }
  return err;
}

/********************************************************************************************\
   Load Provisioning Settings
 \*********************************************************************************************/
String loadProvisioningSettings(ProvisioningStruct& ProvisioningSettings)
{
  String err = LoadFromFile(getFileName(FileType::PROVISIONING_DAT, 0).c_str(),
                            0,
                            (uint8_t *)&ProvisioningSettings,
                            sizeof(ProvisioningStruct));

# ifndef BUILD_NO_DEBUG

  if (COMPUTE_STRUCT_CHECKSUM(ProvisioningStruct, ProvisioningSettings))
  {
    addLog(LOG_LEVEL_INFO, F("CRC  : ProvisioningSettings CRC   ...OK "));

    if (memcmp(ProvisioningSettings.ProgmemMd5, CRCValues.runTimeMD5, 16) != 0) {
      addLog(LOG_LEVEL_INFO, F("CRC  : binary has changed since last save of Settings"));
    }
  }
  else {
    addLog(LOG_LEVEL_ERROR, F("CRC  : ProvisioningSettings CRC   ...FAIL"));
  }
# endif // ifndef BUILD_NO_DEBUG
  ProvisioningSettings.validate();
  return err;
}

#endif // if FEATURE_CUSTOM_PROVISIONING

#if FEATURE_NOTIFIER

/********************************************************************************************\
   Save Controller settings to file system
 \*********************************************************************************************/
String SaveNotificationSettings(int NotificationIndex, const uint8_t *memAddress, int datasize)
{
  # ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("SaveNotificationSettings"));
  # endif // ifndef BUILD_NO_RAM_TRACKER
  return SaveToFile(SettingsType::Enum::NotificationSettings_Type, NotificationIndex, memAddress, datasize);
}

/********************************************************************************************\
   Load Controller settings to file system
 \*********************************************************************************************/
String LoadNotificationSettings(int NotificationIndex, uint8_t *memAddress, int datasize)
{
  # ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("LoadNotificationSettings"));
  # endif // ifndef BUILD_NO_RAM_TRACKER
  return LoadFromFile(SettingsType::Enum::NotificationSettings_Type, NotificationIndex, memAddress, datasize);
}

#endif


/********************************************************************************************\
   Handle certificate files on the file system.
   The content will be stripped from unusable character like quotes, spaces etc.
 \*********************************************************************************************/
#if FEATURE_TLS
static inline bool is_base64(char c) {
  return (isalnum(c) || (c == '+') || (c == '/'));
}

bool cleanupCertificate(String & certificate, bool &changed)
{
  changed = false;
  // "-----BEGIN CERTIFICATE-----" positions in dash_pos[0] and dash_pos[1]
  // "-----END CERTIFICATE-----"   positions in dash_pos[2] and dash_pos[3]
  int dash_pos[4] = { 0 };
  int last_pos = 0;
  for (int i = 0; i < 4 && last_pos != -1; ++i) {
    dash_pos[i] = certificate.indexOf(F("-----"), last_pos);
    last_pos = dash_pos[i] + 5;
//    addLog(LOG_LEVEL_INFO, String(F(" dash_pos: ")) + String(dash_pos[i]));
  }
  if (last_pos == -1) return false;

  int read_pos = dash_pos[1] + 5; // next char after "-----BEGIN CERTIFICATE-----"
  String newCert;
  newCert.reserve((dash_pos[3] + 6) - dash_pos[0]);

  // "-----BEGIN CERTIFICATE-----" 
  newCert += certificate.substring(dash_pos[0], read_pos); 

  char last_char = certificate[read_pos - 1];
  for (; read_pos < dash_pos[2]; ++read_pos) {
    const char c = certificate[read_pos];
    if ((c == 'n' && last_char == '\\') || (c == '\n')) {
      if (!newCert.endsWith(String('\n'))) {
        newCert += '\n';
      }
    } else if (is_base64(c) || c == '=') {
      newCert += c;
    }
    last_char = c;
  }

  // "-----END CERTIFICATE-----" 
  newCert += certificate.substring(dash_pos[2], dash_pos[3] + 5);
  newCert += '\n';

  changed = !certificate.equals(newCert);
  certificate = std::move(newCert);
  return true;
}

String SaveCertificate(const String& fname, const String& certificate)
{
  return SaveToFile(fname.c_str(), 0, (const uint8_t *)certificate.c_str(), certificate.length() + 1);
}

String LoadCertificate(const String& fname, String& certificate, bool cleanup)
{
  bool changed = false;
  if (fileExists(fname)) {
    fs::File f = tryOpenFile(fname, "r");
    SPIFFS_CHECK(f, fname.c_str());
    #ifndef BUILD_NO_DEBUG
    String log = F("LoadCertificate: ");
    log += fname;
    #else
    String log = F("LoadCertificate error");
    #endif

    certificate.clear();

    if (!certificate.reserve(f.size())) {
      #ifndef BUILD_NO_DEBUG
      log += F(" ERROR, Out of memory");
      #endif
      addLog(LOG_LEVEL_ERROR, log);
      f.close();
      return log;
    }
    bool done = false;
    while (f.available() && !done) { 
      const char c = (char)f.read(); 
      if (c == '\0') {
        done = true;
      } else {
        certificate += c;
      }
    }
    f.close();

    if (cleanup) {
      if (!cleanupCertificate(certificate, changed)) {
        certificate.clear();
        #ifndef BUILD_NO_DEBUG
        log += F(" ERROR, Invalid certificate format");
        #endif
        addLog(LOG_LEVEL_ERROR, log);
        return log;
      } else if (changed) {
        //return SaveCertificate(fname, certificate);
      }
    }
  }

  return EMPTY_STRING;
}
#endif

/********************************************************************************************\
   Init a file with zeros on file system
 \*********************************************************************************************/
String InitFile(const String& fname, int datasize)
{
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("InitFile"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  FLASH_GUARD();

  fs::File f = tryOpenFile(fname, "w");

  if (f) {
    for (int x = 0; x < datasize; x++)
    {
      // See https://github.com/esp8266/Arduino/commit/b1da9eda467cc935307d553692fdde2e670db258#r32622483
      uint8_t zero_value = 0;
      SPIFFS_CHECK(f.write(&zero_value, 1), fname.c_str());
    }
    f.close();
  }

  // OK
  return EMPTY_STRING;
}

String InitFile(SettingsType::Enum settingsType)
{
  return InitFile(SettingsType::getSettingsFile(settingsType));
}

String InitFile(SettingsType::SettingsFileEnum file_type)
{
  return InitFile(SettingsType::getSettingsFileName(file_type),
                  SettingsType::getInitFileSize(file_type));
}

/********************************************************************************************\
   Save data into config file on file system
 \*********************************************************************************************/
String SaveToFile(const char *fname, int index, const uint8_t *memAddress, int datasize)
{
  return doSaveToFile(
    fname, index, memAddress, datasize,
    fileExists(fname) ? "r+" : "w+");
}

String SaveToFile_trunc(const char *fname, int index, const uint8_t *memAddress, int datasize)
{
  return doSaveToFile(fname, index, memAddress, datasize, "w+");
}

// See for mode description: https://github.com/esp8266/Arduino/blob/master/doc/filesystem.rst
String doSaveToFile(const char *fname, int index, const uint8_t *memAddress, int datasize, const char *mode)
{
#ifndef BUILD_NO_DEBUG
# ifndef ESP32

  if (allocatedOnStack(memAddress)) {
    addLog(LOG_LEVEL_ERROR, strformat(F("SaveToFile: %s ERROR, Data allocated on stack"), fname));

    //    return log;  // FIXME TD-er: Should this be considered a breaking error?
  }
# endif // ifndef ESP32
#endif  // ifndef BUILD_NO_DEBUG

  if (index < 0) {
    #ifndef BUILD_NO_DEBUG
    const String log = strformat(F("SaveToFile: %s ERROR, invalid position in file"), fname);
    #else // ifndef BUILD_NO_DEBUG
    const String log = F("Save error");
    #endif // ifndef BUILD_NO_DEBUG
    addLog(LOG_LEVEL_ERROR, log);
    return log;
  }
  START_TIMER;
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("SaveToFile"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  FLASH_GUARD();

  #ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, concat(F("SaveToFile: free stack: "),  getCurrentFreeStack()));
  }
  #endif // ifndef BUILD_NO_DEBUG
  delay(1);
  unsigned long timer = millis() + 50;
  fs::File f          = tryOpenFile(fname, mode);

  if (f) {
    clearAllButTaskCaches();
    SPIFFS_CHECK(f,                          fname);
    if (index > 0) {
      SPIFFS_CHECK(f.seek(index, fs::SeekSet), fname);
    }
    const uint8_t *pointerToByteToSave = memAddress;

    for (int x = 0; x < datasize; x++)
    {
      // See https://github.com/esp8266/Arduino/commit/b1da9eda467cc935307d553692fdde2e670db258#r32622483
      uint8_t byteToSave = *pointerToByteToSave;
      SPIFFS_CHECK(f.write(&byteToSave, 1), fname);
      pointerToByteToSave++;

      if (x % 256 == 0) {
        // one page written, do some background tasks
        timer = millis() + 50;
        delay(0);
      }

      if (timeOutReached(timer)) {
        timer += 50;
        delay(0);
      }
    }
    f.close();
    #ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, strformat(F("FILE : Saved %s offset: %d size: %d"), fname, index, datasize));
    }
    #endif // ifndef BUILD_NO_DEBUG
  } else {
    #ifndef BUILD_NO_DEBUG
    const String log = strformat(F("SaveToFile: %s ERROR, Cannot save to file"), fname);
    #else // ifndef BUILD_NO_DEBUG
    const String log = F("Save error");
    #endif // ifndef BUILD_NO_DEBUG

    addLog(LOG_LEVEL_ERROR, log);
    return log;
  }
  STOP_TIMER(SAVEFILE_STATS);
  #ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO, concat(F("SaveToFile: free stack after: "), getCurrentFreeStack()));
  }
  #endif // ifndef BUILD_NO_DEBUG

  // OK
  return EMPTY_STRING;
}

/********************************************************************************************\
   Clear a certain area in a file (set to 0)
 \*********************************************************************************************/
String ClearInFile(const char *fname, int index, int datasize)
{
  if (index < 0) {
    #ifndef BUILD_NO_DEBUG
    const String log = strformat(F("ClearInFile: %s ERROR, invalid position in file"), fname);
    #else // ifndef BUILD_NO_DEBUG
    const String log = F("Save error");
    #endif // ifndef BUILD_NO_DEBUG

    addLog(LOG_LEVEL_ERROR, log);
    return log;
  }

  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("ClearInFile"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  FLASH_GUARD();

  fs::File f = tryOpenFile(fname, "r+");

  if (f) {
    SPIFFS_CHECK(f.seek(index, fs::SeekSet), fname);

    for (int x = 0; x < datasize; x++)
    {
      // See https://github.com/esp8266/Arduino/commit/b1da9eda467cc935307d553692fdde2e670db258#r32622483
      uint8_t zero_value = 0;
      SPIFFS_CHECK(f.write(&zero_value, 1), fname);
    }
    f.close();
  } else {
    #ifndef BUILD_NO_DEBUG
    const String log = strformat(F("ClearInFile: %s ERROR, Cannot save to file"), fname);
    #else // ifndef BUILD_NO_DEBUG
    const String log = F("Save error");
    #endif // ifndef BUILD_NO_DEBUG
    addLog(LOG_LEVEL_ERROR, log);
    return log;
  }

  // OK
  return EMPTY_STRING;
}

/********************************************************************************************\
   Load data from config file on file system
 \*********************************************************************************************/
String LoadFromFile(const char *fname, int offset, uint8_t *memAddress, int datasize)
{
  if (offset < 0) {
    #ifndef BUILD_NO_DEBUG
    const String log = strformat(F("LoadFromFile: %s ERROR, invalid position in file"), fname);
    #else // ifndef BUILD_NO_DEBUG
    const String log = F("Load error");
    #endif // ifndef BUILD_NO_DEBUG
    addLog(LOG_LEVEL_ERROR, log);
    return log;
  }
  delay(0);
  START_TIMER;
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("LoadFromFile"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  fs::File f = tryOpenFile(fname, "r");
  SPIFFS_CHECK(f, fname);
  const int fileSize = f.size();

  if (fileSize > offset) {
    SPIFFS_CHECK(f.seek(offset, fs::SeekSet), fname);

    if (fileSize < (offset + datasize)) {
      const int newdatasize = datasize + offset - fileSize;

      // File is smaller, make sure to set excess memory to 0.
      memset(memAddress + newdatasize, 0u, (datasize - newdatasize));

      datasize = newdatasize;
    }
    SPIFFS_CHECK(f.read(memAddress, datasize), fname);
  }
  f.close();

  STOP_TIMER(LOADFILE_STATS);
  delay(0);

  return EMPTY_STRING;
}

String LoadFromFile(const char *fname, String& data, int offset)
{
  fs::File f = tryOpenFile(fname, "r");
  SPIFFS_CHECK(f, fname);
  #ifndef BUILD_NO_DEBUG
  String log = F("LoadFromFile: ");
  log += fname;
  #else
  String log = F("Load error");
  #endif

  if (!f || offset < 0 || (offset >= static_cast<int>(f.size()))) {
    #ifndef BUILD_NO_DEBUG
    log += F(" ERROR, invalid position in file");
    #endif
    addLog(LOG_LEVEL_ERROR, log);
    return log;
  }
  delay(0);
  START_TIMER;
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("LoadFromFile"));
  #endif
  
  SPIFFS_CHECK(f.seek(offset, fs::SeekSet),  fname);
  if (f) {
    if (!data.reserve(f.size() - offset)) {
      #ifndef BUILD_NO_DEBUG
      log += F(" ERROR, Out of memory");
      #endif
      addLog(LOG_LEVEL_ERROR, log);
      f.close();
      return log;
    }

    while (f.available()) { data += (char)f.read(); }
    f.close();
  }
  

  STOP_TIMER(LOADFILE_STATS);
  delay(0);

  return EMPTY_STRING;
}

/********************************************************************************************\
   Wrapper functions to handle errors in accessing settings
 \*********************************************************************************************/
String getSettingsFileIndexRangeError(bool read, SettingsType::Enum settingsType, int index) {
  if (settingsType >= SettingsType::Enum::SettingsType_MAX) {
    return concat(F("Unknown settingsType: "), static_cast<int>(settingsType));
  }
  String error = read ? F("Load") : F("Save");

  #ifndef BUILD_NO_DEBUG
  error += SettingsType::getSettingsTypeString(settingsType);
  error += concat(F(" index out of range: "), index);
  #else // ifndef BUILD_NO_DEBUG
  error += F(" error");
  #endif // ifndef BUILD_NO_DEBUG
  return error;
}

String getSettingsFileDatasizeError(bool read, SettingsType::Enum settingsType, int index, int datasize, int max_size) {
  String error = read ? F("Load") : F("Save");

  #ifndef BUILD_NO_DEBUG
  error += SettingsType::getSettingsTypeString(settingsType);
  error += strformat(F("(%d) datasize(%d) > max_size(%d)"), index, datasize, max_size);
  #else // ifndef BUILD_NO_DEBUG
  error += F(" error");
  #endif // ifndef BUILD_NO_DEBUG

  return error;
}

String LoadFromFile(SettingsType::Enum settingsType, int index, uint8_t *memAddress, int datasize, int offset_in_block) {
  bool read = true;
  int  offset, max_size;

  if (!getAndLogSettingsParameters(read, settingsType, index, offset, max_size)) {
    return getSettingsFileIndexRangeError(read, settingsType, index);
  }

  if ((datasize + offset_in_block) > max_size) {
    return getSettingsFileDatasizeError(read, settingsType, index, datasize, max_size);
  }

  int dataOffset = 0;

  #if FEATURE_EXTENDED_CUSTOM_SETTINGS
  int taskIndex = INVALID_TASK_INDEX; // Use base filename

  if ((SettingsType::Enum::CustomTaskSettings_Type == settingsType) &&
      ((offset_in_block + datasize) > DAT_TASKS_CUSTOM_SIZE)) {
    if (offset_in_block < DAT_TASKS_CUSTOM_SIZE) { // block starts in regular Custom config: Load first part
      const String fname = SettingsType::getSettingsFileName(settingsType);
      dataOffset = DAT_TASKS_CUSTOM_SIZE - offset_in_block;
      const String res = LoadFromFile(fname.c_str(), offset + offset_in_block, memAddress, dataOffset);

      if (!res.isEmpty()) { return res; } // Error occurred?

      datasize       -= dataOffset;
      offset_in_block = DAT_TASKS_CUSTOM_SIZE;
    }
    const String fname = SettingsType::getSettingsFileName(settingsType, index);

    if (fileExists(fname)) { // Do we have a task-specific extension stored?
      if (offset_in_block >= DAT_TASKS_CUSTOM_SIZE) {
        offset_in_block -= DAT_TASKS_CUSTOM_SIZE;
      }
      offset    = 0;
      taskIndex = index; // Use task-specific filename
    }
  }
  #endif // if FEATURE_EXTENDED_CUSTOM_SETTINGS

  const String fname = SettingsType::getSettingsFileName(settingsType
                                                         #if FEATURE_EXTENDED_CUSTOM_SETTINGS
                                                         , taskIndex
                                                         #endif // if FEATURE_EXTENDED_CUSTOM_SETTINGS
                                                         );
  return LoadFromFile(fname.c_str(), (offset + offset_in_block), memAddress + dataOffset, datasize);
}

String SaveToFile(SettingsType::Enum settingsType, int index, const uint8_t *memAddress, int datasize, int posInBlock) {
  bool read = false;
  int  offset, max_size;

  if (!getAndLogSettingsParameters(read, settingsType, index, offset, max_size)) {
    return getSettingsFileIndexRangeError(read, settingsType, index);
  }

  if ((datasize > max_size) || ((posInBlock + datasize) > max_size)) {
    return getSettingsFileDatasizeError(read, settingsType, index, datasize, max_size);
  }

  int dataOffset = 0;

  #if FEATURE_EXTENDED_CUSTOM_SETTINGS
  int taskIndex = INVALID_TASK_INDEX;                      // Use base filename

  if ((SettingsType::Enum::CustomTaskSettings_Type == settingsType) &&
      (posInBlock + datasize > (DAT_TASKS_CUSTOM_SIZE))) { // max_size already handled above
    if (posInBlock < DAT_TASKS_CUSTOM_SIZE) {              // Partial in regular config.dat, save that part first
      const String fname = SettingsType::getSettingsFileName(settingsType);
      dataOffset = (DAT_TASKS_CUSTOM_SIZE - posInBlock);   // Bytes to keep 'local'
      # ifndef BUILD_NO_DEBUG
      const String styp = SettingsType::getSettingsTypeString(settingsType);

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLog(LOG_LEVEL_INFO, strformat(F("ExtraSaveToFile: %s file: %s size: %d pos: %d"),
                                         styp.c_str(), fname.c_str(), dataOffset, posInBlock));
      }
      # endif // ifndef BUILD_NO_DEBUG
      const String res = SaveToFile(fname.c_str(), offset + posInBlock, memAddress, dataOffset);

      if (!res.isEmpty()) { return res; } // Error occurred

      datasize  -= dataOffset;
      posInBlock = 0;
    } else {
      posInBlock -= DAT_TASKS_CUSTOM_SIZE;
    }
    offset    = 0;     // Start of the extension file
    taskIndex = index; // Use task-specific filename
  }
  #endif // if FEATURE_EXTENDED_CUSTOM_SETTINGS

  const String fname = SettingsType::getSettingsFileName(settingsType
                                                         #if FEATURE_EXTENDED_CUSTOM_SETTINGS
                                                         , taskIndex
                                                         #endif // if FEATURE_EXTENDED_CUSTOM_SETTINGS
                                                         );

  if (!fileExists(fname)) {
    #if FEATURE_EXTENDED_CUSTOM_SETTINGS

    if (!validTaskIndex(taskIndex)) {
    #endif // if FEATURE_EXTENDED_CUSTOM_SETTINGS
    InitFile(settingsType);
    #if FEATURE_EXTENDED_CUSTOM_SETTINGS
  } else {
    InitFile(fname, DAT_TASKS_CUSTOM_EXTENSION_SIZE); // Initialize task-specific file
  }
    #endif // if FEATURE_EXTENDED_CUSTOM_SETTINGS
  }
  #ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, concat(F("SaveToFile: "), SettingsType::getSettingsTypeString(settingsType)) +
           strformat(F(" file: %s task: %d"), fname.c_str(), index + 1));
  }
  #endif // ifndef BUILD_NO_DEBUG
  return SaveToFile(fname.c_str(), offset + posInBlock, memAddress + dataOffset, datasize);
}

String ClearInFile(SettingsType::Enum settingsType, int index) {
  bool read = false;
  int  offset, max_size;

  if (!getAndLogSettingsParameters(read, settingsType, index, offset, max_size)) {
    return getSettingsFileIndexRangeError(read, settingsType, index);
  }
  #if FEATURE_EXTENDED_CUSTOM_SETTINGS

  if (SettingsType::Enum::CustomTaskSettings_Type == settingsType) {
    max_size = DAT_TASKS_CUSTOM_SIZE; // Don't also wipe the external size inside the config.dat file...
    DeleteExtendedCustomTaskSettingsFile(settingsType, index);
  }
  #endif // if FEATURE_EXTENDED_CUSTOM_SETTINGS

  const String fname = SettingsType::getSettingsFileName(settingsType);
  return ClearInFile(fname.c_str(), offset, max_size);
}

#if FEATURE_EXTENDED_CUSTOM_SETTINGS
bool DeleteExtendedCustomTaskSettingsFile(SettingsType::Enum settingsType, int index) {
  if ((SettingsType::Enum::CustomTaskSettings_Type == settingsType) && validTaskIndex(index)) {
    const String fname = SettingsType::getSettingsFileName(settingsType, index);

    if (fileExists(fname)) {
      const bool deleted = tryDeleteFile(fname); // Don't need the extension file anymore, so delete it
      # ifndef BUILD_NO_DEBUG

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLog(LOG_LEVEL_INFO, concat(F("CustomTaskSettings: Removing no longer needed file: "), fname));
      }
      # endif // ifndef BUILD_NO_DEBUG
      return deleted;
    }
  }
  return false;
}

#endif // if FEATURE_EXTENDED_CUSTOM_SETTINGS

/********************************************************************************************\
   Check file system area settings
 \*********************************************************************************************/
int SpiffsSectors()
{
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("SpiffsSectors"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  #if defined(ESP8266)
    # ifdef CORE_POST_2_6_0
  uint32_t _sectorStart = ((uint32_t)&_FS_start - 0x40200000) / SPI_FLASH_SEC_SIZE;
  uint32_t _sectorEnd   = ((uint32_t)&_FS_end - 0x40200000) / SPI_FLASH_SEC_SIZE;
    # else // ifdef CORE_POST_2_6_0
  uint32_t _sectorStart = ((uint32_t)&_SPIFFS_start - 0x40200000) / SPI_FLASH_SEC_SIZE;
  uint32_t _sectorEnd   = ((uint32_t)&_SPIFFS_end - 0x40200000) / SPI_FLASH_SEC_SIZE;
    # endif // ifdef CORE_POST_2_6_0

  return _sectorEnd - _sectorStart;
  #endif // if defined(ESP8266)
  #if defined(ESP32)
  return 32;
  #endif // if defined(ESP32)
}

size_t SpiffsUsedBytes() {
  size_t result = 1; // Do not output 0, this may be used in divisions.

  #ifdef ESP32
  result = ESPEASY_FS.usedBytes();
  #endif // ifdef ESP32
  #ifdef ESP8266
  fs::FSInfo fs_info;
  ESPEASY_FS.info(fs_info);
  result = fs_info.usedBytes;
  #endif // ifdef ESP8266
  return result;
}

size_t SpiffsTotalBytes() {
  static size_t result = 1; // Do not output 0, this may be used in divisions.

  if (result == 1) {
    #ifdef ESP32
    result = ESPEASY_FS.totalBytes();
    #endif // ifdef ESP32
    #ifdef ESP8266
    fs::FSInfo fs_info;
    ESPEASY_FS.info(fs_info);
    result = fs_info.totalBytes;
    #endif // ifdef ESP8266
  }
  return result;
}

size_t SpiffsBlocksize() {
  static size_t result = 1;

  if (result == 1) {
    #ifdef ESP32
    result = 8192; // Just assume 8k, since we cannot query it
    #endif // ifdef ESP32
    #ifdef ESP8266
    fs::FSInfo fs_info;
    ESPEASY_FS.info(fs_info);
    result = fs_info.blockSize;
    #endif // ifdef ESP8266
  }
  return result;
}

size_t SpiffsPagesize() {
  static size_t result = 1;

  if (result == 1) {
    #ifdef ESP32
    result = 256; // Just assume 256, since we cannot query it
    #endif // ifdef ESP32
    #ifdef ESP8266
    fs::FSInfo fs_info;
    ESPEASY_FS.info(fs_info);
    result = fs_info.pageSize;
    #endif // ifdef ESP8266
  }
  return result;
}

size_t SpiffsFreeSpace() {
  int freeSpace          = SpiffsTotalBytes() - SpiffsUsedBytes();
  const size_t blocksize = SpiffsBlocksize();

  if (freeSpace < static_cast<int>(2 * blocksize)) {
    // Not enough free space left to store anything
    // There needs to be minimum of 2 free blocks.
    return 0;
  }
  return freeSpace - 2 * blocksize;
}

bool SpiffsFull() {
  return SpiffsFreeSpace() == 0;
}

#if FEATURE_RTC_CACHE_STORAGE

/********************************************************************************************\
   Handling cached data
 \*********************************************************************************************/
String createCacheFilename(unsigned int count) {
  String fname;

  fname.reserve(16);
  # ifdef ESP32
  fname = '/';
  # endif // ifdef ESP32
  fname += strformat(F("cache_%d.bin"), count);
  return fname;
}

// Match string with an integer between '_' and ".bin"
int getCacheFileCountFromFilename(const String& fname) {
  if (!isCacheFile(fname)) { return -1; }
  int startpos = fname.indexOf('_');

  if (startpos < 0) { return -1; }
  int endpos = fname.indexOf(F(".bin"));

  if (endpos < 0) { return -1; }

  //  String digits = fname.substring(startpos + 1, endpos);
  int32_t result;

  if (validIntFromString(fname.substring(startpos + 1, endpos), result)) {
    return result;
  }
  return -1;
}

bool isCacheFile(const String& fname) {
  return fname.indexOf(F("cache_")) != -1;
}

// Look into the filesystem to see if there are any cache files present on the filesystem
// Return true if any found.
bool getCacheFileCounters(uint16_t& lowest, uint16_t& highest, size_t& filesizeHighest) {
  lowest          = 65535;
  highest         = 0;
  filesizeHighest = 0;
# ifdef ESP8266
  fs::Dir dir = ESPEASY_FS.openDir(F("cache"));

  while (dir.next()) {
    String filename = dir.fileName();
    int    count    = getCacheFileCountFromFilename(filename);

    if (count >= 0) {
      if (lowest > count) {
        lowest = count;
      }

      if (highest < count) {
        highest         = count;
        filesizeHighest = dir.fileSize();
      }
    }
  }
# endif // ESP8266
# ifdef ESP32
  fs::File root = ESPEASY_FS.open(F("/"));
  fs::File file = root.openNextFile();

  while (file)
  {
    if (!file.isDirectory()) {
      const String fname(file.name());

      if (fname.startsWith(F("/cache")) || fname.startsWith(F("cache"))) {
        int count = getCacheFileCountFromFilename(fname);

        if (count >= 0) {
          if (lowest > count) {
            lowest = count;
          }

          if (highest < count) {
            highest         = count;
            filesizeHighest = file.size();
          }
#  ifndef BUILD_NO_DEBUG
        } else {
          if (loglevelActiveFor(LOG_LEVEL_INFO)) {
            addLog(LOG_LEVEL_INFO, concat(F("RTC  : Cannot get count from: "), fname));
          }
#  endif // ifndef BUILD_NO_DEBUG
        }
      }
    }
    file = root.openNextFile();
  }
# endif // ESP32

  if (lowest <= highest) {
    return true;
  }
  lowest  = 0;
  highest = 0;
  return false;
}

#endif // if FEATURE_RTC_CACHE_STORAGE

/********************************************************************************************\
   Get partition table information
 \*********************************************************************************************/
#ifdef ESP32
String getPartitionType(uint8_t pType, uint8_t pSubType) {
  esp_partition_type_t partitionType       = static_cast<esp_partition_type_t>(pType);
  esp_partition_subtype_t partitionSubType = static_cast<esp_partition_subtype_t>(pSubType);

  if (partitionType == ESP_PARTITION_TYPE_APP) {
    if ((partitionSubType >= ESP_PARTITION_SUBTYPE_APP_OTA_MIN) &&
        (partitionSubType < ESP_PARTITION_SUBTYPE_APP_OTA_MAX)) {
      return concat(F("OTA partition "), partitionSubType - ESP_PARTITION_SUBTYPE_APP_OTA_MIN);
    }

    switch (partitionSubType) {
      case ESP_PARTITION_SUBTYPE_APP_FACTORY: return F("Factory app");
      case ESP_PARTITION_SUBTYPE_APP_TEST:    return F("Test app");
      default: break;
    }
  }

  if (partitionSubType == 0x99) {
    return F("EEPROM"); // Not defined in esp_partition_subtype_t
  }

  if (partitionType == ESP_PARTITION_TYPE_DATA) {
    switch (partitionSubType) {
      case ESP_PARTITION_SUBTYPE_DATA_OTA:      return F("OTA selection");
      case ESP_PARTITION_SUBTYPE_DATA_PHY:      return F("PHY init data");
      case ESP_PARTITION_SUBTYPE_DATA_NVS:      return F("NVS");
      case ESP_PARTITION_SUBTYPE_DATA_COREDUMP: return F("COREDUMP");
      case ESP_PARTITION_SUBTYPE_DATA_ESPHTTPD: return F("ESPHTTPD");
      case ESP_PARTITION_SUBTYPE_DATA_FAT:      return F("FAT");
      case ESP_PARTITION_SUBTYPE_DATA_SPIFFS:
        # ifdef USE_LITTLEFS
        return F("LittleFS");
        # else // ifdef USE_LITTLEFS
        return F("SPIFFS");
        # endif // ifdef USE_LITTLEFS
      default: break;
    }
  }
  return strformat(F("Unknown(%d)"), partitionSubType);
}

String getPartitionTableHeader(const String& itemSep, const String& lineEnd) {
  const char *itemSep_str = itemSep.c_str();

  return strformat(F("Address%sSize%sLabel%sPartition Type%sEncrypted%s"),
                   itemSep_str, itemSep_str, itemSep_str, itemSep_str, lineEnd.c_str());
}

String getPartitionTable(uint8_t pType, const String& itemSep, const String& lineEnd) {
  esp_partition_type_t partitionType = static_cast<esp_partition_type_t>(pType);
  String result;
  esp_partition_iterator_t _mypartiterator = esp_partition_find(partitionType, ESP_PARTITION_SUBTYPE_ANY, nullptr);

  if (_mypartiterator) {
    do {
      const esp_partition_t *_mypart = esp_partition_get(_mypartiterator);
      const char *itemSep_str        = itemSep.c_str();
      result += strformat(F("%x%s%s%s%s%s%s%s%s%s"),
                          _mypart->address,
                          itemSep_str,
                          formatToHex_decimal(_mypart->size, 1024).c_str(),
                          itemSep_str,
                          _mypart->label,
                          itemSep_str,
                          getPartitionType(_mypart->type, _mypart->subtype).c_str(),
                          itemSep_str,
                          String(_mypart->encrypted ? F("Yes") : F("-")).c_str(),
                          lineEnd.c_str());
    } while ((_mypartiterator = esp_partition_next(_mypartiterator)) != nullptr);
  }
  esp_partition_iterator_release(_mypartiterator);
  return result;
}

#endif // ifdef ESP32

#if FEATURE_DOWNLOAD
String downloadFileType(const String& url, const String& user, const String& pass, FileType::Enum filetype, unsigned int filenr)
{
  if (!getDownloadFiletypeChecked(filetype, filenr)) {
    // Not selected, so not downloaded
    return F("Not Allowed");
  }

  String filename = getFileName(filetype, filenr);
  String fullUrl  = joinUrlFilename(url, filename);
  String error;

  if (ResetFactoryDefaultPreference.deleteFirst()) {
    if (fileExists(filename) && !tryDeleteFile(filename)) {
      return F("Could not delete existing file");
    }

    if (!downloadFile(fullUrl, filename, user, pass, error)) {
      return error;
    }
  } else {
    if (fileExists(filename)) {
      const String filename_bak = strformat(F("%s_bak"), filename.c_str());

      if (fileExists(filename_bak)) {
        if (!ResetFactoryDefaultPreference.delete_Bak_Files() || !tryDeleteFile(filename_bak)) {
          return F("Could not rename to _bak");
        }
      }

      // Must download it to a tmp file.
      const String tmpfile = strformat(F("%s_tmp"), filename.c_str());

      if (!downloadFile(fullUrl, tmpfile, user, pass, error)) {
        return error;
      }

      if (fileExists(filename) && !tryRenameFile(filename, filename_bak)) {
        return F("Could not rename to _bak");
      } else {
        // File does not exist (anymore)
        if (!tryRenameFile(tmpfile, filename)) {
          error = F("Could not rename tmp file");

          if (tryRenameFile(filename_bak, filename)) {
            error += F("... reverted");
          } else {
            error += F(" Not reverted!");
          }
          return error;
        }
      }
    } else {
      if (!downloadFile(fullUrl, filename, user, pass, error)) {
        return error;
      }
    }
  }
  return error;
}

#endif // if FEATURE_DOWNLOAD

bool validateUploadConfigDat(const uint8_t *buf) {
  bool result = false;
  struct TempStruct {
    unsigned long PID;
    int           Version;
  } Temp;

  for (unsigned int x = 0; x < sizeof(struct TempStruct); x++) {
    memcpy(reinterpret_cast<uint8_t *>(&Temp) + x, &buf[x], 1);
  }
  #ifndef BUILD_NO_DEBUG
  addLog(LOG_LEVEL_INFO, strformat(F("Validate config.dat, Version: %d = %d, PID: %d = %d"),
                                   Temp.Version, VERSION, Temp.PID, ESP_PROJECT_PID));
  #endif // ifndef BUILD_NO_DEBUG

  if ((Temp.Version == VERSION) && (Temp.PID == ESP_PROJECT_PID)) {
    result = true;
  }
  return result;
}

#if FEATURE_CUSTOM_PROVISIONING

String downloadFileType(FileType::Enum filetype, unsigned int filenr)
{
  String url, user, pass;

  {
    MakeProvisioningSettings(ProvisioningSettings);

    if (ProvisioningSettings.get()) {
      loadProvisioningSettings(*ProvisioningSettings);

      if (!ProvisioningSettings->fetchFileTypeAllowed(filetype, filenr)) {
        return F("Not Allowed");
      }

      if (!ProvisioningSettings->url[0]) {
        return F("Provision Config incomplete");
      }

      url  = ProvisioningSettings->url;
      user = ProvisioningSettings->user;
      pass = ProvisioningSettings->pass;
    }
  }
  String res = downloadFileType(url, user, pass, filetype, filenr);

  clearAllCaches();
  return res;
}

#endif // if FEATURE_CUSTOM_PROVISIONING

#include "../Helpers/StringProvider.h"

#if FEATURE_ETHERNET
# include <ETH.h>
#endif // if FEATURE_ETHERNET

#include "../../ESPEasy-Globals.h"

#include "../CustomBuild/CompiletimeDefines.h"

#include "../ESPEasyCore/ESPEasyNetwork.h"
#include "../ESPEasyCore/ESPEasyWifi.h"
#if FEATURE_ETHERNET
#include "../ESPEasyCore/ESPEasyEth.h"
#endif

#include "../Globals/Device.h"
#include "../Globals/ESPEasy_Console.h"
#include "../Globals/ESPEasy_Scheduler.h"
#include "../Globals/ESPEasy_time.h"
#include "../Globals/ESPEasyWiFiEvent.h"

#if FEATURE_ETHERNET
#include "../Globals/ESPEasyEthEvent.h"
#endif

#include "../Globals/NetworkState.h"
#include "../Globals/SecuritySettings.h"
#include "../Globals/Settings.h"
#include "../Globals/WiFi_AP_Candidates.h"

#include "../Helpers/Convert.h"
#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/Hardware_device_info.h"
#include "../Helpers/Hardware_temperature_sensor.h"
#include "../Helpers/Memory.h"
#include "../Helpers/Misc.h"
#include "../Helpers/Networking.h"
#include "../Helpers/Scheduler.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringGenerator_System.h"
#include "../Helpers/StringGenerator_WiFi.h"

#include "../WebServer/JSON.h"
#include "../WebServer/AccessControl.h"

#ifdef ESP32
#include <soc/rtc.h>
#endif

String getInternalLabel(LabelType::Enum label, char replaceSpace) {
  return to_internal_string(getLabel(label), replaceSpace);
}

const __FlashStringHelper * getLabel(LabelType::Enum label) {
  switch (label)
  {
    case LabelType::UNIT_NR:                return F("Unit Number");
    #if FEATURE_ZEROFILLED_UNITNUMBER
    case LabelType::UNIT_NR_0:              return F("Unit Number 0-filled");
    #endif // FEATURE_ZEROFILLED_UNITNUMBER
    case LabelType::UNIT_NAME:              return F("Unit Name");
    case LabelType::HOST_NAME:              return F("Hostname");

    case LabelType::LOCAL_TIME:             return F("Local Time");
    case LabelType::TIME_SOURCE:            return F("Time Source");
    case LabelType::TIME_WANDER:            return F("Time Wander");
    #if FEATURE_EXT_RTC
    case LabelType::EXT_RTC_UTC_TIME:       return F("UTC time stored in RTC chip");
    #endif
    case LabelType::UPTIME:                 return F("Uptime");
    case LabelType::LOAD_PCT:               return F("Load");
    case LabelType::LOOP_COUNT:             return F("Load LC");
    case LabelType::CPU_ECO_MODE:           return F("CPU Eco Mode");
#if FEATURE_SET_WIFI_TX_PWR
    case LabelType::WIFI_TX_MAX_PWR:        return F("Max WiFi TX Power");
    case LabelType::WIFI_CUR_TX_PWR:        return F("Current WiFi TX Power");
    case LabelType::WIFI_SENS_MARGIN:       return F("WiFi Sensitivity Margin");
    case LabelType::WIFI_SEND_AT_MAX_TX_PWR:return F("Send With Max TX Power");
#endif
    case LabelType::WIFI_NR_EXTRA_SCANS:    return F("Extra WiFi scan loops");
    case LabelType::WIFI_USE_LAST_CONN_FROM_RTC: return F("Use Last Connected AP from RTC");

    case LabelType::FREE_MEM:               return F("Free RAM");
    case LabelType::FREE_STACK:             return F("Free Stack");
#ifdef USE_SECOND_HEAP
    case LabelType::FREE_HEAP_IRAM:         return F("Free 2nd Heap");
#endif

#if defined(CORE_POST_2_5_0) || defined(ESP32)
  #ifndef LIMIT_BUILD_SIZE
    case LabelType::HEAP_MAX_FREE_BLOCK:    return F("Heap Max Free Block");
  #endif
#endif // if defined(CORE_POST_2_5_0) || defined(ESP32)
#if defined(CORE_POST_2_5_0)
  #ifndef LIMIT_BUILD_SIZE
    case LabelType::HEAP_FRAGMENTATION:     return F("Heap Fragmentation");
  #endif
#endif // if defined(CORE_POST_2_5_0)

#ifdef ESP32
    case LabelType::HEAP_SIZE:              return F("Heap Size");
    case LabelType::HEAP_MIN_FREE:          return F("Heap Min Free");
    #ifdef BOARD_HAS_PSRAM
    case LabelType::PSRAM_SIZE:             return F("PSRAM Size");
    case LabelType::PSRAM_FREE:             return F("PSRAM Free");
    case LabelType::PSRAM_MIN_FREE:         return F("PSRAM Min Free");
    case LabelType::PSRAM_MAX_FREE_BLOCK:   return F("PSRAM Max Free Block");
    #endif // BOARD_HAS_PSRAM
#endif // ifdef ESP32

    case LabelType::JSON_BOOL_QUOTES:           return F("JSON bool output without quotes");
#if FEATURE_TIMING_STATS
    case LabelType::ENABLE_TIMING_STATISTICS:   return F("Collect Timing Statistics");
#endif
    case LabelType::ENABLE_RULES_CACHING:       return F("Enable Rules Cache");
    case LabelType::ENABLE_SERIAL_PORT_CONSOLE: return F("Enable Serial Port Console");
    case LabelType::CONSOLE_SERIAL_PORT:        return F("Console Serial Port");
#if USES_ESPEASY_CONSOLE_FALLBACK_PORT
    case LabelType::CONSOLE_FALLBACK_TO_SERIAL0: return F("Fallback to Serial 0");
    case LabelType::CONSOLE_FALLBACK_PORT:       return F("Console Fallback Port");
#endif

//    case LabelType::ENABLE_RULES_EVENT_REORDER: return F("Optimize Rules Cache Event Order"); // TD-er: Disabled for now
    case LabelType::TASKVALUESET_ALL_PLUGINS:   return F("Allow TaskValueSet on all plugins");
    case LabelType::ALLOW_OTA_UNLIMITED:        return F("Allow OTA without size-check");
#if FEATURE_CLEAR_I2C_STUCK
    case LabelType::ENABLE_CLEAR_HUNG_I2C_BUS:  return F("Try clear I2C bus when stuck");
#endif
    #if FEATURE_I2C_DEVICE_CHECK
    case LabelType::ENABLE_I2C_DEVICE_CHECK:    return F("Check I2C devices when enabled");
    #endif // if FEATURE_I2C_DEVICE_CHECK
#ifndef BUILD_NO_RAM_TRACKER
    case LabelType::ENABLE_RAM_TRACKING:    return F("Enable RAM Tracker");
#endif
#if FEATURE_AUTO_DARK_MODE
    case LabelType::ENABLE_AUTO_DARK_MODE:  return F("Web light/dark mode");
#endif // FEATURE_AUTO_DARK_MODE
#if FEATURE_RULES_EASY_COLOR_CODE
    case LabelType::DISABLE_RULES_AUTOCOMPLETE:  return F("Disable Rules auto-completion");
#endif // if FEATURE_RULES_EASY_COLOR_CODE
#if FEATURE_TARSTREAM_SUPPORT
    case LabelType::DISABLE_SAVE_CONFIG_AS_TAR:  return F("Disable Save Config as .tar");
#endif // if FEATURE_TARSTREAM_SUPPORT
    #if FEATURE_TASKVALUE_UNIT_OF_MEASURE
    case LabelType::SHOW_UOM_ON_DEVICES_PAGE: return F("Show Unit of Measure");
    #endif // if FEATURE_TASKVALUE_UNIT_OF_MEASURE
    #if FEATURE_MQTT_CONNECT_BACKGROUND
    case LabelType::MQTT_CONNECT_IN_BACKGROUND: return F("MQTT Connect in background");
    #endif // if FEATURE_MQTT_CONNECT_BACKGROUND

    case LabelType::BOOT_TYPE:              return F("Last Boot Cause");
    case LabelType::BOOT_COUNT:             return F("Boot Count");
    case LabelType::DEEP_SLEEP_ALTERNATIVE_CALL: return F("Deep Sleep Alternative");
    case LabelType::RESET_REASON:           return F("Reset Reason");
    case LabelType::LAST_TASK_BEFORE_REBOOT: return F("Last Action before Reboot");
    case LabelType::SW_WD_COUNT:            return F("SW WD count");


    case LabelType::WIFI_CONNECTION:        return F("WiFi Connection");
    case LabelType::WIFI_RSSI:              return F("RSSI");
    case LabelType::IP_CONFIG:              return F("IP Config");
#if FEATURE_USE_IPV6
    case LabelType::IP6_LOCAL:              return F("IPv6 link local");
    case LabelType::IP6_GLOBAL:             return F("IPv6 global");
//    case LabelType::IP6_ALL_ADDRESSES:      return F("IPv6 all addresses");
#endif
    case LabelType::IP_CONFIG_STATIC:       return F("Static");
    case LabelType::IP_CONFIG_DYNAMIC:      return F("DHCP");
    case LabelType::IP_ADDRESS:             return F("IP Address");
    case LabelType::IP_SUBNET:              return F("IP Subnet");
    case LabelType::IP_ADDRESS_SUBNET:      return F("IP / Subnet");
    case LabelType::GATEWAY:                return F("Gateway");
    case LabelType::CLIENT_IP:              return F("Client IP");
    #if FEATURE_MDNS
    case LabelType::M_DNS:                  return F("mDNS");
    #endif // if FEATURE_MDNS
    case LabelType::DNS:                    return F("DNS");
    case LabelType::DNS_1:                  return F("DNS 1");
    case LabelType::DNS_2:                  return F("DNS 2");
    case LabelType::ALLOWED_IP_RANGE:       return F("Allowed IP Range");
    case LabelType::STA_MAC:                return F("STA MAC");
    case LabelType::AP_MAC:                 return F("AP MAC");
    case LabelType::SSID:                   return F("SSID");
    case LabelType::BSSID:                  return F("BSSID");
    case LabelType::CHANNEL:                return F("Channel");
    case LabelType::ENCRYPTION_TYPE_STA:    return F("Encryption Type");
    case LabelType::CONNECTED:              return F("Connected");
    case LabelType::CONNECTED_MSEC:         return F("Connected msec");
    case LabelType::LAST_DISCONNECT_REASON: return F("Last Disconnect Reason");
    case LabelType::LAST_DISC_REASON_STR:   return F("Last Disconnect Reason str");
    case LabelType::NUMBER_RECONNECTS:      return F("Number Reconnects");
    case LabelType::WIFI_STORED_SSID1:      return F("Configured SSID1");
    case LabelType::WIFI_STORED_SSID2:      return F("Configured SSID2");


    case LabelType::FORCE_WIFI_BG:          return F("Force WiFi B/G");
    case LabelType::RESTART_WIFI_LOST_CONN: return F("Restart WiFi Lost Conn");
    case LabelType::FORCE_WIFI_NOSLEEP:     return F("Force WiFi No Sleep");
    case LabelType::PERIODICAL_GRAT_ARP:    return F("Periodical send Gratuitous ARP");
    case LabelType::CONNECTION_FAIL_THRESH: return F("Connection Failure Threshold");
#ifndef ESP32
    case LabelType::WAIT_WIFI_CONNECT:      return F("Extra Wait WiFi Connect");
#endif
    case LabelType::CONNECT_HIDDEN_SSID:    return F("Include Hidden SSID");
#ifdef ESP32
    case LabelType::WIFI_PASSIVE_SCAN:      return F("Passive WiFi Scan");
#endif
    case LabelType::HIDDEN_SSID_SLOW_CONNECT: return F("Hidden SSID Slow Connect");
    case LabelType::SDK_WIFI_AUTORECONNECT: return F("Enable SDK WiFi Auto Reconnect");
#if FEATURE_USE_IPV6
    case LabelType::ENABLE_IPV6:            return F("Enable IPv6");
#endif


    case LabelType::BUILD_DESC:             return F("Build");
    case LabelType::GIT_BUILD:              return F("Git Build");
    case LabelType::SYSTEM_LIBRARIES:       return F("System Libraries");
#ifdef ESP32
    case LabelType::ESP_IDF_SDK_VERSION:    return F("ESP-IDF Version");
#endif
    case LabelType::PLUGIN_COUNT:           return F("Plugin Count");
    case LabelType::PLUGIN_DESCRIPTION:     return F("Plugin Description");
    case LabelType::BUILD_TIME:             return F("Build Time");
    case LabelType::BINARY_FILENAME:        return F("Binary Filename");
    case LabelType::BUILD_PLATFORM:         return F("Build Platform");
    case LabelType::GIT_HEAD:               return F("Git HEAD");
    #ifdef CONFIGURATION_CODE
    case LabelType::CONFIGURATION_CODE_LBL: return F("Configuration code");
    #endif // ifdef CONFIGURATION_CODE

    case LabelType::I2C_BUS_STATE:          return F("I2C Bus State");
    case LabelType::I2C_BUS_CLEARED_COUNT:  return F("I2C bus cleared count");

    case LabelType::SYSLOG_LOG_LEVEL:       return F("Syslog Log Level");
    case LabelType::SERIAL_LOG_LEVEL:       return F("Serial Log Level");
    case LabelType::WEB_LOG_LEVEL:          return F("Web Log Level");
  #if FEATURE_SD
    case LabelType::SD_LOG_LEVEL:           return F("SD Log Level");
  #endif // if FEATURE_SD

    case LabelType::ESP_CHIP_ID:            return F("ESP Chip ID");
    case LabelType::ESP_CHIP_FREQ:          return F("ESP Chip Frequency");
#ifdef ESP32
    case LabelType::ESP_CHIP_XTAL_FREQ:     return F("ESP Crystal Frequency");
    case LabelType::ESP_CHIP_APB_FREQ:      return F("ESP APB Frequency");
#endif
    case LabelType::ESP_CHIP_MODEL:         return F("ESP Chip Model");
    case LabelType::ESP_CHIP_REVISION:      return F("ESP Chip Revision");
    case LabelType::ESP_CHIP_CORES:         return F("ESP Chip Cores");

    case LabelType::BOARD_NAME:         return F("ESP Board Name");

    case LabelType::FLASH_CHIP_ID:          return F("Flash Chip ID");
    case LabelType::FLASH_CHIP_VENDOR:      return F("Flash Chip Vendor");
    case LabelType::FLASH_CHIP_MODEL:       return F("Flash Chip Model");
    case LabelType::FLASH_CHIP_REAL_SIZE:   return F("Flash Chip Real Size");
    case LabelType::FLASH_CHIP_SPEED:       return F("Flash Chip Speed");
    case LabelType::FLASH_IDE_SIZE:         return F("Flash IDE Size");
    case LabelType::FLASH_IDE_SPEED:        return F("Flash IDE Speed");
    case LabelType::FLASH_IDE_MODE:         return F("Flash IDE Mode");
    case LabelType::FLASH_WRITE_COUNT:      return F("Flash Writes");
    case LabelType::SKETCH_SIZE:            return F("Sketch Size");
    case LabelType::SKETCH_FREE:            return F("Sketch Free");
    #ifdef USE_LITTLEFS
    case LabelType::FS_SIZE:                return F("Little FS Size");
    case LabelType::FS_FREE:                return F("Little FS Free");
    #else // ifdef USE_LITTLEFS
    case LabelType::FS_SIZE:                return F("SPIFFS Size");
    case LabelType::FS_FREE:                return F("SPIFFS Free");
    #endif // ifdef USE_LITTLEFS
    case LabelType::MAX_OTA_SKETCH_SIZE:    return F("Max. OTA Sketch Size");
    case LabelType::OTA_2STEP:              return F("OTA 2-step Needed");
    case LabelType::OTA_POSSIBLE:           return F("OTA possible");
    #if FEATURE_INTERNAL_TEMPERATURE
    case LabelType::INTERNAL_TEMPERATURE:   return F("Internal Temperature");
    #endif // if FEATURE_INTERNAL_TEMPERATURE
#if FEATURE_ETHERNET
    case LabelType::ETH_IP_ADDRESS:         return F("Eth IP Address");
    case LabelType::ETH_IP_SUBNET:          return F("Eth IP Subnet");
    case LabelType::ETH_IP_ADDRESS_SUBNET:  return F("Eth IP / Subnet");
    case LabelType::ETH_IP_GATEWAY:         return F("Eth Gateway");
    case LabelType::ETH_IP_DNS:             return F("Eth DNS");
#if FEATURE_USE_IPV6
    case LabelType::ETH_IP6_LOCAL:          return F("Eth IPv6 link local");
#endif
    case LabelType::ETH_MAC:                return F("Eth MAC");
    case LabelType::ETH_DUPLEX:             return F("Eth Mode");
    case LabelType::ETH_SPEED:              return F("Eth Speed");
    case LabelType::ETH_STATE:              return F("Eth State");
    case LabelType::ETH_SPEED_STATE:        return F("Eth Speed State");
    case LabelType::ETH_CONNECTED:          return F("Eth connected");
    case LabelType::ETH_CHIP:               return F("Eth chip");
#endif // if FEATURE_ETHERNET
# if FEATURE_ETHERNET || defined(USES_ESPEASY_NOW)
    case LabelType::ETH_WIFI_MODE:          return F("Network Type");
#endif
    case LabelType::SUNRISE:                return F("Sunrise");
    case LabelType::SUNSET:                 return F("Sunset");
    case LabelType::SUNRISE_S:              return F("Sunrise sec.");
    case LabelType::SUNSET_S:               return F("Sunset sec.");
    case LabelType::SUNRISE_M:              return F("Sunrise min.");
    case LabelType::SUNSET_M:               return F("Sunset min.");
    case LabelType::ISNTP:                  return F("Use NTP");
    case LabelType::UPTIME_MS:              return F("Uptime (ms)");
    case LabelType::TIMEZONE_OFFSET:        return F("Timezone Offset");
    case LabelType::LATITUDE:               return F("Latitude");
    case LabelType::LONGITUDE:              return F("Longitude");

    case LabelType::MAX_LABEL:
      break;

  }
  return F("MissingString");
}

String getValue(LabelType::Enum label) {
  int retval = INT_MAX;
  switch (label)
  {
    case LabelType::UNIT_NR:                retval = Settings.Unit; break;
    #if FEATURE_ZEROFILLED_UNITNUMBER
    case LabelType::UNIT_NR_0: 
    {
      // Fixed 3-digit unitnumber
      return formatIntLeadingZeroes(Settings.Unit, 3);
    }
    #endif // FEATURE_ZEROFILLED_UNITNUMBER
    case LabelType::UNIT_NAME:              return Settings.getName(); // Only return the set name, no appended unit.
    case LabelType::HOST_NAME:              return NetworkGetHostname();


    case LabelType::LOCAL_TIME:             return node_time.getDateTimeString('-', ':', ' ');
    case LabelType::TIME_SOURCE:
    {
      String timeSource_str = toString(node_time.getTimeSource());
      if (((node_time.getTimeSource() == timeSource_t::ESPEASY_p2p_UDP) ||
           (node_time.getTimeSource() == timeSource_t::ESP_now_peer)) &&
          (node_time.timeSource_p2p_unit != 0))
      {
        return strformat(F("%s (%u)"), timeSource_str.c_str(), node_time.timeSource_p2p_unit);
      }
      return timeSource_str;
    }
    case LabelType::TIME_WANDER:            return String(node_time.timeWander, 3);
    #if FEATURE_EXT_RTC
    case LabelType::EXT_RTC_UTC_TIME:
    {
      if (Settings.ExtTimeSource() != ExtTimeSource_e::None) {
        // Try to read the stored time in the ext. time source to allow to check if it is working properly.
        uint32_t unixtime;
        if (node_time.ExtRTC_get(unixtime)) {
          struct tm RTC_time;
          breakTime(unixtime, RTC_time);
          return formatDateTimeString(RTC_time);
        } else {
          return F("Not Set");
        }
      }
      return String('-');
    }
    #endif
    case LabelType::UPTIME:                 retval = getUptimeMinutes(); break;
    case LabelType::LOAD_PCT:               return toString(getCPUload(), 2);
    case LabelType::LOOP_COUNT:             retval = getLoopCountPerSec(); break;
    case LabelType::CPU_ECO_MODE:           return jsonBool(Settings.EcoPowerMode());
#if FEATURE_SET_WIFI_TX_PWR
    case LabelType::WIFI_TX_MAX_PWR:        return toString(Settings.getWiFi_TX_power(), 2);
    case LabelType::WIFI_CUR_TX_PWR:        return toString(WiFiEventData.wifi_TX_pwr, 2);
    case LabelType::WIFI_SENS_MARGIN:       retval = Settings.WiFi_sensitivity_margin; break;
    case LabelType::WIFI_SEND_AT_MAX_TX_PWR:return jsonBool(Settings.UseMaxTXpowerForSending());
#endif
    case LabelType::WIFI_NR_EXTRA_SCANS:    retval = Settings.NumberExtraWiFiScans; break;
    case LabelType::WIFI_USE_LAST_CONN_FROM_RTC: return jsonBool(Settings.UseLastWiFiFromRTC());

    case LabelType::FREE_MEM:               retval = FreeMem(); break;
    case LabelType::FREE_STACK:             retval = getCurrentFreeStack(); break;

#ifdef USE_SECOND_HEAP
    case LabelType::FREE_HEAP_IRAM:         retval = FreeMem2ndHeap(); break;
#endif

#if defined(CORE_POST_2_5_0)
  #ifndef LIMIT_BUILD_SIZE
    case LabelType::HEAP_MAX_FREE_BLOCK:    retval = ESP.getMaxFreeBlockSize(); break;
  #endif
#endif // if defined(CORE_POST_2_5_0)
#if  defined(ESP32)
  #ifndef LIMIT_BUILD_SIZE
    case LabelType::HEAP_MAX_FREE_BLOCK:    retval = ESP.getMaxAllocHeap(); break;
  #endif
#endif // if  defined(ESP32)
#if defined(CORE_POST_2_5_0)
  #ifndef LIMIT_BUILD_SIZE
    case LabelType::HEAP_FRAGMENTATION:     retval = ESP.getHeapFragmentation(); break;
  #endif
#endif // if defined(CORE_POST_2_5_0)
#ifdef ESP32
    case LabelType::HEAP_SIZE:              retval = ESP.getHeapSize(); break;
    case LabelType::HEAP_MIN_FREE:          retval = ESP.getMinFreeHeap(); break;
    #ifdef BOARD_HAS_PSRAM
    case LabelType::PSRAM_SIZE:             retval = UsePSRAM() ? ESP.getPsramSize() : 0; break;
    case LabelType::PSRAM_FREE:             retval = UsePSRAM() ? ESP.getFreePsram() : 0; break;
    case LabelType::PSRAM_MIN_FREE:         retval = UsePSRAM() ? ESP.getMinFreePsram() : 0; break;
    case LabelType::PSRAM_MAX_FREE_BLOCK:   retval = UsePSRAM() ? ESP.getMaxAllocPsram() : 0; break;
    #endif // BOARD_HAS_PSRAM
#endif // ifdef ESP32


    case LabelType::JSON_BOOL_QUOTES:           return jsonBool(Settings.JSONBoolWithoutQuotes());
#if FEATURE_TIMING_STATS
    case LabelType::ENABLE_TIMING_STATISTICS:   return jsonBool(Settings.EnableTimingStats());
#endif
    case LabelType::ENABLE_RULES_CACHING:       return jsonBool(Settings.EnableRulesCaching());
    case LabelType::ENABLE_SERIAL_PORT_CONSOLE: return jsonBool(Settings.UseSerial);
    case LabelType::CONSOLE_SERIAL_PORT:        return ESPEasy_Console.getPortDescription();

#if USES_ESPEASY_CONSOLE_FALLBACK_PORT
    case LabelType::CONSOLE_FALLBACK_TO_SERIAL0: return jsonBool(Settings.console_serial0_fallback);
    case LabelType::CONSOLE_FALLBACK_PORT:       return ESPEasy_Console.getFallbackPortDescription();
#endif

//    case LabelType::ENABLE_RULES_EVENT_REORDER: return jsonBool(Settings.EnableRulesEventReorder()); // TD-er: Disabled for now
    case LabelType::TASKVALUESET_ALL_PLUGINS:   return jsonBool(Settings.AllowTaskValueSetAllPlugins());
    case LabelType::ALLOW_OTA_UNLIMITED:        return jsonBool(Settings.AllowOTAUnlimited());
#if FEATURE_CLEAR_I2C_STUCK
    case LabelType::ENABLE_CLEAR_HUNG_I2C_BUS:  return jsonBool(Settings.EnableClearHangingI2Cbus());
#endif
#if FEATURE_I2C_DEVICE_CHECK
    case LabelType::ENABLE_I2C_DEVICE_CHECK:    return jsonBool(Settings.CheckI2Cdevice());
#endif // if FEATURE_I2C_DEVICE_CHECK
#ifndef BUILD_NO_RAM_TRACKER
    case LabelType::ENABLE_RAM_TRACKING:        return jsonBool(Settings.EnableRAMTracking());
#endif
#if FEATURE_AUTO_DARK_MODE
    case LabelType::ENABLE_AUTO_DARK_MODE:      return toString(Settings.getCssMode());
#endif // FEATURE_AUTO_DARK_MODE
#if FEATURE_RULES_EASY_COLOR_CODE
    case LabelType::DISABLE_RULES_AUTOCOMPLETE: return jsonBool(Settings.DisableRulesCodeCompletion());
#endif // if FEATURE_RULES_EASY_COLOR_CODE
#if FEATURE_TARSTREAM_SUPPORT
    case LabelType::DISABLE_SAVE_CONFIG_AS_TAR: return jsonBool(Settings.DisableSaveConfigAsTar());
#endif // if FEATURE_TARSTREAM_SUPPORT
    #if FEATURE_TASKVALUE_UNIT_OF_MEASURE
    case LabelType::SHOW_UOM_ON_DEVICES_PAGE:   return jsonBool(Settings.ShowUnitOfMeasureOnDevicesPage());
    #endif // if FEATURE_TASKVALUE_UNIT_OF_MEASURE
    #if FEATURE_MQTT_CONNECT_BACKGROUND
    case LabelType::MQTT_CONNECT_IN_BACKGROUND: return jsonBool(Settings.MQTTConnectInBackground());
    #endif // if FEATURE_MQTT_CONNECT_BACKGROUND

    case LabelType::BOOT_TYPE:              return getLastBootCauseString();
    case LabelType::BOOT_COUNT:             break;
    case LabelType::DEEP_SLEEP_ALTERNATIVE_CALL: return jsonBool(Settings.UseAlternativeDeepSleep());
    case LabelType::RESET_REASON:           return getResetReasonString();
    case LabelType::LAST_TASK_BEFORE_REBOOT: return ESPEasy_Scheduler::decodeSchedulerId(lastMixedSchedulerId_beforereboot);
    case LabelType::SW_WD_COUNT:            retval = sw_watchdog_callback_count; break;

    case LabelType::WIFI_CONNECTION:        break;
    case LabelType::WIFI_RSSI:              retval = WiFi.RSSI(); break;
    case LabelType::IP_CONFIG:              return useStaticIP() 
                                                     ? getLabel(LabelType::IP_CONFIG_STATIC) 
                                                     : getLabel(LabelType::IP_CONFIG_DYNAMIC);
    case LabelType::IP_CONFIG_STATIC:       break;
    case LabelType::IP_CONFIG_DYNAMIC:      break;
    case LabelType::IP_ADDRESS:             return formatIP(NetworkLocalIP());
    case LabelType::IP_SUBNET:              return formatIP(NetworkSubnetMask());
    case LabelType::IP_ADDRESS_SUBNET:      return strformat(F("%s / %s"), getValue(LabelType::IP_ADDRESS).c_str(), getValue(LabelType::IP_SUBNET).c_str());
    case LabelType::GATEWAY:                return formatIP(NetworkGatewayIP());
#if FEATURE_USE_IPV6
    case LabelType::IP6_LOCAL:              return formatIP(NetworkLocalIP6(), true);
    case LabelType::IP6_GLOBAL:             return formatIP(NetworkGlobalIP6());
#if FEATURE_ETHERNET
    case LabelType::ETH_IP6_LOCAL:          return formatIP(NetworkLocalIP6(), true);
#endif
/*
    case LabelType::IP6_ALL_ADDRESSES:
    {
      IP6Addresses_t addresses = NetworkAllIPv6();
      String res;
      for (auto it = addresses.begin(); it != addresses.end(); ++it)
      {
        if (!res.isEmpty()) {
          res += F("<br>");
        }
        res += it->toString();
      }
      return res;
    }
*/
#endif
    case LabelType::CLIENT_IP:              return formatIP(web_server.client().remoteIP(), true);
    #if FEATURE_INTERNAL_TEMPERATURE
    case LabelType::INTERNAL_TEMPERATURE:   return toString(getInternalTemperature());
    #endif // if FEATURE_INTERNAL_TEMPERATURE

    #if FEATURE_MDNS
    case LabelType::M_DNS:                  return NetworkGetHostname() + F(".local");
    #endif // if FEATURE_MDNS
    case LabelType::DNS:                    return strformat(F("%s / %s"), getValue(LabelType::DNS_1).c_str(), getValue(LabelType::DNS_2).c_str());
    case LabelType::DNS_1:                  return formatIP(NetworkDnsIP(0));
    case LabelType::DNS_2:                  return formatIP(NetworkDnsIP(1));
    case LabelType::ALLOWED_IP_RANGE:       return describeAllowedIPrange();
    case LabelType::STA_MAC:                return WifiSTAmacAddress().toString();
    case LabelType::AP_MAC:                 return WifiSoftAPmacAddress().toString();
    case LabelType::SSID:                   return WiFi.SSID();
    case LabelType::BSSID:                  return WiFi.BSSIDstr();
    case LabelType::CHANNEL:                retval = WiFi.channel(); break;
    case LabelType::ENCRYPTION_TYPE_STA:    return // WiFi_AP_Candidates.getCurrent().encryption_type();
                                                   WiFi_encryptionType(WiFiEventData.auth_mode);
    case LabelType::CONNECTED:
      #if FEATURE_ETHERNET
      if(active_network_medium == NetworkMedium_t::Ethernet) {
        return format_msec_duration(EthEventData.lastConnectMoment.millisPassedSince());
      }
      #endif // if FEATURE_ETHERNET
      return format_msec_duration(WiFiEventData.lastConnectMoment.millisPassedSince());

    // Use only the nr of seconds to fit it in an int32, plus append '000' to have msec format again.
    case LabelType::CONNECTED_MSEC:         
      #if FEATURE_ETHERNET
      if(active_network_medium == NetworkMedium_t::Ethernet) {
        return String(static_cast<int32_t>(EthEventData.lastConnectMoment.millisPassedSince() / 1000ll)) + F("000"); 
      }
      #endif // if FEATURE_ETHERNET
      return String(static_cast<int32_t>(WiFiEventData.lastConnectMoment.millisPassedSince() / 1000ll)) + F("000");
    case LabelType::LAST_DISCONNECT_REASON: return String(WiFiEventData.lastDisconnectReason);
    case LabelType::LAST_DISC_REASON_STR:   return getLastDisconnectReason();
    case LabelType::NUMBER_RECONNECTS:      retval = WiFiEventData.wifi_reconnects; break;
    case LabelType::WIFI_STORED_SSID1:      return String(SecuritySettings.WifiSSID);
    case LabelType::WIFI_STORED_SSID2:      return String(SecuritySettings.WifiSSID2);


    case LabelType::FORCE_WIFI_BG:          return jsonBool(Settings.ForceWiFi_bg_mode());
    case LabelType::RESTART_WIFI_LOST_CONN: return jsonBool(Settings.WiFiRestart_connection_lost());
    case LabelType::FORCE_WIFI_NOSLEEP:     return jsonBool(Settings.WifiNoneSleep());
    case LabelType::PERIODICAL_GRAT_ARP:    return jsonBool(Settings.gratuitousARP());
    case LabelType::CONNECTION_FAIL_THRESH: retval = Settings.ConnectionFailuresThreshold; break;
#ifndef ESP32
    case LabelType::WAIT_WIFI_CONNECT:      return jsonBool(Settings.WaitWiFiConnect());
#endif
    case LabelType::CONNECT_HIDDEN_SSID:    return jsonBool(Settings.IncludeHiddenSSID());
#ifdef ESP32
    case LabelType::WIFI_PASSIVE_SCAN:      return jsonBool(Settings.PassiveWiFiScan());
#endif
    case LabelType::HIDDEN_SSID_SLOW_CONNECT: return jsonBool(Settings.HiddenSSID_SlowConnectPerBSSID());
    case LabelType::SDK_WIFI_AUTORECONNECT: return jsonBool(Settings.SDK_WiFi_autoreconnect());
#if FEATURE_USE_IPV6
    case LabelType::ENABLE_IPV6:            return jsonBool(Settings.EnableIPv6());
#endif


    case LabelType::BUILD_DESC:             return getSystemBuildString();
    case LabelType::GIT_BUILD:
    {
      const String res(F(BUILD_GIT));

      if (!res.isEmpty()) { return res; }
      return get_git_head();
    }
    case LabelType::SYSTEM_LIBRARIES:       return getSystemLibraryString();
#ifdef ESP32
    case LabelType::ESP_IDF_SDK_VERSION:    return strformat(
      F("%d.%d.%d"), 
      ESP_IDF_VERSION_MAJOR, 
      ESP_IDF_VERSION_MINOR, 
      ESP_IDF_VERSION_PATCH);
#endif

    case LabelType::PLUGIN_COUNT:           retval = getDeviceCount() + 1; break;
    case LabelType::PLUGIN_DESCRIPTION:     return getPluginDescriptionString();
    case LabelType::BUILD_TIME:             return String(get_build_date()) + ' ' + get_build_time();
    case LabelType::BINARY_FILENAME:        return get_binary_filename();
    case LabelType::BUILD_PLATFORM:         return get_build_platform();
    case LabelType::GIT_HEAD:               return get_git_head();
    #ifdef CONFIGURATION_CODE
    case LabelType::CONFIGURATION_CODE_LBL: return getConfigurationCode();
    #endif // ifdef CONFIGURATION_CODE
    case LabelType::I2C_BUS_STATE:          return toString(I2C_state);
    case LabelType::I2C_BUS_CLEARED_COUNT:  retval = I2C_bus_cleared_count; break;
    case LabelType::SYSLOG_LOG_LEVEL:       return getLogLevelDisplayString(Settings.SyslogLevel);
    case LabelType::SERIAL_LOG_LEVEL:       return getLogLevelDisplayString(getSerialLogLevel());
    case LabelType::WEB_LOG_LEVEL:          return getLogLevelDisplayString(getWebLogLevel());
  #if FEATURE_SD
    case LabelType::SD_LOG_LEVEL:           return getLogLevelDisplayString(Settings.SDLogLevel);
  #endif // if FEATURE_SD

    case LabelType::ESP_CHIP_ID:            return formatToHex(getChipId(), 6);
    case LabelType::ESP_CHIP_FREQ:          retval = ESP.getCpuFreqMHz(); break;
#ifdef ESP32
    case LabelType::ESP_CHIP_XTAL_FREQ:     retval = getXtalFrequencyMHz(); break;
    case LabelType::ESP_CHIP_APB_FREQ:      retval = rtc_clk_apb_freq_get() / 1000000; break;
    //getApbFrequency() / 1000000; break;
#endif
    case LabelType::ESP_CHIP_MODEL:         return getChipModel();
    case LabelType::ESP_CHIP_REVISION:      return getChipRevision();
    case LabelType::ESP_CHIP_CORES:         retval = getChipCores(); break;
    case LabelType::BOARD_NAME:             return get_board_name();
    case LabelType::FLASH_CHIP_ID:          return formatToHex(getFlashChipId(), 6);
    case LabelType::FLASH_CHIP_VENDOR:      return formatToHex(getFlashChipId() & 0xFF, 2);
    case LabelType::FLASH_CHIP_MODEL:
    {
      const uint32_t flashChipId = getFlashChipId();
      const uint32_t flashDevice = (flashChipId & 0xFF00) | ((flashChipId >> 16) & 0xFF);
      return formatToHex(flashDevice, 4);
    }
    case LabelType::FLASH_CHIP_REAL_SIZE:   retval = getFlashRealSizeInBytes(); break;
    case LabelType::FLASH_CHIP_SPEED:       retval = getFlashChipSpeed() / 1000000; break;
    case LabelType::FLASH_IDE_SIZE:         break;
    case LabelType::FLASH_IDE_SPEED:        break;
    case LabelType::FLASH_IDE_MODE:         return getFlashChipMode();
    case LabelType::FLASH_WRITE_COUNT:      break;
    case LabelType::SKETCH_SIZE:            break;
    case LabelType::SKETCH_FREE:            break;
    case LabelType::FS_SIZE:                retval = SpiffsTotalBytes(); break;
    case LabelType::FS_FREE:                retval = SpiffsFreeSpace(); break;
    case LabelType::MAX_OTA_SKETCH_SIZE:    break;
    case LabelType::OTA_2STEP:              break;
    case LabelType::OTA_POSSIBLE:           break;
#if FEATURE_ETHERNET
    case LabelType::ETH_IP_ADDRESS:         return formatIP(NetworkLocalIP());
    case LabelType::ETH_IP_SUBNET:          return formatIP(NetworkSubnetMask());
    case LabelType::ETH_IP_ADDRESS_SUBNET:  return strformat(
                                                          F("%s / %s"),
                                                          getValue(LabelType::ETH_IP_ADDRESS).c_str(),
                                                          getValue(LabelType::ETH_IP_SUBNET).c_str());
    case LabelType::ETH_IP_GATEWAY:         return formatIP(NetworkGatewayIP());
    case LabelType::ETH_IP_DNS:             return formatIP(NetworkDnsIP(0));
    case LabelType::ETH_MAC:                return NetworkMacAddress().toString();
    case LabelType::ETH_DUPLEX:             return EthLinkUp() ? (EthFullDuplex() ? F("Full Duplex") : F("Half Duplex")) : F("Link Down");
    case LabelType::ETH_SPEED:              return EthLinkUp() ? getEthSpeed() : F("Link Down");
    case LabelType::ETH_STATE:              return EthLinkUp() ? F("Link Up") : F("Link Down");
    case LabelType::ETH_SPEED_STATE:        return EthLinkUp() ? getEthLinkSpeedState() : F("Link Down");
    case LabelType::ETH_CONNECTED:          return ETHConnected() ? F("CONNECTED") : F("DISCONNECTED"); // 0=disconnected, 1=connected
    case LabelType::ETH_CHIP:               return toString(Settings.ETH_Phy_Type);
#endif // if FEATURE_ETHERNET
# if FEATURE_ETHERNET || defined(USES_ESPEASY_NOW)
    case LabelType::ETH_WIFI_MODE:          return toString(active_network_medium);
#endif
    case LabelType::SUNRISE:                return node_time.getSunriseTimeString(':');
    case LabelType::SUNSET:                 return node_time.getSunsetTimeString(':');
    case LabelType::SUNRISE_S:              retval = node_time.sunRise.tm_hour * 3600 + node_time.sunRise.tm_min * 60 + node_time.sunRise.tm_sec; break;
    case LabelType::SUNSET_S:               retval = node_time.sunSet.tm_hour * 3600 + node_time.sunSet.tm_min * 60 + node_time.sunSet.tm_sec; break;
    case LabelType::SUNRISE_M:              retval = node_time.sunRise.tm_hour * 60 + node_time.sunRise.tm_min; break;
    case LabelType::SUNSET_M:               retval = node_time.sunSet.tm_hour * 60 + node_time.sunSet.tm_min; break;
    case LabelType::ISNTP:                  return jsonBool(Settings.UseNTP());
    case LabelType::UPTIME_MS:              return ull2String(getMicros64() / 1000);
    case LabelType::TIMEZONE_OFFSET:        retval = Settings.TimeZone; break;
    case LabelType::LATITUDE:               return toString(Settings.Latitude, 6);
    case LabelType::LONGITUDE:              return toString(Settings.Longitude, 6);

    case LabelType::MAX_LABEL:
      break;
  }
  if (retval != INT_MAX) return String(retval);
  return F("MissingString");
}

#if FEATURE_ETHERNET
String getEthSpeed() {
  return strformat(F("%d [Mbps]"), EthLinkSpeed());
}

String getEthLinkSpeedState() {
  if (EthLinkUp()) {
    return strformat(F("%s %s %s"), 
    getValue(LabelType::ETH_STATE).c_str(), 
    getValue(LabelType::ETH_DUPLEX).c_str(), 
    getEthSpeed().c_str());
  }
  return getValue(LabelType::ETH_STATE);
}

#endif // if FEATURE_ETHERNET

String getExtendedValue(LabelType::Enum label) {
  switch (label)
  {
    case LabelType::UPTIME:
    {
      return minutesToDayHourMinute(getUptimeMinutes());
    }

    default:
      break;
  }
  return EMPTY_STRING;
}

String getFormNote(LabelType::Enum label)
{
  // Keep flash string till the end of the function, to reduce build size
  // Otherwise lots of calls to String() constructor are included.
  const __FlashStringHelper *flash_str = F("");

  switch (label) {
#ifndef MINIMAL_OTA
    case LabelType::CONNECT_HIDDEN_SSID:
      flash_str = F("Must be checked to connect to a hidden SSID");
      break;
#ifdef ESP32
    case LabelType::WIFI_PASSIVE_SCAN:
      flash_str = F("Passive scan listens for WiFi beacons, Active scan probes for AP. Passive scan is typically faster.");
      break;
#endif // ifdef ESP32
    case LabelType::HIDDEN_SSID_SLOW_CONNECT:
      flash_str = F("Required for some AP brands like Mikrotik to connect to hidden SSID");
      break;
#if FEATURE_USE_IPV6
    case LabelType::ENABLE_IPV6:
      flash_str = F("Toggling IPv6 requires reboot");
      break;
#endif // if FEATURE_USE_IPV6
#ifndef NO_HTTP_UPDATER
    case LabelType::ALLOW_OTA_UNLIMITED:
      flash_str = F("When enabled, OTA updating can overwrite the filesystem and settings!<br>Requires reboot to activate");
      break;
#endif // ifndef NO_HTTP_UPDATER
#if FEATURE_RULES_EASY_COLOR_CODE
    case LabelType::DISABLE_RULES_AUTOCOMPLETE:
      flash_str = F("Also disables Rules syntax highlighting!");
      break;
#endif // if FEATURE_RULES_EASY_COLOR_CODE

    case LabelType::FORCE_WIFI_NOSLEEP:
      flash_str = F("Change WiFi sleep settings requires reboot to activate");
      break;

    case LabelType::CPU_ECO_MODE:
      flash_str = F("Node may miss receiving packets with Eco mode enabled");
      break;

    case LabelType::WIFI_NR_EXTRA_SCANS:
      flash_str = F("Number of extra times to scan all channels to have higher chance of finding the desired AP");
      break;
#ifndef ESP32
    case LabelType::WAIT_WIFI_CONNECT:
      flash_str = F("Wait for 1000 msec right after connecting to WiFi.<BR>May improve success on some APs like Fritz!Box");
      break;
#endif

#endif

#if FEATURE_SET_WIFI_TX_PWR
    case LabelType::WIFI_TX_MAX_PWR:
    case LabelType::WIFI_SENS_MARGIN:
    {
      float maxTXpwr;
      float sensitivity = GetRSSIthreshold(maxTXpwr);
      if (LabelType::WIFI_TX_MAX_PWR == label) {
        return strformat(
          F("Current max: %.2f dBm"), maxTXpwr);
      }
      return strformat(
        F("Adjust TX power to target the AP with (sensitivity + margin) dBm signal strength. Current sensitivity: %.2f dBm"),
        sensitivity);
    }
#endif // if FEATURE_SET_WIFI_TX_PWR

    default:
      return EMPTY_STRING;
  }

  return flash_str;
}


String getFormUnit(LabelType::Enum label)
{
    const __FlashStringHelper *flash_str = F("");

  switch (label) {
#if FEATURE_SET_WIFI_TX_PWR
    case LabelType::WIFI_TX_MAX_PWR:
    case LabelType::WIFI_CUR_TX_PWR:
    case LabelType::WIFI_RSSI:
      flash_str = F("dBm");
      break;
    case LabelType::WIFI_SENS_MARGIN:
      flash_str = F("dB");
      break;
#endif
    case LabelType::TIME_WANDER:
      flash_str = F("ppm");
      break;
#ifdef ESP32
    case LabelType::HEAP_SIZE:
    case LabelType::HEAP_MIN_FREE:
    #ifdef BOARD_HAS_PSRAM
    case LabelType::PSRAM_SIZE:
    case LabelType::PSRAM_FREE:
    case LabelType::PSRAM_MIN_FREE:
    case LabelType::PSRAM_MAX_FREE_BLOCK:
    #endif // BOARD_HAS_PSRAM
#endif // ifdef ESP32
    case LabelType::FREE_MEM:
    case LabelType::FREE_STACK:
#ifdef USE_SECOND_HEAP
    case LabelType::FREE_HEAP_IRAM:
#endif
#if defined(CORE_POST_2_5_0) || defined(ESP32)
  #ifndef LIMIT_BUILD_SIZE
    case LabelType::HEAP_MAX_FREE_BLOCK:
  #endif
#endif // if defined(CORE_POST_2_5_0) || defined(ESP32)

      flash_str = F("byte");
      break;
    case LabelType::FLASH_CHIP_REAL_SIZE:
    case LabelType::FLASH_IDE_SIZE:
      flash_str = F("kB");
      break;
/*
    case LabelType::UPTIME:
      flash_str = F("min");
      break;
*/
    case LabelType::LOAD_PCT:
#if defined(CORE_POST_2_5_0)
  #ifndef LIMIT_BUILD_SIZE
    case LabelType::HEAP_FRAGMENTATION:
  #endif
#endif // if defined(CORE_POST_2_5_0)

      flash_str = F("%");
      break;

    case LabelType::ESP_CHIP_FREQ:
#ifdef ESP32
    case LabelType::ESP_CHIP_XTAL_FREQ:
    case LabelType::ESP_CHIP_APB_FREQ:
#endif
    case LabelType::FLASH_CHIP_SPEED:
    case LabelType::FLASH_IDE_SPEED:
      flash_str = F("MHz");
      break;
#if FEATURE_INTERNAL_TEMPERATURE
    case LabelType::INTERNAL_TEMPERATURE:
      flash_str = F("&deg;C");
      break;
#endif // if FEATURE_INTERNAL_TEMPERATURE



    default:
      return EMPTY_STRING;
  }

  return flash_str;
}
#include "../Helpers/Network.h"

#include "../../ESPEasy_common.h"

#include "../ESPEasyCore/ESPEasyWifi.h"
#include "../ESPEasyCore/ESPEasyNetwork.h"

#include "../Globals/Settings.h"
#include "../Globals/Services.h"

#include "../Helpers/ESPEasy_time_calc.h"
#include "../Helpers/Hardware_GPIO.h"
#include "../Helpers/Hardware_PWM.h"
#include "../Helpers/Misc.h"


#ifdef ESP32
# include <esp32-hal-periman.h>
#endif // ifdef ESP32


/********************************************************************************************\
   Status LED
 \*********************************************************************************************/
void statusLED(bool traffic)
{
  static int gnStatusValueCurrent = -1;
  static long int gnLastUpdate    = millis();

  // Reset GPIO when status LED pin has changed to free up LEDC channel
  #ifdef ESP32
  static int8_t last_status_led_pin = -1;
  if (last_status_led_pin != Settings.Pin_status_led) {
    if (last_status_led_pin != -1) {
      set_Gpio_PWM(last_status_led_pin, 0);
    }
    last_status_led_pin = Settings.Pin_status_led;
  }
  #endif

  if (!validGpio(Settings.Pin_status_led)) {
    return;
  }

  #ifdef ESP32
  if (gnStatusValueCurrent < 0) {
    set_Gpio_PWM(Settings.Pin_status_led, 0);
  }
  #endif

  int nStatusValue = gnStatusValueCurrent;

  if (traffic)
  {
    nStatusValue += STATUS_PWM_TRAFFICRISE; // ramp up fast
  }
  else
  {
    if (NetworkConnected())
    {
      long int delta = timePassedSince(gnLastUpdate);

      if ((delta > 0) || (delta < 0))
      {
        nStatusValue -= STATUS_PWM_NORMALFADE; // ramp down slowly
        nStatusValue  = std::max(nStatusValue, STATUS_PWM_NORMALVALUE);
        gnLastUpdate  = millis();
      }
    }

    // AP mode is active
    else if (WifiIsAP(WiFi.getMode()))
    {
      nStatusValue = ((millis() >> 1) & PWMRANGE_FULL) - (PWMRANGE_FULL >> 2); // ramp up for 2 sec, 3/4 luminosity
    }

    // Disconnected
    else
    {
      nStatusValue = (millis() >> 1) & (PWMRANGE_FULL >> 2); // ramp up for 1/2 sec, 1/4 luminosity
    }
  }

  nStatusValue = constrain(nStatusValue, 0, PWMRANGE_FULL);

  if (gnStatusValueCurrent != nStatusValue)
  {
    gnStatusValueCurrent = nStatusValue;

    long pwm = nStatusValue * nStatusValue; // simple gamma correction
    pwm >>= 10;

    if (Settings.Pin_status_led_Inversed) {
      pwm = PWMRANGE_FULL - pwm;
    }

    set_Gpio_PWM(Settings.Pin_status_led, pwm, 1000);
#ifdef ESP32
    // Need to have this string as C-string, not F-string
    perimanSetPinBusExtraType(Settings.Pin_status_led, "WiFi Status LED");
#endif
  }
}
#include "../Helpers/Scheduler.h"

#include "../DataStructs/Scheduler_PluginTaskTimerID.h"

#include "../Globals/Settings.h"

/*********************************************************************************************\
* Plugin Task Timer
\*********************************************************************************************/
void ESPEasy_Scheduler::setPluginTaskTimer(
  unsigned long msecFromNow,
  taskIndex_t taskIndex,
  int Par1, int Par2, int Par3, int Par4, int Par5)
{
  setPluginTaskTimer(msecFromNow,
                     taskIndex,
                     PLUGIN_TASKTIMER_IN,
                     Par1, Par2, Par3, Par4, Par5);
}

void ESPEasy_Scheduler::setPluginTaskTimer(
  unsigned long msecFromNow,
  taskIndex_t taskIndex,
  PluginFunctions_e function,
  int Par1, int Par2, int Par3, int Par4, int Par5)
{
  // taskIndex and par1 form a unique key that can be used to restart a timer
  if (!validTaskIndex(taskIndex)) { return; }

  if (!Settings.TaskDeviceEnabled[taskIndex]) { return; }

  const PluginTaskTimerID timerID(taskIndex, Par1);

  systemTimerStruct timer_data;

  timer_data.fromEvent(taskIndex, Par1, Par2, Par3, Par4, Par5);
  systemTimers[timerID.mixed_id] = timer_data;
  setNewTimerAt(timerID, millis() + msecFromNow);
}

void ESPEasy_Scheduler::process_plugin_task_timer(SchedulerTimerID id) {
  #ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  #endif // ifdef USE_SECOND_HEAP

  auto it = systemTimers.find(id.mixed_id);

  if (it == systemTimers.end()) { return; }

  struct EventStruct TempEvent(it->second.toEvent());

  // TD-er: Not sure if we have to keep original source for notifications.
  TempEvent.Source = EventValueSource::Enum::VALUE_SOURCE_SYSTEM;


  /*
     String log = F("proc_system_timer: Pluginid: ");
     log += deviceIndex;
     log += F(" taskIndex: ");
     log += it->second.TaskIndex;
     log += F(" sysTimerID: ");
     log += id;
     addLog(LOG_LEVEL_INFO, log);
   */
  systemTimers.erase(id.mixed_id);

  {
    String dummy;
    const PluginTaskTimerID *tmp      = reinterpret_cast<const PluginTaskTimerID *>(&id);
    const PluginFunctions_e  function = tmp->getFunction();
    PluginCall(function, &TempEvent, dummy);
  }
}

#include "../Helpers/Hardware_ADC_cali.h"

#ifdef ESP32

// # include "../Helpers/ESPEasy_math.h"
# include "../Helpers/Hardware.h"


Hardware_ADC_cali_t::~Hardware_ADC_cali_t()
{
# if ESP_IDF_VERSION_MAJOR >= 5

  if (_useFactoryCalibration) {
#  if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
    adc_cali_delete_scheme_curve_fitting(_adc_cali_handle);

#  elif ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED
    adc_cali_delete_scheme_line_fitting(_adc_cali_handle);
#  endif // if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
  }

# endif  // if ESP_IDF_VERSION_MAJOR >= 5
}

bool Hardware_ADC_cali_t::init(int         pin,
                               adc_atten_t attenuation)
{
# if ESP_IDF_VERSION_MAJOR >= 5 &&  ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
  _useHighResInterpolation = false;
# elif ESP_IDF_VERSION_MAJOR >= 5
  _useHighResInterpolation = attenuation != adc_atten_t::ADC_ATTEN_DB_12;
# else // if ESP_IDF_VERSION_MAJOR >= 5 &&  ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
  _useHighResInterpolation = attenuation != adc_atten_t::ADC_ATTEN_DB_11;
# endif // if ESP_IDF_VERSION_MAJOR >= 5 &&  ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED

# if ESP_IDF_VERSION_MAJOR >= 5

  if (_adc_cali_handle != nullptr) {
#  if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
    adc_cali_delete_scheme_curve_fitting(_adc_cali_handle);
    _adc_cali_handle = nullptr;

#  elif ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED
    adc_cali_delete_scheme_line_fitting(_adc_cali_handle);
    _adc_cali_handle = nullptr;

#  endif // if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
  }

  _useFactoryCalibration = Hardware_ADC_cali_t::adc_calibration_init(
    pin,
    attenuation,
    &_adc_cali_handle);

  if (_useFactoryCalibration) {
    int tmp{};
    adc_cali_raw_to_voltage(_adc_cali_handle, 0,             &tmp);
    _min_out = tmp;
    adc_cali_raw_to_voltage(_adc_cali_handle, MAX_ADC_VALUE, &tmp);
    _max_out = tmp;
  }

# else // if ESP_IDF_VERSION_MAJOR >= 5
  #  ifndef DEFAULT_VREF
  #   define DEFAULT_VREF 1100
  #  endif // ifndef DEFAULT_VREF
  constexpr adc_bits_width_t adc_bit_width = static_cast<adc_bits_width_t>(ADC_WIDTH_MAX - 1);
  _adc_calibration_type =
    esp_adc_cal_characterize((getADC_num_for_gpio(pin) == 1) ? ADC_UNIT_1 : ADC_UNIT_2,
                             static_cast<adc_atten_t>(attenuation),
                             adc_bit_width,
                             DEFAULT_VREF,
                             &_adc_chars);
  _useFactoryCalibration = esp_adc_cal_check_efuse(_adc_calibration_type) == ESP_OK;

  if (_useFactoryCalibration) {
    _min_out = esp_adc_cal_raw_to_voltage(0, &_adc_chars);
    _max_out = esp_adc_cal_raw_to_voltage(MAX_ADC_VALUE, &_adc_chars);
  }
# endif // if ESP_IDF_VERSION_MAJOR >= 5

  _initialized = true;

  return _useFactoryCalibration;
}

float Hardware_ADC_cali_t::applyFactoryCalibration(float rawValue) const {
  if (!_useFactoryCalibration) {
    return rawValue;
  }

  if (!_useHighResInterpolation) {
# if ESP_IDF_VERSION_MAJOR >= 5
    int adc_low  = (static_cast<int>(rawValue) - 128) & 0xFFFFFF80;
    int adc_high = (static_cast<int>(rawValue) + 128) & 0xFFFFFF80;

    if (adc_low < 0) { adc_low = 0; }

    if (adc_high > MAX_ADC_VALUE) { adc_high = MAX_ADC_VALUE; }

    int volt_low{};
    int volt_high{};

    if (
      (adc_cali_raw_to_voltage(_adc_cali_handle, adc_low, &volt_low) == ESP_OK) &&
      (adc_cali_raw_to_voltage(_adc_cali_handle, adc_high, &volt_high) == ESP_OK)) {
      return mapADCtoFloat(
        rawValue,
        adc_low,
        adc_high,
        volt_low,
        volt_high);
    }
# else // if ESP_IDF_VERSION_MAJOR >= 5
    const int raw = rawValue;
    return esp_adc_cal_raw_to_voltage(raw, &_adc_chars);
# endif // if ESP_IDF_VERSION_MAJOR >= 5
  }

  // All other attenuations do appear to have a straight calibration curve.
  // But applying the factory calibration then reduces resolution.
  // So we interpolate using the calibrated extremes

  return mapADCtoFloat(
    rawValue,
    0,
    MAX_ADC_VALUE,
    _min_out,
    _max_out);
}

const __FlashStringHelper * Hardware_ADC_cali_t::getADC_factory_calibration_type() const {
# if ESP_IDF_VERSION_MAJOR >= 5

  if (_useFactoryCalibration) {
    #  if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
    return F("Calibration Curve Fitting");
    #  endif // if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
    #  if ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED
    return F("Calibration Line Fitting");
    #  endif // if ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED
  }

# else // if ESP_IDF_VERSION_MAJOR >= 5

  switch (_adc_calibration_type) {
    case ESP_ADC_CAL_VAL_EFUSE_VREF:   return F("V_ref in eFuse");
    case ESP_ADC_CAL_VAL_EFUSE_TP:     return F("Two Point values in eFuse");
    case ESP_ADC_CAL_VAL_DEFAULT_VREF: return F("Default reference voltage");
    case ESP_ADC_CAL_VAL_EFUSE_TP_FIT: return F("Two Point values and fitting curve in eFuse");
    case ESP_ADC_CAL_VAL_NOT_SUPPORTED:
      break;
  }
# endif // if ESP_IDF_VERSION_MAJOR >= 5
  return F("Unknown");
}

# if ESP_IDF_VERSION_MAJOR >= 5
bool Hardware_ADC_cali_t::adc_calibration_init(
  int                pin,
  adc_atten_t        atten,
  adc_cali_handle_t *out_handle)
{
  int ch{};
  const int adc               = getADC_num_for_gpio(pin, ch);
  const adc_channel_t channel = static_cast<adc_channel_t>(ch);

#  if HAS_ADC2
  const adc_unit_t unit = (adc == 1) ? ADC_UNIT_1 : ADC_UNIT_2;
#  else // if HAS_ADC2
  const adc_unit_t unit = ADC_UNIT_1;
#  endif // if HAS_ADC2

  adc_cali_handle_t handle = NULL;
  esp_err_t ret            = ESP_FAIL;
  bool calibrated          = false;

#  if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED

  if (!calibrated) {
    // calibration scheme version: Curve Fitting
    adc_cali_curve_fitting_config_t cali_config = {
      .unit_id  = unit,
      .chan     = channel,
      .atten    = atten,
      .bitwidth = ADC_BITWIDTH_DEFAULT,
    };
    ret = adc_cali_create_scheme_curve_fitting(&cali_config, &handle);

    if (ret == ESP_OK) {
      calibrated = true;
    }
  }
#  endif // if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED

#  if ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED

  if (!calibrated) {
    // calibration scheme version: Line Fitting
    adc_cali_line_fitting_config_t cali_config = {
      .unit_id  = unit,
      .atten    = atten,
      .bitwidth = ADC_BITWIDTH_DEFAULT,
    };
    ret = adc_cali_create_scheme_line_fitting(&cali_config, &handle);

    if (ret == ESP_OK) {
      calibrated = true;
    }
  }
#  endif // if ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED

  *out_handle = handle;

  /*
     if (ret == ESP_OK) {
      // Calibration Success
     } else if (ret == ESP_ERR_NOT_SUPPORTED || !calibrated) {
      // eFuse not burnt, skip software calibration
     } else {
      // Invalid arg or no memory
     }
   */

  return calibrated;
}

# endif // if ESP_IDF_VERSION_MAJOR >= 5

#endif  // ifdef ESP32

#include "../Helpers/Numerical.h"

#include "../DataStructs/TimingStats.h"

#include "../Globals/Settings.h"
#include "../Helpers/StringConverter.h"

/********************************************************************************************\
   Check if string is valid float
 \*********************************************************************************************/
bool isValidFloat(float f) {
#ifdef ESP32
  return !isnanf(f) && !isinff(f);
#else
  return !isnan(f) && !isinf(f);
#endif
}

bool isValidDouble(ESPEASY_RULES_FLOAT_TYPE f) {
  return !isnan(f) && !isinf(f);
}

bool validIntFromString(const String& tBuf, int32_t& result) {
  NumericalType detectedType;
  const String  numerical = getNumerical(tBuf, NumericalType::Integer, detectedType);

  if ((detectedType == NumericalType::BinaryUint) ||
      (detectedType == NumericalType::HexadecimalUInt)) {
    uint32_t tmp;
    bool isvalid = validUIntFromString(numerical, tmp);

    // FIXME TD-er: What to do here if the uint value > max_int ?
    result = static_cast<int32_t>(tmp);
    return isvalid;
  }
  const bool isvalid = numerical.length() > 0;

  if (isvalid) {
    result = strtol(numerical.c_str(), nullptr, DEC);
  }
  return isvalid;
}

bool validInt64FromString(const String& tBuf, int64_t& result) {
  NumericalType detectedType;
  const String  numerical = getNumerical(tBuf, NumericalType::Integer, detectedType);

  if ((detectedType == NumericalType::BinaryUint) ||
      (detectedType == NumericalType::HexadecimalUInt)) {
    uint64_t tmp;
    bool     isvalid = validUInt64FromString(numerical, tmp);
    result = static_cast<int64_t>(tmp);
    return isvalid;
  }

  const bool isvalid = numerical.length() > 0;

  if (isvalid) {
    result = strtoll(numerical.c_str(), nullptr, DEC);
  }
  return isvalid;
}

bool validUIntFromString(const String& tBuf, uint32_t& result) {
  NumericalType detectedType;
  String numerical   = getNumerical(tBuf, NumericalType::HexadecimalUInt, detectedType);
  const bool isvalid = numerical.length() > 0;

  if (isvalid) {
    int base = DEC;

    if (detectedType == NumericalType::HexadecimalUInt) {
      numerical = numerical.substring(2);
      base      = HEX;
    } else if (detectedType == NumericalType::BinaryUint) {
      numerical = numerical.substring(2);
      base      = BIN;
    }
    result = strtoul(numerical.c_str(), nullptr, base);
  }
  return isvalid;
}

bool validUInt64FromString(const String& tBuf, uint64_t& result) {
  NumericalType detectedType;
  String numerical   = getNumerical(tBuf, NumericalType::HexadecimalUInt, detectedType);
  const bool isvalid = numerical.length() > 0;

  if (isvalid) {
    int base = DEC;

    if (detectedType == NumericalType::HexadecimalUInt) {
      numerical = numerical.substring(2);
      base      = HEX;
    } else if (detectedType == NumericalType::BinaryUint) {
      numerical = numerical.substring(2);
      base      = BIN;
    }
    result = strtoull(numerical.c_str(), nullptr, base);
  }
  return isvalid;
}

bool validFloatFromString(const String& tBuf, float& result) {
  int nrDecimals{};
  return validFloatFromString(tBuf, result, nrDecimals);
}

bool validFloatFromString(const String& tBuf, float& result, int& nrDecimals) {
  // DO not call validDoubleFromString and then cast to float.
  // Working with double values is quite CPU intensive as it must be done in software
  // since the ESP does not have large enough registers for handling double values in hardware.
  nrDecimals = -1;
  NumericalType detectedType;
  const String  numerical = getNumerical(tBuf, NumericalType::FloatingPoint, detectedType);

  if ((detectedType == NumericalType::BinaryUint) ||
      (detectedType == NumericalType::HexadecimalUInt)) {
    uint32_t tmp;
    bool isvalid = validUIntFromString(tBuf, tmp);
    result = static_cast<float>(tmp);
    nrDecimals = 0;
    return isvalid;
  }

  const bool isvalid = numerical.length() > 0;

  if (isvalid) {
    result = numerical.toFloat();
    const int index_dot = numerical.indexOf('.');
    if (index_dot >= 0) {
      nrDecimals = numerical.length() - index_dot - 1;
      if (nrDecimals < 0) {
        nrDecimals = 0;
      }
    }
  }
  return isvalid;
}

bool validDoubleFromString(const String& tBuf, ESPEASY_RULES_FLOAT_TYPE& result) {
  int nrDecimals{};
  return validDoubleFromString(tBuf, result, nrDecimals);
}

bool validDoubleFromString(const String& tBuf, ESPEASY_RULES_FLOAT_TYPE& result, int& nrDecimals) {
  nrDecimals = -1;
  #if defined(CORE_POST_2_5_0) || defined(ESP32)

  // String.toDouble() is introduced in core 2.5.0
  NumericalType detectedType;
  const String  numerical = getNumerical(tBuf, NumericalType::FloatingPoint, detectedType);

  if ((detectedType == NumericalType::BinaryUint) ||
      (detectedType == NumericalType::HexadecimalUInt)) {
    uint64_t tmp;
    bool     isvalid = validUInt64FromString(tBuf, tmp);
    result = static_cast<ESPEASY_RULES_FLOAT_TYPE>(tmp);
    nrDecimals = 0;
    return isvalid;
  }

  const bool isvalid = numerical.length() > 0;

  if (isvalid) {
    result = numerical.toDouble();
    const int index_dot = numerical.indexOf('.');
    if (index_dot >= 0) {
      nrDecimals = numerical.length() - index_dot - 1;
      if (nrDecimals < 0) {
        nrDecimals = 0;
      }
    }
  }
  return isvalid;
  #else // if defined(CORE_POST_2_5_0) || defined(ESP32)
  float tmp = static_cast<float>(result);
  bool  res = validFloatFromString(tBuf, tmp, nrDecimals);
  result = static_cast<ESPEASY_RULES_FLOAT_TYPE>(tmp);

  return res;
  #endif // if defined(CORE_POST_2_5_0) || defined(ESP32)
}

bool mustConsiderAsString(NumericalType detectedType) {
  return detectedType != NumericalType::FloatingPoint &&
         detectedType != NumericalType::Integer;
/*
  switch (detectedType) {
    case NumericalType::FloatingPoint:
    case NumericalType::Integer:
      break;
    case NumericalType::HexadecimalUInt:
    case NumericalType::BinaryUint: // Has '0x' or '0b' as prefix
    case NumericalType::Not_a_number:
      return true;
  }
  return false;
*/
}

bool mustConsiderAsJSONString(const String& value) {
  if (value.isEmpty()) {
    // Empty string
    return true;
  }
  const char c = value[0];

  if (isDigit(c) || c == '-' || c == '.' || c == '+' || c == ' ') {
    NumericalType detectedType;
    if (isNumerical(value, detectedType)) {
      return mustConsiderAsString(detectedType);
    }
  }
  if (equals(value, F("true")) ||
      equals(value, F("false")) ||
      equals(value, F("null"))) 
  {
    return !Settings.JSONBoolWithoutQuotes();
  }

  return true;
}

String getNumerical(const String& tBuf, NumericalType requestedType, NumericalType& detectedType) {
  const unsigned int bufLength = tBuf.length();
  unsigned int firstDec        = 0;
  String result;

  // Strip leading spaces
  while (firstDec < bufLength && tBuf.charAt(firstDec) == ' ') {
    ++firstDec;
  }

  if (firstDec >= bufLength) {
    detectedType = NumericalType::Not_a_number;
    return result;
  }
  bool decPt = false;

  detectedType = NumericalType::Integer;
  char c = tBuf.charAt(firstDec);

  if ((c == '+') || (c == '-')) {
    if ((requestedType != NumericalType::HexadecimalUInt) &&
        (requestedType != NumericalType::BinaryUint)) {
      if (c == '-') {
        result += c;
      }
      ++firstDec;

      if (firstDec < bufLength) {
        c = tBuf.charAt(firstDec);
      }
    }
  }

  // Strip leading zeroes
  while (c == '0' && 
         (firstDec + 1) < bufLength && 
         isdigit(tBuf.charAt(firstDec + 1))) {
    ++firstDec;
    c = tBuf.charAt(firstDec);
  }

  if (c == '0') {
    ++firstDec;
    result += c;

    if (firstDec < bufLength) {
      c = tBuf.charAt(firstDec);

      if ((c == 'x') || (c == 'X')) {
        ++firstDec;
        result      += c;
        detectedType = NumericalType::HexadecimalUInt;
      } else if ((c == 'b') || (c == 'B')) {
        ++firstDec;
        result      += c;
        detectedType = NumericalType::BinaryUint;
      } else if (NumericalType::Integer == requestedType) {
        // Allow leading zeroes in Integer types (e.g. in time notation)
        while (c == '0' && firstDec < bufLength) {
          // N.B. intentional "reverse order" of reading char and ++firstDec
          c = tBuf.charAt(firstDec);
          ++firstDec;
        }
      } else if ((NumericalType::FloatingPoint == requestedType) && (c == '.')) {
        // Only floating point numbers should start with '0.'
        // All other combinations are not valid.
        ++firstDec;
        result      += c;
        decPt        = true;
        detectedType = NumericalType::FloatingPoint;
      } else {
        if (equals(result, '-')) {
          detectedType = NumericalType::Not_a_number;
          return emptyString;
        }
        return result;
      }
    }
  } else {
    // Does not start with a 0 and already tested for +/-
    // Only allowed to have a '.' or digits.
    if ((c != '.') && !isdigit(c)) {
      detectedType = NumericalType::Not_a_number;
      return result;
    }
  }

  bool done = false;

  result.reserve(bufLength - firstDec + result.length());

  for (unsigned int x = firstDec; !done && x < bufLength; ++x) {
    c = tBuf.charAt(x);

    if (c == '.') {
      if (NumericalType::FloatingPoint != requestedType) { done = true; }

      // Only one decimal point allowed
      if (decPt) { done = true; }
      else {
        decPt        = true;
        detectedType = NumericalType::FloatingPoint;
      }
    } else {
      switch (detectedType) {
        case NumericalType::FloatingPoint:
        case NumericalType::Integer:

          if (!isdigit(c)) {
            done = true;
          }
          break;
        case NumericalType::HexadecimalUInt:

          if (!isxdigit(c)) {
            done = true;
          }
          break;
        case NumericalType::BinaryUint:

          if ((c != '0') && (c != '1')) {
            done = true;
          }
          break;
        case NumericalType::Not_a_number:
          done = true;
          break;
      }
    }

    if (!done) {
      result += c;
    }
  }

  if (equals(result, '-')) {
    detectedType = NumericalType::Not_a_number;
    return emptyString;
  }
  return result;
}

bool isNumerical(const String& tBuf, NumericalType& detectedType) {
  START_TIMER;
  NumericalType requestedType = NumericalType::FloatingPoint;
  const String  result        = getNumerical(tBuf, requestedType, detectedType);

  if (detectedType == NumericalType::Not_a_number) { return false; }

  if (result.length() > 0)
  {
    String tmp(tBuf);
    tmp.trim(); // remove leading and trailing spaces

    // Resulting size should be the same size as the given string.
    // Not sure if it is possible to have a longer result, but better be sure to also allow for larger resulting strings.
    // For example ".123" -> "0.123"
    STOP_TIMER(IS_NUMERICAL);
    return result.length() >= tmp.length();
  }

  return result.length() > 0;
}

#include "../Helpers/msecTimerHandlerStruct.h"


#include "../Helpers/ESPEasy_time_calc.h"


#define MAX_SCHEDULER_WAIT_TIME 50 // Max delay used in the scheduler for passing idle time.

  msecTimerHandlerStruct::msecTimerHandlerStruct() : get_called(0), get_called_ret_id(0), max_queue_length(0),
    last_exec_time_usec(0), total_idle_time_usec(0),  idle_time_pct(0.0f), is_idle(false), eco_mode(true)
  {
    last_log_start_time = millis();
  }

  void msecTimerHandlerStruct::setEcoMode(bool enabled) {
    eco_mode = enabled;
  }

  void msecTimerHandlerStruct::registerAt(unsigned long id, unsigned long timer) {
    timer_id_couple item(id, timer);

    insert(item);
  }

  void msecTimerHandlerStruct::remove(unsigned long id) {
    timer_id_couple item(id, 0);
    remove(item);
  }

  // Check if timeout has been reached and also return its set timer.
  // Return 0 if no item has reached timeout moment.
  unsigned long msecTimerHandlerStruct::getNextId(unsigned long& timer) {
    ++get_called;

    if (_timer_ids.empty()) {
      recordIdle();

      if (eco_mode) {
        delay(MAX_SCHEDULER_WAIT_TIME); // Nothing to do, try save some power.
      }
      return 0;
    }
    timer_id_couple item = _timer_ids.front();
    const long passed    = timePassedSince(item._timer);

    if (passed < 0) {
      // No timeOutReached
      recordIdle();

      if (eco_mode) {
        long waitTime = (-1 * passed) - 1; // will be non negative

        if (waitTime > MAX_SCHEDULER_WAIT_TIME) {
          waitTime = MAX_SCHEDULER_WAIT_TIME;
        } else if (waitTime < 0) {  //-V547
          // Should not happen, but just to be sure we will not wait forever.
          waitTime = 0;
        }
        delay(waitTime);
      }
      return 0;
    }
    recordRunning();
    unsigned long size = _timer_ids.size();

    if (size > max_queue_length) { max_queue_length = size; }
    _timer_ids.pop_front();
    timer = item._timer;
    ++get_called_ret_id;
    return item._id;
  }


  bool msecTimerHandlerStruct::getTimerForId(unsigned long id, unsigned long& timer) const {
    for (auto it = _timer_ids.begin(); it != _timer_ids.end(); ++it) {
      if (it->_id == id) {
        timer = it->_timer;
        return true;
      }
    }
    return false;
  }

  String msecTimerHandlerStruct::getQueueStats() {
    String result;

    result           += get_called;
    result           += '/';
    result           += get_called_ret_id;
    result           += '/';
    result           += max_queue_length;
    result           += '/';
    result           += idle_time_pct;
    get_called        = 0;
    get_called_ret_id = 0;

    // max_queue_length = 0;
    return result;
  }

  void msecTimerHandlerStruct::updateIdleTimeStats() {
    const long duration = timePassedSince(last_log_start_time);

    last_log_start_time  = millis();
    idle_time_pct        = static_cast<float>(total_idle_time_usec) / duration / 10.0f;
    total_idle_time_usec = 0;
  }

  float msecTimerHandlerStruct::getIdleTimePct() const {
    return idle_time_pct;
  }

  struct match_id {
    match_id(unsigned long id) : _id(id) {}

    bool operator()(const timer_id_couple& item) {
      return _id == item._id;
    }

    unsigned long _id;
  };

  void msecTimerHandlerStruct::insert(const timer_id_couple& item) {
    if (item._id == 0) { return; }

    // Make sure only one is present with the same id.
    _timer_ids.remove_if(match_id(item._id));
    const bool mustSort = !_timer_ids.empty();
    _timer_ids.push_front(item);

    if (mustSort) {
      _timer_ids.sort(); // TD-er: Must check if this is an expensive operation.
    }

    // It should be a relative light operation, to insert into a sorted list.
    // Perhaps it is better to use std::set ????
    // Keep in mind: order is based on timer, uniqueness is based on id.
  }

  void msecTimerHandlerStruct::remove(const timer_id_couple& item) {
    if (item._id == 0) { return; }

    // Make sure only one is present with the same id.
    _timer_ids.remove_if(match_id(item._id));
  }

  void msecTimerHandlerStruct::recordIdle() {
    if (is_idle) { return; }
    last_exec_time_usec = micros();
    is_idle             = true;
    delay(0); // Nothing to do, so leave time for backgroundtasks
  }

  void msecTimerHandlerStruct::recordRunning() {
    if (!is_idle) { return; }
    is_idle               = false;
    total_idle_time_usec += usecPassedSince_fast(last_exec_time_usec);
  }

#include "../Helpers/_Plugin_Helper_serial.h"

#ifdef PLUGIN_USES_SERIAL


#include "../../_Plugin_Helper.h"

#include "../DataStructs/ESPEasy_EventStruct.h"
#include "../Globals/Cache.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringGenerator_GPIO.h"

#include "../WebServer/HTML_wrappers.h"
#include "../WebServer/Markup.h"
#include "../WebServer/Markup_Forms.h"

#include <ESPEasySerialType.h>


String serialHelper_getSerialTypeLabel(ESPEasySerialPort serType, bool shortName) {
  return ESPEasySerialPort_toString(serType, shortName);
}

void serialHelper_log_GpioDescription(ESPEasySerialPort typeHint, int config_pin1, int config_pin2) {
  #ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    String log = F("Serial : ");
    log += serialHelper_getGpioDescription(typeHint, config_pin1, config_pin2, " ");
    addLogMove(LOG_LEVEL_DEBUG, log);
  }
  #endif // ifndef BUILD_NO_DEBUG
}

String serialHelper_getGpioDescription(ESPEasySerialPort typeHint, int config_pin1, int config_pin2, const String& newline) {
  String result;

  result.reserve(20);

  const ESPEasySerialPort porttype = ESPeasySerialType::getSerialType(typeHint, config_pin1, config_pin2);
#if USES_I2C_SC16IS752
  if (porttype == ESPEasySerialPort::sc16is752)
  {
    result += formatToHex(config_pin1);
    result += newline;
    result += F(" ch: ");
    result += config_pin2 == 0 ? 'A' : 'B';
    return result;
  }
#endif
#if USES_HWCDC || USES_USBCDC
#if USES_HWCDC
  if (porttype == ESPEasySerialPort::usb_hw_cdc)
#else
  if (porttype == ESPEasySerialPort::usb_cdc_0 /*||
      porttype == ESPEasySerialPort::usb_cdc_1*/)
#endif
  {
    result += getConflictingUse(PIN_USB_D_MIN);
    result += formatGpioLabel(PIN_USB_D_MIN, false);
    result += newline;
    result += getConflictingUse(PIN_USB_D_PLUS);
    result += formatGpioLabel(PIN_USB_D_PLUS, false);
    return result;
  }
#endif
  if (useGPIOpins(porttype)) 
  {
    result += F("RX: ");
    result += formatGpioLabel(config_pin1, false);
    result += newline;
    result += F("TX: ");
    result += formatGpioLabel(config_pin2, false);
  }
  return result;
}

void serialHelper_getGpioNames(struct EventStruct *event, bool rxOptional, bool txOptional) {
  event->String1 = formatGpioName_serialRX(rxOptional);
  event->String2 = formatGpioName_serialTX(txOptional);
}

void serialHelper_modbus_getGpioNames(struct EventStruct *event,
                               bool                rxOptional,
                               bool                txOptional,
                               bool                DE_RE_optional)
{
  serialHelper_getGpioNames(event, rxOptional, txOptional);
  event->String1.replace(F("TX"), F("TX (RO)"));
  event->String2.replace(F("RX"), F("RX (DI)"));
  if (DE_RE_optional)
    event->String3 = formatGpioName_output_optional(F("~RE/DE"));
  else 
    event->String3 = formatGpioName_output(F("~RE/DE"));
}

int8_t serialHelper_getRxPin(struct EventStruct *event) {
  return CONFIG_PIN1;
}

int8_t serialHelper_getTxPin(struct EventStruct *event) {
  return CONFIG_PIN2;
}

ESPEasySerialPort serialHelper_getSerialType(struct EventStruct *event) {
  ESPEasySerialPort serialType = static_cast<ESPEasySerialPort>(CONFIG_PORT);

  if (serialType != ESPEasySerialPort::not_set) {
    return serialType;
  }

  return ESPeasySerialType::getSerialType(
    serialType,
    serialHelper_getRxPin(event),
    serialHelper_getTxPin(event));
}

String serialHelper_getSerialTypeLabel(struct EventStruct *event) {
  return serialHelper_getSerialTypeLabel(serialHelper_getSerialType(event));
}

#ifndef DISABLE_SC16IS752_Serial
void serialHelper_addI2CuartSelectors(int address, int channel) {
  # define     SC16IS752_I2C_ADDRESSES             16
  # define     SC16IS752_I2C_BASE_ADDR             (0x90 >> 1)
  # define     SC16IS752_CHANNELS                  2
  # define     SC16IS752_CHANNEL_A                 0x00
  # define     SC16IS752_CHANNEL_B                 0x01
  {
    String id = F("i2cuart_addr");
    addRowLabel_tr_id(F("I2C Address"), id);
    do_addSelector_Head(id, F(""), EMPTY_STRING, false);

    if ((address < SC16IS752_I2C_BASE_ADDR) || (address >= (SC16IS752_I2C_BASE_ADDR + SC16IS752_I2C_ADDRESSES))) {
      // selected address is not in range
      address = SC16IS752_I2C_BASE_ADDR;
    }

    for (int i = 0; i < SC16IS752_I2C_ADDRESSES; i++)
    {
      int addr = SC16IS752_I2C_BASE_ADDR + i;
      String option;
      option.reserve(24);
      option  = formatToHex(addr);
      option += F(" (datasheet: ");
      option += formatToHex(addr * 2);
      option += ')';
      addSelector_Item(option, addr, addr == address);
    }
    addSelector_Foot();
  }
  {
    if ((channel != SC16IS752_CHANNEL_A) && (channel != SC16IS752_CHANNEL_B)) {
      channel = SC16IS752_CHANNEL_A;
    }
    const __FlashStringHelper *chOptions[] = {
      F("A"),
      F("B"),
    };
    /*
    const int chValues[] = {
      SC16IS752_CHANNEL_A,
      SC16IS752_CHANNEL_B,
    };
    */
    const FormSelectorOptions selector(NR_ELEMENTS(chOptions), chOptions);
    selector.addFormSelector(F("Channel"), F("i2cuart_ch"), channel);
  }
}

#endif // ifndef DISABLE_SC16IS752_Serial

void serialHelper_webformLoad(struct EventStruct *event) {
  serialHelper_webformLoad(event, true);
}

// These helper functions were made to create a generic interface to setup serial port config.
// See issue #2343 and Pull request https://github.com/letscontrolit/ESPEasy/pull/2352
// For now P020 and P044 have been reverted to make them work again.
void serialHelper_webformLoad(struct EventStruct *event, bool allowSoftwareSerial) {
  serialHelper_webformLoad(static_cast<ESPEasySerialPort>(CONFIG_PORT),
                           serialHelper_getRxPin(event),
                           serialHelper_getTxPin(event),
                           allowSoftwareSerial);
}

void serialHelper_webformLoad(ESPEasySerialPort port, int rxPinDef, int txPinDef, bool allowSoftwareSerial) {
  // Field for I2C addr & RX are shared
  // Field for channel and TX are shared
  #ifdef ESP8266

  // Script to show GPIO pins for SoftwareSerial or I2C addresses for the I2C to UART bridge
  // "function serialPortChanged(elem) {var style = elem.value == 6 ? '' : 'none';var i2cstyle = elem.value == 1 ? '' :
  // 'none';document.getElementById('tr_taskdevicepin1').style.display = style;document.getElementById('tr_taskdevicepin2').style.display =
  // style;document.getElementById('tr_i2cuart_addr').style.display = i2cstyle;document.getElementById('tr_i2cuart_ch').style.display =
  // i2cstyle;}"),
  html_add_script(F("function serialPortChanged(e){var t=6==e.value?'':'none',l=1==e.value?'':'none';"
                    "document.getElementById('tr_taskdevicepin1').style.display=t,"
                    "document.getElementById('tr_taskdevicepin2').style.display=t,"
                    "document.getElementById('tr_i2cuart_addr').style.display=l,"
                    "document.getElementById('tr_i2cuart_ch').style.display=l}"),
                  false);
  #endif // ifdef ESP8266
  #ifdef ESP32

  // Script to show GPIO pins for HW serial ports or I2C addresses for the I2C to UART bridge
  // "function serialPortChanged(elem) {var style = (elem.value == 2 || elem.value == 4 || elem.value == 5) ? '' : 'none';var i2cstyle =
  // elem.value == 1 ? '' : 'none';document.getElementById('tr_taskdevicepin1').style.display =
  // style;document.getElementById('tr_taskdevicepin2').style.display = style;document.getElementById('tr_i2cuart_addr').style.display =
  // i2cstyle;document.getElementById('tr_i2cuart_ch').style.display = i2cstyle;}"),
  html_add_script(F("function serialPortChanged(elem) {"
" var style = 'none';"
" var i2cstyle = elem.value == 1 ? '' : 'none';"
"	if (elem.value == 2) {"
//"	  document.querySelector('#taskdevicepin1').value =" STRINGIFY(SOC_RX0) ";"
//"	  document.querySelector('#taskdevicepin2').value =" STRINGIFY(SOC_TX0) ";"
"   style = '';"
# if USABLE_SOC_UART_NUM > 1
"	} else if (elem.value == 4) {"
//"	  document.querySelector('#taskdevicepin1').value =" STRINGIFY(SOC_RX1) ";"
//"	  document.querySelector('#taskdevicepin2').value =" STRINGIFY(SOC_TX1) ";"
"   style = '';"
#endif
# if USABLE_SOC_UART_NUM > 2
"	} else if (elem.value == 5) {"
//"	  document.querySelector('#taskdevicepin1').value =" STRINGIFY(SOC_RX2) ";"
//"	  document.querySelector('#taskdevicepin2').value =" STRINGIFY(SOC_TX2) ";"
"   style = '';"
#endif
#if USES_SW_SERIAL
"	} else if (elem.value == 6) {"
"   style = '';"
#endif
"	}"
" document.getElementById('tr_taskdevicepin1').style.display = style;"
" document.getElementById('tr_taskdevicepin2').style.display = style;"
" document.getElementById('tr_i2cuart_addr').style.display   = i2cstyle;"
" document.getElementById('tr_i2cuart_ch').style.display     = i2cstyle;"
"}"),
                  true);
  #endif // ifdef ESP32

  #if !USES_SW_SERIAL
  allowSoftwareSerial = false;
  #endif

  const int ids[] = {
     static_cast<int>(ESPEasySerialPort::not_set)
    ,static_cast<int>(ESPEasySerialPort::serial0)
#ifdef ESP8266
    ,static_cast<int>(ESPEasySerialPort::serial0_swap)
#endif // ifdef ESP8266
#if USABLE_SOC_UART_NUM > 1
    ,static_cast<int>(ESPEasySerialPort::serial1)
#endif
#if USABLE_SOC_UART_NUM > 2
    ,static_cast<int>(ESPEasySerialPort::serial2)
#endif // if USABLE_SOC_UART_NUM > 2
#if USES_SW_SERIAL
    ,static_cast<int>(ESPEasySerialPort::software)
#endif // if USES_SW_SERIAL

#if USES_HWCDC
    ,static_cast<int>(ESPEasySerialPort::usb_hw_cdc)
#endif // if USES_HWCDC
#if USES_USBCDC
    ,static_cast<int>(ESPEasySerialPort::usb_cdc_0)
//    ,static_cast<int>(ESPEasySerialPort::usb_cdc_1)
#endif // if USES_USBCDC
#if USES_I2C_SC16IS752
    ,static_cast<int>(ESPEasySerialPort::sc16is752)
#endif // if USES_I2C_SC16IS752
  };

  constexpr int NR_ESPEASY_SERIAL_TYPES = NR_ELEMENTS(ids);
  String options[NR_ESPEASY_SERIAL_TYPES];

//  String attr[NR_ESPEASY_SERIAL_TYPES];

  for (int i = 0; (i < NR_ESPEASY_SERIAL_TYPES); ++i) {
    ESPEasySerialPort serType = static_cast<ESPEasySerialPort>(ids[i]);

    String option;
    option.reserve(48);
    option = serialHelper_getSerialTypeLabel(serType);
#ifdef ESP8266
    int rxPin, txPin;
    if (ESPeasySerialType::getSerialTypePins(serType, rxPin, txPin)) {

      if (isHWserial(serType))
      {
        // Show pins for ports with fixed pins
        option += F(": ");
        option += formatGpioLabel(rxPin, false);
        option += ' ';
        option += formatGpioDirection(gpio_direction::gpio_input);
        option += F("TX / ");
        option += formatGpioLabel(txPin, false);
        option += ' ';
        option += formatGpioDirection(gpio_direction::gpio_output);
        option += F("RX");
      }
    }
#endif
    options[i] = option;
  }
  FormSelectorOptions selector(NR_ESPEASY_SERIAL_TYPES, options, ids);

  // Script to toggle GPIO visibility when changing selection.
  selector.onChangeCall = F("serialPortChanged(this)"); 
  selector.addFormSelector(
    F("Serial Port"), F("serPort"),
    static_cast<int>(ESPeasySerialType::getSerialType(port, rxPinDef, txPinDef)));
#if USES_I2C_SC16IS752
  serialHelper_addI2CuartSelectors(rxPinDef, txPinDef);
#endif // ifndef DISABLE_SC16IS752_Serial

#ifdef ESP8266

  if ((rxPinDef == 15) || (txPinDef == 15)) {
    addFormNote(F("GPIO-15 (D8) requires a Buffer Circuit (PNP transistor) or ESP boot may fail."));
  }
#endif // ifdef ESP8266
}

void serialHelper_webformSave(uint8_t& port, int8_t& rxPin, int8_t& txPin) {
  int serialPortSelected = getFormItemInt(F("serPort"), -1);
  if (serialPortSelected < 0) { return; }

  rxPin = getFormItemInt(F("taskdevicepin1"), -1);
  txPin = getFormItemInt(F("taskdevicepin2"), -1);

  ESPEasySerialPort serType = static_cast<ESPEasySerialPort>(serialPortSelected);

  port = serialPortSelected;

  #ifndef DISABLE_SC16IS752_Serial
  if (serType == ESPEasySerialPort::sc16is752) {
    rxPin = getFormItemInt(F("i2cuart_addr"), rxPin);
    txPin = getFormItemInt(F("i2cuart_ch"), txPin);
  }
  #endif // ifndef DISABLE_SC16IS752_Serial
  #ifdef ESP8266
  if (isHWserial(serType)) {

    // Ports with a fixed pin layout, so load the defaults.
    int tmprxPin, tmptxPin;

    if (ESPeasySerialType::getSerialTypePins(serType, tmprxPin, tmptxPin)) {
      rxPin = tmprxPin;
      txPin = tmptxPin;
    }
  }
  #endif // ifdef ESP8266
}

void serialHelper_webformSave(struct EventStruct *event) {
  serialHelper_webformSave(CONFIG_PORT, CONFIG_PIN1, CONFIG_PIN2);
}

bool serialHelper_isValid_serialconfig(uint8_t serialconfig) {
  if ((serialconfig >= 0x10) && (serialconfig <= 0x3f)) {
    return true;
  }
  return false;
}

void serialHelper_serialconfig_webformLoad(struct EventStruct *event, uint8_t currentSelection) {
  // nrOptions = 4 * 3 * 2  = 24  (bits 5..8 , parity N/E/O  , stopbits 1/2)
  String id = F("serConf");

  addRowLabel_tr_id(F("Serial Config"), id);
  do_addSelector_Head(id, F(""), EMPTY_STRING, false);

  if (currentSelection == 0) {
    // Must truncate it to 1 uint8_t, since ESP32 uses a 32-bit value. We add these high bits later for ESP32.
    currentSelection = static_cast<uint8_t>(SERIAL_8N1 & 0xFF); // Some default
  }

  for (uint8_t parity = 0; parity < 3; ++parity) {
    for (uint8_t stopBits = 1; stopBits <= 2; ++stopBits) {
      for (uint8_t bits = 5; bits <= 8; ++bits) {
        String label;
        label.reserve(36);
        label  = String(bits);
        label += F(" bit / parity: ");
        int value = ((bits - 5) << 2);

        switch (parity) {
          case 0: label += F("None"); break;
          case 1: label += F("Even"); value += 2; break;
          case 2: label += F("Odd");  value += 3; break;
        }
        label += F(" / stop bits: ");
        label += String(stopBits);

        // There are also values for 0 and "1.5" stop bit, not used now.
        switch (stopBits) {
          case 1:  value += 0x10; break;
          case 2:  value += 0x30; break;
        }
        addSelector_Item(label, value, value == currentSelection);
      }
    }
  }
  addSelector_Foot();
}

uint8_t serialHelper_serialconfig_webformSave() {
  int serialConfSelected = getFormItemInt(F("serConf"), 0);

  if (serialHelper_isValid_serialconfig(serialConfSelected)) {
    return serialConfSelected;
  }

  // Must truncate it to 1 uint8_t, since ESP32 uses a 32-bit value. We add these high bits later for ESP32.
  return static_cast<uint8_t>(SERIAL_8N1 & 0xFF); // Some default
}

// Used by some plugins, which used several TaskDevicePluginConfigLong
uint8_t serialHelper_convertOldSerialConfig(uint8_t newLocationConfig) {
  if (serialHelper_isValid_serialconfig(newLocationConfig)) {
    return newLocationConfig;
  }
  uint8_t serialconfig = 0x10;                                                // Default stopbits = 1

  serialconfig += ExtraTaskSettings.TaskDevicePluginConfigLong[3];            // Parity
  serialconfig += (ExtraTaskSettings.TaskDevicePluginConfigLong[2] - 5) << 2; // databits

  if (ExtraTaskSettings.TaskDevicePluginConfigLong[4] == 2) {
    serialconfig += 0x20;                                                     // Stopbits = 2
  }

  if (serialHelper_isValid_serialconfig(serialconfig)) {
    return serialconfig;
  }

  // Must truncate it to 1 uint8_t, since ESP32 uses a 32-bit value. We add these high bits later for ESP32.
  return static_cast<uint8_t>(SERIAL_8N1 & 0xFF); // Some default
}

#endif
#include "../Helpers/ESPEasy_checks.h"


#include "../../ESPEasy_common.h"
#ifndef BUILD_MINIMAL_OTA

#include "../DataStructs/CRCStruct.h"
#include "../DataStructs/ControllerSettingsStruct.h"
#include "../DataStructs/DeviceStruct.h"
#include "../DataStructs/ESPEasy_EventStruct.h"
#include "../DataStructs/ExtraTaskSettingsStruct.h"
#include "../DataStructs/FactoryDefaultPref.h"
#include "../DataStructs/GpioFactorySettingsStruct.h"
#include "../DataStructs/LogStruct.h"
#if FEATURE_ESPEASY_P2P
#include "../DataStructs/NodeStruct.h"
#endif
#include "../DataStructs/PortStatusStruct.h"
#include "../DataStructs/ProtocolStruct.h"
#if FEATURE_CUSTOM_PROVISIONING
#include "../DataStructs/ProvisioningStruct.h"
#endif
#include "../DataStructs/RTCStruct.h"
#include "../DataStructs/SecurityStruct.h"
#include "../DataStructs/SettingsStruct.h"
#include "../DataStructs/SystemTimerStruct.h"

#include "../Globals/ExtraTaskSettings.h"
#include "../Globals/Settings.h"

#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/StringConverter.h"

#include <cstddef>

#ifdef USES_C013
#include "../DataStructs/C013_p2p_SensorDataStruct.h"
#include "../DataStructs/C013_p2p_SensorInfoStruct.h"
#endif

#ifdef USES_C016
#include "../ControllerQueue/C016_queue_element.h"
#endif

#if FEATURE_NOTIFIER
#include "../DataStructs/NotificationStruct.h"
#include "../DataStructs/NotificationSettingsStruct.h"
#endif // if FEATURE_NOTIFIER


// ********************************************************************************
// Check struct sizes at compile time
// Usage:
//   struct foo
//   {
//     char bla[16];
//   };
//
//   check_size<foo, 8>();
// ********************************************************************************
template <typename ToCheck, std::size_t ExpectedSize, std::size_t RealSize = sizeof(ToCheck)>
void check_size() {
  static_assert(ExpectedSize == RealSize, "");
}
//   check_max_size<foo, 1024>();
// ********************************************************************************
template <typename ToCheck, std::size_t MaxSize, std::size_t RealSize = sizeof(ToCheck)>
void check_max_size() {
  static_assert(MaxSize >= RealSize, "structure size exceeds storeable size!");
}



// ********************************************************************************
// Check struct sizes at compile time
// Usage:
//   struct X { int a, b, c, d; }
//   static_assert(ExpectedSize == offsetof(X, c), "");
// ********************************************************************************

void run_compiletime_checks() {
  #ifndef LIMIT_BUILD_SIZE
  check_size<CRCStruct,                             204u>();
  check_size<SecurityStruct,                        593u>();
  check_max_size<SecurityStruct,                    DAT_SECURITYSETTINGS_SIZE>();
  #ifdef ESP32
  constexpr unsigned int SettingsStructSize = (340 + 84 * TASKS_MAX);
  #endif
  #ifdef ESP8266
  constexpr unsigned int SettingsStructSize = (316 + 84 * TASKS_MAX);
  #endif
  #if FEATURE_CUSTOM_PROVISIONING
  check_size<ProvisioningStruct,                    256u>();
  #endif
  check_size<SettingsStruct,                        SettingsStructSize>();
  check_max_size<SettingsStruct,                    DAT_BASIC_SETTINGS_SIZE>();
  check_size<ControllerSettingsStruct,              1012u>();
  check_max_size<ControllerSettingsStruct,          DAT_CONTROLLER_SIZE>();
  #if FEATURE_NOTIFIER
  check_size<NotificationSettingsStruct,            1000u>();
  check_max_size<NotificationSettingsStruct,        DAT_NOTIFICATION_SIZE>();
  #endif // if FEATURE_NOTIFIER
  check_size<ExtraTaskSettingsStruct,               536u>();
  check_max_size<ExtraTaskSettingsStruct,           DAT_TASKS_SIZE>();
  #if ESP_IDF_VERSION_MAJOR > 3
  // String class has increased with 4 bytes
  check_size<EventStruct,                           136u>(); // Is not stored
  #else
  check_size<EventStruct,                           120u>(); // Is not stored
  #endif


  // LogStruct is mainly dependent on the number of lines.
  // Has to be round up to multiple of 4.
  #if ESP_IDF_VERSION_MAJOR > 3
  // String class has increased with 4 bytes
  const unsigned int LogStructSize = ((13u + 24 * LOG_STRUCT_MESSAGE_LINES) + 3) & ~3;
  #else
  const unsigned int LogStructSize = ((13u + 20 * LOG_STRUCT_MESSAGE_LINES) + 3) & ~3;
  #endif
  check_size<LogStruct,                             LogStructSize>(); // Is not stored
  check_size<DeviceStruct,                          12u>(); // Is not stored
  check_size<ProtocolStruct,                        8u>();
  #if FEATURE_NOTIFIER
  check_size<NotificationStruct,                    3u>();
  #endif // if FEATURE_NOTIFIER
  #if FEATURE_ESPEASY_P2P
  check_size<NodeStruct,                            66u>();
  #endif
  #if FEATURE_CUSTOM_PROVISIONING
  check_size<ProvisioningStruct,                    256u>();
  #endif
  check_size<systemTimerStruct,                     28u>();
  check_size<RTCStruct,                             32u>();
  check_size<portStatusStruct,                      6u>();
  check_size<ResetFactoryDefaultPreference_struct,  4u>();
  check_size<GpioFactorySettingsStruct,             18u>();
  #ifdef USES_C013
  check_size<C013_SensorInfoStruct,                 233u>();
  check_size<C013_SensorDataStruct,                 40u>(); 
  #endif
  #ifdef USES_C016
  check_size<C016_binary_element,                   24u>();
  #endif


  #if FEATURE_NON_STANDARD_24_TASKS && defined(ESP8266)
    static_assert(TASKS_MAX == 24, "TASKS_MAX invalid size");
  #endif

  // Check for alignment issues at compile time
  {
    const unsigned int ControllerUser_offset = 256u;
    static_assert(ControllerUser_offset == offsetof(SecurityStruct, ControllerUser), "");

    const unsigned int ControllerPassword_offset = 256u + (CONTROLLER_MAX * 26);
    static_assert(ControllerPassword_offset == offsetof(SecurityStruct, ControllerPassword), "");

    const unsigned int Password_offset = ControllerPassword_offset + (CONTROLLER_MAX * 64);
    static_assert(Password_offset == offsetof(SecurityStruct, Password), "");

    const unsigned int AllowedIPrangeLow_offset = Password_offset + 26;
    static_assert(AllowedIPrangeLow_offset == offsetof(SecurityStruct, AllowedIPrangeLow), "");

    const unsigned int IPblockLevel_offset = AllowedIPrangeLow_offset + 8;
    static_assert(IPblockLevel_offset == offsetof(SecurityStruct, IPblockLevel), "");

    const unsigned int ProgmemMd5_offset = IPblockLevel_offset + 1;
    static_assert(ProgmemMd5_offset == offsetof(SecurityStruct, ProgmemMd5), "");

    const unsigned int md5_offset = ProgmemMd5_offset + 16;
    static_assert(md5_offset == offsetof(SecurityStruct, md5), "");

    #if FEATURE_CUSTOM_PROVISIONING
    const unsigned int prov_pass_offset = 62u;
    static_assert(prov_pass_offset == offsetof(ProvisioningStruct, pass), "");


    #endif
  }


  static_assert(192u == offsetof(SettingsStruct, Protocol), "");
  static_assert(195u == offsetof(SettingsStruct, Notification), "CONTROLLER_MAX has changed?");
  static_assert(198u == offsetof(SettingsStruct, TaskDeviceNumber), "NOTIFICATION_MAX has changed?");

  // All settings related to N_TASKS
  static_assert((228 + TASKS_MAX) == offsetof(SettingsStruct, OLD_TaskDeviceID), ""); // 32-bit alignment, so offset of 2 bytes.
  static_assert((200 + (67 * TASKS_MAX)) == offsetof(SettingsStruct, ControllerEnabled), "");

  // Used to compute true offset.
  //const size_t offset = offsetof(SettingsStruct, ControllerEnabled);
  //check_size<SettingsStruct, offset>();


  // ESP8266 toolchain does not support constexpr macros in struct defines
  // to determine nr of bits in a struct.
  static_assert(GPIO_DIRECTION_NR_BITS== NR_BITS(static_cast<uint8_t>(gpio_direction::gpio_direction_MAX)), "Correct GPIO_DIRECTION_NR_BITS");


  #endif
}

#ifndef LIMIT_BUILD_SIZE
String ReportOffsetErrorInStruct(const String& structname, size_t offset) {
  String error;
  if (error.reserve(48 + structname.length())) {
    error  = F("Error: Incorrect offset in struct: ");
    error += structname;
    error += wrap_braces(String(offset));
  }
  return error;
}
#endif

/*********************************************************************************************\
*  Analyze SettingsStruct and report inconsistencies
*  Not a member function to be able to use the F-macro
\*********************************************************************************************/
bool SettingsCheck(String& error) {
  free_string(error);
  #ifndef LIMIT_BUILD_SIZE
#ifdef esp8266
  size_t offset = offsetof(SettingsStruct, ResetFactoryDefaultPreference);

  if (offset != 1224) {
    error = ReportOffsetErrorInStruct(F("SettingsStruct"), offset);
  }
#endif // ifdef esp8266

  if (!Settings.networkSettingsEmpty()) {
    if ((Settings.IP[0] == 0) || (Settings.Gateway[0] == 0) || (Settings.Subnet[0] == 0) || (Settings.DNS[0] == 0)) {
      error += F("Error: Either fill all IP settings fields or leave all empty");
    }
  }

  #endif

  return error.isEmpty();
}

#include "../Helpers/Numerical.h"

String checkTaskSettings(taskIndex_t taskIndex) {
  String err = LoadTaskSettings(taskIndex);
  #if !defined(PLUGIN_BUILD_MINIMAL_OTA) && !defined(ESP8266_1M)
  if (err.length() > 0) return err;
  if (!ExtraTaskSettings.checkUniqueValueNames()) {
    return F("Use unique value names");
  }
  if (!ExtraTaskSettings.checkInvalidCharInNames()) {
    return concat(F("Invalid character in name. Do not use space or '"), ExtraTaskSettingsStruct::getInvalidCharsForNames()) + '\'';
  }
  String deviceName = ExtraTaskSettings.TaskDeviceName;
  NumericalType detectedType;
  if (isNumerical(deviceName, detectedType)) {
    return F("Invalid name. Should not be numeric.");
  }
  if (deviceName.isEmpty()) {
    if (Settings.TaskDeviceEnabled[taskIndex]) {
      // Decide what to do here, for now give a warning when task is enabled.
      return F("Warning: Task Device Name is empty. It is adviced to give tasks an unique name");
    }
  }
  // Do not use the cached function findTaskIndexByName since that one does rely on the fact names should be unique.
  for (taskIndex_t i = 0; i < TASKS_MAX; ++i) {
    if (i != taskIndex && Settings.TaskDeviceEnabled[i]) {
      LoadTaskSettings(i);
      if (ExtraTaskSettings.TaskDeviceName[0] != 0) {
        if (strcasecmp(ExtraTaskSettings.TaskDeviceName, deviceName.c_str()) == 0) {
          err = F("Task Device Name is not unique, conflicts with task ID #");
          err += (i+1);
//          return err;
        }
      }
    }
  }

  err += LoadTaskSettings(taskIndex);
  #endif
  return err;
}
#endif
#include "../Helpers/LongTermTimer.h"

LongTermTimer::LongTermTimer(const LongTermTimer& rhs) {
    _timer_usec = rhs.get();
}

LongTermTimer::LongTermTimer(bool usenow) : _timer_usec(0ull) {
    if (usenow) setNow();
}


  LongTermTimer& LongTermTimer::operator=(const LongTermTimer& rhs)
  {
    _timer_usec = rhs.get();
    return *this;
  }

  void LongTermTimer::setMillisFromNow(uint32_t millisFromNow) {
    _timer_usec = getMicros64() + (millisFromNow * 1000ull);
  }

  void LongTermTimer::setNow() {
    _timer_usec = getMicros64();
  }

  LongTermTimer::Duration LongTermTimer::millisPassedSince() const {
    return usecPassedSince() / 1000ll;
  }

  bool LongTermTimer::timeoutReached(uint32_t millisTimeout) const {
    return getMicros64() > (_timer_usec + (millisTimeout * 1000ull));
  }

#include "../Helpers/ESPEasyRTC.h"

#include "../Globals/RTC.h"
#include "../DataStructs/RTCStruct.h"
#include "../DataStructs/RTCCacheStruct.h"
#include "../DataStructs/RTC_cache_handler_struct.h"
#include "../DataStructs/TimingStats.h"
#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../Globals/Plugins.h"
#include "../Globals/RuntimeData.h"
#include "../Globals/Settings.h"
#include "../Helpers/CRC_functions.h"
#include "../../ESPEasy_common.h"

#ifdef ESP8266
#include <user_interface.h>
#endif

#ifdef ESP32
#include <soc/rtc.h>
#endif

/*********************************************************************************************\
* RTC memory stored values
\*********************************************************************************************/

// ESP8266 RTC layout:
/*
   During deep sleep, only RTC still working, so maybe we need to save some user data in RTC memory.
   Only user data area can be used by user.
 |<--------system data--------->|<-----------------user data--------------->|
 | 256 bytes                    | 512 bytes                                 |
   Note:
   RTC memory is 4 bytes aligned for read and write operations.
   Address parameter refers to block number(4 bytes per block).
   So, if we want to access some data at the beginning of user data area,
   address: 256/4 = 64
   data   : data pointer
   size   : data length, uint8_t

   Prototype:
    bool system_rtc_mem_read (
      uint32 src_addr,
      void * data,
      uint32 save_size
    )

    bool system_rtc_mem_write (
      uint32 des_addr,
      void * data,
      uint32 save_size
    )
 */

// ESP8266 RTC layout ESPeasy:
// these offsets are in blocks, bytes = blocks * 4
// 64   RTCStruct  max 40 bytes: ( 74 - 64 ) * 4
// 74   UserVar
// 122  UserVar checksum:  RTC_BASE_USERVAR + (TASKS_MAX * VARS_PER_TASK)
// 128  Cache (C016) metadata  4 blocks
// 132  Cache (C016) data  6 blocks per sample => max 10 samples



// ESP32 has 2 types of RTC memory:
// RTC SLOW:
//   - 8 kB which can be accessed by both CPU cores and ULP core
//   - Persistent even after reset (not power loss)
//   - Needs RTC_NOINIT_ATTR attribute
// RTC FAST:
//   - 8 kB, only accessed by the "PRO_CPU" 
//   - Persistent during sleep, but not after reset
//   - Needs RTC_DATA_ATTR attribute

// Important to realize:
// Since allocation to RTC memory on ESP32 is done by the compiler, there is no
// guarantee the addresses will be the same after a recompile.
// Thus the data stored in RTC may not survive flashing a new build.

// Structs stored in RTC SLOW:
//   - RTCStruct to keep information on reboot reason, last used WiFi, etc.
//   - UserVar   to keep task values persistent just like on ESP8266





//#define RTC_STRUCT_DEBUG



#ifdef ESP32
constexpr size_t UserVar_nrelements = VARS_PER_TASK * TASKS_MAX;


// Since the global UserVar and RTC objects are defined "extern", they cannot be located in the RTC memory.
// Thus we have to keep a copy here.
RTC_NOINIT_ATTR RTCStruct RTC_tmp;
RTC_NOINIT_ATTR uint32_t UserVar_RTC[UserVar_nrelements];
RTC_NOINIT_ATTR uint32_t UserVar_checksum;
#endif


/********************************************************************************************\
   Save RTC struct to RTC memory
 \*********************************************************************************************/
bool saveToRTC()
{
  START_TIMER
  // ESP8266 has the RTC struct stored in memory which we must actively fetch
  // ESP32 can use a compiler flag to mark a struct to be located in RTC_SLOW memory
  #if defined(ESP32)
  RTC_tmp = RTC;
  #else // if defined(ESP32)

  if (!system_rtc_mem_write(RTC_BASE_STRUCT, reinterpret_cast<const uint8_t *>(&RTC), sizeof(RTC)) || !readFromRTC())
  {
      # ifdef RTC_STRUCT_DEBUG
    addLog(LOG_LEVEL_ERROR, F("RTC  : Error while writing to RTC"));
      # endif // ifdef RTC_STRUCT_DEBUG
    return false;
  }
  #endif // if defined(ESP32)
  STOP_TIMER(SAVE_TO_RTC);
  return true;
}

/********************************************************************************************\
   Initialize RTC memory
 \*********************************************************************************************/
void initRTC()
{
  RTC.init();
  saveToRTC();

  UserVar.clear();
  saveUserVarToRTC();
}

/********************************************************************************************\
   Read RTC struct from RTC memory
 \*********************************************************************************************/
bool readFromRTC()
{
  // ESP8266 has the RTC struct stored in memory which we must actively fetch
  // ESP32 can use a compiler flag to mark a struct to be located in RTC_SLOW memory
  #ifdef ESP32
  RTC = RTC_tmp;
  #endif
  #ifdef ESP8266
  if (!system_rtc_mem_read(RTC_BASE_STRUCT, reinterpret_cast<uint8_t *>(&RTC), sizeof(RTC))) {
    return false;
  }
  #endif
  return RTC.ID1 == 0xAA && RTC.ID2 == 0x55;
}

/********************************************************************************************\
   Save values to RTC memory
 \*********************************************************************************************/
bool saveUserVarToRTC()
{
  // ESP8266 has the RTC struct stored in memory which we must actively fetch
  // ESP32   Uses a temp structure which is mapped to the RTC address range.
  #if defined(ESP32)
  for (taskIndex_t task = 0; task < TASKS_MAX; ++task) {
    const TaskValues_Data_t* taskValues = UserVar.getRawTaskValues_Data(task);
    if (taskValues != nullptr) {
      for (uint8_t varNr = 0; varNr < VARS_PER_TASK; ++varNr) {
        const size_t index = (task * VARS_PER_TASK) + varNr;
        constexpr bool raw = true;
        UserVar_RTC[index] = taskValues->getUint32(varNr);
      }
    }
  }
  UserVar_checksum = UserVar.compute_CRC32();
  return true;
  #endif

  #ifdef ESP8266
  // addLog(LOG_LEVEL_DEBUG, F("RTCMEM: saveUserVarToRTC"));
  size_t   size{};
  uint8_t *buffer    = UserVar.get(size);
  const uint32_t sum = UserVar.compute_CRC32();
  bool  ret    = system_rtc_mem_write(RTC_BASE_USERVAR, buffer, size);
  ret &= system_rtc_mem_write(RTC_BASE_USERVAR + (size >> 2), reinterpret_cast<const uint8_t *>(&sum), 4);
  return ret;
  #endif
}

/********************************************************************************************\
   Read RTC struct from RTC memory
 \*********************************************************************************************/
bool readUserVarFromRTC()
{
  // ESP8266 has the RTC struct stored in memory which we must actively fetch
  // ESP32   Uses a temp structure which is mapped to the RTC address range.
  #if defined(ESP32)
  if (calc_CRC32(reinterpret_cast<const uint8_t *>(&UserVar_RTC[0]), UserVar_nrelements * sizeof(float)) == UserVar_checksum) {
    for (size_t i = 0; i < UserVar_nrelements; ++i) {
      const taskIndex_t taskIndex = i / VARS_PER_TASK;
      const uint8_t varNr = i % VARS_PER_TASK;
      // Store in raw form, so we don't apply formula as we don't really know what type is required.
      TaskValues_Data_t* taskValues = UserVar.getRawTaskValues_Data(taskIndex);
      taskValues->setUint32(varNr, UserVar_RTC[i]);
    }
    return true;
  }
  return false;
  #endif

  #ifdef ESP8266
  // addLog(LOG_LEVEL_DEBUG, F("RTCMEM: readUserVarFromRTC"));
  size_t   size{};
  uint8_t *buffer = UserVar.get(size);
  bool ret        = system_rtc_mem_read(RTC_BASE_USERVAR, buffer, size);
  uint32_t sumRAM = UserVar.compute_CRC32();
  uint32_t sumRTC = 0;
  ret &= system_rtc_mem_read(RTC_BASE_USERVAR + (size >> 2), reinterpret_cast<uint8_t *>(&sumRTC), 4);

  if (!ret || (sumRTC != sumRAM))
  {
      # ifdef RTC_STRUCT_DEBUG
    addLog(LOG_LEVEL_ERROR, F("RTC  : Checksum error on reading RTC user var"));
      # endif // ifdef RTC_STRUCT_DEBUG
    memset(buffer, 0, size);
  }
  return ret;
  #endif 
}


#include "../Helpers/CUL_stats.h"

#ifdef USES_P094

# include "../ESPEasyCore/ESPEasy_Log.h"
# include "../Globals/ESPEasy_time.h"
# include "../Helpers/CRC_functions.h"
# include "../Helpers/ESPEasy_time_calc.h"
# include "../Helpers/StringConverter.h"

# include "../WebServer/Markup.h"
# include "../WebServer/HTML_wrappers.h"

String CUL_Stats::toString(const CUL_Stats_struct& element) const
{
  uint8_t LQI        = 0;
  const int16_t rssi = mBusPacket_t::decode_LQI_RSSI(element._lqi_rssi, LQI);

  // e.g.: THC.02.12345678;1674030412;1674031412;123;101,-36
  static size_t estimated_length = 52;

  String res;

  res.reserve(estimated_length);
  {
    auto it = _mBusStatsSourceMap.find(element._sourceHash);
    if (element._sourceHash != 0u && it != _mBusStatsSourceMap.end()) {
      res += it->second;
    } else {
      res += '-';
    }
  }
  res += ';';

  if (element._id1 != 0u) {
    mBusPacket_header_t deviceID;
    deviceID.decode_fromUint64(element._id1);
    res += deviceID.toString();
  } else {
    res += '-';
  }
  res += ';';

  if (element._id2 != 0u) {
    mBusPacket_header_t deviceID;
    deviceID.decode_fromUint64(element._id2);
    res += deviceID.toString();
  } else {
    res += '-';
  }
  res += ';';

  res += element._UnixTimeFirstSeen;
  res += ';';
  res += element._UnixTimeLastSeen;
  res += ';';
  res += element._count;
  res += ';';
  res += LQI;
  res += ';';
  res += rssi;

  if (res.length() > estimated_length) {
    estimated_length = res.length();
  }
  return res;
}

bool CUL_Stats::add(const mBusPacket_t& packet)
{
  const CUL_stats_hash sourceHash{};
  return add(packet, packet.deviceID_to_map_key_no_length(), sourceHash);
}


bool CUL_Stats::add(const mBusPacket_t& packet, const String& source)
{
  CUL_stats_hash key = packet.deviceID_to_map_key_no_length();
  CUL_stats_hash sourceHash{};

  if (!source.isEmpty()) {
    sourceHash = calc_CRC32((const uint8_t *)(source.c_str()), source.length());
    _mBusStatsSourceMap[sourceHash] = source;
    key ^= sourceHash;
  }

  return add(packet, key, sourceHash);
}

bool CUL_Stats::add(const mBusPacket_t& packet, CUL_stats_hash key, CUL_stats_hash sourceHash)
{
  if (key == 0) { return false; }

  auto it = _mBusStatsMap.find(key);

  if (it == _mBusStatsMap.end()) {
    CUL_Stats_struct tmp;
    tmp._count              = 1;
    tmp._id1                = packet._deviceId1.encode_toUInt64();
    tmp._id2                = packet._deviceId2.encode_toUInt64();
    tmp._lqi_rssi           = packet._lqi_rssi;
    tmp._UnixTimeFirstSeen  = node_time.getLocalUnixTime();
    tmp._UnixTimeLastSeen   = tmp._UnixTimeFirstSeen;
    tmp._sourceHash         = sourceHash;
    _mBusStatsMap[key]      = tmp;
    return true;
  }
  it->second._count++;
  it->second._lqi_rssi         = packet._lqi_rssi;
  it->second._UnixTimeLastSeen = node_time.getLocalUnixTime();
  return false;
}

String CUL_Stats::getFront()
{
  auto it = _mBusStatsMap.begin();

  if (it == _mBusStatsMap.end()) { return EMPTY_STRING; }
  const String res = toString(it->second);

  _mBusStatsMap.erase(it);
  return res;
}

void CUL_Stats::toHtml() const
{
  addRowLabel(F("CUL stats"));

  for (auto it = _mBusStatsMap.begin(); it != _mBusStatsMap.end(); ++it) {
    addHtml(toString(it->second));
    addHtml(F("<BR>"));
  }
}

#endif // ifdef USES_P094

#include "../Helpers/Dallas1WireHelper.h"

#if FEATURE_DALLAS_HELPER

# include "../../_Plugin_Helper.h"
# include "../ESPEasyCore/ESPEasy_Log.h"
# include "../Helpers/ESPEasy_Storage.h"
# include "../Helpers/Misc.h"

# include "../WebServer/JSON.h"


// For timings, see table 1:
// https://www.analog.com/en/resources/technical-articles/1wire-communication-with-a-microchip-picmicro-microcontroller.html

// DEBUG code using logic analyzer for timings
// #define DEBUG_LOGIC_ANALYZER_PIN  27
// #define DEBUG_LOGIC_ANALYZER_PIN_ERROR  26


// Macros to perform direct access on GPIOs
// Macros written by Paul Stoffregen
// See: https://github.com/PaulStoffregen/OneWire/blob/master/util/
# include <GPIO_Direct_Access.h>


// ESP8266 does work fine without the IRAM attribute
// But ESP32 may benefit from having the code always loaded in RAM.
# ifdef ESP8266
#  define DALLAS_IRAM_ATTR
# endif // ifdef ESP8266
# ifdef ESP32
#  define DALLAS_IRAM_ATTR IRAM_ATTR
# endif // ifdef ESP32


# include <vector>

unsigned char ROM_NO[8]{ 0 };
uint8_t LastDiscrepancy{};
uint8_t LastFamilyDiscrepancy{};
uint8_t LastDeviceFlag{};

int32_t usec_release{};
int32_t presence_start{};
int32_t presence_end{};


void DALLAS_IRAM_ATTR Dallas_pinModeInput(uint32_t gpio_pin_rx, uint32_t gpio_pin_tx)
{
  if (gpio_pin_rx == gpio_pin_tx) {
    // let pin float, pull up will raise level
# ifdef ESP8266

    // We're running out of IRAM on ESP8266
    DIRECT_PINMODE_INPUT(gpio_pin_rx);
# else // ifdef ESP8266
    DIRECT_PINMODE_INPUT_ISR(gpio_pin_rx);
# endif // ifdef ESP8266
  } else {
    DIRECT_pinWrite_ISR(gpio_pin_tx, 1);
  }
}

void DALLAS_IRAM_ATTR Dallas_pinWrite(uint32_t gpio_pin_rx, uint32_t gpio_pin_tx, bool pinstate)
{
  DIRECT_pinWrite_ISR(gpio_pin_tx, pinstate);

  if (gpio_pin_rx == gpio_pin_tx) {
# ifdef ESP8266

    // We're running out of IRAM on ESP8266
    DIRECT_PINMODE_OUTPUT(gpio_pin_rx);
# else // ifdef ESP8266
    DIRECT_PINMODE_OUTPUT_ISR(gpio_pin_rx);
# endif // ifdef ESP8266
  }
}

int32_t DALLAS_IRAM_ATTR Dallas_measureWaitForPinState(uint32_t gpio_pin_rx, uint32_t start_usec, int32_t timeout_usec, bool newState)
{
  int32_t passed{};

  do {
    passed = usecPassedSince_fast(start_usec);
  } while (passed < timeout_usec &&
           (!DIRECT_pinRead_ISR(gpio_pin_rx) == newState)); // Using '!' to do a quick cast to bool

  if ((passed > timeout_usec) ||
      (!DIRECT_pinRead_ISR(gpio_pin_rx) == newState)) {
    return -1;
  }

  // N.B. we allow the situation where passed == timeout_usec
  // and pin might have reached the state we're waiting for.
  // This way we don't need to average for time before reading pin state and after.
  return passed;
}

inline bool Dallas_waitForPinState(int8_t gpio_pin_rx, uint32_t start_usec, int32_t timeout_usec, uint32_t newState)
{
  return Dallas_measureWaitForPinState(gpio_pin_rx, start_usec, timeout_usec, newState) >= 0;
}

# define Dallas_pinLow   Dallas_pinWrite(gpio_pin_rx, gpio_pin_tx, false)
# define Dallas_pinHigh  Dallas_pinWrite(gpio_pin_rx, gpio_pin_tx, true)
# define Dallas_pinInput Dallas_pinModeInput(gpio_pin_rx, gpio_pin_tx)

// # define Dallas_waitForPinLow(P, S, T) Dallas_waitForPinState(P, S, T, 0)
# define Dallas_waitForPinHigh(P, S, T) Dallas_waitForPinState(P, S, T, 1)
# define Dallas_measureWaitForPinLow(P, S, T) Dallas_measureWaitForPinState(P, S, T, 0)
# define Dallas_measureWaitForPinHigh(P, S, T) Dallas_measureWaitForPinState(P, S, T, 1)

int Dallas_measure_rise_time(int8_t gpio_pin_rx, int8_t gpio_pin_tx)
{
  int res   = 15; // Duration of minimal pulse length from sensor
  int retry = 5;

  while (--retry && res > 1) {
    // No need to check for rise time less than 1 usec
    ISR_noInterrupts();
    Dallas_pinLow;
    delayMicroseconds(1);

    const uint32_t pre_start = micros();
    Dallas_pinInput;
    const uint32_t start = micros();
    int32_t duration     = Dallas_measureWaitForPinHigh(gpio_pin_rx, start, res + 1);
    Dallas_pinHigh;
    ISR_interrupts();

    if (duration >= 0) {
      if (start != pre_start) {
        // Assume pin is set to input halfway between pre_start and start
        const int32_t diff = (pre_start - start) / 2;

        if (diff < duration) {
          duration += diff;
        }
      }

      if (duration < res) {
        res = duration;
      }
    }
    delayMicroseconds(10);
  }
  return res;
}

// References to 1-wire family codes:
// http://owfs.sourceforge.net/simple_family.html
// https://github.com/owfs/owfs-doc/wiki/1Wire-Device-List
const __FlashStringHelper* Dallas_getModel(uint8_t family, const bool hasFixedResolution) {
  switch (family) {
    case 0x28: return F("DS18B20");
    case 0x3b: return hasFixedResolution ? F("MAX31826") : F("DS1825");
    case 0x22: return F("DS1822");
    case 0x10: return F("DS1820 / DS18S20");
    case 0x42: return F("DS28EA00");
    case 0x1D: return F("DS2423");  // 4k RAM with counter
    case 0x01: return F("DS1990A"); // Serial Number iButton
  }
  return F("Unknown");
}

String Dallas_format_address(const uint8_t addr[], const bool hasFixedResolution) {
  String result;

  result.reserve(40);

  for (uint8_t j = 0; j < 8; j++)
  {
    appendHexChar(addr[j], result);

    if (j < 7) { result += '-'; }
  }
  result += F(" [");
  result += Dallas_getModel(addr[0], hasFixedResolution);
  result += ']';

  return result;
}

uint64_t Dallas_addr_to_uint64(const uint8_t addr[]) {
  uint64_t tmpAddr_64 = 0;

  for (uint8_t i = 0; i < 8; ++i) {
    tmpAddr_64 <<= 8;
    tmpAddr_64  += addr[i];
  }
  return tmpAddr_64;
}

void Dallas_uint64_to_addr(uint64_t value, uint8_t addr[]) {
  uint8_t i = 8;

  while (i > 0) {
    --i;
    addr[i] = static_cast<uint8_t>(value & 0xFF);
    value >>= 8;
  }
}

void Dallas_addr_selector_webform_load(taskIndex_t TaskIndex, int8_t gpio_pin_rx, int8_t gpio_pin_tx, uint8_t nrVariables) {
  if ((gpio_pin_rx == -1) ||
      (gpio_pin_tx == -1) ||
      !validTaskIndex(TaskIndex)) {
    return;
  }

  if (nrVariables >= VARS_PER_TASK) {
    nrVariables = VARS_PER_TASK;
  }

  std::map<uint64_t, String> addr_task_map;

  for (taskIndex_t task = 0; validTaskIndex(task); ++task) {
    if (Dallas_plugin(Settings.getPluginID_for_task(task))) {
      uint8_t tmpAddress[8] = { 0 };

      const uint8_t valueCount = getValueCountForTask(task);

      for (uint8_t var_index = 0; var_index < valueCount; ++var_index) {
        Dallas_plugin_get_addr(tmpAddress, task, var_index);
        uint64_t tmpAddr_64 = Dallas_addr_to_uint64(tmpAddress);

        if (tmpAddr_64 != 0) {
          addr_task_map.emplace(
            std::make_pair(
              tmpAddr_64,
              strformat(
                F(" (task %d [%s#%s])")
                , task + 1
                , getTaskDeviceName(task).c_str()
                , Cache.getTaskDeviceValueName(task, var_index).c_str())
              ));
        }
      }
    }
  }

  // find all suitable devices
  std::vector<uint64_t> scan_res;
  std::vector<bool> fixed_res;

  Dallas_reset_search();
  uint8_t tmpAddress[8]{};

  while (Dallas_search(tmpAddress, gpio_pin_rx, gpio_pin_tx))
  {
    scan_res.push_back(Dallas_addr_to_uint64(tmpAddress));
    bool hasFixedResolution = false;
    Dallas_getResolution(tmpAddress, gpio_pin_rx, gpio_pin_tx, hasFixedResolution);
    fixed_res.push_back(hasFixedResolution);
  }

  for (uint8_t var_index = 0; var_index < nrVariables; ++var_index) {
    String rowLabel = F("Device Address");

    if (nrVariables > 1) {
      rowLabel += ' ';
      rowLabel += (var_index + 1);
    }
    addRowLabel(rowLabel);
    addSelector_Head(concat(F("dallas_addr"), static_cast<int>(var_index)));
    addSelector_Item(F("- None -"), -1, false); // Empty choice

    // get currently saved address
    uint8_t savedAddress[8];
    Dallas_plugin_get_addr(savedAddress, TaskIndex, var_index); // Need to fetch only once?

    for (uint8_t index = 0; index < scan_res.size(); ++index) {
      uint8_t tmpAddress[8]{};
      Dallas_uint64_to_addr(scan_res[index], tmpAddress);

      String option;
# ifndef LIMIT_BUILD_SIZE
      bool parasitePowered = false;
      Dallas_is_parasite(tmpAddress, gpio_pin_rx, gpio_pin_tx, parasitePowered);

      if (parasitePowered) {
        option += F("[P] ");
      }
# endif // ifndef LIMIT_BUILD_SIZE
      option += Dallas_format_address(tmpAddress, fixed_res[index]);
      auto it = addr_task_map.find(scan_res[index]);

      if (it != addr_task_map.end()) {
        option += it->second;
      }

      const bool selected = (memcmp(tmpAddress, savedAddress, 8) == 0);
      addSelector_Item(option, index, selected);
    }
    addSelector_Foot();
  }
}

# ifndef LIMIT_BUILD_SIZE
void Dallas_show_sensor_stats_webform_load(const Dallas_SensorData& sensor_data)
{
  if (sensor_data.addr == 0) {
    return;
  }
  addRowLabel(F("Address"));
  addHtml(sensor_data.get_formatted_address());

  addRowLabel(F("Resolution"));
  addHtmlInt(sensor_data.actual_res);

  if (sensor_data.fixed_resolution) {
    addHtml(F(" (fixed)"));
  }

  addRowLabel(F("Parasite Powered"));
  addHtml(jsonBool(sensor_data.parasitePowered));

  if (sensor_data.parasitePowered) {
    addHtml(F("&nbsp;"));
    addEnabled(false);
    addHtml(F("&nbsp;Unsupported!"));
  }

  addRowLabel(F("Read Success"));
  addHtmlInt(sensor_data.read_success);

  addRowLabel(F("Sensor No Reply"));
  addHtmlInt(sensor_data.start_read_failed);

  addRowLabel(F("Sensor Power Lost"));
  addHtmlInt(sensor_data.sensor_power_on_reset);

  addRowLabel(F("Read CRC error"));
  addHtmlInt(sensor_data.read_CRC);

  addRowLabel(F("Read Retry"));
  addHtmlInt(sensor_data.read_retry);

  addRowLabel(F("Read Failed"));
  addHtmlInt(sensor_data.read_failed);
}

# endif // ifndef LIMIT_BUILD_SIZE

void Dallas_addr_selector_webform_save(taskIndex_t TaskIndex, int8_t gpio_pin_rx, int8_t gpio_pin_tx, uint8_t nrVariables)
{
  if ((gpio_pin_rx == -1) ||
      (gpio_pin_tx == -1) ||
      !validTaskIndex(TaskIndex)) {
    return;
  }

  if (nrVariables >= VARS_PER_TASK) {
    nrVariables = VARS_PER_TASK;
  }

  uint8_t addr[8]{};

  for (uint8_t var_index = 0; var_index < nrVariables; ++var_index) {
    const int selection = getFormItemInt(concat(F("dallas_addr"), static_cast<int>(var_index)), -1);

    if (selection != -1) {
      Dallas_scan(selection, addr, gpio_pin_rx, gpio_pin_tx);
      Dallas_plugin_set_addr(addr, TaskIndex, var_index);
    }
  }
}

bool Dallas_plugin(pluginID_t pluginID)
{
  constexpr pluginID_t PLUGIN_ID_P004_DALLAS_TEMP(4);
  constexpr pluginID_t PLUGIN_ID_P080_DALLAS_IBUTTON(80);
  constexpr pluginID_t PLUGIN_ID_P100_DS2423_COUNTER(100);

  return (pluginID == PLUGIN_ID_P004_DALLAS_TEMP) ||
         (pluginID == PLUGIN_ID_P080_DALLAS_IBUTTON) ||
         (pluginID == PLUGIN_ID_P100_DS2423_COUNTER);
}

void Dallas_plugin_get_addr(uint8_t addr[], taskIndex_t TaskIndex, uint8_t var_index)
{
  if (var_index >= 4) {
    return;
  }

  for (uint8_t x = 0; x < 8; x++) {
    uint32_t value = (uint32_t)Cache.getTaskDevicePluginConfigLong(TaskIndex, x);
    addr[x] = static_cast<uint8_t>((value >> (var_index * 8)) & 0xFF);
  }
}

void Dallas_plugin_set_addr(uint8_t addr[], taskIndex_t TaskIndex, uint8_t var_index)
{
  if (var_index >= 4) {
    return;
  }
  LoadTaskSettings(TaskIndex);
  const uint32_t mask = ~(0xFF << (var_index * 8));

  for (uint8_t x = 0; x < 8; x++) {
    uint32_t value = (uint32_t)ExtraTaskSettings.TaskDevicePluginConfigLong[x];
    value                                          &= mask;
    value                                          += (static_cast<uint32_t>(addr[x]) << (var_index * 8));
    ExtraTaskSettings.TaskDevicePluginConfigLong[x] = (long)value;
  }
  Cache.updateExtraTaskSettingsCache();
}

/*********************************************************************************************\
   Dallas Scan bus
\*********************************************************************************************/
uint8_t Dallas_scan(uint8_t getDeviceROM, uint8_t *ROM, int8_t gpio_pin_rx, int8_t gpio_pin_tx)
{
  uint8_t tmpaddr[8];
  uint8_t devCount = 0;

  Dallas_reset_search();

  while (Dallas_search(tmpaddr, gpio_pin_rx, gpio_pin_tx))
  {
    if (getDeviceROM == devCount) {
      for (uint8_t  i = 0; i < 8; i++) {
        ROM[i] = tmpaddr[i];
      }
    }
    devCount++;
  }
  return devCount;
}

// read power supply
# ifndef LIMIT_BUILD_SIZE
bool Dallas_is_parasite(const uint8_t ROM[8], int8_t gpio_pin_rx, int8_t gpio_pin_tx, bool& isParasitePowered)
{
  if (!Dallas_address_ROM(ROM, gpio_pin_rx, gpio_pin_tx)) {
    return false;
  }
  Dallas_write(0xB4, gpio_pin_rx, gpio_pin_tx); // read power supply
  isParasitePowered = !Dallas_read_bit(gpio_pin_rx, gpio_pin_tx);
  return isParasitePowered;
}

# endif // ifndef LIMIT_BUILD_SIZE

/*
   void Dallas_startConversion(const uint8_t ROM[8], int8_t gpio_pin_rx, int8_t gpio_pin_tx)
   {
   Dallas_reset(gpio_pin_rx, gpio_pin_tx);
   Dallas_write(0x55, gpio_pin_rx, gpio_pin_tx); // Choose ROM

   for (uint8_t i = 0; i < 8; i++) {
    Dallas_write(ROM[i], gpio_pin_rx, gpio_pin_tx);
   }
   Dallas_write(0x44, gpio_pin_rx, gpio_pin_tx);
   }
 */
/*********************************************************************************************\
*  Dallas Read temperature from scratchpad
\*********************************************************************************************/
Dallas_read_result Dallas_readTemp(const uint8_t ROM[8], float *value, int8_t gpio_pin_rx, int8_t gpio_pin_tx)
{
  int16_t DSTemp;
  uint8_t ScratchPad[12]{};

  if (!Dallas_address_ROM(ROM, gpio_pin_rx, gpio_pin_tx)) {
    return Dallas_read_result::NoReply;
  }
  Dallas_write(0xBE, gpio_pin_rx, gpio_pin_tx); // Read scratchpad

  for (uint8_t i = 0; i < 9; i++) {             // read 9 bytes
    ScratchPad[i] = Dallas_read(gpio_pin_rx, gpio_pin_tx);
  }

  bool crc_ok = Dallas_crc8(ScratchPad);

  # ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    String log = F("DS: SP: ");

    for (uint8_t x = 0; x < 9; x++)
    {
      if (x != 0) {
        log += ',';
      }
      log += formatToHex(ScratchPad[x], 2);
    }

    if (crc_ok) {
      log += F(",OK");
    } else {
      log += F(",ERR");
    }

  #  ifndef LIMIT_BUILD_SIZE
    bool isParasitePowered = false;

    //    Dallas_is_parasite(ROM, gpio_pin_rx, gpio_pin_tx, isParasitePowered);

    if (isParasitePowered) {
      log += F(",P");
    }
  #  endif // ifndef LIMIT_BUILD_SIZE
    log += ',';
    log += ll2String(usec_release, DEC);
    log += ',';
    log += ll2String(presence_start, DEC);
    log += ',';
    log += ll2String(presence_end, DEC);
    addLogMove(LOG_LEVEL_DEBUG, log);
  }
  # endif // ifndef BUILD_NO_DEBUG

  if (!crc_ok)
  {
# ifdef DEBUG_LOGIC_ANALYZER_PIN_ERROR

    // Toggle the CRC error pin to make it better visible in the logic analyzer trace
    static bool error_pin_toggle = false;
    error_pin_toggle = !error_pin_toggle;
    DIRECT_pinWrite(DEBUG_LOGIC_ANALYZER_PIN_ERROR, error_pin_toggle ? 1 : 0);
# endif // ifdef DEBUG_LOGIC_ANALYZER_PIN_ERROR


    *value = 0;
    return Dallas_read_result::CRCerr;
  }

  if ((ROM[0] == 0x28)     // DS18B20
      || (ROM[0] == 0x3b)  // DS1825
      || (ROM[0] == 0x22)  // DS1822
      || (ROM[0] == 0x42)) // DS28EA00
  {
    DSTemp = (ScratchPad[1] << 8) + ScratchPad[0];

    if (DSTemp == 0x550) { // power-on reset value
      *value = 0;
      return Dallas_read_result::PowerOnResetValue;
    }
    *value = (float(DSTemp) * 0.0625f);
  }
  else if (ROM[0] == 0x10)       // DS1820 DS18S20
  {
    if (ScratchPad[0] == 0xaa) { // power-on reset value
      return Dallas_read_result::PowerOnResetValue;
    }
    DSTemp = (ScratchPad[1] << 11) | ScratchPad[0] << 3;
    DSTemp = ((DSTemp & 0xfff0) << 3) - 16 +
             (((ScratchPad[7] - ScratchPad[6]) << 7) / ScratchPad[7]);
    *value = float(DSTemp) * 0.0078125f;
  }
  return Dallas_read_result::OK;
}

# ifdef USES_P080
bool Dallas_readiButton(const uint8_t addr[8], int8_t gpio_pin_rx, int8_t gpio_pin_tx, int8_t lastState)
{
  // maybe this is needed to trigger the reading
  //    uint8_t ScratchPad[12];

  Dallas_reset(gpio_pin_rx, gpio_pin_tx);
  Dallas_write(0x55, gpio_pin_rx, gpio_pin_tx); // Choose ROM

  for (uint8_t i = 0; i < 8; i++) {
    Dallas_write(addr[i], gpio_pin_rx, gpio_pin_tx);
  }

  Dallas_write(0xBE, gpio_pin_rx, gpio_pin_tx); // Read scratchpad

  //    for (uint8_t i = 0; i < 9; i++) // read 9 bytes
  //        ScratchPad[i] = Dallas_read();
  // end maybe this is needed to trigger the reading

  uint8_t tmpaddr[8];
  bool    found = false;

  String log;

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    log  = F("DS   : iButton searching for address: ");
    log += Dallas_format_address(addr);
    log += F(" found: ");
  }
  Dallas_reset_search();

  while (Dallas_search(tmpaddr, gpio_pin_rx, gpio_pin_tx))
  {
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      log += Dallas_format_address(tmpaddr);
      log += ',';
    }

    if (memcmp(addr, tmpaddr, 8) == 0)
    {
      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        log += F("Success. Button was found");
      }
      found = true;
    }
  }

  if ((-1 == lastState) || (lastState != found)) {
    addLogMove(LOG_LEVEL_INFO, log);
  }
  return found;
}

# endif // ifdef USES_P080

/*********************************************************************************************\
   Dallas read DS2423 counter
   Taken from https://github.com/jbechter/arduino-onewire-DS2423
\*********************************************************************************************/
# ifdef USES_P100
#  define DS2423_READ_MEMORY_COMMAND 0xa5
#  define DS2423_PAGE_ONE 0xc0
#  define DS2423_PAGE_TWO 0xe0

bool Dallas_readCounter(const uint8_t ROM[8], float *value, int8_t gpio_pin_rx, int8_t gpio_pin_tx, uint8_t counter)
{
  uint8_t data[45];

  data[0] = DS2423_READ_MEMORY_COMMAND;
  data[1] = (counter == 0 ? DS2423_PAGE_ONE : DS2423_PAGE_TWO);
  data[2] = 0x01;

  if (!Dallas_address_ROM(ROM, gpio_pin_rx, gpio_pin_tx)) {
    return false;
  }

  Dallas_write(data[0], gpio_pin_rx, gpio_pin_tx);
  Dallas_write(data[1], gpio_pin_rx, gpio_pin_tx);
  Dallas_write(data[2], gpio_pin_rx, gpio_pin_tx);

  for (int j = 3; j < 45; j++) {
    data[j] = Dallas_read(gpio_pin_rx, gpio_pin_tx);
  }

  Dallas_reset(gpio_pin_rx, gpio_pin_tx);

  uint32_t count = (uint32_t)data[38];

  for (int j = 37; j >= 35; j--) {
    count = (count << 8) + (uint32_t)data[j];
  }

  uint16_t crc            = Dallas_crc16(data, 43, 0);
  const uint8_t *crcBytes = reinterpret_cast<const uint8_t *>(&crc);
  uint8_t crcLo           = ~data[43];
  uint8_t crcHi           = ~data[44];
  bool    error           = (crcLo != crcBytes[0]) || (crcHi != crcBytes[1]);

  if (!error)
  {
    *value = count;
    return true;
  }
  else
  {
    *value = 0;
    return false;
  }
}

# endif // ifdef USES_P100

/*********************************************************************************************\
* Dallas Check for MAX31826 fixed 12 bit resolution, see datasheet page 9 'Memory'
\*********************************************************************************************/
bool Dallas_check_hasFixedResolution(const uint8_t ROM[8], const uint8_t ScratchPad[12]) {
  return (0x3B == ROM[0]) &&        // MAX31826: Family code 0x3B
         (0xFF == ScratchPad[2]) && // All 1s
         (0xFF == ScratchPad[3]) &&
         (0xFF == ScratchPad[5]) &&
         (0xFF == ScratchPad[6]) &&
         (0xFF == ScratchPad[7]) &&
         (0xF0 == (ScratchPad[4] & 0xF0)); // Ignore lower 4 bits used for 'Location'
}

/*********************************************************************************************\
* Dallas Get Resolution
\*********************************************************************************************/
uint8_t Dallas_getResolution(const uint8_t ROM[8], int8_t gpio_pin_rx, int8_t gpio_pin_tx) {
  bool hasFixedResolution; // Ignored

  return Dallas_getResolution(ROM, gpio_pin_rx, gpio_pin_tx, hasFixedResolution);
}

uint8_t Dallas_getResolution(const uint8_t ROM[8], int8_t gpio_pin_rx, int8_t gpio_pin_tx, bool& hasFixedResolution)
{
  // DS1820 and DS18S20 have no resolution configuration register
  if (ROM[0] == 0x10) { return 12; }

  uint8_t ScratchPad[12];

  if (!Dallas_address_ROM(ROM, gpio_pin_rx, gpio_pin_tx)) {
    return 0;
  }
  Dallas_write(0xBE, gpio_pin_rx, gpio_pin_tx); // Read scratchpad

  for (uint8_t i = 0; i < 9; i++) {             // read 9 bytes
    ScratchPad[i] = Dallas_read(gpio_pin_rx, gpio_pin_tx);
  }

  if (Dallas_crc8(ScratchPad)) {
    if (Dallas_check_hasFixedResolution(ROM, ScratchPad)) {
      hasFixedResolution = true;
      return 12;
    }

    switch (ScratchPad[4])
    {
      case 0x7F: // 12 bit
        return 12;

      case 0x5F: // 11 bit
        return 11;

      case 0x3F: // 10 bit
        return 10;

      case 0x1F: //  9 bit
      default:
        return 9;
    }
  }
  return 0;
}

/*********************************************************************************************\
* Dallas Set Resolution
\*********************************************************************************************/
bool Dallas_setResolution(const uint8_t ROM[8], uint8_t res, int8_t gpio_pin_rx, int8_t gpio_pin_tx)
{
  // DS1820 and DS18S20 have no resolution configuration register
  if (ROM[0] == 0x10) { return true; }

  uint8_t ScratchPad[12];

  if (!Dallas_address_ROM(ROM, gpio_pin_rx, gpio_pin_tx)) {
    return false;
  }
  Dallas_write(0xBE, gpio_pin_rx, gpio_pin_tx); // Read scratchpad

  for (uint8_t i = 0; i < 9; i++) {             // read 9 bytes
    ScratchPad[i] = Dallas_read(gpio_pin_rx, gpio_pin_tx);
  }

  if (!Dallas_crc8(ScratchPad)) {
    addLog(LOG_LEVEL_ERROR, F("DS   : Cannot set resolution"));
    return false;
  }
  else
  {
    if (Dallas_check_hasFixedResolution(ROM, ScratchPad)) {
      return true; // Can't change a fixed resolution
    }

    uint8_t old_configuration = ScratchPad[4];

    switch (res)
    {
      case 12:
        ScratchPad[4] = 0x7F; // 12 bits
        break;
      case 11:
        ScratchPad[4] = 0x5F; // 11 bits
        break;
      case 10:
        ScratchPad[4] = 0x3F; // 10 bits
        break;
      case 9:
      default:
        ScratchPad[4] = 0x1F; //  9 bits
        break;
    }

    if (ScratchPad[4] == old_configuration) {
      return true;
    }

    if (!Dallas_address_ROM(ROM, gpio_pin_rx, gpio_pin_tx)) { return false; }
    Dallas_write(0x4E,          gpio_pin_rx, gpio_pin_tx); // Write to EEPROM
    Dallas_write(ScratchPad[2], gpio_pin_rx, gpio_pin_tx); // high alarm temp
    Dallas_write(ScratchPad[3], gpio_pin_rx, gpio_pin_tx); // low alarm temp
    Dallas_write(ScratchPad[4], gpio_pin_rx, gpio_pin_tx); // configuration register

    if (!Dallas_address_ROM(ROM, gpio_pin_rx, gpio_pin_tx)) { return false; }

    // save the newly written values to eeprom
    Dallas_write(0x48, gpio_pin_rx, gpio_pin_tx);
    delay(100);  // <--- added 20ms delay to allow 10ms long EEPROM write operation (as specified by datasheet)
    Dallas_reset(gpio_pin_rx, gpio_pin_tx);

    return true; // new value set
  }
}

/*********************************************************************************************\
*  Dallas Reset
\*********************************************************************************************/
uint8_t Dallas_reset(int8_t gpio_pin_rx, int8_t gpio_pin_tx)
{
  presence_start = 0;
  presence_end   = 0;

  // Keep track of usec_release as it is an indicator for the recovery time
  usec_release = Dallas_measure_rise_time(gpio_pin_rx, gpio_pin_tx);
  delayMicroseconds(10);

  ISR_noInterrupts();

# ifdef DEBUG_LOGIC_ANALYZER_PIN

  // DEBUG code using logic analyzer for timings
  DIRECT_pinWrite(DEBUG_LOGIC_ANALYZER_PIN, 1);
# endif // ifdef DEBUG_LOGIC_ANALYZER_PIN

  Dallas_pinInput;

# ifdef DEBUG_LOGIC_ANALYZER_PIN

  // DEBUG code using logic analyzer for timings
  DIRECT_pinWrite(DEBUG_LOGIC_ANALYZER_PIN, 0);
# endif // ifdef DEBUG_LOGIC_ANALYZER_PIN

  // wait until the wire is high... just in case
  if (Dallas_waitForPinHigh(gpio_pin_rx, micros(), 250)) {
# ifdef DEBUG_LOGIC_ANALYZER_PIN

    // DEBUG code using logic analyzer for timings
    DIRECT_pinWrite(DEBUG_LOGIC_ANALYZER_PIN, 1);
# endif // ifdef DEBUG_LOGIC_ANALYZER_PIN

    // The master starts a transmission with a reset pulse,
    // which pulls the wire to 0 volts for at least 480 µs.
    // This resets communication of every slave device on the bus.
    Dallas_pinLow;

    delayMicroseconds(480); // t_RSTL 480 ... 960 usec

    // puling pin high will be very fast, so start measurement before pulling high
    const uint32_t start = micros();
    Dallas_pinHigh;

    //    digitalWrite(gpio_pin_tx, 1);
    delayMicroseconds(1);


    // Set to 'input', state will be pulled high by pull-up resistor
    // Or will be kept pulled low by sensor
    Dallas_pinInput;

# ifdef DEBUG_LOGIC_ANALYZER_PIN

    // DEBUG code using logic analyzer for timings
    DIRECT_pinWrite(DEBUG_LOGIC_ANALYZER_PIN, 0);
# endif // ifdef DEBUG_LOGIC_ANALYZER_PIN


    // After that, any slave device, if present, shows that it exists with a "presence" pulse:
    // it holds the bus low for at least 60 µs after the master releases the bus.
    // This may take about 25 usec after release for present sensors to pull the line low.
    // Sequence:
    // - Release => pin high (typ: 1 usec as it was actively pulled high by GPIO)
    // - Presence condition start (typ: 25 usec after release)
    // - Presence condition end   (minimal duration 60 usec, typ: 110 usec)
    // - Wait till 480 usec after release.

    // First check will only be to make sure the pin isn't pulled down for whatever reason
    // Since we set the pin to high, this should return immediately
    if (Dallas_waitForPinHigh(gpio_pin_rx, start, 15)) {
      // Signal fall time will be quite fast, so no correction needed
      presence_start = Dallas_measureWaitForPinLow(gpio_pin_rx, start, 60);

      if (presence_start > 15) {
        // t_PDH 15 ... 60 usec
        // t_PDL 60 ... 240 usec

        // Signal will rise only due to pull-up resistor
        // Meaning measured duration may be off by usec_release (too long)
        presence_end = Dallas_measureWaitForPinHigh(gpio_pin_rx, start, 60 + 240);

        // Set the pin high, just in case we have a (single) parasitic powered sensor
        Dallas_pinHigh;

        // Enable interrupts again as soon as timing-critical section is done
        ISR_interrupts();

        const int32_t presence_duration = presence_end - presence_start;

        if ((presence_duration >= 60) && (presence_duration < (240 /* + usec_release*/))) {
          // t_RSTH = 480 usec
          const int32_t timeLeft = 480 - usecPassedSince_fast(start);

          if (timeLeft > 0) {
            delayMicroseconds(timeLeft);
          }

          return 1;
        }
        return 0;
      }
    }
  }

  // Set the pin high, just in case we have a (single) parasitic powered sensor
  Dallas_pinHigh;

  ISR_interrupts();
  return 0;
}

# define FALSE 0
# define TRUE  1

/*********************************************************************************************\
*  Dallas Reset Search
\*********************************************************************************************/
void Dallas_reset_search()
{
  // reset the search state
  LastDiscrepancy       = 0;
  LastDeviceFlag        = FALSE;
  LastFamilyDiscrepancy = 0;

  for (uint8_t i = 0; i < 8; i++) {
    ROM_NO[i] = 0;
  }
}

/*********************************************************************************************\
*  Dallas Search bus
\*********************************************************************************************/
uint8_t Dallas_search(uint8_t *newAddr, int8_t gpio_pin_rx, int8_t gpio_pin_tx)
{
  uint8_t id_bit_number;
  uint8_t last_zero, rom_byte_number, search_result;
  unsigned char rom_byte_mask, search_direction;

  // initialize for search
  id_bit_number   = 1;
  last_zero       = 0;
  rom_byte_number = 0;
  rom_byte_mask   = 1;
  search_result   = 0;

  // if the last call was not the last one
  if (!LastDeviceFlag)
  {
    // 1-Wire reset
    if (!Dallas_reset(gpio_pin_rx, gpio_pin_tx))
    {
      // reset the search
      LastDiscrepancy       = 0;
      LastDeviceFlag        = FALSE;
      LastFamilyDiscrepancy = 0;
      return FALSE;
    }

    // issue the search command
    Dallas_write(0xF0, gpio_pin_rx, gpio_pin_tx);

    // loop to do the search
    do
    {
      // read a bit and its complement
      const uint8_t id_bit     = Dallas_read_bit(gpio_pin_rx, gpio_pin_tx);
      const uint8_t cmp_id_bit = Dallas_read_bit(gpio_pin_rx, gpio_pin_tx);

      // check for no devices on 1-wire
      if ((id_bit == 1) && (cmp_id_bit == 1)) {
        break;
      }
      else
      {
        // all devices coupled have 0 or 1
        if (id_bit != cmp_id_bit) {
          search_direction = id_bit; // bit write value for search
        }
        else
        {
          // if this discrepancy if before the Last Discrepancy
          // on a previous next then pick the same as last time
          if (id_bit_number < LastDiscrepancy) {
            search_direction = ((ROM_NO[rom_byte_number] & rom_byte_mask) > 0);
          }
          else {
            // if equal to last pick 1, if not then pick 0
            search_direction = (id_bit_number == LastDiscrepancy);
          }

          // if 0 was picked then record its position in LastZero
          if (search_direction == 0)
          {
            last_zero = id_bit_number;

            // check for Last discrepancy in family
            if (last_zero < 9) {
              LastFamilyDiscrepancy = last_zero;
            }
          }
        }

        // set or clear the bit in the ROM byte rom_byte_number
        // with mask rom_byte_mask
        if (search_direction == 1) {
          ROM_NO[rom_byte_number] |= rom_byte_mask;
        }
        else {
          ROM_NO[rom_byte_number] &= ~rom_byte_mask;
        }

        Dallas_pinHigh;

        // serial number search direction write bit
        Dallas_write_bit(search_direction, gpio_pin_rx, gpio_pin_tx);

        // increment the byte counter id_bit_number
        // and shift the mask rom_byte_mask
        id_bit_number++;
        rom_byte_mask <<= 1;

        // if the mask is 0 then go to new SerialNum byte rom_byte_number and reset mask
        if (rom_byte_mask == 0)
        {
          rom_byte_number++;
          rom_byte_mask = 1;
        }
      }
    }
    while (rom_byte_number < 8); // loop until through all ROM bytes 0-7

    // if the search was successful then
    if (!(id_bit_number < 65))
    {
      // search successful so set LastDiscrepancy,LastDeviceFlag,search_result
      LastDiscrepancy = last_zero;

      // check for last device
      if (LastDiscrepancy == 0) {
        LastDeviceFlag = TRUE;
      }

      search_result = TRUE;
    }
  }

  // if no device found then reset counters so next 'search' will be like a first
  if (!search_result || !ROM_NO[0])
  {
    LastDiscrepancy       = 0;
    LastDeviceFlag        = FALSE;
    LastFamilyDiscrepancy = 0;
    search_result         = FALSE;
  }

  for (int i = 0; i < 8; i++) {
    newAddr[i] = ROM_NO[i];
  }

  return search_result;
}

# undef FALSE
# undef TRUE

/*********************************************************************************************\
*  Dallas Read byte
\*********************************************************************************************/
uint8_t Dallas_read(int8_t gpio_pin_rx, int8_t gpio_pin_tx)
{
  uint8_t bitMask;
  uint8_t r = 0;

  for (bitMask = 0x01; bitMask; bitMask <<= 1) {
    if (Dallas_read_bit(gpio_pin_rx, gpio_pin_tx)) {
      r |= bitMask;
    }
  }

  return r;
}

/*********************************************************************************************\
*  Dallas Write byte
\*********************************************************************************************/
void Dallas_write(uint8_t ByteToWrite, int8_t gpio_pin_rx, int8_t gpio_pin_tx)
{
  uint8_t bitMask;

  Dallas_pinHigh;

# ifdef DEBUG_LOGIC_ANALYZER_PIN

  // DEBUG code using logic analyzer for timings
  DIRECT_pinWrite(DEBUG_LOGIC_ANALYZER_PIN, 1);
# endif // ifdef DEBUG_LOGIC_ANALYZER_PIN

  for (bitMask = 0x01; bitMask; bitMask <<= 1) {
    Dallas_write_bit((bitMask & ByteToWrite) ? 1 : 0, gpio_pin_rx, gpio_pin_tx);
  }
# ifdef DEBUG_LOGIC_ANALYZER_PIN

  // DEBUG code using logic analyzer for timings
  DIRECT_pinWrite(DEBUG_LOGIC_ANALYZER_PIN, 0);
# endif // ifdef DEBUG_LOGIC_ANALYZER_PIN
}

/*********************************************************************************************\
*  Dallas Read bit
\*********************************************************************************************/
uint8_t Dallas_read_bit(int8_t gpio_pin_rx, int8_t gpio_pin_tx)
{
  if (gpio_pin_rx == -1) { return 0; }

  if (gpio_pin_tx == -1) { return 0; }

  uint32_t start;
  const uint8_t r = Dallas_read_bit_ISR(gpio_pin_rx, gpio_pin_tx, start);

  // Recovery time, make sure we at least wait for 10 usec + RC-rise time
  delayMicroseconds(2 * usec_release + 10);

  while (usecPassedSince_fast(start) < 60) {
    // Allow for some
    // Wait for another 45 usec
    // Complete read cycle:
    // LOW: 6 usec
    // Float: 9 msec
    // Read value. Typically the sensor keeps the level low for 27 usec.
    // Wait for 55 usec => complete cycle = 6 + 9 + 55 = 70 usec.
  }
  return r;
}

uint8_t DALLAS_IRAM_ATTR Dallas_read_bit_ISR(
  int8_t    gpio_pin_rx,
  int8_t    gpio_pin_tx,
  uint32_t& start)
{
  uint8_t r = 0;

  ISR_noInterrupts();

  Dallas_pinLow;
  start = micros();

  // delayMicroseconds(1);
  // Make sure there is at least 5 usec 'low' level.
  // Minimum state by Infineon, however other brands, like Analog,
  // claim lower minimum.
  while (usecPassedSince_fast(start) < 4) {}

  // Set to 'input', state will be pulled high by pull-up resistor
  // This will take t_RC, which is typically 2 - 3 usec.
  // We have to wait upto 15 usec to see the pin be pulled up,
  // otherwise it is apparently be pulled low by the sensor
  Dallas_pinInput;

  if (Dallas_waitForPinHigh(gpio_pin_rx, start, 15 + usec_release)) {
    // Wait for max 15 usec to have a high level
    r = 1;
    Dallas_pinHigh;
  }

  ISR_interrupts();

  if (r == 0) {
    // Wait upto 60 usec to have a high level
    // Pin should be released within 15 ... 60 usec from start of read slot.
    // We must make sure we don't try to do a strong pull-up
    // while the sensor is still trying to pull-down.
    Dallas_waitForPinHigh(gpio_pin_rx, start, 60);
  }

  // Pull high again so we can support parasite mode for 1 sensor
  // Sending out a high signal level is stronger than a pull-up resistor can supply
  Dallas_pinHigh;

  return r;
}

/*********************************************************************************************\
*  Dallas Write bit
\*********************************************************************************************/
void Dallas_write_bit(uint8_t v, int8_t gpio_pin_rx, int8_t gpio_pin_tx)
{
  if (gpio_pin_tx == -1) { return; }

  // Determine times in usec for high and low
  // write 1: low 6 usec, high 64 usec  (no less than 6 usec low, or else scanning will no longer work)
  // write 0: low 60 usec, high 20 usec
  // High time is based on the recovery time, which is detected during reset
  const long low_time  = (v & 1) ? 7 : 60;
  const long high_time = (v & 1) ? (40 + usec_release) : (2 * usec_release + 20); // Recovery time
  uint32_t   start     = 0;

  Dallas_write_bit_ISR(v, gpio_pin_rx, gpio_pin_tx, low_time, high_time, start);

  // Minimum Recovery time
  delayMicroseconds(10);

  while (usecPassedSince_fast(start) <  high_time) {
    // output remains high
  }
}

void DALLAS_IRAM_ATTR Dallas_write_bit_ISR(uint8_t   v,
                                           int8_t    gpio_pin_rx,
                                           int8_t    gpio_pin_tx,
                                           long      low_time,
                                           long      high_time,
                                           uint32_t& start)
{
  ISR_noInterrupts();
  start = micros();
  Dallas_pinLow;

  while (usecPassedSince_fast(start) < low_time) {
    // output remains low
  }
  start = micros();
  Dallas_pinHigh;
  ISR_interrupts();
}

/*********************************************************************************************\
*  Standard function to initiate addressing a sensor.
\*********************************************************************************************/
bool Dallas_address_ROM(const uint8_t ROM[8], int8_t gpio_pin_rx, int8_t gpio_pin_tx)
{
  if (!Dallas_reset(gpio_pin_rx, gpio_pin_tx)) { return false; }
  Dallas_write(0x55, gpio_pin_rx, gpio_pin_tx); // Choose ROM

  for (uint8_t i = 0; i < 8; i++) {
    Dallas_write(ROM[i], gpio_pin_rx, gpio_pin_tx);
  }
  return true;
}

/*********************************************************************************************\
*  Dallas Calculate CRC8 and compare it of addr[0-7] and compares it to addr[8]
\*********************************************************************************************/
bool Dallas_crc8(const uint8_t *addr)
{
  uint8_t crc = 0;
  uint8_t len = 8;

  while (len--)
  {
    uint8_t inbyte = *addr++; // from 0 to 7

    for (uint8_t i = 8; i; i--)
    {
      uint8_t mix = (crc ^ inbyte) & 0x01;
      crc >>= 1;

      if (mix) { crc ^= 0x8C; }
      inbyte >>= 1;
    }
  }
  return crc == *addr; // addr 8
}

/*********************************************************************************************\
*  Dallas Calculate CRC16
\*********************************************************************************************/
uint16_t Dallas_crc16(const uint8_t *input, uint16_t len, uint16_t crc)
{
  static const uint8_t oddparity[16] =
  { 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0 };

  for (uint16_t i = 0; i < len; i++) {
    // Even though we're just copying a byte from the input,
    // we'll be doing 16-bit computation with it.
    uint16_t cdata = input[i];
    cdata = (cdata ^ crc) & 0xff;
    crc >>= 8;

    if (oddparity[cdata & 0x0F] ^ oddparity[cdata >> 4]) {
      crc ^= 0xC001;
    }

    cdata <<= 6;
    crc    ^= cdata;
    cdata <<= 1;
    crc    ^= cdata;
  }

  return crc;
}

void Dallas_SensorData::clear() {
  addr                  = 0u;
  value                 = 0.0f;
  start_read_failed     = 0u;
  start_read_retry      = 0u;
  read_success          = 0u;
  sensor_power_on_reset = 0u;
  read_CRC              = 0u;
  read_retry            = 0u;
  read_failed           = 0u;
  reinit_count          = 0u;
  actual_res            = 0u;


  measurementActive = false;
  valueRead         = false;
  lastReadError     = false;
  fixed_resolution  = false;
# ifndef LIMIT_BUILD_SIZE
  parasitePowered = false;
# endif // ifndef LIMIT_BUILD_SIZE
}

void Dallas_SensorData::set_measurement_inactive() {
  measurementActive = false;
  value             = 0.0f;
  valueRead         = false;
}

bool Dallas_SensorData::initiate_read(int8_t gpio_rx, int8_t gpio_tx, int8_t res) {
  if (addr == 0) { return false; }
  uint8_t tmpaddr[8];

  Dallas_uint64_to_addr(addr, tmpaddr);

  if (lastReadError) {
    if (!check_sensor(gpio_rx, gpio_tx, res)) {
      return false;
    }
    lastReadError = false;
  }

  if (!Dallas_address_ROM(tmpaddr, gpio_rx, gpio_tx)) {
    ++start_read_retry;
  # ifndef LIMIT_BUILD_SIZE

    if (!parasitePowered) {
      //      Dallas_is_parasite(tmpaddr, gpio_rx, gpio_tx, parasitePowered);
    }
# endif // ifndef LIMIT_BUILD_SIZE

    if (!Dallas_address_ROM(tmpaddr, gpio_rx, gpio_tx)) {
      ++start_read_failed;
      lastReadError = true;
      return false;
    }
  }
  Dallas_write(0x44, gpio_rx, gpio_tx); // Take temperature measurement
  return true;
}

bool Dallas_SensorData::collect_value(int8_t gpio_rx, int8_t gpio_tx) {
  if ((addr != 0) && measurementActive) {
    uint8_t tmpaddr[8];
    Dallas_uint64_to_addr(addr, tmpaddr);

    uint8_t nrRetries = 2;

    while (nrRetries) {
      --nrRetries;
      Dallas_read_result res = Dallas_readTemp(tmpaddr, &value, gpio_rx, gpio_tx);

      switch (res) {
        case Dallas_read_result::OK:
          ++read_success;
          lastReadError = false;
          valueRead     = true;
          return true;
        case Dallas_read_result::PowerOnResetValue:
          // No need to retry, sensor did reset
          nrRetries = 0;
          ++sensor_power_on_reset;
          break;
        case Dallas_read_result::CRCerr:
          ++read_CRC;
          ++read_retry;
          break;
        case Dallas_read_result::NoReply:
          // No need to retry, sensor not found
          nrRetries = 0;
          ++start_read_failed;
          break;
      }
# ifndef LIMIT_BUILD_SIZE

      if (!parasitePowered) {
        Dallas_is_parasite(tmpaddr, gpio_rx, gpio_tx, parasitePowered);
      }
# endif // ifndef LIMIT_BUILD_SIZE
    }
  }
  valueRead     = false;
  lastReadError = true;
  ++read_failed;
  return false;
}

String Dallas_SensorData::get_formatted_address() const {
  if (addr == 0) { return EMPTY_STRING; }

  uint8_t tmpaddr[8];

  Dallas_uint64_to_addr(addr, tmpaddr);
  return Dallas_format_address(tmpaddr, fixed_resolution);
}

bool Dallas_SensorData::check_sensor(int8_t gpio_rx, int8_t gpio_tx, int8_t res) {
  if (addr == 0) { return false; }
  uint8_t tmpaddr[8];

  fixed_resolution = false; // reset

  Dallas_uint64_to_addr(addr, tmpaddr);
# ifndef LIMIT_BUILD_SIZE

  if (!parasitePowered) {
    Dallas_is_parasite(tmpaddr, gpio_rx, gpio_tx, parasitePowered);
  }
# endif // ifndef LIMIT_BUILD_SIZE

  actual_res = Dallas_getResolution(tmpaddr, gpio_rx, gpio_tx, fixed_resolution);

  if (actual_res == 0) {
    ++read_failed;
    lastReadError = true;
    return false;
  }

  if ((res != actual_res) && !fixed_resolution) {
    if (!Dallas_setResolution(tmpaddr, res, gpio_rx, gpio_tx)) {
      return false;
    }
    actual_res = res; // Update for later use
  }

  return true;
}

#endif // if FEATURE_DALLAS_HELPER

#include "../Helpers/PrintToLog.h"

#include "../Helpers/StringConverter.h"

PrintToLog::PrintToLog(uint8_t logLevel) : _logLevel(logLevel)
{
  _logLevelActive = loglevelActiveFor(_logLevel);
}

PrintToLog::PrintToLog(uint8_t logLevel, const String& logPrefix)
  : _prefix(logPrefix), _logLevel(logLevel)
{
  _logLevelActive = loglevelActiveFor(_logLevel);
}

PrintToLog::PrintToLog(uint8_t logLevel, const __FlashStringHelper *logPrefix)
  : _prefix(logPrefix), _logLevel(logLevel)
{
  _logLevelActive = loglevelActiveFor(_logLevel);
}

PrintToLog::~PrintToLog()
{
  sendToLog();
}

PrintToLog::operator bool() const
{
  return loglevelActiveFor(_logLevel);
}

void PrintToLog::flush()
{
  // For compatibility
  sendToLog();
}

void PrintToLog::sendToLog()
{
  // Update cached loglevel
  _logLevelActive = loglevelActiveFor(_logLevel);

  if (_logLevelActive) {
    if (!_str.isEmpty()) {
      if (_prefix.isEmpty()) {
        addLogMove(_logLevel, _str);
      } else {
        addLog(_logLevel, concat(_prefix, _str));
      }
    }
  }
  _str.clear();
}

void PrintToLog::clear() {
  _str.clear();
  _logLevelActive = loglevelActiveFor(_logLevel);
}

size_t PrintToLog::length() const
{
  return _str.length();
}

bool   PrintToLog::reserve(unsigned int size) { return _str.reserve(size); }

size_t PrintToLog::write(uint8_t c)
{
  if (!_logLevelActive) { return 0; }
  _str += (char)c;
  return 1;
}

String PrintToLog::get() const
{
  if (!_str.isEmpty() && !_prefix.isEmpty()) {
    return concat(_prefix, _str);
  }
  return _str;
}

#include "../Helpers/Hardware_GPIO.h"

#ifdef ESP32C3
# include <driver/gpio.h>

# include "../Helpers/Hardware_device_info.h"

// ********************************************************************************
// Get info of a specific GPIO pin
// ********************************************************************************

bool getGpioInfo(int gpio, int& pinnr, bool& input, bool& output, bool& warning) {
  pinnr = -1; // ESP32 does not label the pins, they just use the GPIO number.

  input   = GPIO_IS_VALID_GPIO(gpio);
  output  = GPIO_IS_VALID_OUTPUT_GPIO(gpio);
  warning = isBootStrapPin(gpio);

  if (!(GPIO_IS_VALID_GPIO(gpio))) { return false; }


  // FIXME TD-er: Implement for ESP32-C3
  // See:
  // - https://docs.espressif.com/projects/esp-idf/en/v5.0/esp32c3/hw-reference/esp32c3/user-guide-devkitm-1.html
  // - https://docs.espressif.com/projects/esp-idf/en/latest/esp32c3/api-reference/peripherals/gpio.html
  // Datasheet: https://www.espressif.com/sites/default/files/documentation/esp32-c3_datasheet_en.pdf

  if (gpio == 11) {
    if (getChipFeatures().embeddedFlash /* || !flashVddPinCanBeUsedAsGPIO() */) {
      // See: https://www.letscontrolit.com/forum/viewtopic.php?p=71880#p71874
      //
      // By default VDD_SPI is the power supply pin for embedded flash or external flash.
      // It can only be used as GPIO11 only when the chip is connected to an
      // external flash, and this flash is powered by an external power supply
      input  = false;
      output = false;
    }
    warning = true;
  }

  if (isFlashInterfacePin_ESPEasy(gpio)) {
    if (getChipFeatures().embeddedFlash) {
      // Connected to the integrated SPI flash.
      input  = false;
      output = false;
    } else {
      // See: https://www.letscontrolit.com/forum/viewtopic.php?p=71880#p71874
      if ((gpio == 12) || (gpio == 13)) { 
        // SPIHD/GPIO12 
        // SPIWP/GPIO13 
        if ((ESP.getFlashChipMode() != FM_DOUT) &&
            (ESP.getFlashChipMode() != FM_DIO)) {
          input  = false;
          output = false;
        }
      }
    }
    warning = true;
  }

  if ((gpio == PIN_USB_D_MIN) || (gpio == PIN_USB_D_PLUS)) {
    // USB OTG and USB Serial/JTAG function. USB signal is a differential
    // signal transmitted over a pair of D+ and D- wires.
    warning = true;
  }

  // GPIO 18: USB_D-
  // GPIO 19: USB_D+

  // GPIO 20: U0RXD
  // GPIO 21: U0TXD

  return (input || output);
}

bool isBootStrapPin(int gpio)
{
  if (gpio == 2) {
    // Strapping pin which must be high during boot
    return true;
  }

  if (gpio == 8) {
    // Strapping pin which must be high during flashing
    return true;
  }

  if (gpio == 9) {
    // Strapping pin to force download mode (like GPIO-0 on ESP8266/ESP32-classic)
    return true;
  }
  return false;
}

bool getGpioPullResistor(int gpio, bool& hasPullUp, bool& hasPullDown) {
  hasPullDown = false;
  hasPullUp   = false;

  if (validGpio(gpio)) {
    hasPullUp   = true;
    hasPullDown = true;
  }
  return true;
}

bool validGpio(int gpio) {
  if (!GPIO_IS_VALID_GPIO(gpio)) { return false; }

  int  pinnr;
  bool input;
  bool output;
  bool warning;

  return getGpioInfo(gpio, pinnr, input, output, warning);
}

// Get ADC related info for a given GPIO pin
// @param gpio_pin   GPIO pin number
// @param adc        Number of ADC unit (0 == Hall effect)
// @param ch         Channel number on ADC unit
// @param t          index of touch pad ID
bool getADC_gpio_info(int gpio_pin, int& adc, int& ch, int& t)
{
  adc = -1;
  ch  = -1;
  t   = -1;

  if ((gpio_pin >= 0) && (gpio_pin <= 4)) {
    adc = 1;
    ch  = gpio_pin;
    return true;
  }
  # if ESP_IDF_VERSION_MAJOR >= 5

  // Support for ADC2 has been dropped.
  # else // if ESP_IDF_VERSION_MAJOR >= 5

  if (gpio_pin == 5) {
    adc = 2;
    ch  = 0;
    return true;
  }
  # endif // if ESP_IDF_VERSION_MAJOR >= 5
  return false;
}

#if SOC_TOUCH_SENSOR_SUPPORTED
int touchPinToGpio(int touch_pin)
{
  // No touch pin support
  return -1;
}
#endif

#if SOC_DAC_SUPPORTED
// Get DAC related info for a given GPIO pin
// @param gpio_pin   GPIO pin number
// @param dac        Number of DAC unit
bool getDAC_gpio_info(int gpio_pin, int& dac)
{
  // ESP32-C3, ESP32-S3, ESP32-C2, ESP32-C6 and ESP32-H2 don't have a DAC onboard
  return false;
}
#endif

#endif // ifdef ESP32C3

#include "../Helpers/DeepSleep.h"

#include "../../ESPEasy_common.h"
#include "../../ESPEasy-Globals.h"

#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../ESPEasyCore/ESPEasyEth.h"
#include "../ESPEasyCore/ESPEasyNetwork.h"
#include "../ESPEasyCore/ESPEasyWifi.h"
#include "../ESPEasyCore/ESPEasyRules.h"

#include "../Globals/EventQueue.h"
#include "../Globals/RTC.h"
#include "../Globals/Settings.h"
#include "../Globals/Statistics.h"

#include "../Helpers/ESPEasy_time_calc.h"
#include "../Helpers/Misc.h"
#include "../Helpers/PeriodicalActions.h"

#include <limits.h>


/**********************************************************
*                                                         *
* Deep Sleep related functions                            *
*                                                         *
**********************************************************/
int getDeepSleepMax()
{
  #ifdef ESP8266
  int dsmax = 4294; // About 71 minutes, limited by hardware
  #endif // ifdef ESP8266
  #ifdef ESP32
  int dsmax = 281474976; // / 3600 (hour) / 24 (day) / 365 (year) = ~8 years. (max. 48 bits in microseconds)
  #endif // ifdef ESP32

#if defined(CORE_POST_2_5_0)
  dsmax = INT_MAX;

  // Convert to sec and add 5% margin.
  // See: https://github.com/esp8266/Arduino/pull/4936#issuecomment-410435875
  const uint64_t sdk_dsmax_sec = ESP.deepSleepMax() / 1050000ULL;

  if (sdk_dsmax_sec <= static_cast<uint64_t>(INT_MAX)) {
    dsmax = sdk_dsmax_sec;
  }
#endif // if defined(CORE_POST_2_5_0)
  return dsmax;
}

bool isDeepSleepEnabled()
{
  if (!Settings.deepSleep_wakeTime) {
    return false;
  }

  // cancel deep sleep loop by pulling the pin GPIO16(D0) to GND
  // recommended wiring: 3-pin-header with 1=RST, 2=D0, 3=GND
  //                    short 1-2 for normal deep sleep / wakeup loop
  //                    short 2-3 to cancel sleep loop for modifying settings

#ifndef ESP32 // pinMode() crashes the ESP32 when PSRAM is enabled and available. So don't check for ESP32.
  pinMode(16, INPUT_PULLUP);

  if (!digitalRead(16))
  {
    return false;
  }
#endif
  return true;
}

bool readyForSleep()
{
  if (!isDeepSleepEnabled()) {
    return false;
  }

  if (!NetworkConnected()) {
    // Allow 12 seconds to establish connections
    return timeOutReached(timerAwakeFromDeepSleep + 12000);
  }
  return timeOutReached(timerAwakeFromDeepSleep + 1000 * Settings.deepSleep_wakeTime);
}

void prepare_deepSleep(int dsdelay)
{
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("prepare_deepSleep"));
  #endif

  if (!isDeepSleepEnabled())
  {
    // Deep sleep canceled by GPIO16(D0)=LOW
    return;
  }

  // first time deep sleep? offer a way to escape
  if (lastBootCause != BOOT_CAUSE_DEEP_SLEEP)
  {
    addLog(LOG_LEVEL_INFO, F("SLEEP: Entering deep sleep in 30 seconds."));

    if (Settings.UseRules && isDeepSleepEnabled())
    {
      eventQueue.add(F("System#NoSleep=30"));
      while (processNextEvent()) {
        delay(1);
      }
    }
    delayBackground(30000);

    // disabled?
    if (!isDeepSleepEnabled())
    {
      addLog(LOG_LEVEL_INFO, F("SLEEP: Deep sleep cancelled (GPIO16 connected to GND)"));
      return;
    }
  }
  deepSleepStart(dsdelay); // Call deepSleepStart function after these checks
}

void deepSleepStart(int dsdelay)
{
  // separate function that is called from above function or directly from rules, usign deepSleep_wakeTime as a one-shot
  if (Settings.UseRules)
  {
    eventQueue.add(F("System#Sleep"));
    while (processNextEvent()) {
      delay(1);
    }
  }

  addLog(LOG_LEVEL_INFO, F("SLEEP: Powering down to deepsleep..."));
  RTC.deepSleepState = 1;
  prepareShutdown(IntendedRebootReason_e::DeepSleep);

  #if defined(ESP8266)
    # if defined(CORE_POST_2_5_0)
  if ((dsdelay < 0) || dsdelay > getDeepSleepMax()) {
    dsdelay = getDeepSleepMax();
  }
  uint64_t deepSleep_usec = dsdelay * 1000000ULL;

  if (Settings.UseAlternativeDeepSleep()) {
    // See: https://github.com/esp8266/Arduino/issues/6318#issuecomment-711389479
    #include <c_types.h>
    // system_phy_set_powerup_option:
    // 1 = RF initialization only calibrate VDD33 and Tx power which will take about 18 ms
    // 2 = RF initialization only calibrate VDD33 which will take about 2 ms
    system_phy_set_powerup_option(2); // calibrate only 2ms;
    system_deep_sleep_set_option(static_cast<int>(WAKE_RF_DEFAULT));
    uint32_t*RT= (uint32_t *)0x60000700;
    uint32 t_us = 1.31 * deepSleep_usec;
    {
      RT[4] = 0;
      *RT = 0;
      RT[1]=100;
      RT[3] = 0x10010;
      RT[6] = 8;
      RT[17] = 4;
      RT[2] = 1<<20;
      ets_delay_us(10);
      RT[1]=t_us>>3;
      RT[3] = 0x640C8;
      RT[4]= 0;
      RT[6] = 0x18;
      RT[16] = 0x7F;
      RT[17] = 0x20;
      RT[39] = 0x11;
      RT[40] = 0x03;
      RT[2] |= 1<<20;
      __asm volatile ("waiti 0");
    }
    yield();
  } else {
    ESP.deepSleep(deepSleep_usec, WAKE_RF_DEFAULT);
  }
    # else // if defined(CORE_POST_2_5_0)

  if ((dsdelay > 4294) || (dsdelay < 0)) {
    dsdelay = 4294; // max sleep time ~71 minutes
  }
  ESP.deepSleep((uint32_t)dsdelay * 1000000, WAKE_RF_DEFAULT);
    # endif // if defined(CORE_POST_2_5_0)
  #endif // if defined(ESP8266)
  #if defined(ESP32)
  if ((dsdelay > getDeepSleepMax()) || (dsdelay < 0)) {
    dsdelay = getDeepSleepMax(); // Max sleep time ~8 years! Reference: ESP32 technical reference manual, ULP Coprocessor, RTC Timer: https://www.espressif.com/sites/default/files/documentation/esp32_technical_reference_manual_en.pdf#ulp
  }
  esp_sleep_enable_timer_wakeup(static_cast<uint64_t>(dsdelay) * 1000000ULL); // max 48 bits used, value in microseconds: 0xFFFFFFFFFFFF (281474976710655 dec.) / 1000000 [usec](281474976 seconds) / 3600 (hours) / 24 (day) / 365 (year) = ~8
  esp_deep_sleep_start();
  #endif // if defined(ESP32)
}


#include "../Helpers/ESPEasy_NVS_Helper.h"

#ifdef ESP32

# include "../Helpers/StringConverter.h"


ESPEasy_NVS_Helper::~ESPEasy_NVS_Helper()
{
  _preferences.end();
}

bool ESPEasy_NVS_Helper::begin(const String& nvs_namespace, bool readOnly)
{
  return _preferences.begin(nvs_namespace.c_str(), readOnly);
}

void ESPEasy_NVS_Helper::end()
{
  _preferences.end();
}

void ESPEasy_NVS_Helper::remove(const String& key)
{
  if (_preferences.isKey(key.c_str())) {
    _preferences.remove(key.c_str());
  }
}

bool ESPEasy_NVS_Helper::getPreference(const String& key, String& value)
{
  if (!_preferences.isKey(key.c_str())) {
    return false;
  }

  value = _preferences.getString(key.c_str());

  const bool res = !value.isEmpty();

  addLog(res ? LOG_LEVEL_INFO : LOG_LEVEL_ERROR, concat(F("NVS  : Load "), key));
  return res;
}

void ESPEasy_NVS_Helper::setPreference(const String& key, const String& value)
{
  if (value.isEmpty()) {
    _preferences.remove(key.c_str());
  }
  else if (!_preferences.getString(key.c_str(), EMPTY_STRING.c_str()).equals(value)) {
    _preferences.putString(key.c_str(), value);
    addLog(LOG_LEVEL_INFO, concat(F("NVS  : Save "), key));
  }
}

bool ESPEasy_NVS_Helper::getPreference(const String& key, uint32_t& value)
{
  if (!_preferences.isKey(key.c_str())) {
    return false;
  }
  constexpr uint32_t defaultValue = std::numeric_limits<uint32_t>::max();

  value = _preferences.getUInt(key.c_str(), defaultValue);

  const bool res = value != defaultValue;

  addLog(res ? LOG_LEVEL_INFO : LOG_LEVEL_ERROR, concat(F("NVS  : Load "), key));
  return res;
}

void ESPEasy_NVS_Helper::setPreference(const String& key, const uint32_t& value)
{
  constexpr uint32_t defaultValue = std::numeric_limits<uint32_t>::max();

  if (value == defaultValue) {
    _preferences.remove(key.c_str());
  }
  else if (_preferences.getUInt(key.c_str()) != value) {
    _preferences.putUInt(key.c_str(), value);
    addLog(LOG_LEVEL_INFO, concat(F("NVS  : Save "), key));
  }
}

bool ESPEasy_NVS_Helper::getPreference(const String& key, uint64_t& value)
{
  if (!_preferences.isKey(key.c_str())) {
    return false;
  }

  // Make this a signed value as an erased flash chip only has 0xFF's
  // and max uint64 also contains only 0xFF bytes.
  constexpr uint64_t defaultValue = std::numeric_limits<int64_t>::max();

  value = _preferences.getULong64(key.c_str(), defaultValue);

  const bool res = value != defaultValue;

  addLog(res ? LOG_LEVEL_INFO : LOG_LEVEL_ERROR, concat(F("NVS  : Load "), key));
  return res;
}

void ESPEasy_NVS_Helper::setPreference(const String& key, const uint64_t& value)
{
  constexpr uint64_t defaultValue = std::numeric_limits<uint64_t>::max();

  if (value == defaultValue) {
    _preferences.remove(key.c_str());
  }
  else if (_preferences.getULong64(key.c_str()) != value) {
    _preferences.putULong64(key.c_str(), value);
    addLog(LOG_LEVEL_INFO, concat(F("NVS  : Save "), key));
  }
}

bool ESPEasy_NVS_Helper::getPreference(const String& key, uint8_t *data, size_t length)
{
  if (!_preferences.isKey(key.c_str())) {
    return false;
  }

  const bool res = _preferences.getBytes(key.c_str(), data, length) == length;

  addLog(res ? LOG_LEVEL_INFO : LOG_LEVEL_ERROR, concat(F("NVS  : Load "), key));
  return res;
}

void ESPEasy_NVS_Helper::setPreference(const String& key, const uint8_t *data, size_t length)
{
  {
    uint8_t tmp[length]{};

    if (_preferences.getBytes(key.c_str(), tmp, length) == length) {
      if (memcmp(tmp, data, length) == 0) {
        return;
      }
    }
  }

  if (_preferences.putBytes(key.c_str(), data, length) == length) {
    addLog(LOG_LEVEL_INFO, concat(F("NVS  : Save "), key));
  }
}

#endif // ifdef ESP32

#include "../Helpers/Scheduler.h"

#include "../DataStructs/PinMode.h"
#include "../DataStructs/Scheduler_GPIOTimerID.h"

#include "../ESPEasyCore/ESPEasyGPIO.h"

#include "../Globals/GlobalMapPortStatus.h"

#include "../Helpers/PortStatus.h"

/*********************************************************************************************\
* GPIO Timer
* Special timer to handle timed GPIO actions
\*********************************************************************************************/
void ESPEasy_Scheduler::setGPIOTimer(
  unsigned long msecFromNow,
  pluginID_t    pluginID,
  int           pinnr,
  int           state,
  int           repeatInterval,
  int           recurringCount,
  int           alternateInterval)
{
  uint8_t GPIOType = GPIO_TYPE_INVALID;

  switch (pluginID.value) {
    case PLUGIN_GPIO_INT:
      GPIOType = GPIO_TYPE_INTERNAL;
      break;
    case PLUGIN_PCF_INT:
      GPIOType = GPIO_TYPE_PCF;
      break;
    case PLUGIN_MCP_INT:
      GPIOType = GPIO_TYPE_MCP;
      break;
  }

  if (GPIOType != GPIO_TYPE_INVALID) {
    // Par1 & Par2 & GPIOType form a unique key
    const GPIOTimerID timerID(GPIOType, pinnr, state);
    const systemTimerStruct timer_data(
      recurringCount,
      repeatInterval,
      state,
      alternateInterval);
    systemTimers[timerID.mixed_id] = timer_data;
    setNewTimerAt(timerID, millis() + msecFromNow);
  }
}

void ESPEasy_Scheduler::clearGPIOTimer(pluginID_t pluginID, int pinnr)
{
  uint8_t GPIOType = GPIO_TYPE_INVALID;

  switch (pluginID.value) {
    case PLUGIN_GPIO_INT:
      GPIOType = GPIO_TYPE_INTERNAL;
      break;
    case PLUGIN_PCF_INT:
      GPIOType = GPIO_TYPE_PCF;
      break;
    case PLUGIN_MCP_INT:
      GPIOType = GPIO_TYPE_MCP;
      break;
  }

  if (GPIOType != GPIO_TYPE_INVALID) {
    // Par1 & Par2 & GPIOType form a unique key
    for (int state = 0; state <= 1; ++state) {
      const GPIOTimerID timerID(GPIOType, pinnr, state);
      auto it = systemTimers.find(timerID.mixed_id);

      if (it != systemTimers.end()) {
        systemTimers.erase(it);
      }
      msecTimerHandler.remove(timerID.mixed_id);
    }
  }
}

void ESPEasy_Scheduler::process_gpio_timer(SchedulerTimerID timerID, unsigned long lasttimer) {
  auto it = systemTimers.find(timerID.mixed_id);

  if (it == systemTimers.end()) {
    return;
  }

  // Reschedule before sending the event, as it may get rescheduled in handling the timer event.
  if (it->second.isRecurring()) {
    // Recurring timer
    it->second.markNextRecurring();

    unsigned long newTimer = lasttimer;
    setNextTimeInterval(newTimer, it->second.getInterval());
    setNewTimerAt(timerID, newTimer);
  }

  const GPIOTimerID *tmp = reinterpret_cast<const GPIOTimerID *>(&timerID);

  uint8_t GPIOType      = tmp->getGPIO_type();
  uint8_t pinNumber     = tmp->getPinNumber();
  uint8_t pinStateValue = tmp->getPinStateValue();

  if (it->second.isAlternateState()) {
    pinStateValue = (pinStateValue > 0) ? 0 : 1;
  }

  pluginID_t pluginID = PLUGIN_GPIO;

  switch (GPIOType)
  {
    case GPIO_TYPE_INTERNAL:
      GPIO_Internal_Write(pinNumber, pinStateValue);
      pluginID = PLUGIN_GPIO;
      break;
#ifdef USES_P009
    case GPIO_TYPE_MCP:
      GPIO_MCP_Write(pinNumber, pinStateValue);
      pluginID = PLUGIN_MCP;
      break;
#endif // ifdef USES_P009
#ifdef USES_P019
    case GPIO_TYPE_PCF:
      GPIO_PCF_Write(pinNumber, pinStateValue);
      pluginID = PLUGIN_PCF;
      break;
#endif // ifdef USES_P019
    default:
      return;
  }


  if (!it->second.isRecurring()) {
    clearGPIOTimer(pluginID, pinNumber);
  }

  const uint32_t key = createKey(pluginID, pinNumber);

  // WARNING: operator [] creates an entry in the map if key does not exist
  portStatusStruct tempStatus = globalMapPortStatus[key];

  tempStatus.mode    = PIN_MODE_OUTPUT;
  tempStatus.command = 1; // set to 1 in order to display the status in the PinStatus page

  if (tempStatus.state != pinStateValue) {
    tempStatus.state        = pinStateValue;
    tempStatus.output       = pinStateValue;
    tempStatus.forceEvent   = 1;
    tempStatus.forceMonitor = 1;
  }
  savePortStatus(key, tempStatus);
}

#include "../Helpers/Hardware_GPIO.h"

#ifdef ESP32S3
# include <driver/gpio.h>

# include "../Helpers/Hardware_device_info.h"

// ********************************************************************************
// Get info of a specific GPIO pin
// ********************************************************************************

bool getGpioInfo(int gpio, int& pinnr, bool& input, bool& output, bool& warning) {
  pinnr = -1; // ESP32 does not label the pins, they just use the GPIO number.

  input   = GPIO_IS_VALID_GPIO(gpio);
  output  = GPIO_IS_VALID_OUTPUT_GPIO(gpio);
  warning = isBootStrapPin(gpio);

  if (!(GPIO_IS_VALID_GPIO(gpio))) { return false; }


  // FIXME TD-er: Implement for ESP32-S3
  // See:
  // - https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/peripherals/gpio.html
  // Datasheet: https://www.espressif.com/sites/default/files/documentation/esp32-s3_datasheet_en.pdf

  if ((gpio >= 26) && (gpio <= 37)) {
    // Connected to the integrated SPI flash.
    // SPI0/1: GPIO26-32 are usually used for SPI flash and PSRAM and not recommended for other uses.
    // When using Octal Flash or Octal PSRAM or both,
    //  GPIO33~37 are connected to SPIIO4 ~ SPIIO7 and SPIDQS.
    // Therefore, on boards embedded with ESP32-S3R8 / ESP32-S3R8V chip,
    //  GPIO33~37 are also not recommended for other uses.
    input   = gpio > 32;
    output  = gpio > 32;
    warning = true;
  }


  if ((gpio == 19) || (gpio == 20)) {
    // USB OTG and USB Serial/JTAG function. USB signal is a differential
    // signal transmitted over a pair of D+ and D- wires.
    warning = true;
  }

  return (input || output);
}

bool isBootStrapPin(int gpio)
{
  if (gpio == 45) {
    // GPIO45 is used to select the VDD_SPI power supply voltage at reset:
    // • GPIO45 = 0, VDD_SPI pin is powered directly from VDD3P3_RTC via resistor RSP I . Typically this voltage is
    //   3.3 V. For more information, see Figure: ESP32-S3 Power Scheme in ESP32-S3 Datasheet.
    // • GPIO45 = 1, VDD_SPI pin is powered from internal 1.8 V LDO.
    return true;
  }

  if (gpio == 46) {
    // Strapping pin which must be low during flashing
    // ROM message printing
    return true;
  }

  // Chip boot mode
  // GPIO 0  State during boot determines boot mode.
  if (gpio == 0) { return true; }

  return false;
}

bool getGpioPullResistor(int gpio, bool& hasPullUp, bool& hasPullDown) {
  hasPullDown = false;
  hasPullUp   = false;

  if (validGpio(gpio)) {
    hasPullUp   = true;
    hasPullDown = true;
  }

  return true;
}

bool validGpio(int gpio) {
  if (!GPIO_IS_VALID_GPIO(gpio)) { return false; }

  int  pinnr;
  bool input;
  bool output;
  bool warning;

  return getGpioInfo(gpio, pinnr, input, output, warning);
}

// Get ADC related info for a given GPIO pin
// @param gpio_pin   GPIO pin number
// @param adc        Number of ADC unit (0 == Hall effect)
// @param ch         Channel number on ADC unit
// @param t          index of touch pad ID
bool getADC_gpio_info(int gpio_pin, int& adc, int& ch, int& t)
{
  adc = -1;
  ch  = -1;
  t   = -1;

  switch (gpio_pin) {
    case 1: adc  = 1; ch = 0; t = 1; break;
    case 2: adc  = 1; ch = 1; t = 2; break;
    case 3: adc  = 1; ch = 2; t = 3; break;
    case 4: adc  = 1; ch = 3; t = 4; break;
    case 5: adc  = 1; ch = 4; t = 5; break;
    case 6: adc  = 1; ch = 5; t = 6; break;
    case 7: adc  = 1; ch = 6; t = 7; break;
    case 8: adc  = 1; ch = 7; t = 8; break;
    case 9: adc  = 1; ch = 8; t = 9; break;
    case 10: adc = 1; ch = 9; t = 10; break;
    case 11: adc = 2; ch = 0; t = 11; break;
    case 12: adc = 2; ch = 1; t = 12; break;
    case 13: adc = 2; ch = 2; t = 13; break;
    case 14: adc = 2; ch = 3; t = 14; break;
    case 15: adc = 2; ch = 4;  break;
    case 16: adc = 2; ch = 5;  break;
    case 17: adc = 2; ch = 6;  break;
    case 18: adc = 2; ch = 7;  break;
    case 19: adc = 2; ch = 8;  break;
    case 20: adc = 2; ch = 9;  break;
    default:
      return false;
  }

  return true;
}

#if SOC_TOUCH_SENSOR_SUPPORTED
int touchPinToGpio(int touch_pin)
{
  switch (touch_pin) {
    case 1: return T1;
    case 2: return T2;
    case 3: return T3;
    case 4: return T4;
    case 5: return T5;
    case 6: return T6;
    case 7: return T7;
    case 8: return T8;
    case 9: return T9;
    case 10: return T10;
    case 11: return T11;
    case 12: return T12;
    case 13: return T13;
    case 14: return T14;
    default:
      break;
  }

  return -1;
}
#endif

// Get DAC related info for a given GPIO pin
// @param gpio_pin   GPIO pin number
// @param dac        Number of DAC unit
bool getDAC_gpio_info(int gpio_pin, int& dac)
{
  // ESP32-C3, ESP32-S3, ESP32-C2, ESP32-C6 and ESP32-H2 don't have a DAC onboard
  return false;
}

#endif // ifdef ESP32S3

#include "../Helpers/OTA.h"

#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../ESPEasyCore/Serial.h"
#include "../Globals/SecuritySettings.h"
#include "../Globals/Services.h"
#include "../Globals/Settings.h"
#include "../Helpers/FS_Helper.h"
#include "../Helpers/Hardware_device_info.h"
#include "../Helpers/Misc.h"
#include "../Helpers/StringConverter.h"

#if FEATURE_ARDUINO_OTA
  //enable Arduino OTA updating.
  //Note: This adds around 10kb to the firmware size, and 1kb extra ram.
  #include <ArduinoOTA.h>
#endif


#ifndef ARDUINO_OTA_PORT
  #if defined(ESP32)
    #define ARDUINO_OTA_PORT  3232
  #else
    // Do not use port 8266 for OTA, since that's used for ESPeasy p2p
    #define ARDUINO_OTA_PORT  18266
  #endif
#endif


bool OTA_possible(uint32_t& maxSketchSize, bool& use2step) {
#if defined(ESP8266)

  #define SMALLEST_OTA_IMAGE 276848 // smallest known 2-step OTA image
  #define MAX_SKETCH_SIZE 1044464   // 1020 kB - 16 bytes


  // Compute the current free space and sketch size, rounded to 4k blocks.
  // These block bounaries are needed for erasing a full block on flash.
  const uint32_t freeSketchSpace            = (getFreeSketchSpace() - 0x1000) & 0xFFFFF000;
  const uint32_t currentSketchSize          = (getSketchSize() + 0x1000) & 0xFFFFF000;
  const uint32_t smallestOtaImageSizeNeeded = (((SMALLEST_OTA_IMAGE + 16) + 0x1000) & 0xFFFFF000);
  const bool     otaPossible                = freeSketchSpace >= smallestOtaImageSizeNeeded;
  use2step = freeSketchSpace < currentSketchSize; // Assume the new image has the same size.

  if (use2step) {
    const uint32_t totalSketchSpace = freeSketchSpace + currentSketchSize;
    maxSketchSize = totalSketchSpace - smallestOtaImageSizeNeeded;
  } else {
    maxSketchSize = freeSketchSpace;
  }
  maxSketchSize -= 16; // Must leave 16 bytes at the end.

  if (maxSketchSize > MAX_SKETCH_SIZE) { maxSketchSize = MAX_SKETCH_SIZE; }
  return otaPossible;
#elif defined(ESP32)
  // ESP32 writes an OTA image to the "other" app partition.
  // Thus what is reported as "free" sketch space is the size of the not used app partition.
  maxSketchSize = getFreeSketchSpace();
  use2step      = false;
  return true;
#else // if defined(ESP8266)
  return false;
#endif // if defined(ESP8266)
}

#if FEATURE_ARDUINO_OTA

/********************************************************************************************\
   Allow updating via the Arduino OTA-protocol. (this allows you to upload directly from platformio)
 \*********************************************************************************************/
void ArduinoOTAInit()
{
  if (Settings.ArduinoOTAEnable) {
    # ifndef BUILD_NO_RAM_TRACKER
    checkRAM(F("ArduinoOTAInit"));
    # endif // ifndef BUILD_NO_RAM_TRACKER

    ArduinoOTA.setPort(ARDUINO_OTA_PORT);
    ArduinoOTA.setHostname(Settings.getHostname().c_str());

    if (SecuritySettings.Password[0] != 0) {
      ArduinoOTA.setPassword(SecuritySettings.Password);
    }

    ArduinoOTA.onStart([]() {
      serialPrintln(F("OTA  : Start upload"));
      ArduinoOTAtriggered = true;
      ESPEASY_FS.end(); // important, otherwise it fails
    });

    ArduinoOTA.onEnd([]() {
      serialPrintln(F("\nOTA  : End"));

      // "dangerous": if you reset during flash you have to reflash via serial
      // so dont touch device until restart is complete
      serialPrintln(F("\nOTA  : DO NOT RESET OR POWER OFF UNTIL BOOT+FLASH IS COMPLETE."));

      // delay(100);
      // reboot(); //Not needed, node reboots automaticall after calling onEnd and succesfully flashing
    });
    ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
      if (Settings.UseSerial) {
        serialPrintln(concat(F("OTA  : Progress "), (progress / (total / 100))));
      }
    });

    ArduinoOTA.onError([](ota_error_t error) {
      serialPrint(F("\nOTA  : Error (will reboot): "));

      if (error == OTA_AUTH_ERROR) { serialPrintln(F("Auth Failed")); }
      else if (error == OTA_BEGIN_ERROR) { serialPrintln(F("Begin Failed")); }
      else if (error == OTA_CONNECT_ERROR) { serialPrintln(F("Connect Failed")); }
      else if (error == OTA_RECEIVE_ERROR) { serialPrintln(F("Receive Failed")); }
      else if (error == OTA_END_ERROR) { serialPrintln(F("End Failed")); }

      delay(100);
      reboot(IntendedRebootReason_e::OTA_error);
    });

    #if defined(ESP8266) && FEATURE_MDNS
    ArduinoOTA.begin(true);
    #else
    ArduinoOTA.begin();
    #endif

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      String log = F("OTA  : Arduino OTA enabled on port ");
      log += ARDUINO_OTA_PORT;
      addLogMove(LOG_LEVEL_INFO, log);
    }
  }
}

void ArduinoOTA_handle()
{
  ArduinoOTA.handle();
}

#endif // if FEATURE_ARDUINO_OTA

#include "../Helpers/Hardware_GPIO.h"

#ifdef ESP32C2
# include <driver/gpio.h>

# include "../Helpers/Hardware_device_info.h"

// ********************************************************************************
// Get info of a specific GPIO pin
// ********************************************************************************

bool getGpioInfo(int gpio, int& pinnr, bool& input, bool& output, bool& warning) {
  pinnr = -1; // ESP32 does not label the pins, they just use the GPIO number.

  input   = GPIO_IS_VALID_GPIO(gpio);
  output  = GPIO_IS_VALID_OUTPUT_GPIO(gpio);
  warning = isBootStrapPin(gpio);

  if (!(GPIO_IS_VALID_GPIO(gpio))) { return false; }

  if (gpio == 11) {
    // By default VDD_SPI is the power supply pin for embedded flash or external flash. It can only be used as GPIO11
    // only when the chip is connected to an external flash, and this flash is powered by an external power supply
    input   = false;
    output  = false;
    warning = true;
  }

  if (isFlashInterfacePin_ESPEasy(gpio)) {
    // Connected to the integrated SPI flash.
    input   = false;
    output  = false;
    warning = true;
  }

  return (input || output);
}

bool isBootStrapPin(int gpio)
{
  if (gpio == 8) {
    // Strapping pin which must be high during flashing
    return true;
  }

  if (gpio == 9) {
    // Strapping pin to force download mode (like GPIO-0 on ESP8266/ESP32-classic)
    return true;
  }
  return false;
}

bool getGpioPullResistor(int gpio, bool& hasPullUp, bool& hasPullDown) {
  hasPullDown = false;
  hasPullUp   = false;

  if (validGpio(gpio)) {
    hasPullUp   = true;
    hasPullDown = true;
  }
  return true;
}

bool validGpio(int gpio) {
  if (!GPIO_IS_VALID_GPIO(gpio)) { return false; }

  int  pinnr;
  bool input;
  bool output;
  bool warning;

  return getGpioInfo(gpio, pinnr, input, output, warning);
}

// Get ADC related info for a given GPIO pin
// @param gpio_pin   GPIO pin number
// @param adc        Number of ADC unit (0 == Hall effect)
// @param ch         Channel number on ADC unit
// @param t          index of touch pad ID
bool getADC_gpio_info(int gpio_pin, int& adc, int& ch, int& t)
{
  adc = -1;
  ch  = -1;
  t   = -1;

  if ((gpio_pin >= 0) && (gpio_pin <= 4)) {
    adc = 1;
    ch  = gpio_pin;
    return true;
  }
  return false;
}

#if SOC_TOUCH_SENSOR_SUPPORTED
int touchPinToGpio(int touch_pin)
{
  // No touch pin support
  return -1;
}
#endif

#if SOC_DAC_SUPPORTED
// Get DAC related info for a given GPIO pin
// @param gpio_pin   GPIO pin number
// @param dac        Number of DAC unit
bool getDAC_gpio_info(int gpio_pin, int& dac)
{
  // ESP32-C3, ESP32-S3, ESP32-C2, ESP32-C6 and ESP32-H2 don't have a DAC onboard
  return false;
}
#endif

#endif // ifdef ESP32C2

#include "../Helpers/_Plugin_init.h"

#include "../../ESPEasy_common.h"

#include "../Globals/Device.h"
#include "../Globals/Settings.h"

#include "../Helpers/Misc.h"


// ********************************************************************************
// Initialize all plugins that where defined earlier
// and initialize the function call pointer into the plugin array
// ********************************************************************************



// Vector to match a "DeviceIndex" to a plugin ID.
constexpr /*pluginID_t*/ uint8_t DeviceIndex_to_Plugin_id[] PROGMEM =
{
#ifdef USES_P001
  1,
#endif // ifdef USES_P001

#ifdef USES_P002
  2,
#endif // ifdef USES_P002

#ifdef USES_P003
  3,
#endif // ifdef USES_P003

#ifdef USES_P004
  4,
#endif // ifdef USES_P004

#ifdef USES_P005
  5,
#endif // ifdef USES_P005

#ifdef USES_P006
  6,
#endif // ifdef USES_P006

#ifdef USES_P007
  7,
#endif // ifdef USES_P007

#ifdef USES_P008
  8,
#endif // ifdef USES_P008

#ifdef USES_P009
  9,
#endif // ifdef USES_P009

#ifdef USES_P010
  10,
#endif // ifdef USES_P010

#ifdef USES_P011
  11,
#endif // ifdef USES_P011

#ifdef USES_P012
  12,
#endif // ifdef USES_P012

#ifdef USES_P013
  13,
#endif // ifdef USES_P013

#ifdef USES_P014
  14,
#endif // ifdef USES_P014

#ifdef USES_P015
  15,
#endif // ifdef USES_P015

#ifdef USES_P016
  16,
#endif // ifdef USES_P016

#ifdef USES_P017
  17,
#endif // ifdef USES_P017

#ifdef USES_P018
  18,
#endif // ifdef USES_P018

#ifdef USES_P019
  19,
#endif // ifdef USES_P019

#ifdef USES_P020
  20,
#endif // ifdef USES_P020

#ifdef USES_P021
  21,
#endif // ifdef USES_P021

#ifdef USES_P022
  22,
#endif // ifdef USES_P022

#ifdef USES_P023
  23,
#endif // ifdef USES_P023

#ifdef USES_P024
  24,
#endif // ifdef USES_P024

#ifdef USES_P025
  25,
#endif // ifdef USES_P025

#ifdef USES_P026
  26,
#endif // ifdef USES_P026

#ifdef USES_P027
  27,
#endif // ifdef USES_P027

#ifdef USES_P028
  28,
#endif // ifdef USES_P028

#ifdef USES_P029
  29,
#endif // ifdef USES_P029

#ifdef USES_P030
  30,
#endif // ifdef USES_P030

#ifdef USES_P031
  31,
#endif // ifdef USES_P031

#ifdef USES_P032
  32,
#endif // ifdef USES_P032

#ifdef USES_P033
  33,
#endif // ifdef USES_P033

#ifdef USES_P034
  34,
#endif // ifdef USES_P034

#ifdef USES_P035
  35,
#endif // ifdef USES_P035

#ifdef USES_P036
  36,
#endif // ifdef USES_P036

#ifdef USES_P037
  37,
#endif // ifdef USES_P037

#ifdef USES_P038
  38,
#endif // ifdef USES_P038

#ifdef USES_P039
  39,
#endif // ifdef USES_P039

#ifdef USES_P040
  40,
#endif // ifdef USES_P040

#ifdef USES_P041
  41,
#endif // ifdef USES_P041

#ifdef USES_P042
  42,
#endif // ifdef USES_P042

#ifdef USES_P043
  43,
#endif // ifdef USES_P043

#ifdef USES_P044
  44,
#endif // ifdef USES_P044

#ifdef USES_P045
  45,
#endif // ifdef USES_P045

#ifdef USES_P046
  46,
#endif // ifdef USES_P046

#ifdef USES_P047
  47,
#endif // ifdef USES_P047

#ifdef USES_P048
  48,
#endif // ifdef USES_P048

#ifdef USES_P049
  49,
#endif // ifdef USES_P049

#ifdef USES_P050
  50,
#endif // ifdef USES_P050

#ifdef USES_P051
  51,
#endif // ifdef USES_P051

#ifdef USES_P052
  52,
#endif // ifdef USES_P052

#ifdef USES_P053
  53,
#endif // ifdef USES_P053

#ifdef USES_P054
  54,
#endif // ifdef USES_P054

#ifdef USES_P055
  55,
#endif // ifdef USES_P055

#ifdef USES_P056
  56,
#endif // ifdef USES_P056

#ifdef USES_P057
  57,
#endif // ifdef USES_P057

#ifdef USES_P058
  58,
#endif // ifdef USES_P058

#ifdef USES_P059
  59,
#endif // ifdef USES_P059

#ifdef USES_P060
  60,
#endif // ifdef USES_P060

#ifdef USES_P061
  61,
#endif // ifdef USES_P061

#ifdef USES_P062
  62,
#endif // ifdef USES_P062

#ifdef USES_P063
  63,
#endif // ifdef USES_P063

#ifdef USES_P064
  64,
#endif // ifdef USES_P064

#ifdef USES_P065
  65,
#endif // ifdef USES_P065

#ifdef USES_P066
  66,
#endif // ifdef USES_P066

#ifdef USES_P067
  67,
#endif // ifdef USES_P067

#ifdef USES_P068
  68,
#endif // ifdef USES_P068

#ifdef USES_P069
  69,
#endif // ifdef USES_P069

#ifdef USES_P070
  70,
#endif // ifdef USES_P070

#ifdef USES_P071
  71,
#endif // ifdef USES_P071

#ifdef USES_P072
  72,
#endif // ifdef USES_P072

#ifdef USES_P073
  73,
#endif // ifdef USES_P073

#ifdef USES_P074
  74,
#endif // ifdef USES_P074

#ifdef USES_P075
  75,
#endif // ifdef USES_P075

#ifdef USES_P076
  76,
#endif // ifdef USES_P076

#ifdef USES_P077
  77,
#endif // ifdef USES_P077

#ifdef USES_P078
  78,
#endif // ifdef USES_P078

#ifdef USES_P079
  79,
#endif // ifdef USES_P079

#ifdef USES_P080
  80,
#endif // ifdef USES_P080

#ifdef USES_P081
  81,
#endif // ifdef USES_P081

#ifdef USES_P082
  82,
#endif // ifdef USES_P082

#ifdef USES_P083
  83,
#endif // ifdef USES_P083

#ifdef USES_P084
  84,
#endif // ifdef USES_P084

#ifdef USES_P085
  85,
#endif // ifdef USES_P085

#ifdef USES_P086
  86,
#endif // ifdef USES_P086

#ifdef USES_P087
  87,
#endif // ifdef USES_P087

#ifdef USES_P088
  88,
#endif // ifdef USES_P088

#ifdef USES_P089
  89,
#endif // ifdef USES_P089

#ifdef USES_P090
  90,
#endif // ifdef USES_P090

#ifdef USES_P091
  91,
#endif // ifdef USES_P091

#ifdef USES_P092
  92,
#endif // ifdef USES_P092

#ifdef USES_P093
  93,
#endif // ifdef USES_P093

#ifdef USES_P094
  94,
#endif // ifdef USES_P094

#ifdef USES_P095
  95,
#endif // ifdef USES_P095

#ifdef USES_P096
  96,
#endif // ifdef USES_P096

#ifdef USES_P097
  // Touch (ESP32)
  97,
#endif // ifdef USES_P097

#ifdef USES_P098
  98,
#endif // ifdef USES_P098

#ifdef USES_P099
  99,
#endif // ifdef USES_P099

#ifdef USES_P100
  100,
#endif // ifdef USES_P100

#ifdef USES_P101
  101,
#endif // ifdef USES_P101

#ifdef USES_P102
  102,
#endif // ifdef USES_P102

#ifdef USES_P103
  103,
#endif // ifdef USES_P103

#ifdef USES_P104
  104,
#endif // ifdef USES_P104

#ifdef USES_P105
  105,
#endif // ifdef USES_P105

#ifdef USES_P106
  106,
#endif // ifdef USES_P106

#ifdef USES_P107
  107,
#endif // ifdef USES_P107

#ifdef USES_P108
  108,
#endif // ifdef USES_P108

#ifdef USES_P109
  109,
#endif // ifdef USES_P109

#ifdef USES_P110
  110,
#endif // ifdef USES_P110

#ifdef USES_P111
  111,
#endif // ifdef USES_P111

#ifdef USES_P112
  112,
#endif // ifdef USES_P112

#ifdef USES_P113
  113,
#endif // ifdef USES_P113

#ifdef USES_P114
  114,
#endif // ifdef USES_P114

#ifdef USES_P115
  115,
#endif // ifdef USES_P115

#ifdef USES_P116
  116,
#endif // ifdef USES_P116

#ifdef USES_P117
  117,
#endif // ifdef USES_P117

#ifdef USES_P118
  118,
#endif // ifdef USES_P118

#ifdef USES_P119
  119,
#endif // ifdef USES_P119

#ifdef USES_P120
  120,
#endif // ifdef USES_P120

#ifdef USES_P121
  121,
#endif // ifdef USES_P121

#ifdef USES_P122
  122,
#endif // ifdef USES_P122

#ifdef USES_P123
  123,
#endif // ifdef USES_P123

#ifdef USES_P124
  124,
#endif // ifdef USES_P124

#ifdef USES_P125
  125,
#endif // ifdef USES_P125

#ifdef USES_P126
  126,
#endif // ifdef USES_P126

#ifdef USES_P127
  127,
#endif // ifdef USES_P127

#ifdef USES_P128
  128,
#endif // ifdef USES_P128

#ifdef USES_P129
  129,
#endif // ifdef USES_P129

#ifdef USES_P130
  130,
#endif // ifdef USES_P130

#ifdef USES_P131
  131,
#endif // ifdef USES_P131

#ifdef USES_P132
  132,
#endif // ifdef USES_P132

#ifdef USES_P133
  133,
#endif // ifdef USES_P133

#ifdef USES_P134
  134,
#endif // ifdef USES_P134

#ifdef USES_P135
  135,
#endif // ifdef USES_P135

#ifdef USES_P136
  136,
#endif // ifdef USES_P136

#ifdef USES_P137
  137,
#endif // ifdef USES_P137

#ifdef USES_P138
  138,
#endif // ifdef USES_P138

#ifdef USES_P139
  139,
#endif // ifdef USES_P139

#ifdef USES_P140
  140,
#endif // ifdef USES_P140

#ifdef USES_P141
  141,
#endif // ifdef USES_P141

#ifdef USES_P142
  142,
#endif // ifdef USES_P142

#ifdef USES_P143
  143,
#endif // ifdef USES_P143

#ifdef USES_P144
  144,
#endif // ifdef USES_P144

#ifdef USES_P145
  145,
#endif // ifdef USES_P145

#ifdef USES_P146
  146,
#endif // ifdef USES_P146

#ifdef USES_P147
  147,
#endif // ifdef USES_P147

#ifdef USES_P148
  148,
#endif // ifdef USES_P148

#ifdef USES_P149
  149,
#endif // ifdef USES_P149

#ifdef USES_P150
  150,
#endif // ifdef USES_P150

#ifdef USES_P151
  151,
#endif // ifdef USES_P151

#ifdef USES_P152
  152,
#endif // ifdef USES_P152

#ifdef USES_P153
  153,
#endif // ifdef USES_P153

#ifdef USES_P154
  154,
#endif // ifdef USES_P154

#ifdef USES_P155
  155,
#endif // ifdef USES_P155

#ifdef USES_P156
  156,
#endif // ifdef USES_P156

#ifdef USES_P157
  157,
#endif // ifdef USES_P157

#ifdef USES_P158
  158,
#endif // ifdef USES_P158

#ifdef USES_P159
  159,
#endif // ifdef USES_P159

#ifdef USES_P160
  160,
#endif // ifdef USES_P160

#ifdef USES_P161
  161,
#endif // ifdef USES_P161

#ifdef USES_P162
  162,
#endif // ifdef USES_P162

#ifdef USES_P163
  163,
#endif // ifdef USES_P163

#ifdef USES_P164
  164,
#endif // ifdef USES_P164

#ifdef USES_P165
  165,
#endif // ifdef USES_P165

#ifdef USES_P166
  166,
#endif // ifdef USES_P166

#ifdef USES_P167
  167,
#endif // ifdef USES_P167

#ifdef USES_P168
  168,
#endif // ifdef USES_P168

#ifdef USES_P169
  169,
#endif // ifdef USES_P169

#ifdef USES_P170
  170,
#endif // ifdef USES_P170

#ifdef USES_P171
  171,
#endif // ifdef USES_P171

#ifdef USES_P172
  172,
#endif // ifdef USES_P172

#ifdef USES_P173
  173,
#endif // ifdef USES_P173

#ifdef USES_P174
  174,
#endif // ifdef USES_P174

#ifdef USES_P175
  175,
#endif // ifdef USES_P175

#ifdef USES_P176
  176,
#endif // ifdef USES_P176

#ifdef USES_P177
  177,
#endif // ifdef USES_P177

#ifdef USES_P178
  178,
#endif // ifdef USES_P178

#ifdef USES_P179
  179,
#endif // ifdef USES_P179

#ifdef USES_P180
  180,
#endif // ifdef USES_P180

#ifdef USES_P181
  181,
#endif // ifdef USES_P181

#ifdef USES_P182
  182,
#endif // ifdef USES_P182

#ifdef USES_P183
  183,
#endif // ifdef USES_P183

#ifdef USES_P184
  184,
#endif // ifdef USES_P184

#ifdef USES_P185
  185,
#endif // ifdef USES_P185

#ifdef USES_P186
  186,
#endif // ifdef USES_P186

#ifdef USES_P187
  187,
#endif // ifdef USES_P187

#ifdef USES_P188
  188,
#endif // ifdef USES_P188

#ifdef USES_P189
  189,
#endif // ifdef USES_P189

#ifdef USES_P190
  190,
#endif // ifdef USES_P190

#ifdef USES_P191
  191,
#endif // ifdef USES_P191

#ifdef USES_P192
  192,
#endif // ifdef USES_P192

#ifdef USES_P193
  193,
#endif // ifdef USES_P193

#ifdef USES_P194
  194,
#endif // ifdef USES_P194

#ifdef USES_P195
  195,
#endif // ifdef USES_P195

#ifdef USES_P196
  196,
#endif // ifdef USES_P196

#ifdef USES_P197
  197,
#endif // ifdef USES_P197

#ifdef USES_P198
  198,
#endif // ifdef USES_P198

#ifdef USES_P199
  199,
#endif // ifdef USES_P199

#ifdef USES_P200
  200,
#endif // ifdef USES_P200

#ifdef USES_P201
  201,
#endif // ifdef USES_P201

#ifdef USES_P202
  202,
#endif // ifdef USES_P202

#ifdef USES_P203
  203,
#endif // ifdef USES_P203

#ifdef USES_P204
  204,
#endif // ifdef USES_P204

#ifdef USES_P205
  205,
#endif // ifdef USES_P205

#ifdef USES_P206
  206,
#endif // ifdef USES_P206

#ifdef USES_P207
  207,
#endif // ifdef USES_P207

#ifdef USES_P208
  208,
#endif // ifdef USES_P208

#ifdef USES_P209
  209,
#endif // ifdef USES_P209

#ifdef USES_P210
  210,
#endif // ifdef USES_P210

#ifdef USES_P211
  211,
#endif // ifdef USES_P211

#ifdef USES_P212
  212,
#endif // ifdef USES_P212

#ifdef USES_P213
  213,
#endif // ifdef USES_P213

#ifdef USES_P214
  214,
#endif // ifdef USES_P214

#ifdef USES_P215
  215,
#endif // ifdef USES_P215

#ifdef USES_P216
  216,
#endif // ifdef USES_P216

#ifdef USES_P217
  217,
#endif // ifdef USES_P217

#ifdef USES_P218
  218,
#endif // ifdef USES_P218

#ifdef USES_P219
  219,
#endif // ifdef USES_P219

#ifdef USES_P220
  220,
#endif // ifdef USES_P220

#ifdef USES_P221
  221,
#endif // ifdef USES_P221

#ifdef USES_P222
  222,
#endif // ifdef USES_P222

#ifdef USES_P223
  223,
#endif // ifdef USES_P223

#ifdef USES_P224
  224,
#endif // ifdef USES_P224

#ifdef USES_P225
  225,
#endif // ifdef USES_P225

#ifdef USES_P226
  226,
#endif // ifdef USES_P226

#ifdef USES_P227
  227,
#endif // ifdef USES_P227

#ifdef USES_P228
  228,
#endif // ifdef USES_P228

#ifdef USES_P229
  229,
#endif // ifdef USES_P229

#ifdef USES_P230
  230,
#endif // ifdef USES_P230

#ifdef USES_P231
  231,
#endif // ifdef USES_P231

#ifdef USES_P232
  232,
#endif // ifdef USES_P232

#ifdef USES_P233
  233,
#endif // ifdef USES_P233

#ifdef USES_P234
  234,
#endif // ifdef USES_P234

#ifdef USES_P235
  235,
#endif // ifdef USES_P235

#ifdef USES_P236
  236,
#endif // ifdef USES_P236

#ifdef USES_P237
  237,
#endif // ifdef USES_P237

#ifdef USES_P238
  238,
#endif // ifdef USES_P238

#ifdef USES_P239
  239,
#endif // ifdef USES_P239

#ifdef USES_P240
  240,
#endif // ifdef USES_P240

#ifdef USES_P241
  241,
#endif // ifdef USES_P241

#ifdef USES_P242
  242,
#endif // ifdef USES_P242

#ifdef USES_P243
  243,
#endif // ifdef USES_P243

#ifdef USES_P244
  244,
#endif // ifdef USES_P244

#ifdef USES_P245
  245,
#endif // ifdef USES_P245

#ifdef USES_P246
  246,
#endif // ifdef USES_P246

#ifdef USES_P247
  247,
#endif // ifdef USES_P247

#ifdef USES_P248
  248,
#endif // ifdef USES_P248

#ifdef USES_P249
  249,
#endif // ifdef USES_P249

#ifdef USES_P250
  250,
#endif // ifdef USES_P250

#ifdef USES_P251
  251,
#endif // ifdef USES_P251

#ifdef USES_P252
  252,
#endif // ifdef USES_P252

#ifdef USES_P253
  253,
#endif // ifdef USES_P253

#ifdef USES_P254
  254,
#endif // ifdef USES_P254

#ifdef USES_P255
  255,
#endif // ifdef USES_P255
};

typedef boolean (*Plugin_ptr_t)(uint8_t,
                        struct EventStruct *,
                        String&);

// Array of function pointers to call plugins.
constexpr const Plugin_ptr_t PROGMEM Plugin_ptr[] =
{
#ifdef USES_P001
  &Plugin_001,
#endif // ifdef USES_P001

#ifdef USES_P002
  &Plugin_002,
#endif // ifdef USES_P002

#ifdef USES_P003
  &Plugin_003,
#endif // ifdef USES_P003

#ifdef USES_P004
  &Plugin_004,
#endif // ifdef USES_P004

#ifdef USES_P005
  &Plugin_005,
#endif // ifdef USES_P005

#ifdef USES_P006
  &Plugin_006,
#endif // ifdef USES_P006

#ifdef USES_P007
  &Plugin_007,
#endif // ifdef USES_P007

#ifdef USES_P008
  &Plugin_008,
#endif // ifdef USES_P008

#ifdef USES_P009
  &Plugin_009,
#endif // ifdef USES_P009

#ifdef USES_P010
  &Plugin_010,
#endif // ifdef USES_P010

#ifdef USES_P011
  &Plugin_011,
#endif // ifdef USES_P011

#ifdef USES_P012
  &Plugin_012,
#endif // ifdef USES_P012

#ifdef USES_P013
  &Plugin_013,
#endif // ifdef USES_P013

#ifdef USES_P014
  &Plugin_014,
#endif // ifdef USES_P014

#ifdef USES_P015
  &Plugin_015,
#endif // ifdef USES_P015

#ifdef USES_P016
  &Plugin_016,
#endif // ifdef USES_P016

#ifdef USES_P017
  &Plugin_017,
#endif // ifdef USES_P017

#ifdef USES_P018
  &Plugin_018,
#endif // ifdef USES_P018

#ifdef USES_P019
  &Plugin_019,
#endif // ifdef USES_P019

#ifdef USES_P020
  &Plugin_020,
#endif // ifdef USES_P020

#ifdef USES_P021
  &Plugin_021,
#endif // ifdef USES_P021

#ifdef USES_P022
  &Plugin_022,
#endif // ifdef USES_P022

#ifdef USES_P023
  &Plugin_023,
#endif // ifdef USES_P023

#ifdef USES_P024
  &Plugin_024,
#endif // ifdef USES_P024

#ifdef USES_P025
  &Plugin_025,
#endif // ifdef USES_P025

#ifdef USES_P026
  &Plugin_026,
#endif // ifdef USES_P026

#ifdef USES_P027
  &Plugin_027,
#endif // ifdef USES_P027

#ifdef USES_P028
  &Plugin_028,
#endif // ifdef USES_P028

#ifdef USES_P029
  &Plugin_029,
#endif // ifdef USES_P029

#ifdef USES_P030
  &Plugin_030,
#endif // ifdef USES_P030

#ifdef USES_P031
  &Plugin_031,
#endif // ifdef USES_P031

#ifdef USES_P032
  &Plugin_032,
#endif // ifdef USES_P032

#ifdef USES_P033
  &Plugin_033,
#endif // ifdef USES_P033

#ifdef USES_P034
  &Plugin_034,
#endif // ifdef USES_P034

#ifdef USES_P035
  &Plugin_035,
#endif // ifdef USES_P035

#ifdef USES_P036
  &Plugin_036,
#endif // ifdef USES_P036

#ifdef USES_P037
  &Plugin_037,
#endif // ifdef USES_P037

#ifdef USES_P038
  &Plugin_038,
#endif // ifdef USES_P038

#ifdef USES_P039
  &Plugin_039,
#endif // ifdef USES_P039

#ifdef USES_P040
  &Plugin_040,
#endif // ifdef USES_P040

#ifdef USES_P041
  &Plugin_041,
#endif // ifdef USES_P041

#ifdef USES_P042
  &Plugin_042,
#endif // ifdef USES_P042

#ifdef USES_P043
  &Plugin_043,
#endif // ifdef USES_P043

#ifdef USES_P044
  &Plugin_044,
#endif // ifdef USES_P044

#ifdef USES_P045
  &Plugin_045,
#endif // ifdef USES_P045

#ifdef USES_P046
  &Plugin_046,
#endif // ifdef USES_P046

#ifdef USES_P047
  &Plugin_047,
#endif // ifdef USES_P047

#ifdef USES_P048
  &Plugin_048,
#endif // ifdef USES_P048

#ifdef USES_P049
  &Plugin_049,
#endif // ifdef USES_P049

#ifdef USES_P050
  &Plugin_050,
#endif // ifdef USES_P050

#ifdef USES_P051
  &Plugin_051,
#endif // ifdef USES_P051

#ifdef USES_P052
  &Plugin_052,
#endif // ifdef USES_P052

#ifdef USES_P053
  &Plugin_053,
#endif // ifdef USES_P053

#ifdef USES_P054
  &Plugin_054,
#endif // ifdef USES_P054

#ifdef USES_P055
  &Plugin_055,
#endif // ifdef USES_P055

#ifdef USES_P056
  &Plugin_056,
#endif // ifdef USES_P056

#ifdef USES_P057
  &Plugin_057,
#endif // ifdef USES_P057

#ifdef USES_P058
  &Plugin_058,
#endif // ifdef USES_P058

#ifdef USES_P059
  &Plugin_059,
#endif // ifdef USES_P059

#ifdef USES_P060
  &Plugin_060,
#endif // ifdef USES_P060

#ifdef USES_P061
  &Plugin_061,
#endif // ifdef USES_P061

#ifdef USES_P062
  &Plugin_062,
#endif // ifdef USES_P062

#ifdef USES_P063
  &Plugin_063,
#endif // ifdef USES_P063

#ifdef USES_P064
  &Plugin_064,
#endif // ifdef USES_P064

#ifdef USES_P065
  &Plugin_065,
#endif // ifdef USES_P065

#ifdef USES_P066
  &Plugin_066,
#endif // ifdef USES_P066

#ifdef USES_P067
  &Plugin_067,
#endif // ifdef USES_P067

#ifdef USES_P068
  &Plugin_068,
#endif // ifdef USES_P068

#ifdef USES_P069
  &Plugin_069,
#endif // ifdef USES_P069

#ifdef USES_P070
  &Plugin_070,
#endif // ifdef USES_P070

#ifdef USES_P071
  &Plugin_071,
#endif // ifdef USES_P071

#ifdef USES_P072
  &Plugin_072,
#endif // ifdef USES_P072

#ifdef USES_P073
  &Plugin_073,
#endif // ifdef USES_P073

#ifdef USES_P074
  &Plugin_074,
#endif // ifdef USES_P074

#ifdef USES_P075
  &Plugin_075,
#endif // ifdef USES_P075

#ifdef USES_P076
  &Plugin_076,
#endif // ifdef USES_P076

#ifdef USES_P077
  &Plugin_077,
#endif // ifdef USES_P077

#ifdef USES_P078
  &Plugin_078,
#endif // ifdef USES_P078

#ifdef USES_P079
  &Plugin_079,
#endif // ifdef USES_P079

#ifdef USES_P080
  &Plugin_080,
#endif // ifdef USES_P080

#ifdef USES_P081
  &Plugin_081,
#endif // ifdef USES_P081

#ifdef USES_P082
  &Plugin_082,
#endif // ifdef USES_P082

#ifdef USES_P083
  &Plugin_083,
#endif // ifdef USES_P083

#ifdef USES_P084
  &Plugin_084,
#endif // ifdef USES_P084

#ifdef USES_P085
  &Plugin_085,
#endif // ifdef USES_P085

#ifdef USES_P086
  &Plugin_086,
#endif // ifdef USES_P086

#ifdef USES_P087
  &Plugin_087,
#endif // ifdef USES_P087

#ifdef USES_P088
  &Plugin_088,
#endif // ifdef USES_P088

#ifdef USES_P089
  &Plugin_089,
#endif // ifdef USES_P089

#ifdef USES_P090
  &Plugin_090,
#endif // ifdef USES_P090

#ifdef USES_P091
  &Plugin_091,
#endif // ifdef USES_P091

#ifdef USES_P092
  &Plugin_092,
#endif // ifdef USES_P092

#ifdef USES_P093
  &Plugin_093,
#endif // ifdef USES_P093

#ifdef USES_P094
  &Plugin_094,
#endif // ifdef USES_P094

#ifdef USES_P095
  &Plugin_095,
#endif // ifdef USES_P095

#ifdef USES_P096
  &Plugin_096,
#endif // ifdef USES_P096

#ifdef USES_P097
  // Touch (ESP32)
  &Plugin_097,
#endif // ifdef USES_P097

#ifdef USES_P098
  &Plugin_098,
#endif // ifdef USES_P098

#ifdef USES_P099
  &Plugin_099,
#endif // ifdef USES_P099

#ifdef USES_P100
  &Plugin_100,
#endif // ifdef USES_P100

#ifdef USES_P101
  &Plugin_101,
#endif // ifdef USES_P101

#ifdef USES_P102
  &Plugin_102,
#endif // ifdef USES_P102

#ifdef USES_P103
  &Plugin_103,
#endif // ifdef USES_P103

#ifdef USES_P104
  &Plugin_104,
#endif // ifdef USES_P104

#ifdef USES_P105
  &Plugin_105,
#endif // ifdef USES_P105

#ifdef USES_P106
  &Plugin_106,
#endif // ifdef USES_P106

#ifdef USES_P107
  &Plugin_107,
#endif // ifdef USES_P107

#ifdef USES_P108
  &Plugin_108,
#endif // ifdef USES_P108

#ifdef USES_P109
  &Plugin_109,
#endif // ifdef USES_P109

#ifdef USES_P110
  &Plugin_110,
#endif // ifdef USES_P110

#ifdef USES_P111
  &Plugin_111,
#endif // ifdef USES_P111

#ifdef USES_P112
  &Plugin_112,
#endif // ifdef USES_P112

#ifdef USES_P113
  &Plugin_113,
#endif // ifdef USES_P113

#ifdef USES_P114
  &Plugin_114,
#endif // ifdef USES_P114

#ifdef USES_P115
  &Plugin_115,
#endif // ifdef USES_P115

#ifdef USES_P116
  &Plugin_116,
#endif // ifdef USES_P116

#ifdef USES_P117
  &Plugin_117,
#endif // ifdef USES_P117

#ifdef USES_P118
  &Plugin_118,
#endif // ifdef USES_P118

#ifdef USES_P119
  &Plugin_119,
#endif // ifdef USES_P119

#ifdef USES_P120
  &Plugin_120,
#endif // ifdef USES_P120

#ifdef USES_P121
  &Plugin_121,
#endif // ifdef USES_P121

#ifdef USES_P122
  &Plugin_122,
#endif // ifdef USES_P122

#ifdef USES_P123
  &Plugin_123,
#endif // ifdef USES_P123

#ifdef USES_P124
  &Plugin_124,
#endif // ifdef USES_P124

#ifdef USES_P125
  &Plugin_125,
#endif // ifdef USES_P125

#ifdef USES_P126
  &Plugin_126,
#endif // ifdef USES_P126

#ifdef USES_P127
  &Plugin_127,
#endif // ifdef USES_P127

#ifdef USES_P128
  &Plugin_128,
#endif // ifdef USES_P128

#ifdef USES_P129
  &Plugin_129,
#endif // ifdef USES_P129

#ifdef USES_P130
  &Plugin_130,
#endif // ifdef USES_P130

#ifdef USES_P131
  &Plugin_131,
#endif // ifdef USES_P131

#ifdef USES_P132
  &Plugin_132,
#endif // ifdef USES_P132

#ifdef USES_P133
  &Plugin_133,
#endif // ifdef USES_P133

#ifdef USES_P134
  &Plugin_134,
#endif // ifdef USES_P134

#ifdef USES_P135
  &Plugin_135,
#endif // ifdef USES_P135

#ifdef USES_P136
  &Plugin_136,
#endif // ifdef USES_P136

#ifdef USES_P137
  &Plugin_137,
#endif // ifdef USES_P137

#ifdef USES_P138
  &Plugin_138,
#endif // ifdef USES_P138

#ifdef USES_P139
  &Plugin_139,
#endif // ifdef USES_P139

#ifdef USES_P140
  &Plugin_140,
#endif // ifdef USES_P140

#ifdef USES_P141
  &Plugin_141,
#endif // ifdef USES_P141

#ifdef USES_P142
  &Plugin_142,
#endif // ifdef USES_P142

#ifdef USES_P143
  &Plugin_143,
#endif // ifdef USES_P143

#ifdef USES_P144
  &Plugin_144,
#endif // ifdef USES_P144

#ifdef USES_P145
  &Plugin_145,
#endif // ifdef USES_P145

#ifdef USES_P146
  &Plugin_146,
#endif // ifdef USES_P146

#ifdef USES_P147
  &Plugin_147,
#endif // ifdef USES_P147

#ifdef USES_P148
  &Plugin_148,
#endif // ifdef USES_P148

#ifdef USES_P149
  &Plugin_149,
#endif // ifdef USES_P149

#ifdef USES_P150
  &Plugin_150,
#endif // ifdef USES_P150

#ifdef USES_P151
  &Plugin_151,
#endif // ifdef USES_P151

#ifdef USES_P152
  &Plugin_152,
#endif // ifdef USES_P152

#ifdef USES_P153
  &Plugin_153,
#endif // ifdef USES_P153

#ifdef USES_P154
  &Plugin_154,
#endif // ifdef USES_P154

#ifdef USES_P155
  &Plugin_155,
#endif // ifdef USES_P155

#ifdef USES_P156
  &Plugin_156,
#endif // ifdef USES_P156

#ifdef USES_P157
  &Plugin_157,
#endif // ifdef USES_P157

#ifdef USES_P158
  &Plugin_158,
#endif // ifdef USES_P158

#ifdef USES_P159
  &Plugin_159,
#endif // ifdef USES_P159

#ifdef USES_P160
  &Plugin_160,
#endif // ifdef USES_P160

#ifdef USES_P161
  &Plugin_161,
#endif // ifdef USES_P161

#ifdef USES_P162
  &Plugin_162,
#endif // ifdef USES_P162

#ifdef USES_P163
  &Plugin_163,
#endif // ifdef USES_P163

#ifdef USES_P164
  &Plugin_164,
#endif // ifdef USES_P164

#ifdef USES_P165
  &Plugin_165,
#endif // ifdef USES_P165

#ifdef USES_P166
  &Plugin_166,
#endif // ifdef USES_P166

#ifdef USES_P167
  &Plugin_167,
#endif // ifdef USES_P167

#ifdef USES_P168
  &Plugin_168,
#endif // ifdef USES_P168

#ifdef USES_P169
  &Plugin_169,
#endif // ifdef USES_P169

#ifdef USES_P170
  &Plugin_170,
#endif // ifdef USES_P170

#ifdef USES_P171
  &Plugin_171,
#endif // ifdef USES_P171

#ifdef USES_P172
  &Plugin_172,
#endif // ifdef USES_P172

#ifdef USES_P173
  &Plugin_173,
#endif // ifdef USES_P173

#ifdef USES_P174
  &Plugin_174,
#endif // ifdef USES_P174

#ifdef USES_P175
  &Plugin_175,
#endif // ifdef USES_P175

#ifdef USES_P176
  &Plugin_176,
#endif // ifdef USES_P176

#ifdef USES_P177
  &Plugin_177,
#endif // ifdef USES_P177

#ifdef USES_P178
  &Plugin_178,
#endif // ifdef USES_P178

#ifdef USES_P179
  &Plugin_179,
#endif // ifdef USES_P179

#ifdef USES_P180
  &Plugin_180,
#endif // ifdef USES_P180

#ifdef USES_P181
  &Plugin_181,
#endif // ifdef USES_P181

#ifdef USES_P182
  &Plugin_182,
#endif // ifdef USES_P182

#ifdef USES_P183
  &Plugin_183,
#endif // ifdef USES_P183

#ifdef USES_P184
  &Plugin_184,
#endif // ifdef USES_P184

#ifdef USES_P185
  &Plugin_185,
#endif // ifdef USES_P185

#ifdef USES_P186
  &Plugin_186,
#endif // ifdef USES_P186

#ifdef USES_P187
  &Plugin_187,
#endif // ifdef USES_P187

#ifdef USES_P188
  &Plugin_188,
#endif // ifdef USES_P188

#ifdef USES_P189
  &Plugin_189,
#endif // ifdef USES_P189

#ifdef USES_P190
  &Plugin_190,
#endif // ifdef USES_P190

#ifdef USES_P191
  &Plugin_191,
#endif // ifdef USES_P191

#ifdef USES_P192
  &Plugin_192,
#endif // ifdef USES_P192

#ifdef USES_P193
  &Plugin_193,
#endif // ifdef USES_P193

#ifdef USES_P194
  &Plugin_194,
#endif // ifdef USES_P194

#ifdef USES_P195
  &Plugin_195,
#endif // ifdef USES_P195

#ifdef USES_P196
  &Plugin_196,
#endif // ifdef USES_P196

#ifdef USES_P197
  &Plugin_197,
#endif // ifdef USES_P197

#ifdef USES_P198
  &Plugin_198,
#endif // ifdef USES_P198

#ifdef USES_P199
  &Plugin_199,
#endif // ifdef USES_P199

#ifdef USES_P200
  &Plugin_200,
#endif // ifdef USES_P200

#ifdef USES_P201
  &Plugin_201,
#endif // ifdef USES_P201

#ifdef USES_P202
  &Plugin_202,
#endif // ifdef USES_P202

#ifdef USES_P203
  &Plugin_203,
#endif // ifdef USES_P203

#ifdef USES_P204
  &Plugin_204,
#endif // ifdef USES_P204

#ifdef USES_P205
  &Plugin_205,
#endif // ifdef USES_P205

#ifdef USES_P206
  &Plugin_206,
#endif // ifdef USES_P206

#ifdef USES_P207
  &Plugin_207,
#endif // ifdef USES_P207

#ifdef USES_P208
  &Plugin_208,
#endif // ifdef USES_P208

#ifdef USES_P209
  &Plugin_209,
#endif // ifdef USES_P209

#ifdef USES_P210
  &Plugin_210,
#endif // ifdef USES_P210

#ifdef USES_P211
  &Plugin_211,
#endif // ifdef USES_P211

#ifdef USES_P212
  &Plugin_212,
#endif // ifdef USES_P212

#ifdef USES_P213
  &Plugin_213,
#endif // ifdef USES_P213

#ifdef USES_P214
  &Plugin_214,
#endif // ifdef USES_P214

#ifdef USES_P215
  &Plugin_215,
#endif // ifdef USES_P215

#ifdef USES_P216
  &Plugin_216,
#endif // ifdef USES_P216

#ifdef USES_P217
  &Plugin_217,
#endif // ifdef USES_P217

#ifdef USES_P218
  &Plugin_218,
#endif // ifdef USES_P218

#ifdef USES_P219
  &Plugin_219,
#endif // ifdef USES_P219

#ifdef USES_P220
  &Plugin_220,
#endif // ifdef USES_P220

#ifdef USES_P221
  &Plugin_221,
#endif // ifdef USES_P221

#ifdef USES_P222
  &Plugin_222,
#endif // ifdef USES_P222

#ifdef USES_P223
  &Plugin_223,
#endif // ifdef USES_P223

#ifdef USES_P224
  &Plugin_224,
#endif // ifdef USES_P224

#ifdef USES_P225
  &Plugin_225,
#endif // ifdef USES_P225

#ifdef USES_P226
  &Plugin_226,
#endif // ifdef USES_P226

#ifdef USES_P227
  &Plugin_227,
#endif // ifdef USES_P227

#ifdef USES_P228
  &Plugin_228,
#endif // ifdef USES_P228

#ifdef USES_P229
  &Plugin_229,
#endif // ifdef USES_P229

#ifdef USES_P230
  &Plugin_230,
#endif // ifdef USES_P230

#ifdef USES_P231
  &Plugin_231,
#endif // ifdef USES_P231

#ifdef USES_P232
  &Plugin_232,
#endif // ifdef USES_P232

#ifdef USES_P233
  &Plugin_233,
#endif // ifdef USES_P233

#ifdef USES_P234
  &Plugin_234,
#endif // ifdef USES_P234

#ifdef USES_P235
  &Plugin_235,
#endif // ifdef USES_P235

#ifdef USES_P236
  &Plugin_236,
#endif // ifdef USES_P236

#ifdef USES_P237
  &Plugin_237,
#endif // ifdef USES_P237

#ifdef USES_P238
  &Plugin_238,
#endif // ifdef USES_P238

#ifdef USES_P239
  &Plugin_239,
#endif // ifdef USES_P239

#ifdef USES_P240
  &Plugin_240,
#endif // ifdef USES_P240

#ifdef USES_P241
  &Plugin_241,
#endif // ifdef USES_P241

#ifdef USES_P242
  &Plugin_242,
#endif // ifdef USES_P242

#ifdef USES_P243
  &Plugin_243,
#endif // ifdef USES_P243

#ifdef USES_P244
  &Plugin_244,
#endif // ifdef USES_P244

#ifdef USES_P245
  &Plugin_245,
#endif // ifdef USES_P245

#ifdef USES_P246
  &Plugin_246,
#endif // ifdef USES_P246

#ifdef USES_P247
  &Plugin_247,
#endif // ifdef USES_P247

#ifdef USES_P248
  &Plugin_248,
#endif // ifdef USES_P248

#ifdef USES_P249
  &Plugin_249,
#endif // ifdef USES_P249

#ifdef USES_P250
  &Plugin_250,
#endif // ifdef USES_P250

#ifdef USES_P251
  &Plugin_251,
#endif // ifdef USES_P251

#ifdef USES_P252
  &Plugin_252,
#endif // ifdef USES_P252

#ifdef USES_P253
  &Plugin_253,
#endif // ifdef USES_P253

#ifdef USES_P254
  &Plugin_254,
#endif // ifdef USES_P254

#ifdef USES_P255
  &Plugin_255,
#endif // ifdef USES_P255
};

bool _Plugin_init_setupDone = false;


constexpr size_t DeviceIndex_to_Plugin_id_size = NR_ELEMENTS(DeviceIndex_to_Plugin_id);

// Lowest plugin ID included in the build
constexpr size_t Lowest_Plugin_id = DeviceIndex_to_Plugin_id_size == 0 ? 0 : DeviceIndex_to_Plugin_id[0];

// Highest plugin ID included in the build
constexpr size_t Highest_Plugin_id = DeviceIndex_to_Plugin_id_size > 0 ? DeviceIndex_to_Plugin_id[DeviceIndex_to_Plugin_id_size - 1] : 0;

// Array size including index of highest plugin ID.
constexpr size_t Plugin_id_to_DeviceIndex_size = Highest_Plugin_id + 1 - Lowest_Plugin_id;

// Array filled during init.
// Valid index: 1 ... Highest_Plugin_id
// Returns index to the DeviceIndex_to_Plugin_id array
//
// Vector size should is lowest pluginID ... highest pluginID
deviceIndex_t Plugin_id_to_DeviceIndex[Plugin_id_to_DeviceIndex_size]{};

// Used as lookup for getting an alfabetically sorted deviceIndex
deviceIndex_t DeviceIndex_sorted[DeviceIndex_to_Plugin_id_size];


size_t get_Plugin_id_to_DeviceIndex_arrayIndex(pluginID_t pluginID)
{
  if (pluginID.value >= Lowest_Plugin_id)
  {
    const size_t arrIndex = static_cast<size_t>(pluginID.value) - Lowest_Plugin_id;
    if (arrIndex < Plugin_id_to_DeviceIndex_size) {
      return arrIndex;
    }
  }
  return Plugin_id_to_DeviceIndex_size;
}


/*
// TD-er: Test to make constexpr array Plugin_id_to_DeviceIndex
// Have to postpone, since std::integer_sequence is not available in Espressif SDK

// Constexpr functions to create Plugin_id_to_DeviceIndex at compile time
constexpr uint8_t getDevId_from_PId(unsigned p_id, unsigned d_id) {
  return (p_id == 0 || d_id == DeviceIndex_to_Plugin_id_size) ? DEVICE_INDEX_MAX
  :  (p_id == DeviceIndex_to_Plugin_id[d_id]) ? d_id : getDevId_from_PId(p_id, d_id+1);
}

constexpr uint8_t getDevId_from_PId(unsigned p_id) {
  return getDevId_from_PId(p_id, 0);
}
constexpr auto test = getDevId_from_PId(30);


#include <array>
#include <utility>
#include <type_traits>


template <unsigned... Is>
constexpr auto get_DevId_from_PId_array(std::integer_sequence<unsigned, Is...> a) -> std::array<uint8_t, a.size()> {
    std::array<uint8_t, a.size()> vals{};
    ((vals[Is] = getDevId_from_PId(Is)), ...);
    return vals;
}

constexpr auto x = get_DevId_from_PId_array(std::make_integer_sequence<unsigned, Plugin_id_to_DeviceIndex_size>{});
*/

unsigned getNrBitsDeviceIndex()
{
  // FIXME TD-er: Must somehow make this a constexpr function
  constexpr unsigned nrBits = NR_BITS(DeviceIndex_to_Plugin_id_size);
  return nrBits;
}

unsigned getNrBuiltInDeviceIndex()
{
  return DeviceIndex_to_Plugin_id_size;
}

deviceIndex_t getDeviceIndex_from_PluginID(pluginID_t pluginID)
{
  if (validPluginID(pluginID)) {
    const size_t arrayIndex = get_Plugin_id_to_DeviceIndex_arrayIndex(pluginID);
    if (arrayIndex < Plugin_id_to_DeviceIndex_size)
    {
      return Plugin_id_to_DeviceIndex[arrayIndex];
    }
  }
  return INVALID_DEVICE_INDEX;
}

pluginID_t getPluginID_from_DeviceIndex(deviceIndex_t deviceIndex)
{
  if (validDeviceIndex_init(deviceIndex))
  {
    return pluginID_t::toPluginID(pgm_read_byte(DeviceIndex_to_Plugin_id + deviceIndex.value));
  }
  return INVALID_PLUGIN_ID;
}

bool validDeviceIndex_init(deviceIndex_t deviceIndex)
{
  if (_Plugin_init_setupDone) {
    return deviceIndex < DeviceIndex_to_Plugin_id_size;
  }
  return false;
}

// Array containing "DeviceIndex" alfabetically sorted.
deviceIndex_t getDeviceIndex_sorted(deviceIndex_t deviceIndex)
{
  if (validDeviceIndex_init(deviceIndex)) {
    return DeviceIndex_sorted[deviceIndex.value];
  }
  return INVALID_DEVICE_INDEX;
}


boolean PluginCall(deviceIndex_t deviceIndex, uint8_t function, struct EventStruct *event, String& string)
{
  if (validDeviceIndex_init(deviceIndex))
  {
    Plugin_ptr_t plugin_call = (Plugin_ptr_t)pgm_read_ptr(Plugin_ptr + deviceIndex.value);
    return plugin_call(function, event, string);
  }
  return false;
}

void PluginSetup()
{
  if (_Plugin_init_setupDone) return;

  _Plugin_init_setupDone = true;

  for (size_t id = 0; id < Plugin_id_to_DeviceIndex_size; ++id)
  {
    Plugin_id_to_DeviceIndex[id] = INVALID_DEVICE_INDEX;
  }
  #ifdef ESP8266
  Device = new (std::nothrow) DeviceStruct[DeviceIndex_to_Plugin_id_size];
  #else
  Device.resize(DeviceIndex_to_Plugin_id_size);
  #endif

  for (deviceIndex_t deviceIndex; deviceIndex < DeviceIndex_to_Plugin_id_size; ++deviceIndex)
  {
    #ifdef ESP32
    Device.getDeviceStructForEdit(deviceIndex).clear();
    #elif defined(ESP8266)
    Device[deviceIndex].clear();
    #endif
    const pluginID_t pluginID = getPluginID_from_DeviceIndex(deviceIndex);

    if (validPluginID(pluginID)) { 
      const size_t arrayIndex = get_Plugin_id_to_DeviceIndex_arrayIndex(pluginID);
      if (arrayIndex < Plugin_id_to_DeviceIndex_size) {
        // Should never be outside these limits.
        Plugin_id_to_DeviceIndex[arrayIndex] = deviceIndex;
        struct EventStruct TempEvent;
        TempEvent.idx = deviceIndex.value;
        String dummy;
        PluginCall(deviceIndex, PLUGIN_DEVICE_ADD, &TempEvent, dummy);
      }
    }
  }
#ifndef BUILD_NO_RAM_TRACKER
  logMemUsageAfter(F("PLUGIN_DEVICE_ADD"));
#endif

  // ********************************************************************************
  // Device Sort routine, actual sorting alfabetically by plugin name.
  // Sorting does happen case sensitive.
  // Used in device selector dropdown.
  // ********************************************************************************

  // First fill the existing number of the DeviceIndex.
  for (deviceIndex_t x; x < DeviceIndex_to_Plugin_id_size; ++x) {
    DeviceIndex_sorted[x.value] = x;
  }

  struct
  {
    bool operator()(deviceIndex_t a, deviceIndex_t b) const { 
      return getPluginNameFromDeviceIndex(a) < 
             getPluginNameFromDeviceIndex(b); 
    }
  }
  customLess;
  std::sort(DeviceIndex_sorted, DeviceIndex_sorted + DeviceIndex_to_Plugin_id_size, customLess);
}

void PluginInit(bool priorityOnly)
{

  // Set all not supported plugins to disabled.
  for (taskIndex_t taskIndex = 0; taskIndex < TASKS_MAX; ++taskIndex) {
    if (!supportedPluginID(Settings.getPluginID_for_task(taskIndex))) {
      Settings.TaskDeviceEnabled[taskIndex] = false;
    }
  }


  if (!priorityOnly) {
    String dummy;
    PluginCall(PLUGIN_INIT_ALL, nullptr, dummy);
    #ifndef BUILD_NO_RAM_TRACKER
    logMemUsageAfter(F("PLUGIN_INIT_ALL"));
    #endif
  }
}

#include "../Helpers/Hardware_I2C.h"

#include "../Globals/Settings.h"
#include "../Globals/Statistics.h"
#include "../Helpers/Hardware_defines.h"
#include "../Helpers/Hardware_GPIO.h"
#include "../Helpers/I2C_access.h"
#include "../Helpers/StringConverter.h"


#include <Wire.h>


void initI2C() {
  // configure hardware pins according to eeprom settings.
  if (!Settings.isI2CEnabled(0)
      #if FEATURE_I2C_MULTIPLE
      && !Settings.isI2CEnabled(1)
      # if FEATURE_I2C_INTERFACE_3
      && !Settings.isI2CEnabled(2)
      # endif // if FEATURE_I2C_INTERFACE_3
      #endif // if FEATURE_I2C_MULTIPLE
      )
  {
    return;
  }
  #if !FEATURE_I2C_MULTIPLE
  const uint8_t i2cBus = 0;
  #else // if !FEATURE_I2C_MULTIPLE

  for (uint8_t i2cBus = 0; i2cBus < getI2CBusCount(); ++i2cBus)
  #endif // if !FEATURE_I2C_MULTIPLE
  {
    if (Settings.isI2CEnabled(i2cBus)) {
      #ifndef BUILD_MINIMAL_OTA
      #if !FEATURE_I2C_MULTIPLE
      addLog(LOG_LEVEL_INFO, F("INIT : I2C Bus"));
      #else // if !FEATURE_I2C_MULTIPLE
      addLog(LOG_LEVEL_INFO, concat(F("INIT : I2C Bus "), i2cBus));
      #endif // if !FEATURE_I2C_MULTIPLE
      #endif
      I2CSelectHighClockSpeed(i2cBus); // Set normal clock speed, on I2C Bus 1 (index 0)
    }
  }

  #if FEATURE_I2CMULTIPLEXER

  for (uint8_t i2cBus = 0; i2cBus < getI2CBusCount(); ++i2cBus) {
    const int8_t Multiplexer_ResetPin = Settings.getI2CMultiplexerResetPin(i2cBus);

    if (validGpio(Multiplexer_ResetPin)) { // Initialize Reset pin to High if configured
      pinMode(Multiplexer_ResetPin, OUTPUT);
      digitalWrite(Multiplexer_ResetPin, HIGH);
    }
  }
  #endif // if FEATURE_I2CMULTIPLEXER

  // I2C Watchdog boot status check
  if (Settings.WDI2CAddress != 0)
  {
    delay(500);

    #if FEATURE_I2C_MULTIPLE
    I2CSelectHighClockSpeed(Settings.getI2CInterfaceWDT());
    #endif // if FEATURE_I2C_MULTIPLE

    if (I2C_write8_reg(Settings.WDI2CAddress,
                       0x83, // command to set pointer
                       17))  // pointer value to status uint8_t
    {
      bool is_ok           = false;
      const uint8_t status = I2C_read8(Settings.WDI2CAddress, &is_ok);

      if (is_ok)
      {
        if (status & 0x1)
        {
          addLog(LOG_LEVEL_ERROR, F("INIT : Reset by WD!"));
          lastBootCause = BOOT_CAUSE_EXT_WD;
        }
      }
    }
  }
  I2CSelectHighClockSpeed(0); // Select first interface by default
}

void I2CSelectHighClockSpeed(uint8_t i2cBus) {
  const uint32_t clockSpeed = Settings.getI2CClockSpeed(i2cBus);

  I2CSelectClockSpeed(i2cBus, clockSpeed);
}

void I2CSelectLowClockSpeed(uint8_t i2cBus) {
  const uint32_t clockSpeed = Settings.getI2CClockSpeedSlow(i2cBus);

  I2CSelectClockSpeed(i2cBus, clockSpeed);
}

void I2CSelect_Max100kHz_ClockSpeed(uint8_t i2cBus) {
  const uint32_t clockSpeed      = Settings.getI2CClockSpeed(i2cBus);
  const uint32_t clockSpeed_Slow = Settings.getI2CClockSpeedSlow(i2cBus);

  if (clockSpeed <= 100000) {
    I2CSelectHighClockSpeed(i2cBus);
  } else if (clockSpeed_Slow <= 100000) {
    I2CSelectLowClockSpeed(i2cBus);
  } else {
    I2CSelectClockSpeed(i2cBus, 100000);
  }
}

void I2CSelectClockSpeed(uint8_t i2cBus, uint32_t clockFreq) {
  const int8_t   i2c_sda           = Settings.getI2CSdaPin(i2cBus);
  const int8_t   i2c_scl           = Settings.getI2CSclPin(i2cBus);
  const uint32_t ClockStretchLimit = Settings.getI2CClockStretch(i2cBus);

  I2CBegin(i2c_sda, i2c_scl, clockFreq, ClockStretchLimit);
}

void I2CForceResetBus_swap_pins(uint8_t i2cBus, uint8_t address) {
#if FEATURE_CLEAR_I2C_STUCK

  if (!Settings.EnableClearHangingI2Cbus()) { return; }
#endif // if FEATURE_CLEAR_I2C_STUCK

  // As a final work-around, we temporary swap SDA and SCL, perform a scan and return pin order.
  const int8_t   i2c_sda           = Settings.getI2CSdaPin(i2cBus);
  const int8_t   i2c_scl           = Settings.getI2CSclPin(i2cBus);
  const uint32_t ClockStretchLimit = Settings.getI2CClockStretch(i2cBus);

  I2CBegin(i2c_scl, i2c_sda, 100000, ClockStretchLimit);
  I2C_wakeup(address);
  delay(1);

  // Now we switch back to the correct pins
  I2CSelectClockSpeed(i2cBus, 100000);
}

void I2CBegin(int8_t sda, int8_t scl, uint32_t clockFreq, uint32_t clockStretch) {
  #ifdef ESP32
  uint32_t lastI2CClockSpeed = Wire.getClock();
  #else // ifdef ESP32
  static uint32_t lastI2CClockSpeed = 0;
  #endif // ifdef ESP32
  static int8_t   last_sda     = -1;
  static int8_t   last_scl     = -1;
  static uint32_t last_stretch = 0;

  if ((clockFreq == lastI2CClockSpeed) && (sda == last_sda) && (scl == last_scl) && (clockStretch == last_stretch)) {
    // No need to change the clock speed.
    return;
  }
  if (sda == -1 || scl == -1) {
#ifdef ESP32
    Wire.end();
#endif
    last_sda = sda;
    last_scl = scl;
    return;
  }


  #ifdef ESP32

  if (((sda != last_sda) || (scl != last_scl)) && (last_sda != -1) && (last_scl != -1)) {
    if (!Wire.end()) {
      addLog(LOG_LEVEL_ERROR, strformat(F("I2C  : Wire.end() failed on SDA: %d SCL: %d"), last_sda, last_scl));
    }
    gpio_reset_pin((gpio_num_t)last_sda); // Force-release pins to allow GPIO-multiplexing the Wire object
    gpio_reset_pin((gpio_num_t)last_scl);
    delay(0);                             // Release some cycles
  }
  #endif // ifdef ESP32

  #ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG_DEV)) {
    addLog(LOG_LEVEL_DEBUG_DEV, strformat(F("I2C  : SDA: %d, SCL: %d, Clock: %d (%d) (Stretch: %d)"),
                                          sda, scl, clockFreq, lastI2CClockSpeed, clockStretch));
  }
  #endif // ifndef BUILD_NO_DEBUG

  if ((sda != last_sda) || (scl != last_scl) || (clockFreq != lastI2CClockSpeed)) {
    #ifdef ESP32
    Wire.setPins(sda, scl);
    Wire.begin();
    Wire.setClock(clockFreq);
    #else // ifdef ESP32
    Wire.begin(sda, scl);
    Wire.setClock(clockFreq);
    #endif // ifdef ESP32
  }
  lastI2CClockSpeed = clockFreq;
  last_scl          = scl;
  last_sda          = sda;


  if (clockStretch != last_stretch) {
    #if defined(ESP8266)
    Wire.setClockStretchLimit(clockStretch);
    #endif // if defined(ESP8266)
    #ifdef ESP32
    Wire.setTimeOut(clockStretch);
    #endif // ifdef ESP32
    last_stretch = clockStretch;
  }
}

#if FEATURE_I2CMULTIPLEXER

// Check if the I2C Multiplexer is enabled
bool isI2CMultiplexerEnabled(uint8_t i2cBus) {
  return Settings.getI2CMultiplexerType(i2cBus) != I2C_MULTIPLEXER_NONE
         && Settings.getI2CMultiplexerAddr(i2cBus) != -1;
}

// Reset the I2C Multiplexer, if a pin is assigned for that. Pulled to low to force a reset.
void I2CMultiplexerReset(uint8_t i2cBus) {
  const uint8_t Multiplexer_ResetPin = Settings.getI2CMultiplexerResetPin(i2cBus);

  if (Multiplexer_ResetPin != -1) {
    digitalWrite(Multiplexer_ResetPin, LOW);
    delay(1); // minimum requirement of low for a proper reset seems to be about 6 nsec, so 1 msec should be more than sufficient
    digitalWrite(Multiplexer_ResetPin, HIGH);
  }
}

// Shift the bit in the right position when selecting a single channel
uint8_t I2CMultiplexerShiftBit(uint8_t i2cBus, uint8_t i) {
  uint8_t toWrite               = 0;
  const int8_t Multiplexer_Type = Settings.getI2CMultiplexerType(i2cBus);

  switch (Multiplexer_Type) {
    case I2C_MULTIPLEXER_TCA9543A: // TCA9543/6/8 addressing
    case I2C_MULTIPLEXER_TCA9546A:
    case I2C_MULTIPLEXER_TCA9548A:
      toWrite = (1 << i);
      break;
    case I2C_MULTIPLEXER_PCA9540: // PCA9540 needs bit 2 set to write the channel
      toWrite = 0b00000100;

      if (i == 1) {
        toWrite |= 0b00000010; // And bit 0 not set when selecting channel 0...
      }
      break;
  }
  return toWrite;
}

// As initially constructed by krikk in PR#254, quite adapted
// utility method for the I2C multiplexer
// select the multiplexer port given as parameter, if taskIndex < 0 then take that abs value as the port to select (to allow I2C scanner)
void I2CMultiplexerSelectByTaskIndex(taskIndex_t taskIndex) {
  if (!validTaskIndex(taskIndex)) { return; }

  if (!I2CMultiplexerPortSelectedForTask(taskIndex)) { return; }

  uint8_t toWrite = 0;

  # if FEATURE_I2C_MULTIPLE
  const uint8_t i2cBus = Settings.getI2CInterface(taskIndex);
  # else // if FEATURE_I2C_MULTIPLE
  const uint8_t i2cBus = 0;
  # endif // if FEATURE_I2C_MULTIPLE

  if (!bitRead(Settings.I2C_Flags[taskIndex], I2C_FLAGS_MUX_MULTICHANNEL)) {
    uint8_t i = Settings.I2C_Multiplexer_Channel[taskIndex];

    if (i > 7) { return; }
    toWrite = I2CMultiplexerShiftBit(i2cBus, i);
  } else {
    toWrite = Settings.I2C_Multiplexer_Channel[taskIndex]; // Bitpattern is already correctly stored
  }

  SetI2CMultiplexer(i2cBus, toWrite);
}

void I2CMultiplexerSelect(uint8_t i2cBus, uint8_t i) {
  if (i > 7) { return; }

  uint8_t toWrite = I2CMultiplexerShiftBit(i2cBus, i);

  SetI2CMultiplexer(i2cBus, toWrite);
}

void I2CMultiplexerOff(uint8_t i2cBus) {
  SetI2CMultiplexer(i2cBus, 0); // no channel selected
}

void SetI2CMultiplexer(uint8_t i2cBus, uint8_t toWrite) {
  if (isI2CMultiplexerEnabled(i2cBus)) {
    const int8_t Multiplexer_Addr = Settings.getI2CMultiplexerAddr(i2cBus);

    I2C_write8(Multiplexer_Addr, toWrite);

    // FIXME TD-er: We must check if the chip needs some time to set the output. (delay?)
  }
}

uint8_t I2CMultiplexerMaxChannels(uint8_t i2cBus) {
  uint   channels         = 0;
  int8_t Multiplexer_Type = Settings.getI2CMultiplexerType(i2cBus);

  switch (Multiplexer_Type) {
    case I2C_MULTIPLEXER_TCA9548A: channels = 8; break; // TCA9548A has 8 channels
    case I2C_MULTIPLEXER_TCA9546A: channels = 4; break; // TCA9546A has 4 channels
    case I2C_MULTIPLEXER_PCA9540:                       // PCA9540 has 2 channels
    case I2C_MULTIPLEXER_TCA9543A: channels = 2; break; // TCA9543A has 2 channels
  }
  return channels;
}

// Has this taskIndex a channel selected? Checks for both Single channel and Multiple channel mode
// taskIndex must already be validated! (0..MAX_TASKS)
bool I2CMultiplexerPortSelectedForTask(taskIndex_t taskIndex) {
  if (!validTaskIndex(taskIndex)) { return false; }

  # if FEATURE_I2C_MULTIPLE
  const uint8_t i2cBus = Settings.getI2CInterface(taskIndex);
  # else // if FEATURE_I2C_MULTIPLE
  const uint8_t i2cBus = 0;
  # endif // if FEATURE_I2C_MULTIPLE

  if (!isI2CMultiplexerEnabled(i2cBus)) { return false; }
  return (!bitRead(Settings.I2C_Flags[taskIndex], I2C_FLAGS_MUX_MULTICHANNEL) && Settings.I2C_Multiplexer_Channel[taskIndex] != -1)
         || (bitRead(Settings.I2C_Flags[taskIndex], I2C_FLAGS_MUX_MULTICHANNEL) && Settings.I2C_Multiplexer_Channel[taskIndex] !=  0);
}

#endif // if FEATURE_I2CMULTIPLEXER

#include "../Helpers/ESPEasy_time_calc.h"


#include <limits.h>

#include "../Globals/ESPEasy_time.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/SystemVariables.h"


#define SECS_PER_MIN  (60UL)
#define SECS_PER_HOUR (3600UL)
#define SECS_PER_DAY  (SECS_PER_HOUR * 24UL)


uint32_t unix_time_frac_to_millis(uint32_t unix_time_frac)
{
  return static_cast<float>(unix_time_frac) / 4294967.0f;
}

uint32_t unix_time_frac_to_micros(uint32_t unix_time_frac)
{
  return static_cast<float>(unix_time_frac) / 4294.967f;
}

uint32_t millis_to_unix_time_frac(uint32_t millis)
{
  return static_cast<float>(millis) * 4294967.0f;
}

uint32_t micros_to_unix_time_frac(uint32_t micros)
{
  return static_cast<float>(micros) * 4294.967f;
}

uint32_t micros_to_sec_time_frac(int64_t micros, uint32_t& unix_time_frac)
{
  const uint64_t seconds = static_cast<uint32_t>(micros / 1000000ull);

  // Compute modulo usec
  unix_time_frac = micros_to_unix_time_frac(micros - (1000000ull * seconds));
  return static_cast<uint32_t>(seconds);
}

uint64_t sec_time_frac_to_Micros(uint32_t seconds, uint32_t time_frac)
{
  return
    (static_cast<uint64_t>(seconds) * 1000000ull) +
    unix_time_frac_to_micros(time_frac);
}

uint32_t micros_to_sec_usec(int64_t micros, uint32_t& usec)
{
  const uint64_t seconds = static_cast<uint32_t>(micros / 1000000ull);

  // Compute modulo usec
  usec = static_cast<uint32_t>(micros - (1000000ull * seconds));
  return static_cast<uint32_t>(seconds);
}

uint64_t sec_time_frac_to_uptime_offset_usec(const uint32_t& seconds,
                                   uint32_t        time_frac)
{
  const uint64_t unix_time_usec = sec_time_frac_to_Micros(seconds, time_frac);
  const uint64_t cur_usec = getMicros64();
  if (unix_time_usec < cur_usec) 
    return unix_time_usec;
  return unix_time_usec - cur_usec;
}


bool isLeapYear(int year) {
  return (year > 0) && !(year % 4) && ((year % 100) || !(year % 400));
}

uint8_t getMonthDays(int year, uint8_t month) {
  const uint8_t monthDays[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

  if ((month == 1) && isLeapYear(year)) {
    return 29;
  }

  if (month > 11) {
    return 0;
  }
  return monthDays[month];
}

uint8_t getMonthDays(const struct tm& tm) {
  return getMonthDays(tm.tm_year + 1900, tm.tm_mon);
}

/********************************************************************************************\
   Unix Time computations
 \*********************************************************************************************/
uint32_t makeTime(const struct tm& tm) {
  // assemble time elements into uint32_t
  // note year argument is offset from 1970 (see macros in time.h to convert to other formats)
  // previous version used full four digit year (or digits since 2000),i.e. 2009 was 2009 or 9
  const int tm_year = tm.tm_year + 1900;

  // seconds from 1970 till 1 jan 00:00:00 of the given year
  // tm_year starts at 1900
  uint32_t seconds = 1577836800; // 01/01/2020 @ 12:00am (UTC)
  int year         = 2020;

  if (tm_year < year) {
    // Just in case this function is called on old dates
    year    = 1970;
    seconds = 0;
  }

  for (; year < tm_year; ++year) {
    seconds += SECS_PER_DAY * 365;

    if (isLeapYear(year)) {
      seconds += SECS_PER_DAY; // add extra days for leap years
    }
  }

  // add days for this year, months start from 0
  for (int i = 0; i < tm.tm_mon; i++) {
    seconds += SECS_PER_DAY * getMonthDays(tm_year, i);
  }
  seconds += (tm.tm_mday - 1) * SECS_PER_DAY;
  seconds += tm.tm_hour * SECS_PER_HOUR;
  seconds += tm.tm_min * SECS_PER_MIN;
  seconds += tm.tm_sec;
  return seconds;
}

void breakTime(unsigned long timeInput, struct tm& tm) {
  uint32_t time = (uint32_t)timeInput;

  tm.tm_sec  = time % 60;
  time      /= 60;                   // now it is minutes
  tm.tm_min  = time % 60;
  time      /= 60;                   // now it is hours
  tm.tm_hour = time % 24;
  time      /= 24;                   // now it is days
  tm.tm_wday = ((time + 4) % 7) + 1; // Sunday is day 1

  int year           = 1970;
  unsigned long days = 0;
  #ifndef LIMIT_BUILD_SIZE
  if (time > 20089) {
    // Skip checking 55 years for being leap year.
    year = 2025;
    days = 20089;
  }
  #endif

  while ((unsigned)(days += (isLeapYear(year) ? 366 : 365)) <= time) {
    year++;
  }
/*
  addLog(LOG_LEVEL_INFO, strformat(
    F("breakTime: time: %u year: %d, days: %u"),
    time,
    year,
    days));
*/
  tm.tm_year = year - 1900; // tm_year starts at 1900

  days -= isLeapYear(year) ? 366 : 365;
  time -= days;             // now it is days in this year, starting at 0

  uint8_t month = 0;

  for (month = 0; month < 12; month++) {
    const uint8_t monthLength = getMonthDays(year, month);

    if (time >= monthLength) {
      time -= monthLength;
    } else {
      break;
    }
  }
  tm.tm_mon  = month;    // Jan is month 0
  tm.tm_mday = time + 1; // day of month start at 1
}

String formatDateString(const struct tm& ts, char delimiter) {
  // time format example with ':' delimiter: 23:59:59 (HH:MM:SS)
  char DateString[20]; // 19 digits plus the null char
  const int year = 1900 + ts.tm_year;

  if (delimiter == '\0') {
    sprintf_P(DateString, PSTR("%4d%02d%02d"), year, ts.tm_mon + 1, ts.tm_mday);
  } else {
    sprintf_P(DateString, PSTR("%4d%c%02d%c%02d"), year, delimiter, ts.tm_mon + 1, delimiter, ts.tm_mday);
  }
  return DateString;
}

String formatTimeString(const struct tm& ts)
{
  return formatTimeString(ts, ':', false, true);
}

// returns the current Time separated by the given delimiter
// time format example with ':' delimiter: 23:59:59 (HH:MM:SS)
String formatTimeString(const struct tm& ts, char delimiter, bool am_pm, bool show_seconds, char hour_prefix /*='\0'*/)
{
  char TimeString[20]; // 19 digits plus the null char
  char hour_prefix_s[2] = { 0 };

  if (am_pm) {
    uint8_t hour(ts.tm_hour % 12);

    if (hour == 0) { hour = 12; }
    const char a_or_p = ts.tm_hour < 12 ? 'A' : 'P';

    if (hour < 10) { hour_prefix_s[0] = hour_prefix; }

    if (show_seconds) {
      if (delimiter == '\0') {
        sprintf_P(TimeString, PSTR("%s%d%02d%02d %cM"),
                  hour_prefix_s, hour, ts.tm_min, ts.tm_sec, a_or_p);
      } else {
        sprintf_P(TimeString, PSTR("%s%d%c%02d%c%02d %cM"),
                  hour_prefix_s, hour, delimiter, ts.tm_min, delimiter, ts.tm_sec, a_or_p);
      }
    } else {
      if (delimiter == '\0') {
        sprintf_P(TimeString, PSTR("%s%d%02d %cM"),
                  hour_prefix_s, hour, ts.tm_min, a_or_p);
      } else {
        sprintf_P(TimeString, PSTR("%s%d%c%02d %cM"),
                  hour_prefix_s, hour, delimiter, ts.tm_min, a_or_p);
      }
    }
  } else {
    if (show_seconds) {
      if (delimiter == '\0') {
        sprintf_P(TimeString, PSTR("%02d%02d%02d"),
                  ts.tm_hour, ts.tm_min, ts.tm_sec);
      } else {
        sprintf_P(TimeString, PSTR("%02d%c%02d%c%02d"),
                  ts.tm_hour, delimiter, ts.tm_min, delimiter, ts.tm_sec);
      }
    } else {
      if (ts.tm_hour < 10) { hour_prefix_s[0] = hour_prefix; }

      if (delimiter == '\0') {
        sprintf_P(TimeString, PSTR("%s%d%02d"),
                  hour_prefix_s, ts.tm_hour, ts.tm_min);
      } else {
        sprintf_P(TimeString, PSTR("%s%d%c%02d"),
                  hour_prefix_s, ts.tm_hour, delimiter, ts.tm_min);
      }
    }
  }
  return TimeString;
}

String formatDateTimeString(const struct tm& ts, char dateDelimiter, char timeDelimiter,  char dateTimeDelimiter, bool am_pm)
{
  // if called like this: getDateTimeString('\0', '\0', '\0');
  // it will give back this: 20161231235959  (YYYYMMDDHHMMSS)
  String ret = formatDateString(ts, dateDelimiter);

  if (dateTimeDelimiter != '\0') {
    ret += dateTimeDelimiter;
  }
  ret += formatTimeString(ts, timeDelimiter, am_pm, true);
  return ret;
}

/********************************************************************************************\
   Time computations for rules.
 \*********************************************************************************************/
String timeLong2String(unsigned long lngTime)
{
  unsigned long x = 0;
  String time;

  x = (lngTime >> 16) & 0xf;

  if (x == 0x0f) {
    x = 0;
  }
  const String weekDays = F("AllSunMonTueWedThuFriSatWrkWkd");

  time  = weekDays.substring(x * 3, x * 3 + 3);
  time += ',';

  #ifndef PLUGIN_BUILD_MINIMAL_OTA

  if (bitRead(lngTime, 28) || bitRead(lngTime, 29)) { // Sunrise or Sunset handling
    time += SystemVariables::toString(bitRead(lngTime, 29) ? SystemVariables::Enum::SUNRISE : SystemVariables::Enum::SUNSET);

    if ((lngTime & 0xffff) > 0) {
      time += bitRead(lngTime, 30) ? '-' : '+';  // Sign
      time += lngTime & 0xffff;
      const String  hms = F("smh");
      const uint8_t idx = (lngTime >> 26) & 0x3; // 0/1/2 = s/m/h
      time += hms.substring(idx, idx + 1);
    }
    time += '%';
    return time;
  }
  #endif // ifndef PLUGIN_BUILD_MINIMAL_OTA

  x = (lngTime >> 12) & 0xf;

  if (x == 0xf) {
    time += '*';
  }
  else if (x == 0xe) {
    time += '-';
  }
  else {
    time += x;
  }

  x = (lngTime >> 8) & 0xf;

  if (x == 0xf) {
    time += '*';
  }
  else if (x == 0xe) {
    time += '-';
  }
  else {
    time += x;
  }

  time += ':';

  x = (lngTime >> 4) & 0xf;

  if (x == 0xf) {
    time += '*';
  }
  else if (x == 0xe) {
    time += '-';
  }
  else {
    time += x;
  }

  x = (lngTime) & 0xf;

  if (x == 0xf) {
    time += '*';
  }
  else if (x == 0xe) {
    time += '-';
  }
  else {
    time += x;
  }

  return time;
}

unsigned long string2TimeLong(const String& str)
{
  // format 0NRSHM000000WWWWAAAABBBBCCCCDDDD
  // WWWW=weekday, AAAA=hours tens digit, BBBB=hours, CCCC=minutes tens digit DDDD=minutes
  // N = Negative offset, R = sunRise, S = sunSet, H = offset hours, M = offset minutes -> AAAA..DDDD = offset (default: seconds, binary
  // stored, not bcd)

  char command[20];
  int  w, x, y;
  unsigned long a;
  {
    // Within a scope so the tmpString is only used for copy.
    String tmpString(str);
    tmpString.toLowerCase();
    tmpString.toCharArray(command, 20);
  }
  unsigned long lngTime = 0;
  String TmpStr1;

  if (GetArgv(command, TmpStr1, 1))
  {
    String weekDays = F("AllSunMonTueWedThuFriSatWrkWkd"); // Deduplicated string takes a little less memory...
    weekDays.toLowerCase();

    y = weekDays.indexOf(TmpStr1) / 3;

    if (y == 0) {
      y = 0xf; // wildcard is 0xf
    }
    lngTime |= (unsigned long)y << 16;
  }

  if (GetArgv(command, TmpStr1, 2))
  {
    y = 0;

    #ifndef PLUGIN_BUILD_MINIMAL_OTA

    uint8_t off = TmpStr1.startsWith(SystemVariables::toString(SystemVariables::Enum::SUNRISE)) ? 8u : 0u;

    if (off || TmpStr1.startsWith(SystemVariables::toString(SystemVariables::Enum::SUNSET))) {
      if (off == 0) { off = 7; }
      int lperc = TmpStr1.indexOf('%', off);

      if (lperc >= off) {           // Valid variable used, having a second % sign?
        bitSet(lngTime, 21u + off); // bit 28 = sunset, bit 29 = sunrise
        int delta            = ESPEasy_time::getSecOffset(TmpStr1.substring(off, lperc + 1));
        const bool isSeconds = 's' == TmpStr1[lperc - 1];

        if (delta < 0) {
          bitSet(lngTime, 30); // bit 30 = negative offset
          delta *= -1;
        }

        if (delta > 86400) {                       // > 24 hours = invalid, reset
          delta = 0;
          bitClear(lngTime, 30);
        } else if (isSeconds && (delta < 32768)) { // Seconds, explicitly, max. delta we can store
        } else if (delta >= 3600) {                // Hours
          delta /= 3600;
          bitSet(lngTime, 27);                     // Bit 27: Stored as Hours
        } else if (delta >= 60) {                  // Minutes
          delta /= 60;
          bitSet(lngTime, 26);                     // Bit 26: Stored as Minutes
        }                                          // else: Stored as seconds
        lngTime += delta;                          // Store offset
        return lngTime;
      }
    }
    #endif // ifndef PLUGIN_BUILD_MINIMAL_OTA

    for (x = TmpStr1.length() - 1; x >= 0; x--)
    {
      w = TmpStr1[x];

      if (isDigit(w) || (w == '*'))
      {
        a        = 0xffffffff  ^ (0xfUL << y); // create mask to clean nibble position y
        lngTime &= a;                          // maak nibble leeg

        if (w == '*') {
          lngTime |= (0xFUL << y);             // fill nibble with wildcard value
        }
        else {
          lngTime |= (w - '0') << y;           // fill nibble with token
        }
        y += 4;
      }
      else
      if (w == ':') {}
      else
      {
        break;
      }
    }
  }
  #undef TmpStr1Length
  return lngTime;
}

/********************************************************************************************\
   Match clock event
 \*********************************************************************************************/
bool matchClockEvent(unsigned long clockEvent, unsigned long clockSet)
{
  unsigned long Mask;

  for (uint8_t y = 0; y < 8; y++)
  {
    if (((clockSet >> (y * 4)) & 0xf) == 0xf)         // if nibble y has the wildcard value 0xf
    {
      Mask        = 0xffffffff  ^ (0xFUL << (y * 4)); // Mask to wipe nibble position y.
      clockEvent &= Mask;                             // clear nibble
      clockEvent |= (0xFUL << (y * 4));               // fill with wildcard value 0xf
    }
  }

  if (((clockSet >> (16)) & 0xf) == 0x8) {                        // if weekday nibble has the wildcard value 0x8 (workdays)
    if ((node_time.weekday() >= 2) && (node_time.weekday() <= 6)) // and we have a working day today...
    {
      Mask        = 0xffffffff  ^ (0xFUL << (16));                // Mask to wipe nibble position.
      clockEvent &= Mask;                                         // clear nibble
      clockEvent |= (0x8UL << (16));                              // fill with wildcard value 0x8
    }
  }

  if (((clockSet >> (16)) & 0xf) == 0x9) {                        // if weekday nibble has the wildcard value 0x9 (weekends)
    if ((node_time.weekday() == 1) || (node_time.weekday() == 7)) // and we have a weekend day today...
    {
      Mask        = 0xffffffff  ^ (0xFUL << (16));                // Mask to wipe nibble position.
      clockEvent &= Mask;                                         // clear nibble
      clockEvent |= (0x9UL << (16));                              // fill with wildcard value 0x9
    }
  }

  return clockEvent == clockSet;
}

#include "../Helpers/_CPlugin_LoRa_TTN_helper.h"

#include "../DataStructs/ESPEasy_EventStruct.h"

#include "../Globals/Settings.h"
#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../../_Plugin_Helper.h"

// #######################################################################################################
// #  Helper functions to encode data for use on LoRa/TTN network.
// #######################################################################################################

#if FEATURE_PACKED_RAW_DATA


String getPackedFromPlugin(struct EventStruct *event, uint8_t sampleSetCount)
{
  uint8_t value_count = getValueCountForTask(event->TaskIndex);
  String  raw_packed;

  if (PluginCall(PLUGIN_GET_PACKED_RAW_DATA, event, raw_packed)) {
    value_count = event->Par1;
  }
  String packed;

  packed.reserve(32);
  packed += LoRa_addInt(Settings.getPluginID_for_task(event->TaskIndex).value, PackedData_uint8);
  packed += LoRa_addInt(event->idx, PackedData_uint16);
  packed += LoRa_addInt(sampleSetCount, PackedData_uint8);
  packed += LoRa_addInt(value_count, PackedData_uint8);

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    String log = F("packed header: ");
    log += packed;

    if (raw_packed.length() > 0) {
      log += F(" RAW: ");
      log += raw_packed;
    }
    addLogMove(LOG_LEVEL_INFO, log);
  }

  if (raw_packed.length() > 0) {
    packed += raw_packed;
  } else {
    const Sensor_VType sensorType = event->getSensorType();

    if (sensorType == Sensor_VType::SENSOR_TYPE_ULONG)
    {
      packed += LoRa_addInt(UserVar.getSensorTypeLong(event->TaskIndex), PackedData_uint32);
    } else {
      for (uint8_t i = 0; i < value_count && i < VARS_PER_TASK; ++i) {
        if (isUInt32OutputDataType(sensorType)) {
          packed += LoRa_addInt(UserVar.getUint32(event->TaskIndex, i), PackedData_uint32);
#if FEATURE_EXTENDED_TASK_VALUE_TYPES
        } else if (isInt32OutputDataType(sensorType)) {
          packed += LoRa_addInt(UserVar.getInt32(event->TaskIndex, i), PackedData_int32);
#endif
        } else {
          // For now, just store the floats as an int32 by multiplying the value with 10000.
          packed += LoRa_addFloat(UserVar[event->BaseVarIndex + i], PackedData_int32_1e4);
        }
      }
    }
  }
  return packed;
}

float getLoRaAirTime(uint8_t pl, uint8_t sf, uint16_t bw, uint8_t cr, uint8_t n_preamble, bool header, bool crc)
{
  if (sf > 12) {
    sf = 12;
  } else if (sf < 7) {
    sf = 7;
  }

  if (cr > 4) {
    cr = 4;
  } else if (cr < 1) {
    cr = 1;
  }

  // Symbols in frame
  int payload_length = 8;
  {
    int beta_offset = 28;

    if (crc) { beta_offset += 16; }

    if (!header) { beta_offset -= 20; }
    float beta_f                  = 8.0f * pl - 4.0f * sf + beta_offset;
    bool  lowDataRateOptimization = (bw == 125 && sf >= 11);

    if (lowDataRateOptimization) {
      beta_f = beta_f / (4.0f * (sf - 2));
    } else {
      beta_f = beta_f / (4.0f * sf);
    }
    int beta = static_cast<int>(beta_f + 1.0f); // ceil

    if (beta > 0) {
      payload_length += (beta * (cr + 4));
    }
  }

  // t_symbol and t_air in msec
  float t_symbol = static_cast<float>(1 << sf) / bw;
  float t_air    = ((n_preamble + 4.25f) + payload_length) * t_symbol;

  return t_air;
}

#endif // if FEATURE_PACKED_RAW_DATA

#include "../Helpers/WiFi_AP_CandidatesList.h"

#ifdef ESP32
#include "../DataStructs/WiFi_AP_Candidates_NVS.h"
#endif

#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../Globals/ESPEasyWiFiEvent.h"
#include "../Globals/RTC.h"
#include "../Globals/SecuritySettings.h"
#include "../Globals/Settings.h"
#include "../Helpers/Misc.h"
#include "../Helpers/StringConverter.h"

#if defined(ESP8266)
  # include <ESP8266WiFi.h>
#endif // if defined(ESP8266)
#if defined(ESP32)
  # include <WiFi.h>
#endif // if defined(ESP32)

#define WIFI_CUSTOM_DEPLOYMENT_KEY_INDEX     3
#define WIFI_CUSTOM_SUPPORT_KEY_INDEX        4
#define WIFI_CREDENTIALS_FALLBACK_SSID_INDEX 5

WiFi_AP_CandidatesList::WiFi_AP_CandidatesList() {
  known.clear();
  candidates.clear();
  known_it = known.begin();
}

WiFi_AP_CandidatesList::~WiFi_AP_CandidatesList() {
  candidates.clear();
  known.clear();
  scanned.clear();
  scanned_new.clear();
}

void WiFi_AP_CandidatesList::load_knownCredentials() {
  if (!_mustLoadCredentials && !known.empty()) { return; }
  _mustLoadCredentials = false;
  known.clear();
  candidates.clear();
//  attemptsLeft = 1;
  _addedKnownCandidate = false;
//  addFromRTC();

  {
    // Add the known SSIDs
    String ssid;
    uint8_t   index = 1; // Index 0 is the "unset" value

    bool done = false;

    while (!done) {
      if (get_SSID(index, ssid)) {
        // Make sure emplace_back is not done on the 2nd heap
        # ifdef USE_SECOND_HEAP
        HeapSelectDram ephemeral;
        # endif // ifdef USE_SECOND_HEAP

        known.emplace_back(index, ssid);
        if (SettingsIndexMatchCustomCredentials(index)) {
          if (SettingsIndexMatchEmergencyFallback(index)) {
            known.back().bits.isEmergencyFallback = true;
          } else {
            known.back().bits.lowPriority = true;
          }
        }
        ++index;
      } else {
        if (SettingsIndexMatchCustomCredentials(index)) {
          ++index;
        } else {
          done = true;
        }
      }
    }
  }
  loadCandidatesFromScanned();
  addFromRTC();
}

void WiFi_AP_CandidatesList::clearCache() {
  _mustLoadCredentials = true;
  known.clear();
  known_it = known.begin();
}


void WiFi_AP_CandidatesList::force_reload() {
  clearCache();
  RTC.clearLastWiFi(); // Invalidate the RTC WiFi data.
  candidates.clear();
  loadCandidatesFromScanned();
}

void WiFi_AP_CandidatesList::begin_sync_scan() {
  candidates.clear();
  _addedKnownCandidate = false;
}

void WiFi_AP_CandidatesList::purge_expired() {
  for (auto it = scanned.begin(); it != scanned.end(); ) {
    if (it->expired()) {
      it = scanned.erase(it);
    } else {
      ++it;
    }
  }
}

#if !FEATURE_ESP8266_DIRECT_WIFI_SCAN
void WiFi_AP_CandidatesList::process_WiFiscan(uint8_t scancount) {
  // Append or update found APs from scan.
  for (uint8_t i = 0; i < scancount; ++i) {
    const WiFi_AP_Candidate tmp(i);

    scanned_new.push_back(tmp);
  }

  after_process_WiFiscan();
}
#endif

#ifdef ESP8266
#if FEATURE_ESP8266_DIRECT_WIFI_SCAN
void WiFi_AP_CandidatesList::process_WiFiscan(const bss_info& ap) {
  WiFi_AP_Candidate tmp(ap);
  scanned_new.push_back(tmp);
}
#endif
#endif

void WiFi_AP_CandidatesList::after_process_WiFiscan() {
  scanned_new.sort();
  scanned_new.unique();
  _mustLoadCredentials = true;
  load_knownCredentials();
  WiFi.scanDelete();
  attemptsLeft = 1;
}

bool WiFi_AP_CandidatesList::getNext(bool scanAllowed) {
  load_knownCredentials();

  if (candidates.empty()) { 
    if (scanAllowed) {
      return false;
    }
    loadCandidatesFromScanned();
    attemptsLeft = 1;
    if (candidates.empty()) { return false; }
  }

  currentCandidate = candidates.front();
  bool mustPop = true;

  if (currentCandidate.bits.isHidden) {
    // Iterate over the known credentials to try them all
    // Hidden SSID stations do not broadcast their SSID, so we must fill it in ourselves.
    if (known_it != known.end()) {
      currentCandidate.ssid  = known_it->ssid;
      currentCandidate.index = known_it->index;
      ++known_it;
    }

    if (known_it != known.end()) {
      mustPop = false;
    }
  }

  if (mustPop) {
    if (attemptsLeft == 0) {
      if (currentCandidate.bits.isHidden) {
        // We tried to connect to hidden SSIDs in 1 run, so pop all hidden candidates.
        for (auto cand_it = candidates.begin(); cand_it != candidates.end() && cand_it->bits.isHidden; ) {
          cand_it = candidates.erase(cand_it);
        }
      } else {
        if (!candidates.empty()) {
          candidates.pop_front();
        }
      }

      known_it = known.begin();
      attemptsLeft = 1;
    } else {
      markAttempt();
    }
  }
  return currentCandidate.usable();
}

const WiFi_AP_Candidate& WiFi_AP_CandidatesList::getCurrent() const {
  return currentCandidate;
}

void WiFi_AP_CandidatesList::markAttempt() {
  if (attemptsLeft > 0) attemptsLeft--;
}

WiFi_AP_Candidate WiFi_AP_CandidatesList::getBestCandidate() const {
  for (auto it = candidates.begin(); it != candidates.end(); ++it) {
    if (it->rssi < -1) { return *it; }
  }
  return WiFi_AP_Candidate();
}

bool WiFi_AP_CandidatesList::hasCandidateCredentials() {
  load_knownCredentials();
  return !known.empty();
}

bool WiFi_AP_CandidatesList::hasCandidates() const {
  return !candidates.empty();
}

void WiFi_AP_CandidatesList::markCurrentConnectionStable() {
  clearCache();
  if (currentCandidate.enc_type == 0) {
    bool matchfound = false;
    for (auto it = candidates.begin(); !matchfound && it != candidates.end(); ++it) {
      if (currentCandidate == *it) {
        // We may have gotten the enc_type of the active used candidate
        // Make sure to store the enc type before clearing the candidates list
        currentCandidate.enc_type = it->enc_type;
        matchfound = true;
      }
    }
  }
  if (currentCandidate.usable()) {
    // Store in RTC
    RTC.lastWiFiChannel = currentCandidate.channel;
    currentCandidate.bssid.get(RTC.lastBSSID);
    RTC.lastWiFiSettingsIndex = currentCandidate.index;
#ifdef ESP32
    if (Settings.UseLastWiFiFromRTC())
      WiFi_AP_Candidates_NVS::currentConnection_to_NVS(currentCandidate);
    else
      WiFi_AP_Candidates_NVS::clear_from_NVS();
#endif
  }

  candidates.clear();
  _addedKnownCandidate = false;
  addFromRTC(); // Store the current one from RTC as the first candidate for a reconnect.
}

int8_t WiFi_AP_CandidatesList::scanComplete() const {
  const int8_t scanCompleteStatus = WiFi.scanComplete();
  if (scanCompleteStatus == -1) {
    // Still scanning
    return scanCompleteStatus;
  }

  size_t found = 0;
  for (auto scan = scanned.begin(); scan != scanned.end(); ++scan) {
    if (!scan->expired()) {
      ++found;
    }
  }
  for (auto scan = scanned_new.begin(); scan != scanned_new.end(); ++scan) {
    if (!scan->expired()) {
      ++found;
    }
  }
  if (found == 0) {
    if (scanCompleteStatus == -2) {
      // Not triggered
      return scanCompleteStatus;
    }
  }
  return found;
}

bool WiFi_AP_CandidatesList::SettingsIndexMatchCustomCredentials(uint8_t index)
{
  return (WIFI_CUSTOM_DEPLOYMENT_KEY_INDEX     == index ||
          WIFI_CUSTOM_SUPPORT_KEY_INDEX        == index ||
          SettingsIndexMatchEmergencyFallback(index));
}

bool WiFi_AP_CandidatesList::SettingsIndexMatchEmergencyFallback(uint8_t index)
{
  return (WIFI_CREDENTIALS_FALLBACK_SSID_INDEX == index);
}


void WiFi_AP_CandidatesList::loadCandidatesFromScanned() {
  // Make sure list operations are not done on the 2nd heap
  # ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  # endif // ifdef USE_SECOND_HEAP

  if (scanned_new.size() > 0) {
    // We have new scans to process.
    purge_expired();
    for (auto scan = scanned_new.begin(); scan != scanned_new.end();) {
      #ifndef BUILD_NO_DEBUG
      if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
        addLogMove(LOG_LEVEL_DEBUG, concat(F("WiFi : Scan result: "), scan->toString()));
      }
      #endif // ifndef BUILD_NO_DEBUG

      // Check to see if it is already present, if so, remove existing one.
      for (auto tmp = scanned.begin(); tmp != scanned.end();) {
        if (*tmp == *scan) {
          tmp = scanned.erase(tmp);
        } else {
          ++tmp;
        }
      }

      // We copy instead of move, to make sure it is stored on the 2nd heap.
      scanned.push_back(*scan);
      scan = scanned_new.erase(scan);
    }
    scanned.sort();
    scanned.unique();
  }

  if (candidates.size() > 1) {
    // Do not mess with the current candidates order if > 1 present
    return;
  }
  // Purge unusable from known list.
  for (auto it = known.begin(); it != known.end();) {
    if (it->usable()) {
      ++it;
    } else {
      it = known.erase(it);
    }
  }
  known.sort();
  known.unique();
  known_it = known.begin();

  for (auto scan = scanned.begin(); scan != scanned.end();) {
    if (scan->expired()) {
      scan = scanned.erase(scan);
    } else {
      if (scan->bits.isHidden) {
        if (Settings.IncludeHiddenSSID()) {
          if (SecuritySettings.hasWiFiCredentials()) {
            candidates.push_back(*scan);
          }
        }
      } else if (scan->ssid.length() > 0) {
        for (auto kn_it = known.begin(); kn_it != known.end(); ++kn_it) {
          if (scan->ssid.equals(kn_it->ssid)) {
            WiFi_AP_Candidate tmp = *scan;
            tmp.index = kn_it->index;
            tmp.bits.lowPriority = kn_it->bits.lowPriority;
            tmp.bits.isEmergencyFallback = kn_it->bits.isEmergencyFallback;

            if (tmp.usable()) {
              candidates.push_back(tmp);
              _addedKnownCandidate = true;

              // Check all knowns as we may have several AP's with the same SSID and different passwords.
            }
          }
        }
      }
      ++scan;
    }
  }
  # ifndef BUILD_NO_DEBUG
  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    const WiFi_AP_Candidate bestCandidate = getBestCandidate();
    if (bestCandidate.usable()) {
      addLogMove(LOG_LEVEL_INFO, concat(F("WiFi : Best AP candidate: "), bestCandidate.toString()));
    }
  }
  #endif
  candidates.sort();
  candidates.unique();
  addFromRTC();
  purge_unusable();
}

void WiFi_AP_CandidatesList::addFromRTC() {
  if (!Settings.UseLastWiFiFromRTC()) return;
  if (!RTC.lastWiFi_set()) { 
    #ifdef ESP32
    // Try to load from NVS and store in RTC
    WiFi_AP_Candidate fromNVS;
    if (WiFi_AP_Candidates_NVS::loadCandidate_from_NVS(fromNVS)) {
      RTC.lastWiFiChannel = currentCandidate.channel;
      currentCandidate.bssid.get(RTC.lastBSSID);
      RTC.lastWiFiSettingsIndex = currentCandidate.index;
    } else {
      return;
    }
    #else
    return;
    #endif
  }

  if (SettingsIndexMatchCustomCredentials(RTC.lastWiFiSettingsIndex)) 
  { 
    return;
  }

  String ssid;

  if (!get_SSID(RTC.lastWiFiSettingsIndex, ssid)) {
    return;
  }

  WiFi_AP_Candidate fromRTC(RTC.lastWiFiSettingsIndex, ssid);
  fromRTC.bssid   = RTC.lastBSSID;
  fromRTC.channel = RTC.lastWiFiChannel;

  if (!fromRTC.usable()) {
    return;
  }

  if (candidates.size() > 0 && candidates.front().ssid.equals(fromRTC.ssid)) {
    // Front candidate was already from RTC.
    candidates.pop_front();
  }

  // See if we may have a better candidate for the current network, with a significant better RSSI.
  auto bestMatch = candidates.end();
  auto lastUsed  = bestMatch;
  for (auto it = candidates.begin(); lastUsed == candidates.end() && it != candidates.end(); ++it) {
    if (it->usable() && it->ssid.equals(fromRTC.ssid)) {
      const bool foundLastUsed = fromRTC.bssid_match(it->bssid);
      if (foundLastUsed) {
        lastUsed = it;
      } else if (bestMatch == candidates.end()) {
        bestMatch = it;
      }
    }
  }
  bool matchAdded = false;
  if (bestMatch != candidates.end()) {
    // Found a best match, possibly better than the last used.
    if (lastUsed == candidates.end() || (bestMatch->rssi > (lastUsed->rssi + 10))) {
      // Last used was not found or
      // Other candidate has significant better RSSI
      matchAdded = true;
      candidates.push_front(*bestMatch);
    }
  } else if (lastUsed != candidates.end()) {
    matchAdded = true;
    candidates.push_front(*lastUsed);
  }
  if (!matchAdded) {
    candidates.push_front(fromRTC);
    // This is not taken from a scan, so no idea of the used encryption.
    // Try to find a matching BSSID to get the encryption.
    for (auto it = candidates.begin(); it != candidates.end(); ++it) {
      if ((it->rssi != -1) && candidates.front() == *it) {
        candidates.front().enc_type = it->enc_type;
        return;
      }
    }
  }

  candidates.front().rssi = -1; // Set to best possible RSSI so it is tried first.

  if (!candidates.front().usable() || !candidates.front().allowQuickConnect()) {
    candidates.pop_front();
    return;
  }

  if (currentCandidate == candidates.front()) {
    candidates.front().enc_type = currentCandidate.enc_type;
  }
}

void WiFi_AP_CandidatesList::purge_unusable() {
  for (auto it = candidates.begin(); it != candidates.end();) {
    if (it->usable()) {
      ++it;
    } else {
      it = candidates.erase(it);
    }
  }
  if (candidates.size() > 1) {
    candidates.sort();
    candidates.unique();
  }
}

bool WiFi_AP_CandidatesList::get_SSID_key(uint8_t index, String& ssid, String& key)  {
  switch (index) {
    case 1:
      ssid = SecuritySettings.WifiSSID;
      key  = SecuritySettings.WifiKey;
      break;
    case 2:
      ssid = SecuritySettings.WifiSSID2;
      key  = SecuritySettings.WifiKey2;
      break;
    case WIFI_CUSTOM_DEPLOYMENT_KEY_INDEX:
      #if !defined(CUSTOM_DEPLOYMENT_SSID) || !defined(CUSTOM_DEPLOYMENT_KEY)
      return false;
      #else
      ssid = F(CUSTOM_DEPLOYMENT_SSID);
      key  = F(CUSTOM_DEPLOYMENT_KEY);
      #endif
      break;
    case WIFI_CUSTOM_SUPPORT_KEY_INDEX:
      #if !defined(CUSTOM_SUPPORT_SSID) || !defined(CUSTOM_SUPPORT_KEY)
      return false;
      #else
      ssid = F(CUSTOM_SUPPORT_SSID);
      key  = F(CUSTOM_SUPPORT_KEY);
      #endif
      break;
    case WIFI_CREDENTIALS_FALLBACK_SSID_INDEX:
    {
      #if !defined(CUSTOM_EMERGENCY_FALLBACK_SSID) || !defined(CUSTOM_EMERGENCY_FALLBACK_KEY)
      return false;
      #else
      int allowedUptimeMinutes = 10;
      #ifdef CUSTOM_EMERGENCY_FALLBACK_ALLOW_MINUTES_UPTIME
      allowedUptimeMinutes = CUSTOM_EMERGENCY_FALLBACK_ALLOW_MINUTES_UPTIME;
      #endif
      if (getUptimeMinutes() < allowedUptimeMinutes && SecuritySettings.hasWiFiCredentials()) {
        ssid = F(CUSTOM_EMERGENCY_FALLBACK_SSID);
        key  = F(CUSTOM_EMERGENCY_FALLBACK_KEY);
      } else {
        return false;
      }
      #endif
      break;
    }
    default:
      return false;
  }

  // TODO TD-er: Read other credentials from extra file.



  // Spaces are allowed in both SSID and pass phrase, so make sure to not trim the ssid and key.
  return true;
}

bool WiFi_AP_CandidatesList::get_SSID(uint8_t index, String& ssid)
{
  String key;
  return get_SSID_key(index, ssid, key);
}

String WiFi_AP_CandidatesList::get_key(uint8_t index)
{
  String ssid, key;
  if (get_SSID_key(index, ssid, key))
    return key;
  return EMPTY_STRING;
}
#include "../Helpers/StringParser.h"

#include "../../_Plugin_Helper.h"

#include "../Commands/GPIO.h"

#include "../DataStructs/TimingStats.h"

#include "../ESPEasyCore/ESPEasyRules.h"

#include "../Globals/Cache.h"
#include "../Globals/Plugins_other.h"
#include "../Globals/RulesCalculate.h"
#include "../Globals/RuntimeData.h"

#include "../Helpers/_CPlugin_init.h"
#include "../Helpers/ESPEasy_math.h"
#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/ESPEasy_UnitOfMeasure.h"
#include "../Helpers/Misc.h"
#include "../Helpers/Numerical.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringGenerator_GPIO.h"



/********************************************************************************************\
   Parse string template
 \*********************************************************************************************/
bool hasEscapedCharacter(String& str, const char EscapeChar)
{
  const String EscStr = concat(F("\\"), EscapeChar);
  return (str.indexOf(EscStr)>=0);
}

void stripEscapeCharacters(String& str)
{
  const char braces[]     = { '%', '[', ']', '{', '}', '(', ')' };
  constexpr uint8_t nrbraces = NR_ELEMENTS(braces);

  for (uint8_t i = 0; i < nrbraces; ++i) {
    const String s(concat(F("\\"), braces[i]));
    str.replace(s, s.substring(1));
  }
}

#if FEATURE_STRING_VARIABLES
String parseTemplateAndCalculate(String& tmpString) {
  stripEscapeCharacters(tmpString);
  String str = parseTemplate(tmpString);
  ESPEASY_RULES_FLOAT_TYPE result{};
  if (!str.isEmpty() && (str[0] == '=') && !isError(Calculate(str.substring(1), result, true))) {
    # if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
    str = doubleToString(result, 6, true);
    # else // if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
    str = floatToString(result, 6, true);
    # endif // if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
  }
  return str;
}

uint8_t getDerivedValueCountForTask(taskIndex_t taskIndex) {
  uint8_t derivedVars = 0;
  String postfix;
  const String search = getDerivedValueSearchAndPostfix(getTaskDeviceName(taskIndex), postfix);

  auto it = customStringVar.begin();

  while (it != customStringVar.end()) {
    if (it->first.startsWith(search) && it->first.endsWith(postfix)) {
      ++derivedVars;
    }
    else if (it->first.substring(0, search.length()).compareTo(search) > 0) {
      break;
    }
    ++it;
  }
  return derivedVars;
}

String getDerivedValueSearchAndPostfix(String taskName, String& postfix) {
  taskName.toLowerCase();
  const String search = strformat(F(TASK_VALUE_DERIVED_PREFIX_TEMPLATE), taskName.c_str(), FsP(F("X")));
  postfix = search.substring(search.indexOf('X') + 1);
  return search.substring(0, search.indexOf('X')); // Cut off left of valuename
}

String getDerivedValueNameUomAndVType(String taskName, String valueName, String& uom, String& vType) {
  taskName.toLowerCase();
  valueName.toLowerCase();
  vType = getCustomStringVar(strformat(F(TASK_VALUE_VTYPE_PREFIX_TEMPLATE), 
                                       taskName.c_str(), valueName.c_str()));
  uom   = getCustomStringVar(strformat(F(TASK_VALUE_UOM_PREFIX_TEMPLATE),
                                       taskName.c_str(), valueName.c_str()));
  return  getCustomStringVar(strformat(F(TASK_VALUE_NAME_PREFIX_TEMPLATE), 
                                       taskName.c_str(), valueName.c_str()));
}

String getDerivedValueName(String taskName, String valueName) {
  taskName.toLowerCase();
  valueName.toLowerCase();
  return  getCustomStringVar(strformat(F(TASK_VALUE_NAME_PREFIX_TEMPLATE), 
                                       taskName.c_str(), valueName.c_str()));
}
#endif // if FEATURE_STRING_VARIABLES

String parseTemplate(String& tmpString)
{
  return parseTemplate(tmpString, false);
}

String parseTemplate(String& tmpString, bool useURLencode)
{
  return parseTemplate_padded(tmpString, 0, useURLencode);
}

String parseTemplate_padded(String& tmpString, uint8_t minimal_lineSize)
{
  return parseTemplate_padded(tmpString, minimal_lineSize, false);
}

String parseTemplate_padded(String& tmpString, uint8_t minimal_lineSize, bool useURLencode)
{
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("parseTemplate_padded"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  START_TIMER;

  // Keep current loaded taskSettings to restore at the end.
  const taskIndex_t currentTaskIndex = ExtraTaskSettings.TaskIndex;
  String newString;
  newString.reserve(minimal_lineSize); // Our best guess of the new size.

  if (parseTemplate_CallBack_ptr != nullptr) {
    parseTemplate_CallBack_ptr(tmpString, useURLencode);
  }
  parseSystemVariables(tmpString, useURLencode);

  int startpos = 0;
  int lastStartpos = 0;
  int endpos = 0;
  bool mustReplaceEscapedSquareBracket = false;
  String MaskEscapedBracket;

  if (hasEscapedCharacter(tmpString, '[') || hasEscapedCharacter(tmpString, ']')) {
    // replace the \[ and \] with other characters to mask the escaped square brackets so we can continue parsing.
    // We have to unmask then after we're finished.
    MaskEscapedBracket = static_cast<char>(0x05); // ASCII 0x05 = Enquiry ENQ
    tmpString.replace(F("\\["), MaskEscapedBracket);
    MaskEscapedBracket = static_cast<char>(0x06); // ASCII 0x06 = Acknowledge ACK
    tmpString.replace(F("\\]"), MaskEscapedBracket);
    mustReplaceEscapedSquareBracket = true;
  }

  {
    String deviceName, valueName, format;

    while (findNextDevValNameInString(tmpString, startpos, endpos, deviceName, valueName, format)) {
      // First copy all upto the start of the [...#...] part to be replaced.
      newString += tmpString.substring(lastStartpos, startpos);

      // deviceName is lower case, so we can compare literal string (no need for equalsIgnoreCase)
      const bool devNameEqInt = equals(deviceName, F("int"));
      #if FEATURE_STRING_VARIABLES
      const bool devNameEqStr    = equals(deviceName, F("str"));
      const bool devNameEqLength = equals(deviceName, F("length"));
      #endif // if FEATURE_STRING_VARIABLES
      if (devNameEqInt || equals(deviceName, F("var"))
         #if FEATURE_STRING_VARIABLES
         || devNameEqStr || devNameEqLength
         #endif // if FEATURE_STRING_VARIABLES
         )
      {
        // Address an internal variable either as float or as int
        // For example: Let,10,[VAR#9]
        // For example: Let,10,[INT#bla]

        if (!valueName.isEmpty()) {
         #if FEATURE_STRING_VARIABLES
         if (devNameEqStr) {
           String value(getCustomStringVar(valueName));
           transformValue(
              newString, 
              minimal_lineSize, 
              std::move(value), 
              format, 
              tmpString);
         } else
         if (devNameEqLength) {
           String value(getCustomStringVar(valueName).length());
           transformValue(
              newString, 
              minimal_lineSize, 
              std::move(value), 
              format, 
              tmpString);
         } else
         #endif
         {
          const ESPEASY_RULES_FLOAT_TYPE floatvalue = getCustomFloatVar(valueName);
          unsigned char nr_decimals = maxNrDecimals_fpType(floatvalue);
          bool trimTrailingZeros    = true;

          if (devNameEqInt) {
            nr_decimals = 0;
          } else if (!format.isEmpty())
          {
            // There is some formatting here, so do not throw away decimals
            trimTrailingZeros = false;
          }
          #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
          String value = doubleToString(floatvalue, nr_decimals, trimTrailingZeros);
          #else
          String value = floatToString(floatvalue, nr_decimals, trimTrailingZeros);
          #endif
          transformValue(
            newString, 
            minimal_lineSize, 
            std::move(value), 
            format, 
            tmpString);
         }
        }
      }
      else if (equals(deviceName, F("plugin")))
      {
        // Handle a plugin request.
        // For example: "[Plugin#GPIO#Pinstate#N]"
        // The command is stored in valueName & format
        String command = strformat(F("%s#%s"), valueName.c_str(), format.c_str());
        command.replace('#', ',');

        if (getGPIOPinStateValues(command)) {
          newString += command;
        }
  /* @giig1967g
        if (PluginCall(PLUGIN_REQUEST, 0, command))
        {
          // Do not call transformValue here.
          // The "format" is not empty so must not call the formatter function.
          newString += command;
        }
  */
      }
      else
      {
        // Address a value from a plugin.
        // For example: "[bme#temp]"
        // If value name is unknown, run a PLUGIN_GET_CONFIG_VALUE command.
        // For example: "[<taskname>#getLevel]"
        taskIndex_t taskIndex = findTaskIndexByName(deviceName, true); // Check for enabled/disabled is done separately

        if (validTaskIndex(taskIndex)) {
          bool isHandled = false;
          if (Settings.TaskDeviceEnabled[taskIndex]) {
            uint8_t valueNr = findDeviceValueIndexByName(valueName, taskIndex);

            if (valueNr != VARS_PER_TASK) {
              // here we know the task and value, so find the uservar
              // Try to format and transform the values
              bool   isvalid;
              String value = formatUserVar(taskIndex, valueNr, isvalid);

              if (isvalid) {
                transformValue(newString, minimal_lineSize, std::move(value), format, tmpString
                               #if FEATURE_STRING_VARIABLES
                               , taskIndex, valueNr, valueName // for handling $ format option
                               #endif // if FEATURE_STRING_VARIABLES
                              );
                isHandled = true;
              }
            } else {
              // try if this is a get config request
              struct EventStruct TempEvent(taskIndex);
              String tmpName = valueName;

              if (PluginCall(PLUGIN_GET_CONFIG_VALUE, &TempEvent, tmpName))
              {
                transformValue(newString, minimal_lineSize, std::move(tmpName), format, tmpString
                               #if FEATURE_STRING_VARIABLES
                               , taskIndex, INVALID_TASKVAR_INDEX, valueName // for handling $ format option
                               #endif // if FEATURE_STRING_VARIABLES
                              );
                isHandled = true;
              }
            }
          }
          if (!isHandled && valueName.startsWith(F("settings."))) {  // Task settings values
            String value;
            if (valueName.endsWith(F(".enabled"))) {           // Task state
              value = Settings.TaskDeviceEnabled[taskIndex] ? '1' : '0';
            } else if (valueName.endsWith(F(".interval"))) {   // Task interval
              value = Settings.TaskDeviceTimer[taskIndex];
            } else if (valueName.endsWith(F(".valuecount"))) { // Task value count
              value = getValueCountForTask(taskIndex);
            } else if ((valueName.indexOf(F(".controller")) == 8) && valueName.length() >= 20) { // Task controller values
              String ctrl = valueName.substring(19, 20);
              int32_t ctrlNr = 0;
              if (validIntFromString(ctrl, ctrlNr) && (ctrlNr >= 1) && (ctrlNr <= CONTROLLER_MAX) && 
                  Settings.ControllerEnabled[ctrlNr - 1]) { // Controller nr. valid and enabled
                if (valueName.endsWith(F(".enabled"))) {    // Task-controller enabled
                  value = Settings.TaskDeviceSendData[ctrlNr - 1][taskIndex];
                } else if (valueName.endsWith(F(".idx"))) { // Task-controller idx value
                  protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(ctrlNr - 1);

                  if (validProtocolIndex(ProtocolIndex) && 
                      getProtocolStruct(ProtocolIndex).usesID && (Settings.Protocol[ctrlNr - 1] != 0)) {
                    value = Settings.TaskDeviceID[ctrlNr - 1][taskIndex];
                  }
                }
              }
            }
            if (!value.isEmpty()) {
              transformValue(newString, minimal_lineSize, std::move(value), format, tmpString
                             #if FEATURE_STRING_VARIABLES
                             , taskIndex, INVALID_TASKVAR_INDEX, valueName // for handling $ format option
                             #endif // if FEATURE_STRING_VARIABLES
                            );
              // isHandled = true;
            }
          }
          #if FEATURE_STRING_VARIABLES
          if (!isHandled && Settings.TaskDeviceEnabled[taskIndex]) {
            String value;
            const String valName = parseString(valueName, 1);
            String derived = getCustomStringVar(strformat(F(TASK_VALUE_DERIVED_PREFIX_TEMPLATE), deviceName.c_str(), valName.c_str()));
            if (!derived.isEmpty()) {
              value = parseTemplateAndCalculate(derived);
              if (!value.isEmpty()) {
                transformValue(newString, minimal_lineSize, std::move(value), format, tmpString,
                               taskIndex, INVALID_TASKVAR_INDEX, valName // for handling $ format option
                              );
                isHandled = true;
              }
            }
          }
          #endif // if FEATURE_STRING_VARIABLES

          #if FEATURE_TASKVALUE_ATTRIBUTES
          if (!isHandled && valueName.indexOf('.') > -1) { // TaskValue specific attributes
            const String valName = parseString(valueName, 1, '.');
            const String command = parseString(valueName, 2, '.');
            String value;

            if (!command.isEmpty()) {
              const uint8_t valueCount = getValueCountForTask(taskIndex);

              for (taskVarIndex_t i = 0; i < valueCount; i++) {
                if (valName.equalsIgnoreCase(Cache.getTaskDeviceValueName(taskIndex, i))) {
                  #if FEATURE_TASKVALUE_UNIT_OF_MEASURE
                  if (equals(command, F("uom"))) { // Fetch UnitOfMeasure
                    value = toUnitOfMeasureName(Cache.getTaskVarUnitOfMeasure(taskIndex, i));
                    isHandled = true; // Empty is a valid result
                    break;
                  } else
                  #endif // if FEATURE_TASKVALUE_UNIT_OF_MEASURE
                  if (equals(command, F("decimals"))) { // Fetch decimals
                    value = Cache.getTaskDeviceValueDecimals(taskIndex, i);
                    break;
                  } else
                  if (equals(command, F("hasformula"))) { // Fetch formula status
                    value = Cache.hasFormula(taskIndex, i);
                    break;
                  #if FEATURE_PLUGIN_STATS
                  } else
                  if (equals(command, F("statsenabled"))) { // Fetch Stats enabled
                    value = Cache.enabledPluginStats(taskIndex, i);
                    break;
                  #endif // if FEATURE_PLUGIN_STATS
                  }
                }
              }
              if (!value.isEmpty() || isHandled) {
                transformValue(newString, minimal_lineSize, std::move(value), format, tmpString);
                // isHandled = true;
              }
            }
          }
          #endif // if FEATURE_TASKVALUE_ATTRIBUTES

          #if FEATURE_STRING_VARIABLES
          if (!isHandled && valueName.indexOf('.') > -1) {
            String value;
            const String fullValueName = parseString(valueName, 1);
            const String valName       = parseString(fullValueName, 1, '.');
            const String command       = parseString(fullValueName, 2, '.');
            if (equals(command, F("uom"))) { // Fetch UnitOfMeasure
              value = getCustomStringVar(strformat(F(TASK_VALUE_UOM_PREFIX_TEMPLATE), deviceName.c_str(), valName.c_str()));
            }
            if (!value.isEmpty()) {
              transformValue(newString, minimal_lineSize, std::move(value), format, tmpString,
                              taskIndex, INVALID_TASKVAR_INDEX, valName
                            );
              // isHandled = true;
            }
          }
          #endif // if FEATURE_STRING_VARIABLES
        }
      }


      // Conversion is done (or impossible) for the found "[...#...]"
      // Continue with the next one.
      lastStartpos = endpos + 1;
      startpos     = endpos + 1;

      // This may have taken some time, so call delay()
      delay(0);
    }
  }

  // Copy the rest of the string (or all if no replacements were done)
  newString += tmpString.substring(lastStartpos);
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("parseTemplate2"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  if (mustReplaceEscapedSquareBracket) {
    // We now have to check if we did mask some escaped square bracket and unmask them.
    // Let's hope we don't mess up any Unicode here.
    MaskEscapedBracket = static_cast<char>(0x05); // ASCII 0x05 = Enquiry ENQ
    newString.replace(MaskEscapedBracket, F("\\["));
    MaskEscapedBracket = static_cast<char>(0x06); // ASCII 0x06 = Acknowledge ACK
    newString.replace(MaskEscapedBracket, F("\\]"));
  }

  // Restore previous loaded taskSettings
  if (validTaskIndex(currentTaskIndex))
  {
    LoadTaskSettings(currentTaskIndex);
  }

  parseStandardConversions(newString, useURLencode);

  // process other markups as well
  parse_string_commands(newString);

  // padding spaces
  while (newString.length() < minimal_lineSize) {
    newString += ' ';
  }

  STOP_TIMER(PARSE_TEMPLATE_PADDED);
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("parseTemplate3"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  return newString;
}

/********************************************************************************************\
   Transform values
 \*********************************************************************************************/

bool isTransformString(char c, bool logicVal, String& strValue)
{
  const __FlashStringHelper * value = F("");
  char value_ch = '\0';
  switch (c) {
    case 'O':
      value = logicVal == 0 ? F("OFF") : F(" ON"); // (equivalent to XOR operator)
      break;
    case 'C':
      value = logicVal == 0 ? F("CLOSE") : F(" OPEN");
      break;
    case 'c':
      value = logicVal == 0 ? F("CLOSED") : F("  OPEN");
      break;
    case 'M':
      value = logicVal == 0 ? F("AUTO") : F(" MAN");
      break;
    case 'm':
      value_ch = logicVal == 0 ? 'A' : 'M';
      break;
    case 'H':
      value = logicVal == 0 ? F("COLD") : F(" HOT");
      break;
    case 'U':
      value = logicVal == 0 ? F("DOWN") : F("  UP");
      break;
    case 'u':
      value_ch = logicVal == 0 ? 'D' : 'U';
      break;
    case 'Y':
      value = logicVal == 0 ? F(" NO") : F("YES");
      break;
    case 'y':
      value_ch = logicVal == 0 ? 'N' : 'Y';
      break;
    case 'X':
      value_ch = logicVal == 0 ? 'O' : 'X';
      break;
    case 'I':
      value = logicVal == 0 ? F("OUT") : F(" IN");
      break;
    case 'L':
      value = logicVal == 0 ? F(" LEFT") : F("RIGHT");
      break;
    case 'l':
      value_ch = logicVal == 0 ? 'L' : 'R';
      break;
    case 'Z': // return "0" or "1"
      value_ch = logicVal == 0 ? '0' : '1';
      break;
    default:
      return false;
  }
  if (value_ch != '\0') {
    strValue = value_ch;
  } else {
    strValue = value;
  }
  return true;
}


// Syntax: [task#value#transformation#justification]
// valueFormat="transformation#justification"
void transformValue(
  String      & newString,
  uint8_t       lineSize,
  String        value,
  String      & valueFormat,
  const String& tmpString
  #if FEATURE_STRING_VARIABLES
  , taskIndex_t taskIndex
  , uint8_t     valueIndex
  , String      valueName
  #endif // if FEATURE_STRING_VARIABLES
  )
{
  // FIXME TD-er: This function does append to newString and uses its length to perform right aling.
  // Is this the way it is intended to use?
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("transformValue"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  // start changes by giig1967g - 2018-04-20
  // Syntax: [task#value#transformation#justification]
  // valueFormat="transformation#justification"
  if (valueFormat.length() > 0) // do the checks only if a Format is defined to optimize loop
  {
    String valueJust;

    int hashtagIndex = valueFormat.indexOf('#');

    if (hashtagIndex >= 0)
    {
      valueJust   = valueFormat.substring(hashtagIndex + 1); // Justification part
      valueFormat = valueFormat.substring(0, hashtagIndex);  // Transformation part
    }

    // valueFormat="transformation"
    // valueJust="justification"
    if (valueFormat.length() > 0) // do the checks only if a Format is defined to optimize loop
    {
      int logicVal    = 0;
      ESPEASY_RULES_FLOAT_TYPE valFloat{};

      if (validDoubleFromString(value, valFloat))
      {
        // to be used for binary values (0 or 1)
        logicVal = lround(static_cast<float>(valFloat)) == 0 ? 0 : 1;
      } else {
        if (value.length() > 0) {
          logicVal = 1;
        }
      }
      String tempValueFormat = valueFormat;
      {
        const int invertedIndex = tempValueFormat.indexOf('!');

        if (invertedIndex != -1) {
          // We must invert the value.
          logicVal = (logicVal == 0) ? 1 : 0;

          // Remove the '!' from the string.
          tempValueFormat.remove(invertedIndex, 1);
        }
      }

      const int  rightJustifyIndex = tempValueFormat.indexOf('R');
      const bool rightJustify      = rightJustifyIndex >= 0 ? 1 : 0;

      if (rightJustify) {
        tempValueFormat.remove(rightJustifyIndex, 1);
      }

      const int tempValueFormatLength = tempValueFormat.length();

      // Check Transformation syntax
      if (tempValueFormatLength > 0)
      {
        if (!isTransformString(tempValueFormat[0], logicVal, value)) {
          switch (tempValueFormat[0])
          {
            case 'V': // value = value without transformations
              break;
            case 'p': // Password hide using asterisks or custom character: pc
            {
              char maskChar = '*';

              if (tempValueFormatLength > 1)
              {
                maskChar = tempValueFormat[1];
              }

              if (equals(value, '0')) {
                free_string(value);
              } else {
                const int valueLength = value.length();

                for (int i = 0; i < valueLength; i++) {
                  value[i] = maskChar;
                }
              }
              break;
            }
            case 'D': // Dx.y min 'x' digits zero filled & 'y' decimal fixed digits
            case 'd': // like above but with spaces padding
            {
              int x = 0;
              int y = 0;

              switch (tempValueFormatLength)
              {
                case 2: // Dx

                  if (isDigit(tempValueFormat[1]))
                  {
                    x = static_cast<int>(tempValueFormat[1]) - '0';
                  }
                  break;
                case 3: // D.y

                  if ((tempValueFormat[1] == '.') && isDigit(tempValueFormat[2]))
                  {
                    y = static_cast<int>(tempValueFormat[2]) - '0';
                  }
                  break;
                case 4: // Dx.y

                  if (isDigit(tempValueFormat[1]) && (tempValueFormat[2] == '.') && isDigit(tempValueFormat[3]))
                  {
                    x = static_cast<int>(tempValueFormat[1]) - '0';
                    y = static_cast<int>(tempValueFormat[3]) - '0';
                  }
                  break;
                case 1:  // D
                default: // any other combination x=0; y=0;
                  break;
              }
              bool trimTrailingZeros = false;
#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
              value = doubleToString(valFloat, y, trimTrailingZeros);
#else
              value = floatToString(valFloat, y, trimTrailingZeros);
#endif
              int indexDot = value.indexOf('.');

              if (indexDot == -1) {
                indexDot = value.length();
              }

              for (uint8_t f = 0; f < (x - indexDot); f++) {
                value = (tempValueFormat[0] == 'd' ? ' ' : '0') + value;
              }
              break;
            }
            case 'F': // FLOOR (round down)
            #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
              value = static_cast<int>(floor(valFloat));
            #else
              value = static_cast<int>(floorf(valFloat));
            #endif
              break;
            case 'E': // CEILING (round up)
            #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
              value = static_cast<int>(ceil(valFloat));
            #else
              value = static_cast<int>(ceilf(valFloat));
            #endif
              break;
            #if FEATURE_STRING_VARIABLES
            case TASK_VALUE_PRESENTATION_PREFIX_CHAR: // '$' Apply presentation format
            {
              if (validTaskIndex(taskIndex) && (validTaskVarIndex(valueIndex) || !valueName.isEmpty())) {
                const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(taskIndex);
                bool hasPresentation = false;
                EventStruct TempEvent(taskIndex);
                const String presentation = formatUserVarForPresentation(&TempEvent, valueIndex, hasPresentation, value, DeviceIndex, valueName);
                if (hasPresentation) {
                  value = presentation;
                }
              }
              break;
            }
            #endif // if FEATURE_STRING_VARIABLES
            default:
              value = F("ERR");
              break;
          }
        }

        // Check Justification syntax
        const int valueJustLength = valueJust.length();

        if (valueJustLength > 0) // do the checks only if a Justification is defined to optimize loop
        {
          value.trim();          // remove right justification spaces for backward compatibility

          switch (valueJust[0])
          {
            case 'P': // Prefix Fill with n spaces: Pn

              if (valueJustLength > 1)
              {
                if (isDigit(valueJust[1]))                          // Check Pn where n is between 0 and 9
                {
                  int filler = valueJust[1] - value.length() - '0'; // char '0' = 48; char '9' = 58

                  for (uint8_t f = 0; f < filler; f++) {
                    newString += ' ';
                  }
                }
              }
              break;
            case 'S': // Suffix Fill with n spaces: Sn

              if (valueJustLength > 1)
              {
                if (isDigit(valueJust[1]))                          // Check Sn where n is between 0 and 9
                {
                  int filler = valueJust[1] - value.length() - '0'; // 48

                  for (uint8_t f = 0; f < filler; f++) {
                    value += ' ';
                  }
                }
              }
              break;
            case 'L': // left part of the string

              if (valueJustLength > 1)
              {
                if (isDigit(valueJust[1])) // Check n where n is between 0 and 9
                {
                  value = value.substring(0, static_cast<int>(valueJust[1]) - '0');
                }
              }
              break;
            case 'R': // Right part of the string

              if (valueJustLength > 1)
              {
                if (isDigit(valueJust[1])) // Check n where n is between 0 and 9
                {
                  value = value.substring(std::max(0, static_cast<int>(value.length()) - (static_cast<int>(valueJust[1]) - '0')));
                }
              }
              break;
            case 'U': // Substring Ux.y where x=firstChar and y=number of characters

              if (valueJustLength > 1)
              {
                if (isDigit(valueJust[1]) && (valueJust[2] == '.') && isDigit(valueJust[3]) && (valueJust[1] > '0') && (valueJust[3] > '0'))
                {
                  value = value.substring(std::min(static_cast<int>(value.length()), static_cast<int>(valueJust[1]) - '0' - 1),
                                          static_cast<int>(valueJust[1]) - '0' - 1 + static_cast<int>(valueJust[3]) - '0');
                }
                else
                {
                  newString += F("ERR");
                }
              }
              break;
            case 'C': // Capitalize First Word-Character value (space/period are checked)

              if (value.length() > 0) {
                value.toLowerCase();
                bool nextCapital = true;

                for (uint8_t i = 0; i < value.length(); i++) {
                  if (nextCapital) {
                    value[i] = toupper(value[i]);
                  }
                  nextCapital = (value[i] == ' ' || value[i] == '.'); // Very simple, capitalize-first-after-space/period
                }
              }
              break;
            case 'u': // Uppercase
              value.toUpperCase();
              break;
            case 'l': // Lowercase
              value.toLowerCase();
              break;
            default:
              newString += F("ERR");
              break;
          }
        }
      }

      if (rightJustify)
      {
        int filler = lineSize - newString.length() - value.length() - tmpString.length();

        for (uint8_t f = 0; f < filler; f++) {
          newString += ' ';
        }
      }
      {
#ifndef BUILD_NO_DEBUG

        if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
          String logFormatted = F("DEBUG: Formatted String='");
          logFormatted += newString;
          logFormatted += value;
          logFormatted += '\'';
          addLogMove(LOG_LEVEL_DEBUG, logFormatted);
        }
#endif // ifndef BUILD_NO_DEBUG
      }
    }
  }

  // end of changes by giig1967g - 2018-04-18

  newString += value;
  {
#ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG_DEV)) {
      String logParsed = F("DEBUG DEV: Parsed String='");
      logParsed += newString;
      logParsed += '\'';
      addLogMove(LOG_LEVEL_DEBUG_DEV, logParsed);
    }
#endif // ifndef BUILD_NO_DEBUG
  }
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("transformValue2"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
}

// Find the first (enabled) task with given name
// Return INVALID_TASK_INDEX when not found, else return taskIndex
taskIndex_t findTaskIndexByName(String deviceName, bool allowDisabled)
{
  deviceName.toLowerCase();
  // cache this, since LoadTaskSettings does take some time.
  auto result = Cache.taskIndexName.find(deviceName);

  if (result != Cache.taskIndexName.end()) {
    return result->second;
  }

  for (taskIndex_t taskIndex = 0; taskIndex < TASKS_MAX; taskIndex++)
  {
    if (Settings.TaskDeviceEnabled[taskIndex] || allowDisabled) {
      String taskDeviceName = getTaskDeviceName(taskIndex);

      if (!taskDeviceName.isEmpty())
      {
        // Use entered taskDeviceName can have any case, so compare case insensitive.
        if (deviceName.equalsIgnoreCase(taskDeviceName))
        {
          Cache.taskIndexName.emplace(
            std::make_pair(
              std::move(deviceName), 
              taskIndex));
          return taskIndex;
        }
      }
    }
  }
  return INVALID_TASK_INDEX;
}

// Find the first device value index of a taskIndex.
// Return VARS_PER_TASK if none found.
uint8_t findDeviceValueIndexByName(const String& valueName, taskIndex_t taskIndex)
{
  const deviceIndex_t deviceIndex = getDeviceIndex_from_TaskIndex(taskIndex);

  if (!validDeviceIndex(deviceIndex)) { return VARS_PER_TASK; }

  #ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  #endif


  // cache this, since LoadTaskSettings does take some time.
  // We need to use a cache search key including the taskIndex,
  // to allow several tasks to have the same value names.
  String cache_valueName = strformat(
    F("%s#%d"),                   // The '#' cannot exist in a value name, use it in the cache key.
    valueName.c_str(),
    static_cast<int>(taskIndex));
  cache_valueName.toLowerCase();  // No need to store multiple versions of the same entry with only different case.
  
  auto result = Cache.taskIndexValueName.find(cache_valueName);

  if (result != Cache.taskIndexValueName.end()) {
    return result->second;
  }
  const uint8_t valCount = getValueCountForTask(taskIndex);

  for (uint8_t valueNr = 0; valueNr < valCount; valueNr++)
  {
    // Check case insensitive, since the user entered value name can have any case.
    if (valueName.equalsIgnoreCase(Cache.getTaskDeviceValueName(taskIndex, valueNr)))
    {
      Cache.taskIndexValueName.emplace(
        std::make_pair(
          std::move(cache_valueName), 
          valueNr));
      return valueNr;
    }
  }
  return VARS_PER_TASK;
}

// Find positions of [...#...] in the given string.
// Only update pos values on success.
// Return true when found.
bool findNextValMarkInString(const String& input, int& startpos, int& hashpos, int& endpos) {
  int tmpStartpos = input.indexOf('[', startpos);

  if (tmpStartpos == -1) { return false; }
  const int tmpHashpos = input.indexOf('#', tmpStartpos);

  if (tmpHashpos == -1) { return false; }

  // We found a hash position, check if there is another '[' inbetween.
  for (int i = tmpStartpos; i < tmpHashpos; ++i) {
    if (input[i] == '[') {
      tmpStartpos = i;
    }
  }

  const int tmpEndpos = input.indexOf(']', tmpStartpos);

  if (tmpEndpos == -1) { return false; }

  if (tmpHashpos >= tmpEndpos) {
    return false;
  }

  hashpos  = tmpHashpos;
  startpos = tmpStartpos;
  endpos   = tmpEndpos;
  return true;
}

// Find [deviceName#valueName] or [deviceName#valueName#format]
// DeviceName and valueName will be returned in lower case.
// Format may contain case sensitive formatting syntax.
bool findNextDevValNameInString(const String& input, int& startpos, int& endpos, String& deviceName, String& valueName, String& format) {
  int hashpos;

  if (!findNextValMarkInString(input, startpos, hashpos, endpos)) { return false; }

  move_special(deviceName, input.substring(startpos + 1, hashpos));
  move_special(valueName , input.substring(hashpos + 1, endpos));
  hashpos = valueName.indexOf('#');

  if (hashpos != -1) {
    // Found an extra '#' in the valueName, will split valueName and format.
    move_special(format,    valueName.substring(hashpos + 1));
    move_special(valueName, valueName.substring(0, hashpos));
  } else {
    free_string(format);
  }
  deviceName.toLowerCase();
  valueName.toLowerCase();
  return true;
}

/********************************************************************************************\
   Check to see if a given argument is a valid taskIndex (argc = 0 => command)
 \*********************************************************************************************/
taskIndex_t parseCommandArgumentTaskIndex(const String& string, unsigned int argc)
{
  taskIndex_t taskIndex = INVALID_TASK_INDEX;
  const int   ti        = parseCommandArgumentInt(string, argc);

  if (ti > 0) {
    // Task Index used as argument in commands start at 1.
    taskIndex = static_cast<taskIndex_t>(ti - 1);
  }
  return taskIndex;
}

/********************************************************************************************\
   Get int from command argument (argc = 0 => command)
 \*********************************************************************************************/
int parseCommandArgumentInt(const String& string, unsigned int argc,
                            int errorValue)
{
  int value = 0;

  if (argc > 0) {
    // No need to check for the command (argc == 0)
    String TmpStr;

    if (GetArgv(string.c_str(), TmpStr, argc + 1)) {
      value = CalculateParam(TmpStr, errorValue);
    }
  }
  return value;
}

/********************************************************************************************\
   Parse a command string to event struct
 \*********************************************************************************************/
void parseCommandString(struct EventStruct *event, const String& string)
{
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("parseCommandString"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  for (uint8_t i = 0; i < 5; ++i) {
    event->ParN[i] = parseCommandArgumentInt(string, i + 1);
  }
}

#include "../Helpers/Hardware_device_info.h"

#ifdef ESP32S3

// See:
// - https://github.com/espressif/esptool/blob/master/esptool/targets/esp32s3.py
// - https://github.com/tasmota/esp-idf/blob/206ce4b7f875bf5568ba47aba23f4b28e81b0574/components/efuse/esp32s3/esp_efuse_table.csv#L203-L208
# include <soc/soc.h>
# include <soc/efuse_reg.h>
# include <soc/spi_reg.h>
# include <soc/spi_pins.h>
# include <soc/rtc.h>
# include <esp_chip_info.h>
# include <bootloader_common.h>

// Flash data lines:
// https://github.com/tasmota/esp-idf/blob/206ce4b7f875bf5568ba47aba23f4b28e81b0574/components/efuse/esp32s3/esp_efuse_table.csv#L175

/** EFUSE_FLASH_CAP : R; bitpos: [29:27]; default: 0;
 * register: EFUSE_RD_MAC_SPI_SYS_3_REG
 */
# define EFUSE_FLASH_CAP    0x00000007U
# define EFUSE_FLASH_CAP_M  (EFUSE_FLASH_CAP_V << EFUSE_FLASH_CAP_S)
# define EFUSE_FLASH_CAP_V  0x00000007U
# define EFUSE_FLASH_CAP_S  27


/** EFUSE_FLASH_VENDOR : R; bitpos: [2:0]; default: 0;
 * register: EFUSE_RD_MAC_SPI_SYS_4_REG
 */
# define EFUSE_FLASH_VENDOR    0x00000007U
# define EFUSE_FLASH_VENDOR_M  (EFUSE_FLASH_VENDOR_V << EFUSE_FLASH_VENDOR_S)
# define EFUSE_FLASH_VENDOR_V  0x00000007U
# define EFUSE_FLASH_VENDOR_S  0


/*
   switch (flash_vendor)
   {
   case 1: features += F("(XMC)"); break;
   case 2: features += F("(GD)"); break;
   case 3: features += F("(FM)"); break;
   case 4: features += F("(TT)"); break;
   case 5: features += F("(BY)"); break;
   }
 */
bool isFlashInterfacePin_ESPEasy(int gpio) {
  // GPIO-26 ... 32: SPI flash and PSRAM
  // GPIO-33 ... 37: SPI 8 ­line mode (OPI) pins for flash or PSRAM, like ESP32-S3R8 / ESP32-S3R8V.
//  return (gpio) >= 26 && (gpio) <= 32;
  switch (gpio) {
    case 26: return FoundPSRAM(); // SPICS1   Only when PSRAM is present
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 0)
    case SPI_IOMUX_PIN_NUM_HD:
    case SPI_IOMUX_PIN_NUM_CS:
    case SPI_IOMUX_PIN_NUM_MOSI:
    case SPI_IOMUX_PIN_NUM_CLK:
    case SPI_IOMUX_PIN_NUM_MISO:
    case SPI_IOMUX_PIN_NUM_WP:
      return true;
#else
    case MSPI_IOMUX_PIN_NUM_HD:
    case MSPI_IOMUX_PIN_NUM_WP:
    case MSPI_IOMUX_PIN_NUM_CS0:
    case MSPI_IOMUX_PIN_NUM_CLK:
    case MSPI_IOMUX_PIN_NUM_MOSI:
    case MSPI_IOMUX_PIN_NUM_MISO:
      return true;

    case MSPI_IOMUX_PIN_NUM_D4:
    case MSPI_IOMUX_PIN_NUM_D5:
    case MSPI_IOMUX_PIN_NUM_D6:
    case MSPI_IOMUX_PIN_NUM_D7:
      // Boards with 8M PSRAM also use OPI flash
      return getEmbeddedFlashSize() >= 8;      
#endif
  }
  return false;
}

int32_t getEmbeddedFlashSize()
{
  const uint32_t flash_cap = REG_GET_FIELD(EFUSE_RD_MAC_SPI_SYS_3_REG, EFUSE_FLASH_CAP);

  switch (flash_cap) {
    case 0: return 0;
    case 1: return 8;
    case 2: return 4;
  }

  // Unknown value, thus mark as negative value
  return -1 *  static_cast<int32_t>(flash_cap);
}

/** EFUSE_PSRAM_CAP : R; bitpos: [4:3]; default: 0;
 * register: EFUSE_RD_MAC_SPI_SYS_4_REG
 */
# define EFUSE_PSRAM_CAP    0x00000003U
# define EFUSE_PSRAM_CAP_M  (EFUSE_PSRAM_CAP_V << EFUSE_PSRAM_CAP_S)
# define EFUSE_PSRAM_CAP_V  0x00000003U
# define EFUSE_PSRAM_CAP_S  3

/** EFUSE_PSRAM_VENDOR : R; bitpos: [8:7]; default: 0;
 * register: EFUSE_RD_MAC_SPI_SYS_4_REG
 */
# define EFUSE_PSRAM_VENDOR    0x00000003U
# define EFUSE_PSRAM_VENDOR_M  (EFUSE_PSRAM_VENDOR_V << EFUSE_PSRAM_VENDOR_S)
# define EFUSE_PSRAM_VENDOR_V  0x00000003U
# define EFUSE_PSRAM_VENDOR_S  7


/*
   switch (psram_vendor)
   {
   case 1: features += F("(AP_3v3)"); break;
   case 2: features += F("(AP_1v8)"); break;
   }
 */
int32_t getEmbeddedPSRAMSize()
{
  const uint32_t psram_cap = REG_GET_FIELD(EFUSE_RD_MAC_SPI_SYS_4_REG, EFUSE_PSRAM_CAP);

  switch (psram_cap) {
    case 0: return 0;
    case 1: return 8;
    case 2: return 2;
  }

  // Unknown value, thus mark as negative value
  return -1 * static_cast<int32_t>(psram_cap);
}

# ifndef isPSRAMInterfacePin
bool isPSRAMInterfacePin(int gpio) {
  // GPIO-26 ... 32: SPI flash and PSRAM
  // GPIO-33 ... 37: SPI 8 ­line mode (OPI) pins for flash or PSRAM, like ESP32-S3R8 / ESP32-S3R8V.
  // See Appendix A, page 71: https://www.espressif.com/sites/default/files/documentation/esp32-s3_datasheet_en.pdf
  return FoundPSRAM() ? ((gpio) >= 33 && (gpio) <= 37) : false;
}

# endif // ifndef isPSRAMInterfacePin


const __FlashStringHelper* getChipModel(uint32_t chip_model, uint32_t chip_revision, uint32_t pkg_version, bool single_core)
{
  if ((CHIP_ESP32S3 == chip_model) || // ESP32-S3
      (4 == chip_model) ||            // ESP32-S3(beta2)
      (6 == chip_model)) {            // ESP32-S3(beta3)
    /*
       ESP32-S3 Series
       - 32-bit MCU & 2.4 GHz Wi-Fi & Bluetooth 5 (LE)
       - Xtensa® 32-bit LX7 dual-core processor that operates at up to 240 MHz
       - 512 KB of SRAM and 384 KB of ROM on the chip, and SPI, Dual SPI, Quad SPI, Octal SPI, QPI, and OPI interfaces that allow connection
         to flash and external RAM
       - Additional support for vector instructions in the MCU, which provides acceleration for neural network computing and signal
         processing workloads
       - Peripherals include 45 programmable GPIOs, SPI, I2S, I2C, PWM, RMT, ADC and UART, SD/MMC host and TWAITM
       - Reliable security features ensured by RSA-based secure boot, AES-XTS-based flash encryption, the innovative digital signature and
         the HMAC peripheral, “World Controller”
     */

    /*

       efuse_reg.h:
       EFUSE_RD_MAC_SPI_SYS_0_REG = block1_addr
       EFUSE_RD_MAC_SPI_SYS_3_REG = block1_addr + (4 * num_word)) // (num_word = 3)

     */


# if (ESP_IDF_VERSION_MAJOR >= 5)
    pkg_version = REG_GET_FIELD(EFUSE_RD_MAC_SPI_SYS_3_REG, EFUSE_PKG_VERSION);

    switch (pkg_version) {
      case 0:              return F("ESP32-S3");        // QFN56
      case 1:              return F("ESP32-S3-PICO-1"); // LGA56
    }
# endif // if (ESP_IDF_VERSION_MAJOR >= 5)

    return F("ESP32-S3"); // Max 240MHz, Dual core, QFN 7*7, ESP32-S3-WROOM-1, ESP32-S3-DevKitC-1
  }

  return F("Unknown");
}

#endif // ifdef ESP32S3

#include "../Helpers/RulesMatcher.h"

#include "../Helpers/ESPEasy_math.h"
#include "../Helpers/ESPEasy_time_calc.h"
#include "../Helpers/Numerical.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringParser.h"


bool ruleMatch(String event, String rule) {
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("ruleMatch"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  if (rule.equals("*")) {
    // wildcard, always process
    return true;
  }

  rule.trim();
  parseStandardConversions(rule, false);
  rule = parseTemplate(rule);

  event.trim();

  if (event.equalsIgnoreCase(rule)) {
    return true;
  }

  // clock events need different handling...
  if (event.substring(0, 10).equalsIgnoreCase(F("Clock#Time")))
  {
    int pos1 = event.indexOf('=');
    int pos2 = rule.indexOf('=');

    if ((pos1 > 0) && (pos2 > 0)) {
      if (event.substring(0, pos1).equalsIgnoreCase(rule.substring(0, pos2))) // if this is a clock rule
      {
//        addLog(LOG_LEVEL_INFO, concat(F("Clock#Time="), rule.substring(pos2 + 1)));
        unsigned long clockEvent = string2TimeLong(event.substring(pos1 + 1));
        unsigned long clockSet   = string2TimeLong(rule.substring(pos2 + 1));

        return matchClockEvent(clockEvent, clockSet);
      }
    } else {
      // Not supported yet, see: https://github.com/letscontrolit/ESPEasy/issues/2640
      return false;
    }
  }

  // Handling wildcard in event
  {
    const int asterisk_pos = rule.indexOf('*');

    if (asterisk_pos != -1) // a * sign in rule, so use a 'wildcard' match on message
    {
      return event.substring(0, asterisk_pos).equalsIgnoreCase(rule.substring(0, asterisk_pos));
    }
  }

  // Special handling of literal string events, they should start with '!'
  if (event.charAt(0) == '!') {
    const bool pound_char_found = rule.indexOf('#') != -1;

    if (!pound_char_found)
    {
      // no # sign in rule, use 'wildcard' match on event 'source'
      return event.substring(0, rule.length()).equalsIgnoreCase(rule);
    }
    return event.equalsIgnoreCase(rule);
  }


  // parse event into verb and value
  ESPEASY_RULES_FLOAT_TYPE value{};
  int    equal_pos   = event.indexOf('=');

  int nrDecimalsValue{};

  if (equal_pos >= 0) {
    if (!validDoubleFromString(event.substring(equal_pos + 1), value, nrDecimalsValue)) {
      return false;

      // FIXME TD-er: What to do when trying to match NaN values?
    }
    event = event.substring(0, equal_pos);
  }

  // parse rule
  int  posStart, posEnd;
  char compare;

  if (!findCompareCondition(rule, compare, posStart, posEnd)) {
    // No compare condition found, so just check if the event- and rule string match.
    return event.equalsIgnoreCase(rule);
  }

  const bool stringMatch = event.equalsIgnoreCase(rule.substring(0, posStart));
  ESPEASY_RULES_FLOAT_TYPE     ruleValue{};
  int nrDecimalsRuleValue{};

  if (!validDoubleFromString(rule.substring(posEnd), ruleValue, nrDecimalsRuleValue)) {
    return false;

    // FIXME TD-er: What to do when trying to match NaN values?
  }

  bool match = false;

  if (stringMatch) {
    const int minimalNrDecimals = std::min(nrDecimalsValue, nrDecimalsRuleValue);
    match = compareDoubleValues(compare, value, ruleValue, minimalNrDecimals);
  }
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("ruleMatch2"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  return match;
}


bool compareIntValues(char       compare,
                      int64_t Value1,
                      int64_t Value2)
{
  switch (compare) {
    case '>' + '=': return Value1 >= Value2;
    case '<' + '=': return Value1 <= Value2;
    case '<' + '>': return Value1 != Value2;
    case '>':       return Value1 > Value2;
    case '<':       return Value1 < Value2;
    case '=':       return Value1 == Value2;
  }
  return false;
}

#if FEATURE_STRING_VARIABLES
bool compareStringValues(char       compare,
                         String& Value1,
                         String& Value2)
{
  switch (compare) {
    case '>' + '=': return Value1 >= Value2;
    case '<' + '=': return Value1 <= Value2;
    case '<' + '>': return !Value1.equals(Value2);
    case '>':       return Value1 > Value2;
    case '<':       return Value1 < Value2;
    case '=':       return Value1.equals(Value2);
  }
  return false;
}
#endif // if FEATURE_STRING_VARIABLES

bool compareDoubleValues(char          compare,
                         const ESPEASY_RULES_FLOAT_TYPE& Value1,
                         const ESPEASY_RULES_FLOAT_TYPE& Value2,
                         int nrDecimals)
{
  if (nrDecimals == 0) {
    return compareIntValues(compare, Value1, Value2);
  }
  if (nrDecimals > 0) {
    if (std::abs(Value1 - Value2) < 1) {
      const int factor = computeDecimalFactorForDecimals(nrDecimals);
      return compareIntValues(
        compare, 
        std::round(Value1 * factor), 
        std::round(Value2 * factor));
    }
  }
  switch (compare) {
    case '>' + '=': return !definitelyLessThan(Value1, Value2);
    case '<' + '=': return !definitelyGreaterThan(Value1, Value2);
    case '<' + '>': return !essentiallyEqual(Value1, Value2);
    case '>':       return definitelyGreaterThan(Value1, Value2);
    case '<':       return definitelyLessThan(Value1, Value2);
    case '=':       return essentiallyEqual(Value1, Value2);
  }
  return false;
}

// Find the compare condition.
// @param posStart = first position of the compare condition in the string
// @param posEnd   = first position rest of the string, right after the compare condition.
bool findCompareCondition(const String& check, char& compare, int& posStart, int& posEnd)
{
  posStart = check.length();
  posEnd   = posStart;
  int  comparePos = 0;
  bool found      = false;

  if (((comparePos = check.indexOf(F("!="))) > 0) && (comparePos < posStart)) {
    posStart = comparePos;
    posEnd   = posStart + 2;
    compare  = '<' + '>';
    found    = true;
  }

  if (((comparePos = check.indexOf(F("<>"))) > 0) && (comparePos < posStart)) {
    posStart = comparePos;
    posEnd   = posStart + 2;
    compare  = '<' + '>';
    found    = true;
  }

  if (((comparePos = check.indexOf(F(">="))) > 0) && (comparePos < posStart)) {
    posStart = comparePos;
    posEnd   = posStart + 2;
    compare  = '>' + '=';
    found    = true;
  }

  if (((comparePos = check.indexOf(F("<="))) > 0) && (comparePos < posStart)) {
    posStart = comparePos;
    posEnd   = posStart + 2;
    compare  = '<' + '=';
    found    = true;
  }

  if (((comparePos = check.indexOf(F("=="))) > 0) && (comparePos < posStart)) {
    posStart = comparePos;
    posEnd   = posStart + 2;
    compare  = '=';
    found    = true;
  }

  if (((comparePos = check.indexOf('<')) > 0) && (comparePos < posStart)) {
    posStart = comparePos;
    posEnd   = posStart + 1;
    compare  = '<';
    found    = true;
  }

  if (((comparePos = check.indexOf('>')) > 0) && (comparePos < posStart)) {
    posStart = comparePos;
    posEnd   = posStart + 1;
    compare  = '>';
    found    = true;
  }

  if (((comparePos = check.indexOf('=')) > 0) && (comparePos < posStart)) {
    posStart = comparePos;
    posEnd   = posStart + 1;
    compare  = '=';
    found    = true;
  }
  return found;
}

bool getEventFromRulesLine(const String& line, String& event, String& action)
{
  if (line.length() == 0) {
    return false;
  }

  if (!line.substring(0, 3).equalsIgnoreCase(F("on "))) {
    return false;
  }

  // Make sure to skip any number of spaces between 'on' and the event.
  int startpos_event = 2;
  while (static_cast<int>(line.length()) > startpos_event && line[startpos_event] == ' ') {
    ++startpos_event;
  }

  // Need to look for the " do" part starting after "on " (3 chars)
  // and the event needs to be at least 1 char.
  String line_lc = line;

  line_lc.toLowerCase();
  const int pos_do = line_lc.indexOf(F(" do"), startpos_event + 1);

  if (pos_do == -1) {
    return false;
  }

  // event: The part between on ... do
  event = line.substring(startpos_event, pos_do);
  event.trim();

  // Ignore escape char
//  removeChar(event, '[');
//  removeChar(event, ']');

  // action: The optional part after the " do"
  action = line.substring(pos_do + 3);
  action.trim();
  // Remove optional "endon"
  if (action.endsWith(F("endon"))) {
    action = action.substring(0, action.lastIndexOf(F("endon")));
  }
  return true;
}

#include "../Helpers/ESPEasyMutex.h"

#ifdef ESP8266

  // ESP8266 doesn't have a proper mutex system.
  // This is by no means a perfect implementation, but the assembly implementation was causing lots of issues with esp8266/Arduino 3.0.0
  // See: https://github.com/letscontrolit/ESPEasy/issues/3693#issuecomment-868847669
  ESPEasy_Mutex::ESPEasy_Mutex() {
    _mutex = 1;
  }

#endif
#include "../Helpers/Hardware.h"

#include "../Commands/GPIO.h"
#include "../CustomBuild/ESPEasyLimits.h"
#include "../DataTypes/SPI_options.h"
#include "../ESPEasyCore/ESPEasyGPIO.h"
#include "../ESPEasyCore/ESPEasy_Log.h"

#include "../Globals/Device.h"
#include "../Globals/ESPEasyWiFiEvent.h"
#include "../Globals/ExtraTaskSettings.h"
#include "../Globals/Settings.h"
#include "../Globals/Statistics.h"
#include "../Globals/GlobalMapPortStatus.h"

#include "../Helpers/ESPEasy_FactoryDefault.h"
#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/ESPEasy_UnitOfMeasure.h"
#include "../Helpers/FS_Helper.h"
#include "../Helpers/Hardware_device_info.h"
#include "../Helpers/Hardware_GPIO.h"
#include "../Helpers/Hardware_I2C.h"
#include "../Helpers/I2C_access.h"
#include "../Helpers/Misc.h"
#include "../Helpers/PortStatus.h"
#include "../Helpers/StringConverter.h"

#include "../_Plugin_Helper.h"


#if defined(ESP8266)
  # include <ESP8266WiFi.h>
#endif // if defined(ESP8266)
#if defined(ESP32)
  # include <WiFi.h>
#endif // if defined(ESP32)

// #include "../../ESPEasy-Globals.h"

#ifdef ESP32
  # include <soc/soc.h>
  # include <soc/efuse_reg.h>
  # include <hal/efuse_hal.h>
  # include <soc/spi_reg.h>
  # include <soc/rtc.h>

  # if ESP_IDF_VERSION_MAJOR == 4
    #  if CONFIG_IDF_TARGET_ESP32S3   // ESP32-S3
      #   include <esp32s3/rom/spi_flash.h>
      #   include <esp32s3/spiram.h>
      #   include <esp32s3/rom/rtc.h>
    #  elif CONFIG_IDF_TARGET_ESP32S2   // ESP32-S2
      #   include <esp32s2/rom/spi_flash.h>
      #   include <esp32s2/spiram.h>
      #   include <esp32s2/rom/rtc.h>
    #  elif CONFIG_IDF_TARGET_ESP32C3 // ESP32-C3
      #   include <esp32c3/rom/spi_flash.h>
      #   include <esp32c3/rom/rtc.h>
    #  elif CONFIG_IDF_TARGET_ESP32   // ESP32/PICO-D4
      #   include <esp32/rom/spi_flash.h>
      #   include <esp32/rom/rtc.h>
      #   include <esp32/spiram.h>
    #  else // if CONFIG_IDF_TARGET_ESP32S3
      #   error Target CONFIG_IDF_TARGET is not supported
    #  endif // if CONFIG_IDF_TARGET_ESP32S3
  # else // ESP32 IDF 5.x and later
    #  include <rom/spi_flash.h>
    #  include <rom/rtc.h>
    #  include <bootloader_common.h>
  # endif // if ESP_IDF_VERSION_MAJOR == 4

#if CONFIG_IDF_TARGET_ESP32   // ESP32/PICO-D4
  #  if ESP_IDF_VERSION_MAJOR < 5
  #   define HAS_HALL_EFFECT_SENSOR  1
  #  else // if ESP_IDF_VERSION_MAJOR < 5

// Support for Hall Effect sensor was removed in ESP_IDF 5.x
  #   define HAS_HALL_EFFECT_SENSOR  0
  #  endif // if ESP_IDF_VERSION_MAJOR < 5
# else 
  #  define HAS_HALL_EFFECT_SENSOR  0
# endif


# if ESP_IDF_VERSION_MAJOR >= 5

#  include <esp_chip_info.h>
#  include <soc/soc.h>
#  include <driver/ledc.h>
#  include <esp_psram.h>

// #include <hal/ledc_hal.h>

# endif // if ESP_IDF_VERSION_MAJOR >= 5

# include "../Helpers/Hardware_ADC_cali.h"

#if FEATURE_ETHERNET
#include <ETH.h>
#endif

#endif // ifdef ESP32


#if FEATURE_SD
# include <SD.h>
#endif // if FEATURE_SD


#include <SPI.h>


# define GPIO_PLUGIN_ID  1

/********************************************************************************************\
 * Initialize specific hardware settings (only global ones, others are set through devices)
 \*********************************************************************************************/
void hardwareInit()
{
  // set GPIO pins state if not set to default
  bool hasPullUp, hasPullDown;

  for (int gpio = 0; gpio <= MAX_GPIO; ++gpio) {
    const bool serialPinConflict = isSerialConsolePin(gpio);

    if (!serialPinConflict) {
      const uint32_t key = createKey(PLUGIN_GPIO, gpio);
      #ifdef ESP32
      checkAndClearPWM(key);
      #endif // ifdef ESP32

      if (getGpioPullResistor(gpio, hasPullUp, hasPullDown)) {
        PinBootState bootState = Settings.getPinBootState(gpio);
      #if FEATURE_ETHERNET
/*
        if (Settings.ETH_Pin_power_rst == gpio)
        {
                  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
                    String log = F("ETH  : Reset ETH module on pin ");
                    log += Settings.ETH_Pin_power_rst;
                    addLog(LOG_LEVEL_INFO, log);
                  }
                  bootState = PinBootState::Output_low;
                }
         */
      #endif // if FEATURE_ETHERNET

        #ifdef ESP32
        if (bootState != PinBootState::Default_state) {
          gpio_reset_pin(static_cast<gpio_num_t>(gpio));
        }
        #endif
          
        switch (bootState)
        {
          case PinBootState::Default_state:
            // At startup, pins are configured as INPUT
            break;
          case PinBootState::Output_low:
            createAndSetPortStatus_Mode_State(key, PIN_MODE_OUTPUT, 0);
            GPIO_Write(PLUGIN_GPIO, gpio, LOW, PIN_MODE_OUTPUT);

            // setPinState(1, gpio, PIN_MODE_OUTPUT, LOW);
            break;
          case PinBootState::Output_high:
            createAndSetPortStatus_Mode_State(key, PIN_MODE_OUTPUT, 0);
            GPIO_Write(PLUGIN_GPIO, gpio, HIGH, PIN_MODE_OUTPUT);

            // setPinState(1, gpio, PIN_MODE_OUTPUT, HIGH);
            break;
          case PinBootState::Input_pullup:

            if (hasPullUp) {
              createAndSetPortStatus_Mode_State(key, PIN_MODE_INPUT_PULLUP, 0);
              pinMode(gpio, INPUT_PULLUP);
            }
            break;
          case PinBootState::Input_pulldown:

            if (hasPullDown) {
              createAndSetPortStatus_Mode_State(key, PIN_MODE_INPUT_PULLDOWN, 0);

              #ifdef ESP8266

              if (gpio == 16) {
                pinMode(gpio, INPUT_PULLDOWN_16);
              }
              #endif // ifdef ESP8266
              #ifdef ESP32
              pinMode(gpio, INPUT_PULLDOWN);
              #endif // ifdef ESP32
            }
            break;
          case PinBootState::Input:
            createAndSetPortStatus_Mode_State(key, PIN_MODE_INPUT, 0);
            pinMode(gpio, INPUT);
            break;
        }
      }
    }
  }

  if (getGpioPullResistor(Settings.Pin_Reset, hasPullUp, hasPullDown)) {
    if (hasPullUp) {
      pinMode(Settings.Pin_Reset, INPUT_PULLUP);
    }
  }

  initI2C();

  #if FEATURE_PLUGIN_PRIORITY
  String dummy;
  PluginCall(PLUGIN_PRIORITY_INIT_ALL, nullptr, dummy);
  #endif // if FEATURE_PLUGIN_PRIORITY

  bool tryInitSPI = true;
#if FEATURE_ETHERNET
  if ((Settings.NetworkMedium == NetworkMedium_t::Ethernet) &&
      isValid(Settings.ETH_Phy_Type) && 
      isSPI_EthernetType(Settings.ETH_Phy_Type)) 
  {
#if !ETH_SPI_SUPPORTS_CUSTOM
      tryInitSPI = false;
#endif
  }
#endif


  // SPI Init
  bool SPI_initialized = false;
  if (tryInitSPI && Settings.isSPI_valid())
  {
    SPI.setHwCs(false);

    // MFD: for ESP32 enable the SPI on HSPI as the default is VSPI
    #ifdef ESP32

    const SPI_Options_e SPI_selection = static_cast<SPI_Options_e>(Settings.InitSPI);
    int8_t spi_gpios[3]               = {};

    if (Settings.getSPI_pins(spi_gpios)) {
      if (SPI_selection == SPI_Options_e::Vspi_Fspi) {
        SPI.begin(); // Default SPI bus
      } else {
        SPI.begin(spi_gpios[0], spi_gpios[1], spi_gpios[2]);
      }
      SPI_initialized = true;
    }
    #else // ifdef ESP32
    SPI.begin();
    SPI_initialized = true;
    #endif // ifdef ESP32
  }

  if (SPI_initialized)
  {
    addLog(LOG_LEVEL_INFO, F("INIT : SPI Init (without CS)"));
    #if FEATURE_SD

    if (Settings.Pin_sd_cs >= 0)
    {
      if (SD.begin(Settings.Pin_sd_cs))
      {
        addLog(LOG_LEVEL_INFO, F("SD   : Init OK"));
      }
      else
      {
        SD.end();
        addLog(LOG_LEVEL_ERROR, F("SD   : Init failed"));
      }
    }
#endif // if FEATURE_SD
  } else {
    addLog(LOG_LEVEL_INFO, F("INIT : SPI not enabled"));
  }
}


void checkResetFactoryPin() {
  static uint8_t factoryResetCounter = 0;

  if (Settings.Pin_Reset == -1) {
    return;
  }

  if (digitalRead(Settings.Pin_Reset) == 0) { // active low reset pin
    factoryResetCounter++;                    // just count every second
  }
  else
  {                                           // reset pin released
    if (factoryResetCounter > 9) {
      // factory reset and reboot
      ResetFactory();
    }

    if (factoryResetCounter > 3) {
      // normal reboot
      reboot(IntendedRebootReason_e::ResetFactoryPinActive);
    }
    factoryResetCounter = 0; // count was < 3, reset counter
  }
}

#ifdef ESP8266
int lastADCvalue = 0;

int espeasy_analogRead(int pin) {
  if (!WiFiEventData.wifiConnectInProgress) {
    #if FEATURE_ADC_VCC
      lastADCvalue = ESP.getVcc();
    #else
      lastADCvalue = analogRead(A0);
    #endif // if FEATURE_ADC_VCC
  }
  return lastADCvalue;
}

#endif // ifdef ESP8266

#ifdef ESP32

// ESP32 ADC calibration datatypes.


// FIXME TD-er: For now keep a local array of the adc calibration 
#if ESP_IDF_VERSION_MAJOR < 5
Hardware_ADC_cali_t ESP32_ADC_cali[ADC_ATTEN_MAX]{};
#else
Hardware_ADC_cali_t ESP32_ADC_cali[ADC_ATTENDB_MAX]{};
#endif


void initADC() {
  for (size_t atten = 0; atten < NR_ELEMENTS(ESP32_ADC_cali); ++atten) {
    if (!ESP32_ADC_cali[atten].initialized()) {
      // FIXME TD-er: For now fake some pin which is connected to ADC1
      #ifdef ESP32_CLASSIC
      const int pin = 36;
      #else 
      const int pin = 1;
      #endif
      ESP32_ADC_cali[atten].init(pin, static_cast<adc_atten_t>(atten));
    }
  }
}

float applyADCFactoryCalibration(float raw_value, adc_atten_t attenuation)
{
  if (attenuation < NR_ELEMENTS(ESP32_ADC_cali)) {
    return ESP32_ADC_cali[attenuation].applyFactoryCalibration(raw_value);
  }
  return raw_value;
}

bool hasADC_factory_calibration() {
  return ESP32_ADC_cali[0].useFactoryCalibration();
}

const __FlashStringHelper* getADC_factory_calibration_type()
{
  return ESP32_ADC_cali[0].getADC_factory_calibration_type();
}

float getADC_factory_calibrated_min(adc_atten_t attenuation)
{
  if (attenuation < NR_ELEMENTS(ESP32_ADC_cali)) {
    return ESP32_ADC_cali[attenuation].getMinOut();
  }
  return 0.0f;
}

float getADC_factory_calibrated_max(adc_atten_t attenuation)
{
  if (attenuation < NR_ELEMENTS(ESP32_ADC_cali)) {
    return ESP32_ADC_cali[attenuation].getMaxOut();
  }
  return MAX_ADC_VALUE;
}

int getADC_num_for_gpio(int pin) {
  int ch;

  return getADC_num_for_gpio(pin, ch);
}

int getADC_num_for_gpio(int pin, int& channel)
{
  int adc, t;

  if (getADC_gpio_info(pin, adc, channel, t)) {
    return adc;
  }
  return -1;
}

int espeasy_analogRead(int pin, bool readAsTouch) {
  int value = 0;
  int adc, ch, t;

  if (getADC_gpio_info(pin, adc, ch, t)) {
    bool canread = false;

    switch (adc) {
      case 0:
      # if HAS_HALL_EFFECT_SENSOR
        value = hallRead();
      # endif // if HAS_HALL_EFFECT_SENSOR
        break;
      case 1:
        canread = true;
        break;
      case 2:
#if ESP_IDF_VERSION_MAJOR < 5
        if (WiFi.getMode() == WIFI_OFF) {
          // See:
          // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/adc.html#configuration-and-reading-adc
          // ADC2 is shared with WiFi, so don't read ADC2 when WiFi is on.
          canread = true;
        }
#else
        canread = true;
#endif
        break;
    }

    if (canread) {
      if (readAsTouch && (t >= 0)) {
        #if defined(SOC_TOUCH_SENSOR_SUPPORTED) && SOC_TOUCH_SENSOR_SUPPORTED
        value = touchRead(pin);
        # endif 
      } else {
        value = analogRead(pin);
      }
    }
  }
  return value;
}


int  getCPU_MaxFreqMHz()
{
#if CONFIG_IDF_TARGET_ESP32
    return static_cast<int>(efuse_hal_get_rated_freq_mhz());
#elif CONFIG_IDF_TARGET_ESP32C2
    return 120;
#elif CONFIG_IDF_TARGET_ESP32C3
    return 160;
#elif CONFIG_IDF_TARGET_ESP32C6
    return 160;
#elif CONFIG_IDF_TARGET_ESP32H2
    //IDF-6570
    return 96;
#elif CONFIG_IDF_TARGET_ESP32P4
    return 400;
#elif CONFIG_IDF_TARGET_ESP32S2
    return 240;
#elif CONFIG_IDF_TARGET_ESP32S3
    return 240;

#  else 
  #   error Target CONFIG_IDF_TARGET is not supported
  return 160;
#  endif
}

int  getCPU_MinFreqMHz()
{
  // TODO TD-er: May differ on some ESPs and also some allow less but only without WiFi
  return 80;
}


#endif // ifdef ESP32



/*********************************************************************************************\
* High entropy hardware random generator
* Thanks to DigitalAlchemist
\*********************************************************************************************/

#if ESP_IDF_VERSION_MAJOR >= 5
#include <esp_random.h>
#endif

uint32_t HwRandom() {
#if ESP_IDF_VERSION_MAJOR >= 5
  // See for more info on the HW RNG:
  // https://docs.espressif.com/projects/esp-idf/en/latest/esp32s2/api-reference/system/random.html
  return esp_random();
#else

// Based on code from https://raw.githubusercontent.com/espressif/esp-idf/master/components/esp32/hw_random.c
// https://github.com/arendst/Tasmota/blob/1e6b78a957be538cf494f0e2dc49060d1cb0fe8b/tasmota/support_esp.ino#L805
#if ESP8266

  // https://web.archive.org/web/20160922031242/http://esp8266-re.foogod.com/wiki/Random_Number_Generator
  # define _RAND_ADDR 0x3FF20E44UL
#endif // ESP8266
#ifdef ESP32
  # define _RAND_ADDR 0x3FF75144UL
#endif // ESP32
  static uint32_t last_ccount = 0;
  uint32_t ccount;
  uint32_t result = 0;

  do {
    ccount  = ESP.getCycleCount();
    result ^= *(volatile uint32_t *)_RAND_ADDR;     // -V566
  } while (ccount - last_ccount < 64);
  last_ccount = ccount;
  return result ^ *(volatile uint32_t *)_RAND_ADDR; // -V566
#undef _RAND_ADDR
#endif
}

long HwRandom(long howbig) {
    if(howbig == 0) {
        return 0;
    }
    return HwRandom() % howbig;
}

long HwRandom(long howsmall, long howbig) {
    if(howsmall >= howbig) {
        return howsmall;
    }
    long diff = howbig - howsmall;
    return HwRandom(diff) + howsmall;
}

ESPEASY_RULES_FLOAT_TYPE HwRandom_f(
  ESPEASY_RULES_FLOAT_TYPE howsmall,
  ESPEASY_RULES_FLOAT_TYPE howbig)
{
  if (approximatelyEqual(howsmall, howbig)) {
    return howsmall;
  }
  return mapADCtoFloat(
    HwRandom(),
    0, std::numeric_limits<uint32_t>::max(),
    howsmall, howbig);
}


#ifdef ESP8266
void readBootCause() {
  lastBootCause = BOOT_CAUSE_MANUAL_REBOOT;
  const rst_info *resetInfo = ESP.getResetInfoPtr();

  if (resetInfo != nullptr) {
    switch (resetInfo->reason) {
      // normal startup by power on
      case REASON_DEFAULT_RST:      lastBootCause = BOOT_CAUSE_COLD_BOOT; break;

      // hardware watch dog reset
      case REASON_WDT_RST:          lastBootCause = BOOT_CAUSE_EXT_WD; break;

      // exception reset, GPIO status won’t change
      case REASON_EXCEPTION_RST:    lastBootCause = BOOT_CAUSE_EXCEPTION; break;

      // software watch dog reset, GPIO status won’t change
      case REASON_SOFT_WDT_RST:     lastBootCause = BOOT_CAUSE_SW_WATCHDOG; break;

      // software restart ,system_restart , GPIO status won’t change
      case REASON_SOFT_RESTART:     lastBootCause = BOOT_CAUSE_SOFT_RESTART; break;

      // wake up from deep-sleep
      case REASON_DEEP_SLEEP_AWAKE: lastBootCause = BOOT_CAUSE_DEEP_SLEEP; break;

      // external system reset
      case REASON_EXT_SYS_RST:      lastBootCause = BOOT_CAUSE_MANUAL_REBOOT; break;
      default:
        break;
    }
  }
}

#endif // ifdef ESP8266

#ifdef ESP32
void readBootCause() {
  lastBootCause = BOOT_CAUSE_MANUAL_REBOOT;

  #ifdef ESP32S2

  switch (rtc_get_reset_reason(0)) {
    case NO_MEAN                : break;
    case POWERON_RESET          : lastBootCause = BOOT_CAUSE_MANUAL_REBOOT;    break; /**<1, Vbat power on reset*/
    case RTC_SW_SYS_RESET       : lastBootCause = BOOT_CAUSE_SOFT_RESTART;     break; /**<3, Software reset digital core*/
    case DEEPSLEEP_RESET        : lastBootCause = BOOT_CAUSE_DEEP_SLEEP;       break; /**<5, Deep Sleep reset digital core*/
    case TG0WDT_SYS_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<7, Timer Group0 Watch dog reset digital core*/
    case TG1WDT_SYS_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<8, Timer Group1 Watch dog reset digital core*/
    case RTCWDT_SYS_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<9, RTC Watch dog Reset digital core*/
    case INTRUSION_RESET        : lastBootCause = BOOT_CAUSE_SOFT_RESTART;     break; /**<10, Instrusion tested to reset CPU*/
    case TG0WDT_CPU_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<11, Time Group0 reset CPU*/
    case RTC_SW_CPU_RESET       : lastBootCause = BOOT_CAUSE_SOFT_RESTART;     break; /**<12, Software reset CPU*/
    case RTCWDT_CPU_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<13, RTC Watch dog Reset CPU*/
    case RTCWDT_BROWN_OUT_RESET : lastBootCause = BOOT_CAUSE_POWER_UNSTABLE;   break; /**<15, Reset when the vdd voltage is not stable*/
    case RTCWDT_RTC_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<16, RTC Watch dog reset digital core and rtc module*/
    case TG1WDT_CPU_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<17, Time Group1 reset CPU*/
    case SUPER_WDT_RESET        : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<18, super watchdog reset digital core and rtc module*/
    case GLITCH_RTC_RESET       : lastBootCause = BOOT_CAUSE_POWER_UNSTABLE;   break; /**<19, glitch reset digital core and rtc module*/
    case EFUSE_RESET            : lastBootCause = BOOT_CAUSE_MANUAL_REBOOT;    break; /**<20, efuse reset digital core*/
  }



#elif defined(ESP32S3)
  switch (rtc_get_reset_reason(0)) {
    case NO_MEAN                : break;
    case POWERON_RESET          : lastBootCause = BOOT_CAUSE_MANUAL_REBOOT;    break; /**<1, Vbat power on reset*/
    case RTC_SW_SYS_RESET       : lastBootCause = BOOT_CAUSE_SOFT_RESTART;     break; /**<3, Software reset digital core*/
    case DEEPSLEEP_RESET        : lastBootCause = BOOT_CAUSE_DEEP_SLEEP;       break; /**<5, Deep Sleep reset digital core*/
    case TG0WDT_SYS_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<7, Timer Group0 Watch dog reset digital core*/
    case TG1WDT_SYS_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<8, Timer Group1 Watch dog reset digital core*/
    case RTCWDT_SYS_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<9, RTC Watch dog Reset digital core*/
    case INTRUSION_RESET        : lastBootCause = BOOT_CAUSE_SOFT_RESTART;     break; /**<10, Instrusion tested to reset CPU*/
    case TG0WDT_CPU_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<11, Time Group0 reset CPU*/
    case RTC_SW_CPU_RESET       : lastBootCause = BOOT_CAUSE_SOFT_RESTART;     break; /**<12, Software reset CPU*/
    case RTCWDT_CPU_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<13, RTC Watch dog Reset CPU*/
    case RTCWDT_BROWN_OUT_RESET : lastBootCause = BOOT_CAUSE_POWER_UNSTABLE;   break; /**<15, Reset when the vdd voltage is not stable*/
    case RTCWDT_RTC_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<16, RTC Watch dog reset digital core and rtc module*/
    case TG1WDT_CPU_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<17, Time Group1 reset CPU*/
    case SUPER_WDT_RESET        : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<18, super watchdog reset digital core and rtc module*/
    case GLITCH_RTC_RESET       : lastBootCause = BOOT_CAUSE_POWER_UNSTABLE;   break; /**<19, glitch reset digital core and rtc module*/
    case EFUSE_RESET            : lastBootCause = BOOT_CAUSE_MANUAL_REBOOT;    break; /**<20, efuse reset digital core*/
    case USB_UART_CHIP_RESET    : lastBootCause = BOOT_CAUSE_MANUAL_REBOOT;    break; /**<21, usb uart reset digital core */
    case USB_JTAG_CHIP_RESET    : lastBootCause = BOOT_CAUSE_MANUAL_REBOOT;    break; /**<22, usb jtag reset digital core */
    case POWER_GLITCH_RESET     : lastBootCause = BOOT_CAUSE_POWER_UNSTABLE;   break; /**<23, power glitch reset digital core and rtc module*/
  }


#elif defined(ESP32C2)
  switch (rtc_get_reset_reason(0)) {
    case NO_MEAN                : break;
    case POWERON_RESET          : lastBootCause = BOOT_CAUSE_MANUAL_REBOOT;    break; /**<1, Vbat power on reset*/
    case RTC_SW_SYS_RESET       : lastBootCause = BOOT_CAUSE_SOFT_RESTART;     break; /**<3, Software reset digital core*/
    case DEEPSLEEP_RESET        : lastBootCause = BOOT_CAUSE_DEEP_SLEEP;       break; /**<3, Deep Sleep reset digital core*/
    case TG0WDT_SYS_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<7, Timer Group0 Watch dog reset digital core*/
    case RTCWDT_SYS_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<9, RTC Watch dog Reset digital core*/
    case INTRUSION_RESET        : lastBootCause = BOOT_CAUSE_SOFT_RESTART;     break; /**<10, Instrusion tested to reset CPU*/
    case TG0WDT_CPU_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<11, Time Group0 reset CPU*/
    case RTC_SW_CPU_RESET       : lastBootCause = BOOT_CAUSE_SOFT_RESTART;     break; /**<12, Software reset CPU*/
    case RTCWDT_CPU_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<13, RTC Watch dog Reset CPU*/
    case RTCWDT_BROWN_OUT_RESET : lastBootCause = BOOT_CAUSE_POWER_UNSTABLE;   break; /**<15, Reset when the vdd voltage is not stable*/
    case RTCWDT_RTC_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<16, RTC Watch dog reset digital core and rtc module*/
    case SUPER_WDT_RESET        : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<11, super watchdog reset digital core and rtc module*/
    case GLITCH_RTC_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<19, glitch reset digital core and rtc module*/
    case EFUSE_RESET            : lastBootCause = BOOT_CAUSE_POWER_UNSTABLE;   break; /**<20, efuse reset digital core*/
    case JTAG_RESET             : lastBootCause = BOOT_CAUSE_MANUAL_REBOOT;    break; /**<24, jtag reset CPU*/
  }


#elif defined(ESP32C3)
  switch (rtc_get_reset_reason(0)) {
    case NO_MEAN                : break;
    case POWERON_RESET          : lastBootCause = BOOT_CAUSE_MANUAL_REBOOT;    break; /**<1, Vbat power on reset*/
    case RTC_SW_SYS_RESET       : lastBootCause = BOOT_CAUSE_SOFT_RESTART;     break; /**<3, Software reset digital core*/
    case DEEPSLEEP_RESET        : lastBootCause = BOOT_CAUSE_DEEP_SLEEP;       break; /**<5, Deep Sleep reset digital core*/
    case TG0WDT_SYS_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<7, Timer Group0 Watch dog reset digital core*/
    case TG1WDT_SYS_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<8, Timer Group1 Watch dog reset digital core*/
    case RTCWDT_SYS_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<9, RTC Watch dog Reset digital core*/
    case INTRUSION_RESET        : lastBootCause = BOOT_CAUSE_SOFT_RESTART;     break; /**<10, Instrusion tested to reset CPU*/
    case TG0WDT_CPU_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<11, Time Group0 reset CPU*/
    case RTC_SW_CPU_RESET       : lastBootCause = BOOT_CAUSE_SOFT_RESTART;     break; /**<12, Software reset CPU*/
    case RTCWDT_CPU_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<13, RTC Watch dog Reset CPU*/
    case RTCWDT_BROWN_OUT_RESET : lastBootCause = BOOT_CAUSE_POWER_UNSTABLE;   break; /**<15, Reset when the vdd voltage is not stable*/
    case RTCWDT_RTC_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<16, RTC Watch dog reset digital core and rtc module*/
    case TG1WDT_CPU_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<17, Time Group1 reset CPU*/
    case SUPER_WDT_RESET        : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<18, super watchdog reset digital core and rtc module*/
    case GLITCH_RTC_RESET       : lastBootCause = BOOT_CAUSE_POWER_UNSTABLE;   break; /**<19, glitch reset digital core and rtc module*/
    case EFUSE_RESET            : lastBootCause = BOOT_CAUSE_MANUAL_REBOOT;    break; /**<20, efuse reset digital core*/
    case USB_UART_CHIP_RESET    : lastBootCause = BOOT_CAUSE_MANUAL_REBOOT;    break; /**<21, usb uart reset digital core */
    case USB_JTAG_CHIP_RESET    : lastBootCause = BOOT_CAUSE_MANUAL_REBOOT;    break; /**<22, usb jtag reset digital core */
    case POWER_GLITCH_RESET     : lastBootCause = BOOT_CAUSE_POWER_UNSTABLE;   break; /**<23, power glitch reset digital core and rtc module*/
  }

#elif defined(ESP32C6)
  switch (rtc_get_reset_reason(0)) {
    case NO_MEAN                : break;
    case POWERON_RESET          : lastBootCause = BOOT_CAUSE_MANUAL_REBOOT;    break; /**<1, Vbat power on reset*/
    case RTC_SW_SYS_RESET       : lastBootCause = BOOT_CAUSE_SOFT_RESTART;     break; /**<3, Software reset digital core*/
    case DEEPSLEEP_RESET        : lastBootCause = BOOT_CAUSE_DEEP_SLEEP;       break; /**<5, Deep Sleep reset digital core*/
    case SDIO_RESET             : lastBootCause = BOOT_CAUSE_MANUAL_REBOOT;    break; /**<6, Reset by SLC module, reset digital core (hp system)*/
    case TG0WDT_SYS_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<7, Timer Group0 Watch dog reset digital core*/
    case TG1WDT_SYS_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<8, Timer Group1 Watch dog reset digital core*/
    case RTCWDT_SYS_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<9, RTC Watch dog Reset digital core*/
    case TG0WDT_CPU_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<11, Time Group0 reset CPU*/
    case RTC_SW_CPU_RESET       : lastBootCause = BOOT_CAUSE_SOFT_RESTART;     break; /**<12, Software reset CPU*/
    case RTCWDT_CPU_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<13, RTC Watch dog Reset CPU*/
    case RTCWDT_BROWN_OUT_RESET : lastBootCause = BOOT_CAUSE_POWER_UNSTABLE;   break; /**<15, Reset when the vdd voltage is not stable*/
    case RTCWDT_RTC_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<16, RTC Watch dog reset digital core and rtc module*/
    case TG1WDT_CPU_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<17, Time Group1 reset CPU*/
    case SUPER_WDT_RESET        : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<18, super watchdog reset digital core and rtc module*/
    case EFUSE_RESET            : lastBootCause = BOOT_CAUSE_MANUAL_REBOOT;    break; /**<20, efuse reset digital core*/
    case USB_UART_CHIP_RESET    : lastBootCause = BOOT_CAUSE_MANUAL_REBOOT;    break; /**<21, usb uart reset digital core */
    case USB_JTAG_CHIP_RESET    : lastBootCause = BOOT_CAUSE_MANUAL_REBOOT;    break; /**<22, usb jtag reset digital core */
    case JTAG_RESET             : lastBootCause = BOOT_CAUSE_MANUAL_REBOOT;    break; /**<24, jtag reset CPU*/
  }

# elif defined(ESP32_CLASSIC)
  switch (rtc_get_reset_reason(0)) {
    case NO_MEAN                : break;
    case POWERON_RESET          : lastBootCause = BOOT_CAUSE_MANUAL_REBOOT;    break; /**<1, Vbat power on reset*/
    case SW_RESET               : lastBootCause = BOOT_CAUSE_SOFT_RESTART;     break; /**<3, Software reset digital core*/
    case OWDT_RESET             : lastBootCause = BOOT_CAUSE_SW_WATCHDOG;      break; /**<4, Legacy watch dog reset digital core*/
    case DEEPSLEEP_RESET        : lastBootCause = BOOT_CAUSE_DEEP_SLEEP;       break; /**<3, Deep Sleep reset digital core*/
    case SDIO_RESET             : lastBootCause = BOOT_CAUSE_MANUAL_REBOOT;    break; /**<6, Reset by SLC module, reset digital core*/
    case TG0WDT_SYS_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<7, Timer Group0 Watch dog reset digital core*/
    case TG1WDT_SYS_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<8, Timer Group1 Watch dog reset digital core*/
    case RTCWDT_SYS_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<9, RTC Watch dog Reset digital core*/
    case INTRUSION_RESET        : lastBootCause = BOOT_CAUSE_SOFT_RESTART;     break; /**<10, Instrusion tested to reset CPU*/
    case TGWDT_CPU_RESET        : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<11, Time Group reset CPU*/
    case SW_CPU_RESET           : lastBootCause = BOOT_CAUSE_SOFT_RESTART;     break; /**<12, Software reset CPU*/
    case RTCWDT_CPU_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<13, RTC Watch dog Reset CPU*/
    case EXT_CPU_RESET          : lastBootCause = BOOT_CAUSE_MANUAL_REBOOT;    break; /**<14, for APP CPU, reseted by PRO CPU*/
    case RTCWDT_BROWN_OUT_RESET : lastBootCause = BOOT_CAUSE_POWER_UNSTABLE;   break; /**<15, Reset when the vdd voltage is not stable*/
    case RTCWDT_RTC_RESET       : lastBootCause = BOOT_CAUSE_EXT_WD;           break; /**<16, RTC Watch dog reset digital core and rtc module*/
  }

  # else

    static_assert(false, "Implement processor architecture");

  #endif
}

#endif // ifdef ESP32


/********************************************************************************************\
   Hardware specific configurations
 \*********************************************************************************************/
const __FlashStringHelper* getDeviceModelBrandString(DeviceModel model) {
  switch (model) {
    case DeviceModel::DeviceModel_Sonoff_Basic:
    case DeviceModel::DeviceModel_Sonoff_TH1x:
    case DeviceModel::DeviceModel_Sonoff_S2x:
    case DeviceModel::DeviceModel_Sonoff_TouchT1:
    case DeviceModel::DeviceModel_Sonoff_TouchT2:
    case DeviceModel::DeviceModel_Sonoff_TouchT3:
    case DeviceModel::DeviceModel_Sonoff_4ch:
    case DeviceModel::DeviceModel_Sonoff_POW:
    case DeviceModel::DeviceModel_Sonoff_POWr2:   return F("Sonoff");
    case DeviceModel::DeviceModel_Shelly1:
    case DeviceModel::DeviceModel_ShellyPLUG_S:   return F("Shelly");
# if CONFIG_ETH_USE_ESP32_EMAC
    case DeviceModel::DeviceModel_Olimex_ESP32_PoE:
    case DeviceModel::DeviceModel_Olimex_ESP32_EVB:
    case DeviceModel::DeviceModel_Olimex_ESP32_GATEWAY:
    #ifdef ESP32_CLASSIC
      return F("Olimex");
    #endif // ifdef ESP32_CLASSIC
    case DeviceModel::DeviceModel_wESP32:
    #ifdef ESP32_CLASSIC
      return F("wESP32");
    #endif // ifdef ESP32_CLASSIC
    case DeviceModel::DeviceModel_WT32_ETH01:
    #ifdef ESP32_CLASSIC
      return F("WT32-ETH01");
    #endif // ifdef ESP32_CLASSIC
#endif
    case DeviceModel::DeviceModel_default:
    case DeviceModel::DeviceModel_MAX:      break;

      // Do not use default: as this allows the compiler to detect any missing cases.
  }
  return F("");
}

const __FlashStringHelper* getDeviceModelTypeString(DeviceModel model)
{
  switch (model) {
#if defined(ESP8266) && !defined(LIMIT_BUILD_SIZE)
    case DeviceModel::DeviceModel_Sonoff_Basic:   return F(" Basic");
    case DeviceModel::DeviceModel_Sonoff_TH1x:    return F(" TH1x");
    case DeviceModel::DeviceModel_Sonoff_S2x:     return F(" S2x");
    case DeviceModel::DeviceModel_Sonoff_TouchT1: return F(" TouchT1");
    case DeviceModel::DeviceModel_Sonoff_TouchT2: return F(" TouchT2");
    case DeviceModel::DeviceModel_Sonoff_TouchT3: return F(" TouchT3");
    case DeviceModel::DeviceModel_Sonoff_4ch:     return F(" 4ch");
    case DeviceModel::DeviceModel_Sonoff_POW:     return F(" POW");
    case DeviceModel::DeviceModel_Sonoff_POWr2:   return F(" POW-r2");
    case DeviceModel::DeviceModel_Shelly1:        return F("1");
    case DeviceModel::DeviceModel_ShellyPLUG_S:   return F(" PLUG S");
#else // if defined(ESP8266) && !defined(LIMIT_BUILD_SIZE)
    case DeviceModel::DeviceModel_Sonoff_Basic:
    case DeviceModel::DeviceModel_Sonoff_TH1x:
    case DeviceModel::DeviceModel_Sonoff_S2x:
    case DeviceModel::DeviceModel_Sonoff_TouchT1:
    case DeviceModel::DeviceModel_Sonoff_TouchT2:
    case DeviceModel::DeviceModel_Sonoff_TouchT3:
    case DeviceModel::DeviceModel_Sonoff_4ch:
    case DeviceModel::DeviceModel_Sonoff_POW:
    case DeviceModel::DeviceModel_Sonoff_POWr2:
    case DeviceModel::DeviceModel_Shelly1:
    case DeviceModel::DeviceModel_ShellyPLUG_S:
      return F("default");
#endif // if defined(ESP8266) && !defined(LIMIT_BUILD_SIZE)
#if CONFIG_ETH_USE_ESP32_EMAC
    case DeviceModel::DeviceModel_Olimex_ESP32_PoE:      return F(" ESP32-PoE");
    case DeviceModel::DeviceModel_Olimex_ESP32_EVB:      return F(" ESP32-EVB");
    case DeviceModel::DeviceModel_Olimex_ESP32_GATEWAY:  return F(" ESP32-GATEWAY");
    case DeviceModel::DeviceModel_wESP32:                break;
    case DeviceModel::DeviceModel_WT32_ETH01:            return F(" add-on");
#endif // if CONFIG_ETH_USE_ESP32_EMAC

    case DeviceModel::DeviceModel_default:
    case DeviceModel::DeviceModel_MAX:             return F("default");

      // Do not use default: as this allows the compiler to detect any missing cases.
  }
  return F("");
}

String getDeviceModelString(DeviceModel model) {
  return concat(
    getDeviceModelBrandString(model),
    getDeviceModelTypeString(model));
}

bool modelMatchingFlashSize(DeviceModel model) {
#if defined(ESP8266) || (defined(ESP32_CLASSIC) && FEATURE_ETHERNET)
  const uint32_t size_MB = getFlashRealSizeInBytes() >> 20;
#endif // if defined(ESP8266) || (defined(ESP32_CLASSIC) && FEATURE_ETHERNET)

  // TD-er: This also checks for ESP8266/ESP8285/ESP32_CLASSIC
  switch (model) {
    case DeviceModel::DeviceModel_Sonoff_Basic:
    case DeviceModel::DeviceModel_Sonoff_TH1x:
    case DeviceModel::DeviceModel_Sonoff_S2x:
    case DeviceModel::DeviceModel_Sonoff_TouchT1:
    case DeviceModel::DeviceModel_Sonoff_TouchT2:
    case DeviceModel::DeviceModel_Sonoff_TouchT3:
    case DeviceModel::DeviceModel_Sonoff_4ch:
#ifdef ESP8266
      return size_MB == 1;
#else // ifdef ESP8266
      return false;
#endif // ifdef ESP8266

    case DeviceModel::DeviceModel_Sonoff_POW:
    case DeviceModel::DeviceModel_Sonoff_POWr2:
#ifdef ESP8266
      return size_MB == 4;
#else // ifdef ESP8266
      return false;
#endif // ifdef ESP8266

    case DeviceModel::DeviceModel_Shelly1:
    case DeviceModel::DeviceModel_ShellyPLUG_S:
#ifdef ESP8266
      return size_MB == 2;
#else // ifdef ESP8266
      return false;
#endif // ifdef ESP8266

      // These Olimex boards all have Ethernet
#if CONFIG_ETH_USE_ESP32_EMAC
    case DeviceModel::DeviceModel_Olimex_ESP32_PoE:
    case DeviceModel::DeviceModel_Olimex_ESP32_EVB:
    case DeviceModel::DeviceModel_Olimex_ESP32_GATEWAY:
    case DeviceModel::DeviceModel_wESP32:
    case DeviceModel::DeviceModel_WT32_ETH01:
# if  defined(ESP32_CLASSIC) && FEATURE_ETHERNET
      return size_MB == 4;
# else // if  defined(ESP32_CLASSIC) && FEATURE_ETHERNET
      return false;
# endif // if  defined(ESP32_CLASSIC) && FEATURE_ETHERNET
#endif // if CONFIG_ETH_USE_ESP32_EMAC
    case DeviceModel::DeviceModel_default:
    case DeviceModel::DeviceModel_MAX:
      return true;

      // Do not use default: as this allows the compiler to detect any missing cases.
  }
  return true;
}

void setFactoryDefault(DeviceModel model) {
  ResetFactoryDefaultPreference.setDeviceModel(model);
}

/********************************************************************************************\
   Add pre defined plugins and rules.
 \*********************************************************************************************/
void addSwitchPlugin(taskIndex_t taskIndex, int gpio, const String& name, bool activeLow) {
  setTaskDevice_to_TaskIndex(PLUGIN_GPIO, taskIndex);
  const int pins[] = { gpio, -1, -1 };

  setBasicTaskValues(
    taskIndex,
    0,    // taskdevicetimer
    true, // enabled
    name, // name
    pins);
  Settings.TaskDevicePin1PullUp[taskIndex] = true;

  if (activeLow) {
    Settings.TaskDevicePluginConfig[taskIndex][2] = 1; // SWITCH_TYPE_PUSH_ACTIVE_LOW;
  }
  Settings.TaskDevicePluginConfig[taskIndex][3] = 1;   // "Send Boot state" checked.
}

void addPredefinedPlugins(const GpioFactorySettingsStruct& gpio_settings) {
  taskIndex_t taskIndex = 0;

  for (int i = 0; i < 4; ++i) {
    if (gpio_settings.button[i] >= 0) {
      String label = F("Button");
      label += (i + 1);
      addSwitchPlugin(taskIndex, gpio_settings.button[i], label, true);
      ++taskIndex;
    }

    if (gpio_settings.relais[i] >= 0) {
      String label = F("Relay");
      label += (i + 1);
      addSwitchPlugin(taskIndex, gpio_settings.relais[i], label, false);
      ++taskIndex;
    }
  }
}

void addButtonRelayRule(uint8_t buttonNumber, int relay_gpio) {
  Settings.UseRules = true;
  String fileName;

  #if defined(ESP32)
  fileName += '/';
  #endif // if defined(ESP32)
  fileName += F("rules1.txt");
  String rule = F("on ButtonBNR#state do\n  if [RelayBNR#state]=0\n    gpio,GNR,1\n  else\n    gpio,GNR,0\n  endif\nendon\n");
  rule.replace(F("BNR"), String(buttonNumber));
  rule.replace(F("GNR"), String(relay_gpio));
  String result = appendLineToFile(fileName, rule);

  if (result.length() > 0) {
    addLogMove(LOG_LEVEL_ERROR, result);
  }
}

void addPredefinedRules(const GpioFactorySettingsStruct& gpio_settings) {
  for (int i = 0; i < 4; ++i) {
    if ((gpio_settings.button[i] >= 0) && (gpio_settings.relais[i] >= 0)) {
      addButtonRelayRule((i + 1), gpio_settings.relais[i]);
    }
  }
}

// ********************************************************************************
// change of device: cleanup old device and reset default settings
// ********************************************************************************
void setTaskDevice_to_TaskIndex(pluginID_t taskdevicenumber, taskIndex_t taskIndex) {
  struct EventStruct TempEvent(taskIndex);
  String dummy;

  // let the plugin do its cleanup by calling PLUGIN_EXIT with this TaskIndex
  PluginCall(PLUGIN_EXIT, &TempEvent, dummy);
  taskClear(taskIndex, false); // clear settings, but do not save
  ClearCustomTaskSettings(taskIndex);

  Settings.TaskDeviceNumber[taskIndex] = taskdevicenumber.value;

  //  Settings.getPluginID_for_task(taskIndex) = taskdevicenumber;

  if (validPluginID_fullcheck(taskdevicenumber)) // set default values if a new device has been selected
  {
    // FIXME TD-er: Must check if this is working (e.g. need to set nr. decimals?)
    ExtraTaskSettings.clear();
    ExtraTaskSettings.TaskIndex = taskIndex;

    // NOTE: do not enable task by default. allow user to enter sensible valus first and let him enable it when ready.
    PluginCall(PLUGIN_SET_DEFAULTS,         &TempEvent, dummy);
    PluginCall(PLUGIN_GET_DEVICEVALUENAMES, &TempEvent, dummy); // the plugin should populate ExtraTaskSettings with its default values.

    #if FEATURE_MQTT_DISCOVER && FEATURE_CUSTOM_TASKVAR_VTYPE && FEATURE_TASKVALUE_UNIT_OF_MEASURE
    // Fill in standard Unit of measurement and Value Type, if possible
    const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(taskIndex);
    std::vector<DiscoveryItem> discoveryItems;
    MQTT_DiscoveryGetDeviceVType(taskIndex, discoveryItems, getValueCountForTask(taskIndex), dummy);

    for (uint8_t varNr = 0; varNr < VARS_PER_TASK; ++varNr) {
      // Match varNr with the DiscoveryItems to find the Sensor_VType for the value
      for (uint8_t j = 0; j < discoveryItems.size(); ++j) {
        for (uint8_t k = 0; k < discoveryItems[j].valueCount; ++k) { // Can have multiple values for 1 VType
          if (varNr == discoveryItems[j].varIndex + k) {
            const String uom = getValueType2DefaultHAUoM(discoveryItems[j].VType);

            if (!uom.isEmpty()) {
              const int uomIdx = getUnitOfMeasureIndex(uom);

              if (uomIdx > 0) {
                ExtraTaskSettings.setTaskVarUnitOfMeasure(varNr, uomIdx);
              }
            }

            if (Device[DeviceIndex].CustomVTypeVar) {
              ExtraTaskSettings.setTaskVarCustomVType(varNr, static_cast<uint8_t>(discoveryItems[j].VType));
            }
          }
        }
      }
    }

    #endif // if FEATURE_MQTT_DISCOVER && FEATURE_CUSTOM_TASKVAR_VTYPE && FEATURE_TASKVALUE_UNIT_OF_MEASURE
  } else {
    // New task is empty task, thus save config now.
    taskClear(taskIndex, true);                                 // clear settings, and save
  }
}

// ********************************************************************************
// Initialize task with some default values applicable for almost all tasks
// ********************************************************************************
void setBasicTaskValues(taskIndex_t taskIndex, unsigned long taskdevicetimer,
                        bool enabled, const String& name, const int pins[3]) {
  if (!validTaskIndex(taskIndex)) { return; }
  const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(taskIndex);

  if (!validDeviceIndex(DeviceIndex)) { return; }

  LoadTaskSettings(taskIndex); // Make sure ExtraTaskSettings are up-to-date

  if (taskdevicetimer > 0) {
    Settings.TaskDeviceTimer[taskIndex] = taskdevicetimer;
  } else {
    if (!Device[DeviceIndex].TimerOptional) { // Set default delay, unless it's optional...
      Settings.TaskDeviceTimer[taskIndex] = Settings.Delay;
    }
    else {
      Settings.TaskDeviceTimer[taskIndex] = 0;
    }
  }
  Settings.TaskDeviceEnabled[taskIndex] = enabled;
  //Settings.TaskDeviceEnabled[taskIndex].enabled = enabled;
  safe_strncpy(ExtraTaskSettings.TaskDeviceName, name.c_str(), sizeof(ExtraTaskSettings.TaskDeviceName));

  // FIXME TD-er: Check for valid GPIO pin (and  -1 for "not set")
  Settings.TaskDevicePin1[taskIndex] = pins[0];
  Settings.TaskDevicePin2[taskIndex] = pins[1];
  Settings.TaskDevicePin3[taskIndex] = pins[2];
}

#include "../Helpers/PrintToString.h"

void   PrintToString::clear() { _str.clear(); }

size_t PrintToString::length() const
{
  return _str.length();
}

bool   PrintToString::reserve(unsigned int size) { return _str.reserve(size); }

size_t PrintToString::write(uint8_t c)
{
  _str += (char)c;
  return 1;
}

const String& PrintToString::get() const
{
  return _str;
}

#include "../Helpers/StringConverter_Numerical.h"

#include "../DataStructs/TimingStats.h"

#include "../Helpers/Numerical.h"

#include "../Helpers/StringConverter.h"


/********************************************************************************************\
   Convert a char string to integer
 \*********************************************************************************************/

// FIXME: change original code so it uses String and String.toInt()
unsigned long str2int(const char *string)
{
  uint32_t temp = 0;

  validUIntFromString(string, temp);

  return static_cast<unsigned long>(temp);
}

/*********************************************************************************************\
   Workaround for removing trailing white space when String() converts a float with 0 decimals
\*********************************************************************************************/
String toString(const float& value, unsigned int decimalPlaces, bool trimTrailingZeros)
{
  String res;
  toValidString(res, value, decimalPlaces, trimTrailingZeros);
  return res;
}

bool toValidString(String& str,
  const float& value,
  unsigned int decimalPlaces,
  bool         trimTrailingZeros)
{
  #ifdef ESP32
  if (isnanf(value)) {
    str = F("NaN");
    return false;  
  }
  if (isinff(value)) {
    str = F("Inf");
    return false;
  }
#else
  if (isnan(value)) {
    str = F("NaN");
    return false;
  }
  if (isinf(value)) {
    str = F("Inf");
    return false;
  }
#endif
  const double value_d(value);
  return doubleToValidString(str, value_d, decimalPlaces, trimTrailingZeros);
}


String ull2String(uint64_t value, uint8_t base) {
  String res;

  if (value == 0) {
    res = '0';
    return res;
  }

  while (value > 0) {
    res   += String(static_cast<uint32_t>(value % base), base);
    value /= base;
  }

  int endpos   = res.length() - 1;
  int beginpos = 0;

  while (endpos > beginpos) {
    const char c = res[beginpos];
    res[beginpos] = res[endpos];
    res[endpos]   = c;
    ++beginpos;
    --endpos;
  }

  return res;
}

String ll2String(int64_t value, uint8_t  base) {
  if (value < 0) {
    return concat('-', ull2String(value * -1ll, base));
  } else {
    return ull2String(value, base);
  }
}

String trimTrailingZeros(const String& value) {
  String res(value);
  int dot_pos = res.lastIndexOf('.');

  if (dot_pos != -1) {
    bool someTrimmed = false;

    for (int i = res.length() - 1; i > dot_pos && res[i] == '0'; --i) {
      someTrimmed = true;
      res[i]      = ' ';
    }

    if (someTrimmed) {
      res.trim();
    }

    if (res.endsWith(F("."))) {
      res[dot_pos] = ' ';
      res.trim();
    }
  }
  return res;

}

/**
 * Helper: Convert an integer to string, but return an empty string for 0, to save a little space in settings
 */
String toStringNoZero(int64_t value) {
  if (value != 0) {
    return ll2String(value);
  } else {
    return EMPTY_STRING;
  }
}

String doubleToString(const double& value, unsigned int decimalPlaces, bool trimTrailingZeros_b) {
  String res;
  doubleToValidString(res, value, decimalPlaces, trimTrailingZeros_b);
  return res;
}


bool  doubleToValidString(String& str,
  const double& value,
  unsigned int  decimalPlaces     ,
  bool          trimTrailingZeros_b)
{
  if (isnan(value)) {
    str = F("NaN");
    return false;
  }
  if (isinf(value)) {
    str = F("Inf");
    return false;
  }
#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE

  // We use some trick here to prevent rounding errors 
  // like when representing 23.8, which will be printed like 23.799999...
  // 
  // First calculate factor to represent the value with N decimal places as integer
  // Use maximum of 18 decimals or else the factor will not fit in a 64-bit int
  uint64_t factor = (decimalPlaces > 18) ? 1 : computeDecimalFactorForDecimals(decimalPlaces);

  // Calculate floating point value which could be cast to int64_t later to
  // format the value with N decimal places and later insert the decimal dot.
  const double tmp_value = std::abs(value * factor);
  constexpr double max_uint64 = std::numeric_limits<uint64_t>::max();

  if ((decimalPlaces > 18) || (tmp_value > max_uint64)) {
#endif
    // Cannot use int64_t as intermediate variable
    unsigned int expectedChars = decimalPlaces + 4; // 1 dot, 2 minus signs and terminating zero
    if (value > 1e33 || value < -1e33) {
      expectedChars += 308; // Just assume the worst
    } else {
      expectedChars += 33;
    }
    char *buf = (char *)malloc(expectedChars);

    if (nullptr == buf) {
      str = F("NaN");
      return false;
    }
    move_special(str, String(dtostrf(value, (decimalPlaces + 2), decimalPlaces, buf)));

    free(buf);
#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
  } else {
    // Round the double value, multiplied with the factor 10^decimalPlaces, 
    // to make sure we will not end up with values like 23.799999...
    uint64_t int_value = round(tmp_value);

    if (trimTrailingZeros_b) {
      while (decimalPlaces > 0 && int_value % 10 == 0) {
        int_value /= 10;
        factor /= 10;
        --decimalPlaces;
      }

      if (decimalPlaces > 2) {
        const uint32_t last2digits = int_value % 100;
        if (last2digits == 99u) {
          ++int_value;
        } else if (last2digits == 1u) {
          --int_value;
        }
      }
    }

    // The value before the decimal point can be larger than what a 32-bit int can represent.
    // Those cannot be used in the string format, so use it as a preformatted string
    const String tmp_str_before_dot = ull2String(int_value / factor);
    if (decimalPlaces == 0) {
      str = tmp_str_before_dot;
    } else {
      String tmp_str_after_dot;
      tmp_str_after_dot.reserve(decimalPlaces);
      tmp_str_after_dot = ull2String(int_value % factor);
      while (tmp_str_after_dot.length() < decimalPlaces) {
        // prepend leading zeroes on the fraction part.
        tmp_str_after_dot = concat('0', tmp_str_after_dot);
      }

      str = strformat(
        F("%s.%s"), 
        tmp_str_before_dot.c_str(), 
        tmp_str_after_dot.c_str());
    }
    if (value < 0) {
      str = concat('-', str);
    }
  }
  #endif

  str.trim();

  if (trimTrailingZeros_b) {
    str = trimTrailingZeros(str);
  }
  return true;
}

String floatToString(const float& value,
                      unsigned int  decimalPlaces,
                      bool          trimTrailingZeros_b)
{
  String res = toString(value, decimalPlaces);

  if (trimTrailingZeros_b) {
    return trimTrailingZeros(res);
  }
  return res;
}


/********************************************************************************************\
   Check if valid float and convert string to float.
 \*********************************************************************************************/
bool string2float(const String& string, float& floatvalue) {
  return validFloatFromString(string, floatvalue);
}

#include "../Helpers/StringGenerator_Plugin.h"



String Plugin_valuename(const __FlashStringHelper * name_prefix, uint8_t value_nr, bool displayString) {
  String name = name_prefix;

  if (value_nr != 0) {
    name += value_nr + 1;
  }

  if (!displayString) {
    name.toLowerCase();
  }
  return name;
}

#include "../Helpers/StringGenerator_GPIO.h"

#include "../Globals/Settings.h"
#include "../Helpers/Hardware_GPIO.h"
#include "../Helpers/Hardware_device_info.h"
#include "../Helpers/StringConverter.h"
#include "../../ESPEasy_common.h"

/*********************************************************************************************\
   Device GPIO name functions to share flash strings
\*********************************************************************************************/
const __FlashStringHelper* formatGpioDirection(gpio_direction direction) {
  switch (direction) {
    case gpio_direction::gpio_input:         return F("&larr; ");
    case gpio_direction::gpio_output:        return F("&rarr; ");
    case gpio_direction::gpio_bidirectional: return F("&#8644; ");
    case gpio_direction::gpio_direction_MAX: break;
  }
  return F("");
}

String formatGpioLabel(int gpio, bool includeWarning) {
  int  pinnr = -1;
  bool input, output, warning;

  if (getGpioInfo(gpio, pinnr, input, output, warning)) {
    if (!includeWarning) {
      return createGPIO_label(gpio, pinnr, true, true, false);
    }
    return createGPIO_label(gpio, pinnr, input, output, warning);
  }
  return F("-");
}

String formatGpioName(const __FlashStringHelper * label, gpio_direction direction, bool optional) {
  int reserveLength = 5 /* "GPIO " */ + 8 /* "&#8644; " */ + strlen_P((PGM_P)label);

  if (optional) {
    reserveLength += 11;
  }
  String result;
  if (reserve_special(result, reserveLength)) {
    result += F("GPIO ");
    result += formatGpioDirection(direction);
    result += label;

    if (optional) {
      result += F("(optional)");
    }
  }
  return result;
}

String formatGpioName_input(const __FlashStringHelper * label) {
  return formatGpioName(label, gpio_direction::gpio_input, false);
}

String formatGpioName_output(const __FlashStringHelper * label) {
  return formatGpioName(label, gpio_direction::gpio_output, false);
}

String formatGpioName_bidirectional(const __FlashStringHelper * label) {
  return formatGpioName(label, gpio_direction::gpio_bidirectional, false);
}

String formatGpioName_input_optional(const __FlashStringHelper * label) {
  return formatGpioName(label, gpio_direction::gpio_input, true);
}

String formatGpioName_output_optional(const __FlashStringHelper * label) {
  return formatGpioName(label, gpio_direction::gpio_output, true);
}

// RX/TX are the only signals which are crossed, so they must be labelled like this:
// "GPIO <-- TX" and "GPIO --> RX"
String formatGpioName_TX(bool optional) {
  return formatGpioName(F("RX"), gpio_direction::gpio_output, optional);
}

String formatGpioName_RX(bool optional) {
  return formatGpioName(F("TX"), gpio_direction::gpio_input, optional);
}

String formatGpioName_serialTX(bool optional)
{
  return concat(F("ESP TX "), formatGpioName_TX(optional));
}

String formatGpioName_serialRX(bool optional)
{
  return concat(F("ESP RX "), formatGpioName_RX(optional));
}

String formatGpioName_TX_HW(bool optional) {
  return formatGpioName(F("RX (HW)"), gpio_direction::gpio_output, optional);
}

String formatGpioName_RX_HW(bool optional) {
  return formatGpioName(F("TX (HW)"), gpio_direction::gpio_input, optional);
}

#ifdef ESP32

#if SOC_ADC_SUPPORTED
String formatGpioName_ADC(int gpio_pin) {
  int adc, ch, t;

  if (getADC_gpio_info(gpio_pin, adc, ch, t)) {
    if (adc == 0) {
      return F("Hall Effect");
    }
    String res = F("ADC# ch?");
    res.replace(F("#"), String(adc));
    res.replace(F("?"), String(ch));

    if (t >= 0) {
      res += F(" (T");
      res += t;
      res += ')';
    }
    return res;
  }
  return EMPTY_STRING;
}
#endif

#if SOC_DAC_SUPPORTED
String formatGpioName_DAC(int gpio_pin) {
  int dac;

  if (getDAC_gpio_info(gpio_pin, dac)) {
    return concat(F("DAC"), dac);
  }
  return EMPTY_STRING;
}
#endif


#endif // ifdef ESP32

// ********************************************************************************
// Add a GPIO pin select dropdown list for 8266, 8285 or ESP32
// ********************************************************************************
String createGPIO_label(int gpio, int pinnr, bool input, bool output, bool warning) {
  if (gpio < 0) { return F("- None -"); }
  String result;

  result.reserve(24);
  result  = F("GPIO-");
  result += gpio;

#ifdef ESP8266
  if (pinnr >= 0) {
    result += F(" (D");
    result += pinnr;
    result += ')';
  }
#endif

  if (input != output) {
    result += ' ';
    result += input ? F(HTML_SYMBOL_INPUT) : F(HTML_SYMBOL_OUTPUT);
  }

  if (warning) {
    result += ' ';
    result += F(HTML_SYMBOL_WARNING);
  }
  return result;
}

const __FlashStringHelper* getConflictingUse(int gpio, PinSelectPurpose purpose, bool ignorePSRAMpins)
{
#ifdef PIN_USB_D_MIN
  if (gpio == PIN_USB_D_MIN) { return F("USB_D-"); }
#endif
#ifdef PIN_USB_D_PLUS
  if (gpio == PIN_USB_D_PLUS) { return F("USB_D+"); }
#endif

  if (isFlashInterfacePin_ESPEasy(gpio)) {
    return F("Flash");
  }

#ifdef ESP32
  if (!ignorePSRAMpins && isPSRAMInterfacePin(gpio)) { // PSRAM pins can be shared with SPI
    return F("PSRAM");
  }
#endif

  # ifdef ESP32S2


  #elif defined(ESP32S3)

  // See Appendix A, page 71: https://www.espressif.com/sites/default/files/documentation/esp32-s3_datasheet_en.pdf

  #elif defined(ESP32C6) 

  if (gpio == 27) {
    // By default VDD_SPI is the power supply pin for embedded flash or external flash. It can only be used as GPIO
    // only when the chip is connected to an external flash, and this flash is powered by an external power supply
    return F("Flash Vdd"); 
  }

  #elif defined(ESP32C2) || defined(ESP32C3) 

  if (gpio == 11) {
    // By default VDD_SPI is the power supply pin for embedded flash or external flash. It can only be used as GPIO11
    // only when the chip is connected to an external flash, and this flash is powered by an external power supply
    return F("Flash Vdd"); 
  }

  # elif defined(ESP32_CLASSIC)

  # elif defined(ESP8266)

  # else
    static_assert(false, "Implement processor architecture");

  # endif 

  bool includeI2C = true;
  bool includeSPI = true;
  #if FEATURE_SD
  bool includeSDCard = true;
  #endif
  bool includeSerial = Settings.UseSerial; // Only need to check if Serial Port Console is enabled

  #if FEATURE_ETHERNET
  bool includeEthernet = true;
  #endif // if FEATURE_ETHERNET
  bool includeStatusLed = true;
  bool includeResetPin = true;

  switch (purpose) {
    case PinSelectPurpose::I2C:
#if FEATURE_I2C_MULTIPLE
    case PinSelectPurpose::I2C_2:
#if FEATURE_I2C_INTERFACE_3
    case PinSelectPurpose::I2C_3:
#endif
#endif
      includeI2C = false;
      break;
    case PinSelectPurpose::SPI:
    case PinSelectPurpose::SPI_MISO:
      includeSPI = false;
      break;
    case PinSelectPurpose::Serial_input:
    case PinSelectPurpose::Serial_output:
      includeSerial = false;
      break;
    case PinSelectPurpose::Ethernet:
      #if FEATURE_ETHERNET
      includeEthernet = false;
      #endif // if FEATURE_ETHERNET
      break;
    case PinSelectPurpose::Generic:
    case PinSelectPurpose::Generic_input:
    case PinSelectPurpose::Generic_output:
    case PinSelectPurpose::Generic_bidir:
    case PinSelectPurpose::DAC:
      break;
    #if FEATURE_SD
    case PinSelectPurpose::SD_Card:
      includeSDCard = false;
      break;
    #endif
    case PinSelectPurpose::Status_led:
      includeStatusLed = false;
      break;
    case PinSelectPurpose::Reset_pin:
      includeResetPin = false;
      break;
  }

  if (includeI2C && Settings.isI2C_pin(gpio)) {
    for (uint8_t i2cBus = 0; i2cBus < getI2CBusCount(); ++i2cBus)
    {
      if (Settings.getI2CSdaPin(i2cBus) == gpio) {
        #if FEATURE_I2C_MULTIPLE
        switch (i2cBus) {
          case 0:  return F("I2C SDA (bus 0)");
          case 1:  return F("I2C SDA (bus 1)");
          #if FEATURE_I2C_INTERFACE_3
          case 2:  return F("I2C SDA (bus 2)");
          #endif
        }
        #else
        return F("I2C SDA");
        #endif
      }
      if (Settings.getI2CSclPin(i2cBus) == gpio) {
        #if FEATURE_I2C_MULTIPLE
        switch (i2cBus) {
          case 0:  return F("I2C SCL (bus 0)");
          case 1:  return F("I2C SCL (bus 1)");
          #if FEATURE_I2C_INTERFACE_3
          case 2:  return F("I2C SCL (bus 2)");
          #endif
        }
        #else
        return F("I2C SCL");
        #endif
      }
    }
  }

  if (includeSPI && Settings.isSPI_pin(gpio)) {
    return F("SPI");
  }

  if (includeStatusLed && (Settings.Pin_status_led == gpio) && (-1 != gpio)) {
    return F("Wifi Status LED");
  }

  if (includeResetPin && (Settings.Pin_Reset == gpio) && (-1 != gpio)) {
    return F("Reset Pin");
  }

  if (includeSerial) {
    #if FEATURE_DEFINE_SERIAL_CONSOLE_PORT
    if (Settings.UseSerial && 
        (Settings.console_serial_port == 2  // 2 == ESPEasySerialPort::serial0
         #if USES_ESPEASY_CONSOLE_FALLBACK_PORT
         || Settings.console_serial0_fallback
         #endif // if USES_ESPEASY_CONSOLE_FALLBACK_PORT
        ))
    #else
    if (Settings.UseSerial) 
    #endif
    {
      if (gpio == SOC_TX0) { return F("TX0"); }

      if (gpio == SOC_RX0) { return F("RX0"); }
    }
  }

  #if FEATURE_SD
  if (validGpio(gpio) && Settings.Pin_sd_cs == gpio && includeSDCard) { return F("SD-Card CS"); }
  #endif // if FEATURE_SD


  #if FEATURE_ETHERNET
  if (isSPI_EthernetType(Settings.ETH_Phy_Type)) {
    if (includeEthernet && Settings.isEthernetPinOptional(gpio)) {
      if (Settings.ETH_Pin_mdc_cs == gpio) { return F("Eth SPI CS"); }

      if (Settings.ETH_Pin_mdio_irq == gpio) { return F("Eth SPI IRQ"); }

      if (Settings.ETH_Pin_power_rst == gpio) { return F("Eth SPI RST"); }
    }
  } else {
    if (Settings.isEthernetPin(gpio)) {
      return F("Eth");
    }

    if (includeEthernet && Settings.isEthernetPinOptional(gpio)) {
      if (isGpioUsedInETHClockMode(Settings.ETH_Clock_Mode, gpio)) { return F("Eth Clock"); }

      if (Settings.ETH_Pin_mdc_cs == gpio) { return F("Eth MDC"); }

      if (Settings.ETH_Pin_mdio_irq == gpio) { return F("Eth MDIO"); }

      if (Settings.ETH_Pin_power_rst == gpio) { return F("Eth Pwr"); }

      return F("Eth");
    }
  }
  #endif // if FEATURE_ETHERNET

  if (isBootStrapPin(gpio)) {
    return F("Boot Strapping");
  }

  return F("");
}

String getConflictingUse_wrapped(int gpio, PinSelectPurpose purpose, bool ignorePSRAMpins)
{
  const String conflict = getConflictingUse(gpio, purpose, ignorePSRAMpins);

  if (conflict.isEmpty()) { return conflict; }
  String res = F(" [");

  res += conflict;
  res += ']';
  return res;
}

#include "../Helpers/ESPEasy_time_zone.h"

#include "../DataStructs/TimeChangeRule.h"
#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../Globals/ESPEasy_time.h"
#include "../Globals/Settings.h"

#include "../Helpers/ESPEasy_time_calc.h"

#include <time.h>


#define SECS_PER_MIN  (60UL)
#define SECS_PER_HOUR (3600UL)
#define SECS_PER_DAY  (SECS_PER_HOUR * 24UL)



void ESPEasy_time_zone::getDefaultDst_flash_values(uint16_t& start, uint16_t& end) {
  // DST start: Last Sunday March    2am => 3am
  // DST end:   Last Sunday October  3am => 2am
  TimeChangeRule CEST(TimeChangeRule::Last, TimeChangeRule::Sun, TimeChangeRule::Mar, 2, Settings.TimeZone); // Summer Time
  TimeChangeRule CET(TimeChangeRule::Last, TimeChangeRule::Sun, TimeChangeRule::Oct, 3, Settings.TimeZone);  // Standard Time

  start = CEST.toFlashStoredValue();
  end   = CET.toFlashStoredValue();
}

void ESPEasy_time_zone::applyTimeZone(uint32_t curTime) {
  int dst_offset = Settings.DST ? 60 : 0;
  uint16_t tmpStart(Settings.DST_Start);
  uint16_t tmpEnd(Settings.DST_End);

  for (int i = 0; i < 2; ++i) {
    TimeChangeRule start(tmpStart, Settings.TimeZone + dst_offset); // Summer Time
    TimeChangeRule end(tmpEnd, Settings.TimeZone);                  // Standard Time

    if (start.isValid() && end.isValid()) {
      setTimeZone(start, end, curTime);
      return;
    }
    getDefaultDst_flash_values(tmpStart, tmpEnd);
  }
}

void ESPEasy_time_zone::setTimeZone(const TimeChangeRule& dstStart, const TimeChangeRule& stdStart, uint32_t curTime) {
  m_dst = dstStart;
  m_std = stdStart;

  if (calcTimeChanges(ESPEasy_time::year(curTime))) {
    logTimeZoneInfo();
  }
}

void ESPEasy_time_zone::logTimeZoneInfo() {
  if (!loglevelActiveFor(LOG_LEVEL_INFO)) return;
  String log = F("Current Time Zone: ");

  if (m_std.offset != m_dst.offset) {
    // Summer time
    log += F(" DST time start: ");

    if (m_dstLoc != 0) {
      struct tm tmp;
      breakTime(m_dstLoc, tmp);
      log += formatDateTimeString(tmp, '-', ':', ' ', false);
    }
    log += F(" offset: ");
    log += m_dst.offset;
    log += F(" min ");
  }

  // Standard/Winter time.
  log += F("STD time start: ");

  if (m_stdLoc != 0) {
    struct tm tmp;
    breakTime(m_stdLoc, tmp);
    log += formatDateTimeString(tmp, '-', ':', ' ', false);
  }
  log += F(" offset: ");
  log += m_std.offset;
  log += F(" min");
  addLogMove(LOG_LEVEL_INFO, log);
}


///*----------------------------------------------------------------------*
// * Convert the given time change rule to a uint32_t value                 *
// * for the given year.                                                  *
// *----------------------------------------------------------------------*/
uint32_t ESPEasy_time_zone::calcTimeChangeForRule(const TimeChangeRule& r, int yr)
{
  uint8_t m = r.month;
  uint8_t w = r.week;

  if (w == 0)          // is this a "Last week" rule?
  {
    if (++m > 12)      // yes, for "Last", go to the next month
    {
      m = 1;
      ++yr;
    }
    w = 1; // and treat as first week of next month, subtract 7 days later
  }

  // calculate first day of the month, or for "Last" rules, first day of the next month
  struct tm tm;
  tm.tm_hour = r.hour;
  tm.tm_min  = 0;
  tm.tm_sec  = 0;
  tm.tm_mday = 1;
  tm.tm_mon  = m - 1; // TimeChangeRule month starts at 1
  tm.tm_year = yr - 1900;
  uint32_t t = makeTime(tm);

  // add offset from the first of the month to r.dow, and offset for the given week
  t += ((r.dow - ESPEasy_time::weekday(t) + 7) % 7 + (w - 1) * 7) * SECS_PER_DAY;

  // back up a week if this is a "Last" rule
  if (r.week == 0) { t -= 7 * SECS_PER_DAY; }
  return t;
}

/*----------------------------------------------------------------------*
* Calculate the DST and standard time change points for the given      *
* given year as local and UTC uint32_t values.                           *
*----------------------------------------------------------------------*/
bool ESPEasy_time_zone::calcTimeChanges(int yr)
{
  uint32_t dstLoc  = calcTimeChangeForRule(m_dst, yr);
  uint32_t stdLoc  = calcTimeChangeForRule(m_std, yr);
  bool     changed = (m_dstLoc != dstLoc) || (m_stdLoc != stdLoc);

  m_dstLoc = dstLoc;
  m_stdLoc = stdLoc;
  m_dstUTC = m_dstLoc - m_std.offset * SECS_PER_MIN;
  m_stdUTC = m_stdLoc - m_dst.offset * SECS_PER_MIN;
  return changed;
}

/*----------------------------------------------------------------------*
* Convert the given UTC time to local time, standard or                *
* daylight time, as appropriate.                                       *
*----------------------------------------------------------------------*/
uint32_t ESPEasy_time_zone::toLocal(uint32_t utc)
{
  // recalculate the time change points if needed
  if (ESPEasy_time::year(utc) != ESPEasy_time::year(m_dstUTC)) { calcTimeChanges(ESPEasy_time::year(utc)); }

  if (utcIsDST(utc)) {
    return utc + m_dst.offset * SECS_PER_MIN;
  }
  else {
    return utc + m_std.offset * SECS_PER_MIN;
  }
}

/*----------------------------------------------------------------------*
* Convert the given local time to UTC time, standard or                 *
* daylight time, as appropriate.                                        *
*-----------------------------------------------------------------------*/
uint32_t ESPEasy_time_zone::fromLocal(uint32_t local)
{
  // recalculate the time change points if needed
  if (ESPEasy_time::year(local) != ESPEasy_time::year(m_dstUTC)) { calcTimeChanges(ESPEasy_time::year(local)); }

  if (locIsDST(local)) {
    return local - m_dst.offset * SECS_PER_MIN;
  }
  else {
    return local - m_std.offset * SECS_PER_MIN;
  }
}




/*----------------------------------------------------------------------*
* Determine whether the given UTC uint32_t is within the DST interval    *
* or the Standard time interval.                                       *
*----------------------------------------------------------------------*/
bool ESPEasy_time_zone::utcIsDST(uint32_t utc)
{
  // recalculate the time change points if needed
  if (ESPEasy_time::year(utc) != ESPEasy_time::year(m_dstUTC)) { calcTimeChanges(ESPEasy_time::year(utc)); }

  if (m_stdUTC == m_dstUTC) {     // daylight time not observed in this tz
    return false;
  }
  else if (m_stdUTC > m_dstUTC) { // northern hemisphere
    return utc >= m_dstUTC && utc < m_stdUTC;
  }
  else {                          // southern hemisphere
    return !(utc >= m_stdUTC && utc < m_dstUTC);
  }
}

/*----------------------------------------------------------------------*
* Determine whether the given Local uint32_t is within the DST interval  *
* or the Standard time interval.                                       *
*----------------------------------------------------------------------*/
bool ESPEasy_time_zone::locIsDST(uint32_t local)
{
  // recalculate the time change points if needed
  if (ESPEasy_time::year(local) != ESPEasy_time::year(m_dstLoc)) { calcTimeChanges(ESPEasy_time::year(local)); }

  if (m_stdUTC == m_dstUTC) {     // daylight time not observed in this tz
    return false;
  }
  else if (m_stdLoc > m_dstLoc) { // northern hemisphere
    return local >= m_dstLoc && local < m_stdLoc;
  }
  else {                          // southern hemisphere
    return !(local >= m_stdLoc && local < m_dstLoc);
  }
}



#include "../Helpers/_CPlugin_Helper.h"

#include "../../ESPEasy_common.h"

#include "../CustomBuild/CompiletimeDefines.h"
#include "../CustomBuild/ESPEasyLimits.h"

#include "../DataStructs/SecurityStruct.h"
#include "../DataStructs/SettingsStruct.h"

#include "../DataStructs/ControllerSettingsStruct.h"
#include "../DataStructs/TimingStats.h"

#include "../ESPEasyCore/ESPEasy_backgroundtasks.h"
#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../ESPEasyCore/ESPEasyEth.h"
#include "../ESPEasyCore/ESPEasyNetwork.h"
#include "../ESPEasyCore/ESPEasyWifi.h"

#include "../Globals/Settings.h"
#include "../Globals/SecuritySettings.h"
#include "../Globals/ESPEasyWiFiEvent.h"

#include "../Helpers/ESPEasy_time_calc.h"
#include "../Helpers/Misc.h"
#include "../Helpers/Network.h"
#include "../Helpers/Networking.h"
#include "../Helpers/StringConverter.h"

#include <WiFiClient.h>
#include <WiFiUdp.h>


bool safeReadStringUntil(Stream     & input,
                         String     & str,
                         char         terminator,
                         unsigned int maxSize,
                         unsigned int timeout)
{
  int c;
  const unsigned long start           = millis();
  const unsigned long timer           = start + timeout;
  unsigned long backgroundtasks_timer = start + 10;

  // FIXME TD-er: Should this also de-allocate internal buffer?
  str.clear();

  do {
    // read character
    if (input.available()) {
      c = input.read();

      if (c >= 0) {
        // found terminator, we're ok
        if (c == terminator) {
          return true;
        }

        // found character, add to string
        str += char(c);

        // string at max size?
        if (str.length() >= maxSize) {
          addLog(LOG_LEVEL_ERROR, F("Not enough bufferspace to read all input data!"));
          return false;
        }
      }

      // We must run the backgroundtasks every now and then.
      if (timeOutReached(backgroundtasks_timer)) {
        backgroundtasks_timer += 10;
        backgroundtasks();
      } else {
        delay(0);
      }
    } else {
      delay(0);
    }
  } while (!timeOutReached(timer));

  addLog(LOG_LEVEL_ERROR, strformat(F("Timeout while reading input data! str: `%s`"), str.c_str()));
  return false;
}

#ifndef BUILD_NO_DEBUG
void log_connecting_to(const __FlashStringHelper *prefix, cpluginID_t cpluginID, ControllerSettingsStruct& ControllerSettings) {
  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLogMove(LOG_LEVEL_DEBUG,
               strformat(F("%s%s connecting to %s"),
                         prefix,
                         get_formatted_Controller_number(cpluginID).c_str(),
                         ControllerSettings.getHostPortString().c_str()));
  }
}

#endif // ifndef BUILD_NO_DEBUG

void log_connecting_fail(const __FlashStringHelper *prefix, cpluginID_t cpluginID) {
  if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
    addLogMove(LOG_LEVEL_ERROR,
               strformat(F("%s%s connection failed (%d/%d)"),
                         prefix,
                         get_formatted_Controller_number(cpluginID).c_str(),
                         WiFiEventData.connectionFailures,
                         Settings.ConnectionFailuresThreshold));
  }
}

bool count_connection_results(bool success, const __FlashStringHelper *prefix, cpluginID_t cpluginID, uint64_t statisticsTimerStart) {
#if FEATURE_TIMING_STATS
  const protocolIndex_t protocolIndex = getProtocolIndex_from_CPluginID(cpluginID);
#endif

  if (!success)
  {
    ++WiFiEventData.connectionFailures;
    log_connecting_fail(prefix, cpluginID);
    STOP_TIMER_CONTROLLER(protocolIndex, CPlugin::Function::CPLUGIN_CONNECT_FAIL);
    return false;
  }
  WiFiEventData.connectDurations[cpluginID] = usecPassedSince(statisticsTimerStart) / 1000ul;
  STOP_TIMER_CONTROLLER(protocolIndex, CPlugin::Function::CPLUGIN_CONNECT_SUCCESS);
  statusLED(true);

  if (WiFiEventData.connectionFailures > 0) {
    --WiFiEventData.connectionFailures;
  }
  return true;
}

bool try_connect_host(cpluginID_t cpluginID, WiFiUDP& client, ControllerSettingsStruct& ControllerSettings) {
  const uint64_t statisticsTimerStart(getMicros64()); // START_TIMER;

  if (!NetworkConnected()) {
    client.stop();
    return false;
  }

  // Ignoring the ACK from the server is probably set for a reason.
  // For example because the server does not give an acknowledgement.
  // This way, we always need the set amount of timeout to handle the request.
  // Thus we should not make the timeout dynamic here if set to ignore ack.
  const uint32_t timeout = ControllerSettings.MustCheckReply
    ? WiFiEventData.getSuggestedTimeout(cpluginID, ControllerSettings.ClientTimeout)
    : ControllerSettings.ClientTimeout;

  client.setTimeout(timeout); // in msec as it should be!
  delay(0);
#ifndef BUILD_NO_DEBUG
  log_connecting_to(F("UDP  : "), cpluginID, ControllerSettings);
#endif // ifndef BUILD_NO_DEBUG

  bool success = ControllerSettings.beginPacket(client);

  if (!success) {
    client.stop();
  }
  const bool result = count_connection_results(
    success,
    F("UDP  : "),
    cpluginID,
    statisticsTimerStart);
  STOP_TIMER(TRY_CONNECT_HOST_UDP);
  return result;
}

#if FEATURE_HTTP_CLIENT
bool try_connect_host(cpluginID_t cpluginID, WiFiClient& client, ControllerSettingsStruct& ControllerSettings) {
  return try_connect_host(cpluginID, client, ControllerSettings, F("HTTP : "));
}

bool try_connect_host(cpluginID_t                cpluginID,
                      WiFiClient               & client,
                      ControllerSettingsStruct & ControllerSettings,
                      const __FlashStringHelper *loglabel) {
  const uint64_t statisticsTimerStart(getMicros64()); // START_TIMER;

  if (!NetworkConnected()) {
    client.stop();
    return false;
  }

  // Use WiFiClient class to create TCP connections
  delay(0);

  // Ignoring the ACK from the server is probably set for a reason.
  // For example because the server does not give an acknowledgement.
  // This way, we always need the set amount of timeout to handle the request.
  // Thus we should not make the timeout dynamic here if set to ignore ack.
  const uint32_t timeout = ControllerSettings.MustCheckReply
    ? WiFiEventData.getSuggestedTimeout(cpluginID, ControllerSettings.ClientTimeout)
    : ControllerSettings.ClientTimeout;

  # ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS

  // See: https://github.com/espressif/arduino-esp32/pull/6676
  client.setTimeout((timeout + 500) / 1000); // in seconds!!!!
  Client *pClient = &client;
  pClient->setTimeout(timeout);
  # else // ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS
  client.setTimeout(timeout); // in msec as it should be!
  # endif // ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS

# ifndef BUILD_NO_DEBUG
  log_connecting_to(loglabel, cpluginID, ControllerSettings);
# endif // ifndef BUILD_NO_DEBUG

  const bool success = ControllerSettings.connectToHost(client);

  if (!success) {
    client.stop();
  }
  const bool result = count_connection_results(
    success,
    loglabel,
    cpluginID,
    statisticsTimerStart);
  STOP_TIMER(TRY_CONNECT_HOST_TCP);
  return result;
}

// Use "client.available() || client.connected()" to read all lines from slow servers.
// See: https://github.com/esp8266/Arduino/pull/5113
//      https://github.com/esp8266/Arduino/pull/1829
bool client_available(WiFiClient& client) {
  delay(0);
  return (client.available() != 0) || (client.connected() != 0);
}

String send_via_http(int                             cpluginID,
                     const ControllerSettingsStruct& ControllerSettings,
                     controllerIndex_t               controller_idx,
                     const String                  & uri,
                     const String                  & HttpMethod,
                     const String                  & header,
                     const String                  & postStr,
                     int                           & httpCode) {
  // Ignoring the ACK from the HTTP server is probably set for a reason.
  // For example because the server does not give an acknowledgement.
  // This way, we always need the set amount of timeout to handle the request.
  // Thus we should not make the timeout dynamic here if set to ignore ack.
  const uint32_t timeout = ControllerSettings.MustCheckReply
    ? WiFiEventData.getSuggestedTimeout(cpluginID, ControllerSettings.ClientTimeout)
    : ControllerSettings.ClientTimeout;

  const uint64_t statisticsTimerStart(getMicros64());
  const String result                    = send_via_http(
    get_formatted_Controller_number(cpluginID),
    timeout,
    getControllerUser(controller_idx, ControllerSettings),
    getControllerPass(controller_idx, ControllerSettings),
    ControllerSettings.getHost(),
    ControllerSettings.Port,
    uri,
    HttpMethod,
    header,
    postStr,
    httpCode,
    ControllerSettings.MustCheckReply
    #if FEATURE_HTTP_TLS
    , ControllerSettings.TLStype()
    #endif // if FEATURE_HTTP_TLS
   );

  // FIXME TD-er: Shouldn't this be: success = (httpCode >= 100) && (httpCode < 300)
  // or is reachability of the host the important factor here?
  const bool success = httpCode > 0;

  count_connection_results(
    success,
    F("HTTP  : "),
    cpluginID,
    statisticsTimerStart);

  return result;
}

#endif // FEATURE_HTTP_CLIENT


String getControllerUser(controllerIndex_t controller_idx, const ControllerSettingsStruct& ControllerSettings, bool doParseTemplate)
{
  if (!validControllerIndex(controller_idx)) { return EMPTY_STRING; }

  String res;

  if (ControllerSettings.useExtendedCredentials()) {
    res = ExtendedControllerCredentials.getControllerUser(controller_idx);
  } else {
    res = String(SecuritySettings.ControllerUser[controller_idx]);
  }
  res.trim();

  if (doParseTemplate) {
    res = parseTemplate(res);
  }
  return res;
}

String getControllerPass(controllerIndex_t controller_idx, const ControllerSettingsStruct& ControllerSettings)
{
  if (!validControllerIndex(controller_idx)) { return EMPTY_STRING; }

  if (ControllerSettings.useExtendedCredentials()) {
    return ExtendedControllerCredentials.getControllerPass(controller_idx);
  }
  String res(SecuritySettings.ControllerPassword[controller_idx]);

  res.trim();
  return res;
}

void setControllerUser(controllerIndex_t controller_idx, const ControllerSettingsStruct& ControllerSettings, const String& value)
{
  if (!validControllerIndex(controller_idx)) { return; }

  if (ControllerSettings.useExtendedCredentials()) {
    ExtendedControllerCredentials.setControllerUser(controller_idx, value);
  } else {
    safe_strncpy(SecuritySettings.ControllerUser[controller_idx], value, sizeof(SecuritySettings.ControllerUser[0]));
  }
}

void setControllerPass(controllerIndex_t controller_idx, const ControllerSettingsStruct& ControllerSettings, const String& value)
{
  if (!validControllerIndex(controller_idx)) { return; }

  if (ControllerSettings.useExtendedCredentials()) {
    ExtendedControllerCredentials.setControllerPass(controller_idx, value);
  } else {
    safe_strncpy(SecuritySettings.ControllerPassword[controller_idx], value, sizeof(SecuritySettings.ControllerPassword[0]));
  }
}

bool hasControllerCredentialsSet(controllerIndex_t controller_idx, const ControllerSettingsStruct& ControllerSettings)
{
  return !getControllerUser(controller_idx, ControllerSettings, false).isEmpty() &&
         !getControllerPass(controller_idx, ControllerSettings).isEmpty();
}

#include "../Helpers/Hardware_device_info.h"

#ifdef ESP32C5


const __FlashStringHelper* getChipModel(uint32_t chip_model, uint32_t chip_revision, uint32_t pkg_version, bool single_core)
{
  if (17 == chip_model) {      // ESP32-C5 beta3 (MPW)
    return F("ESP32-C5 beta3");
  }
  else if (23 == chip_model) { // ESP32-C5 MP
    return F("ESP32-C5");
  }

  return F("Unknown");
}

#endif // ifdef ESP32C5

#include "../Helpers/StringConverter.h"


#include "../../_Plugin_Helper.h"

#include "../DataStructs/ESPEasy_EventStruct.h"
#include "../DataStructs/TimingStats.h"

#include "../ESPEasyCore/ESPEasy_Log.h"

#include "../Globals/Cache.h"
#include "../Globals/CRCValues.h"
#include "../Globals/Device.h"
#include "../Globals/ESPEasyWiFiEvent.h"
#include "../Globals/ESPEasy_time.h"
#include "../Globals/MQTT.h"
#include "../Globals/Plugins.h"
#include "../Globals/Settings.h"

#include "../Helpers/Convert.h"
#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/Misc.h"
#include "../Helpers/Networking.h"
#include "../Helpers/Numerical.h"
#include "../Helpers/StringGenerator_System.h"
#include "../Helpers/StringParser.h"
#include "../Helpers/SystemVariables.h"
#include "../Helpers/_Plugin_SensorTypeHelper.h"

// -V::569

unsigned int count_newlines(const String& str)
{
  unsigned int count = 0;
  const size_t strlength = str.length();
  size_t pos = 0;
  while (pos < strlength) {
    if (str[pos] == '\n') ++count;
    ++pos;
  }
  return count;
}

String concat(const __FlashStringHelper * str, const String &val) {
  String res;
  reserve_special(res, strlen_P((PGM_P)str) + val.length());
  res.concat(str);
  res.concat(val);

  /*
  String res(str);
  reserve_special(res, res.length() + val.length());
  res.concat(val);
  */
  return res;
}

String concat(const __FlashStringHelper * str, const __FlashStringHelper *val) {
  return concat(str, String(val));
}

String concat(const char& str, const String &val)
{
  String res(str);
  reserve_special(res, res.length() + val.length());
  res.concat(val);
  return res;
}

bool equals(const String& str, const __FlashStringHelper * f_str) {
  return str.equals(String(f_str));
}

bool equals(const String& str, const char& c) {
  return str.length() == 1 && str[0] == c;
}

void move_special(String& dest, String&& source) {
  // Only try to store larger strings here as those tend to be kept for a longer period.
  if ((source.length() >= 64) 
#ifdef USE_SECOND_HEAP
      && mmu_is_dram(&(source[0]))
#endif
  ) {
    // The string was not allocated on the 2nd heap, so copy instead of move
    if (!reserve_special(dest, source.length())) {
      // Could not allocate on 2nd heap or PSRAM, so just move existing string
      dest = std::move(source);
      return;
    }
  }

  // Try to avoid keeping reserved memory on empty strings
  // So just copy the data, not move
  dest = source;
  free_string(source);  
}

String move_special(String&& source) {
  String dest;
  move_special(dest, std::move(source));
  return dest;
}


bool reserve_special(String& str, size_t size) {
  return String_reserve_special(str, size);
}

void free_string(String& str) {
  // This is a call specifically tailored to what is done in:
  //  void String::move(String &rhs)

#if defined(ESP32) || defined(CORE_POST_3_0_0)
  // Use current implementation of String::copy as this
  // invalidates and thus deallocates current buffer
  str = (const char*)nullptr;
  str = String(); // No idea why this is needed, without it, some ESP32's may bootloop
#else
  str = String();
#endif
}

/********************************************************************************************\
   Format string using vsnprintf
 \*********************************************************************************************/

String strformat(const String& format, ...)
{
  String res;
  {
    va_list arg;
    va_start(arg, format); // variable args start after parameter 'format'
    static char temp[64];
    char* buffer = temp;
    int len = vsnprintf_P(temp, sizeof(temp), format.c_str(), arg);
    va_end(arg);
    if (len > static_cast<int>(sizeof(temp) - 1)) {
        buffer = new (std::nothrow) char[len + 1];
        if (!buffer) {
            return res;
        }
        va_start(arg, format);
        vsnprintf_P(buffer, len + 1, format.c_str(), arg);
        va_end(arg);
    }
    if (len > 64) {
      reserve_special(res, len + 1);
    }
    res = buffer;
    if (buffer != temp) {
        delete[] buffer;
    }
  }
  return res;
}

String strformat(const __FlashStringHelper * format, ...)
{
  String res;
  {
    va_list arg;
    va_start(arg, format); // variable args start after parameter 'format'
    static char temp[64];
    char* buffer = temp;
    int len = vsnprintf_P(temp, sizeof(temp), (PGM_P)format, arg);
    va_end(arg);
    if (len > static_cast<int>(sizeof(temp) - 1)) {
        buffer = new (std::nothrow) char[len + 1];
        if (!buffer) {
            return res;
        }
        va_start(arg, format);
        vsnprintf_P(buffer, len + 1, (PGM_P)format, arg);
        va_end(arg);
    }
    if (len > 64) {
      reserve_special(res, len + 1);
    }
    res = buffer;
    if (buffer != temp) {
        delete[] buffer;
    }
  }
  return res;
}



/********************************************************************************************\
   Convert a char string to IP uint8_t array
 \*********************************************************************************************/
bool isIP(const String& string) {
  IPAddress tmpip;
  return (tmpip.fromString(string));
}

bool str2ip(const String& string, uint8_t *IP) {
  return str2ip(string.c_str(), IP);
}

bool str2ip(const char *string, uint8_t *IP)
{
  IPAddress tmpip; // Default constructor => set to 0.0.0.0

  if ((*string == 0) || tmpip.fromString(string)) {
    // Eiher empty string or a valid IP addres, so copy value.
    for (uint8_t i = 0; i < 4; ++i) {
      IP[i] = tmpip[i];
    }
    return true;
  }
  return false;
}

String formatIP(const IPAddress& ip, bool includeZone) {
#ifdef ESP8266
#if defined(ARDUINO_ESP8266_RELEASE_2_3_0)
  IPAddress tmp(ip);
  return tmp.toString();
#else // if defined(ARDUINO_ESP8266_RELEASE_2_3_0)
  return ip.toString();
#endif // if defined(ARDUINO_ESP8266_RELEASE_2_3_0)
#endif
#ifdef ESP32
/*
  #if LWIP_IPV6
  if (ip.isAny()) {
    IPAddress tmp;
    tmp.setV4();
    return tmp.toString();
  }
  #endif
*/
#if FEATURE_USE_IPV6
  return ip.toString(includeZone);
#else
  return ip.toString();
#endif
#endif
}


/********************************************************************************************\
   Handling HEX strings
 \*********************************************************************************************/

// Convert max. 8 hex decimals to unsigned long
unsigned long hexToUL(const String& input_c, size_t nrHexDecimals) {
  const unsigned long long resULL = hexToULL(input_c, nrHexDecimals);
  return static_cast<unsigned long>(resULL & 0xFFFFFFFFull);
}

unsigned long hexToUL(const String& input_c) {
  return hexToUL(input_c, input_c.length());
}

unsigned long hexToUL(const String& input_c, size_t startpos, size_t nrHexDecimals) {
  return hexToUL(input_c.substring(startpos, startpos + nrHexDecimals), nrHexDecimals);
}

// Convert max. 16 hex decimals to unsigned long long (aka uint64_t)
unsigned long long hexToULL(const String& input_c, size_t nrHexDecimals) {
  size_t nr_decimals = nrHexDecimals;

  if (nr_decimals > 16) {
    nr_decimals = 16;
  }
  const size_t inputLength = input_c.length();

  if (nr_decimals > inputLength) {
    nr_decimals = inputLength;
  } else if (input_c.startsWith(F("0x"))) { // strtoull handles that prefix nicely
    nr_decimals += 2;
  }
  return strtoull(input_c.substring(0, nr_decimals).c_str(), 0, 16);
}

unsigned long long hexToULL(const String& input_c) {
  return hexToULL(input_c, input_c.length());
}

unsigned long long hexToULL(const String& input_c, size_t startpos, size_t nrHexDecimals) {
  return hexToULL(input_c.substring(startpos, startpos + nrHexDecimals), nrHexDecimals);
}

void appendHexChar(uint8_t data, String& string)
{
  const char *hex_chars = "0123456789abcdef";
  string += hex_chars[(data >> 4) & 0xF];
  string += hex_chars[(data) & 0xF];
}

String formatToHex_array(const uint8_t* data, size_t size)
{
  String res;
  res.reserve(2 * size);
  for (size_t i = 0; i < size; ++i) {
    appendHexChar(data[i], res);
  }
  return res;
}

String formatToHex_wordarray(const uint16_t* data, size_t size)
{
  String res;
  res.reserve(4 * size);
  for (size_t i = 0; i < size; ++i) {
    appendHexChar(data[i] >> 8, res);
    appendHexChar(data[i] & 0xFF, res);
  }
  return res;
}

String formatToHex(unsigned long value, 
                   const __FlashStringHelper * prefix,
                   unsigned int minimal_hex_digits) {
  return concat(prefix, formatToHex_no_prefix(value, minimal_hex_digits));
}

String formatToHex(unsigned long value,
                   const __FlashStringHelper * prefix) {
  return formatToHex(value, prefix, 0);
}

String formatToHex(unsigned long value, unsigned int minimal_hex_digits) {
  return formatToHex(value, F("0x"), minimal_hex_digits);
}

String formatToHex_no_prefix(unsigned long value, unsigned int minimal_hex_digits) {
  const String fmt = strformat(F("%%0%dX"), minimal_hex_digits);
  return strformat(fmt, value);
}

String formatHumanReadable(unsigned long value, unsigned long factor) {
  String result = formatHumanReadable(value, factor, 2);

  result.replace(F(".00"), EMPTY_STRING);
  return result;
}

String formatHumanReadable(unsigned long value, unsigned long factor, int NrDecimals) {
  float floatValue(value);
  uint8_t  steps = 0;

  while (value >= factor) {
    value /= factor;
    ++steps;
    floatValue /= float(factor);
  }
  String result = toString(floatValue, NrDecimals);

  switch (steps) {
    case 0: break;
    case 1: result += 'k'; break;
    case 2: result += 'M'; break;
    case 3: result += 'G'; break;
    case 4: result += 'T'; break;
    default:
      result += '*';
      result += factor;
      result += '^';
      result += steps;
      break;
  }
  return result;
}

String formatToHex_decimal(unsigned long value) {
  return formatToHex_decimal(value, 1);
}

String formatToHex_decimal(unsigned long value, unsigned long factor) {
  String result = formatToHex(value);

  result += F(" (");

  if (factor > 1) {
    result += formatHumanReadable(value, factor);
  } else {
    result += value;
  }
  result += ')';
  return result;
}

const __FlashStringHelper * boolToString(bool value) {
  return value ? F("true") : F("false");
}

/*********************************************************************************************\
   Typical string replace functions.
\*********************************************************************************************/
void removeExtraNewLine(String& line) {
  while (line.endsWith(F("\r\n\r\n"))) {
    line.remove(line.length() - 2);
  }
}

void removeChar(String& line, char character) {
  line.replace(String(character), EMPTY_STRING);
}

void addNewLine(String& line) {
  line += F("\r\n");
}

size_t UTF8_charLength(uint8_t firstByte) {
  if (firstByte <= 0x7f) {
    return 1;
  }
  // First Byte  Second Byte Third Byte  Fourth Byte
  // [0x00,0x7F]         
  // [0xC2,0xDF] [0x80,0xBF]     
  // 0xE0        [0xA0,0xBF] [0x80,0xBF] 
  // [0xE1,0xEC] [0x80,0xBF] [0x80,0xBF] 
  // 0xED        [0x80,0x9F] [0x80,0xBF] 
  // [0xEE,0xEF] [0x80,0xBF] [0x80,0xBF] 
  // 0xF0        [0x90,0xBF] [0x80,0xBF] [0x80,0xBF]
  // [0xF1,0xF3] [0x80,0xBF] [0x80,0xBF] [0x80,0xBF]
  // 0xF4        [0x80,0x8F] [0x80,0xBF] [0x80,0xBF]
  // See: https://lemire.me/blog/2018/05/09/how-quickly-can-you-check-that-a-string-is-valid-unicode-utf-8/

  size_t charLength = 2;
  if (firstByte > 0xEF) {
    charLength = 4;
  } else if (firstByte > 0xDF) {
    charLength = 3;
  }
  return charLength;
}

void replaceUnicodeByChar(String& line, char replChar) {
  size_t pos = 0;
  while (pos < line.length()) {
    const size_t charLength = UTF8_charLength((uint8_t)line[pos]);

    if (charLength > 1) {
      // Is unicode char in UTF-8 format
      // Need to find how many characters we need to replace.
      const size_t charsLeft = line.length() - pos;
      if (charsLeft >= charLength) {
        line.replace(line.substring(pos, pos + charLength), String(replChar));
      }
    }
    ++pos;
  }
}

/*********************************************************************************************\
   Format a value to the set number of decimals
\*********************************************************************************************/
String doFormatUserVar(struct EventStruct *event, uint8_t rel_index, bool mustCheck, bool& isvalid) {
  if (event == nullptr) return EMPTY_STRING;
  START_TIMER;
  isvalid = true;

  const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(event->TaskIndex);

  if (!validDeviceIndex(DeviceIndex)) {
    isvalid = false;
    return EMPTY_STRING;
  }

  if (Device[DeviceIndex].HasFormatUserVar) {
    // First try to format using the plugin specific formatting.
    String result;
    EventStruct tempEvent;
    tempEvent.deep_copy(event);
    tempEvent.idx = rel_index;
    PluginCall(PLUGIN_FORMAT_USERVAR, &tempEvent, result);
    if (result.length() > 0) {
      return result;
    }
  }
  
  // Spent upto 400 usec till here
  const uint8_t valueCount      = getValueCountForTask(event->TaskIndex);
  const Sensor_VType sensorType = event->getSensorType();

  if (valueCount <= rel_index) {
    isvalid = false;

    #ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
      addLogMove(LOG_LEVEL_ERROR, strformat(
        F("No sensor value for TaskIndex: %d varnumber: %d type: %s"),
        event->TaskIndex + 1,
        rel_index + 1,
        String(getSensorTypeLabel(sensorType)).c_str()));
    }
    #endif // ifndef BUILD_NO_DEBUG
    return EMPTY_STRING;
  }

  if (sensorType == Sensor_VType::SENSOR_TYPE_STRING) {
    return event->String2;
  }

  uint8_t nrDecimals = 0;
  if (Device[DeviceIndex].configurableDecimals()) {
    nrDecimals = Cache.getTaskDeviceValueDecimals(event->TaskIndex, rel_index);
  }

  if (mustCheck) {
    if (!UserVar.isValid(event->TaskIndex, rel_index, sensorType)) {
      isvalid = false;
#ifndef BUILD_NO_DEBUG

      if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
        addLogMove(LOG_LEVEL_DEBUG, strformat(
          F("Invalid float value for TaskIndex: %d varnumber: %d"),
          event->TaskIndex + 1,
          rel_index + 1));
      }
#endif // ifndef BUILD_NO_DEBUG
      const float f{};
      return toString(f, nrDecimals);
    }
  }
  String res =  UserVar.getAsString(event->TaskIndex, rel_index, sensorType, nrDecimals);
  STOP_TIMER(FORMAT_USER_VAR);
  return res;
}

String formatUserVarNoCheck(taskIndex_t TaskIndex, uint8_t rel_index) {
  bool isvalid;

  // FIXME TD-er: calls to this function cannot handle Sensor_VType::SENSOR_TYPE_STRING
  struct EventStruct TempEvent(TaskIndex);

  return doFormatUserVar(&TempEvent, rel_index, false, isvalid);
}

String formatUserVar(taskIndex_t TaskIndex, uint8_t rel_index, bool& isvalid) {
  // FIXME TD-er: calls to this function cannot handle Sensor_VType::SENSOR_TYPE_STRING
  struct EventStruct TempEvent(TaskIndex);

  return doFormatUserVar(&TempEvent, rel_index, true, isvalid);
}

String formatUserVarNoCheck(struct EventStruct *event, uint8_t rel_index)
{
  bool isvalid;

  return doFormatUserVar(event, rel_index, false, isvalid);
}

String formatUserVar(struct EventStruct *event, uint8_t rel_index, bool& isvalid)
{
  return doFormatUserVar(event, rel_index, true, isvalid);
}

#if FEATURE_STRING_VARIABLES
String formatUserVarForPresentation(struct EventStruct *event,
                                    taskVarIndex_t      varNr,
                                    bool              & hasPresentation,
                                    const String      & value,
                                    const deviceIndex_t DeviceIndex,
                                    String              valueName) {
  const taskIndex_t taskIndex = event->TaskIndex;
  String formula = Cache.getTaskDeviceFormula(taskIndex, varNr);
  hasPresentation = Cache.hasFormula(taskIndex, varNr)
                            && formula.startsWith(F(TASK_VALUE_PRESENTATION_PREFIX_STRING));
  if (hasPresentation) {
    formula.remove(0, 1);
    formula.replace(F("%value%"), value);
    if (Cache.hasFormula_with_prevValue(taskIndex, varNr)) {
      event->sensorType = Device[DeviceIndex].VType;
      String dummy;
      PluginCall(PLUGIN_GET_DEVICEVTYPE, event, dummy);
      const String prev_str = UserVar.getPreviousValue(taskIndex, varNr, event->sensorType);
      formula.replace(F("%pvalue%"), prev_str.isEmpty() ? value : prev_str);
    }
    formula = parseTemplate(formula);
    return formula;
  } else {
    String taskName = getTaskDeviceName(taskIndex);
    taskName.toLowerCase();
    if (valueName.isEmpty() && validTaskVarIndex(varNr)) {
      valueName = getTaskValueName(taskIndex, varNr);
    }
    valueName.toLowerCase();
    String presentation = getCustomStringVar(strformat(F(TASK_VALUE_PRESENTATION_PREFIX_TEMPLATE), taskName.c_str(), valueName.c_str()));
    if (!presentation.isEmpty()) {
      stripEscapeCharacters(presentation);
      presentation.replace(F("%value%"), value);
      presentation = parseTemplate(presentation);
    }
    hasPresentation = !presentation.isEmpty();
    return presentation;
  }
  return EMPTY_STRING;
}
#endif // if FEATURE_STRING_VARIABLES

String get_formatted_Controller_number(cpluginID_t cpluginID) {
  if (!validCPluginID(cpluginID)) {
    return F("C---");
  }
  return strformat(F("C%03d"), cpluginID);
}

String get_formatted_Plugin_number(pluginID_t pluginID)
{
  return pluginID.toDisplayString();
}

String formatIntLeadingZeroes(int value, int nrDigits)
{
  const String fmt = strformat(F("%%0%dd"), nrDigits);
  return strformat(fmt, value);
//  return formatIntLeadingZeroes(String(value), nrDigits);
}

String formatIntLeadingZeroes(const String& value, int nrDigits)
{
  String res;
  res.reserve(nrDigits);
  int nrZeroes = nrDigits - value.length();
  while (nrZeroes > 0) {
    --nrZeroes;
    res += '0';
  }
  res += value;
  return res;
}

/*********************************************************************************************\
   Wrap a string with given pre- and postfix string.
\*********************************************************************************************/
String wrap_braces(const String& string) {
  return wrap_String(string, '(', ')');
}

String wrap_String(const String& string, char wrap) {
  return wrap_String(string, wrap, wrap);
}

String wrap_String(const String& string, char char1, char char2) {
  return strformat(F("%c%s%c"), char1, string.c_str(), char2);
}

String wrapIfContains(const String& value, char contains, char wrap) {
  if (value.indexOf(contains) != -1) {
    return wrap_String(value, wrap, wrap);
  }
  return value;
}

String wrapWithQuotes(const String& text) {
  if (isWrappedWithQuotes(text)) {
    return text;
  }
  // Try to find unused quote char and wrap
  char quotechar = '_';
  if (!findUnusedQuoteChar(text, quotechar)) {
    if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
      addLogMove(LOG_LEVEL_ERROR, strformat(
        F("No unused quote to wrap: _%s_"), 
        text.c_str()));
    }
  }
  return wrap_String(text, quotechar);
}

String wrapWithQuotesIfContainsParameterSeparatorChar(const String& text) {
  if (isWrappedWithQuotes(text)) {
    return text;
  }
  if (stringContainsSeparatorChar(text)) {
    return wrapWithQuotes(text);
  }
  return text;
}

/*********************************************************************************************\
   Format an object value pair for use in JSON.
\*********************************************************************************************/
String to_json_object_value(const __FlashStringHelper * object,
                            const __FlashStringHelper * value,
                            bool wrapInQuotes) 
{
  return to_json_object_value(String(object), String(value), wrapInQuotes);
}


String to_json_object_value(const __FlashStringHelper * object,
                            const String& value,
                            bool wrapInQuotes) 
{
  return to_json_object_value(String(object), value, wrapInQuotes);
}

String to_json_object_value(const __FlashStringHelper * object,
                            String&& value,
                            bool wrapInQuotes) 
{
  return to_json_object_value(String(object), value, wrapInQuotes);
}

String to_json_object_value(const __FlashStringHelper * object,
                            int value,
                            bool wrapInQuotes)
{
  return to_json_object_value(String(object), value, wrapInQuotes);
}

String to_json_object_value(const String& object,
                            int value,
                            bool wrapInQuotes)
{
  if (wrapInQuotes) {
    return strformat(
      F("\"%s\":\"%d\""),
      object.c_str(),
      value);
  }
    
  return strformat(
    F("\"%s\":%d"), 
    object.c_str(),
    value);
}

String to_json_object_value(const String& object, const String& value, bool wrapInQuotes) {
  return strformat(
    F("\"%s\":%s"), 
    object.c_str(),
    to_json_value(value, wrapInQuotes).c_str());
}

String to_json_value(const String& value, bool wrapInQuotes) {
  if (value.isEmpty()) {
    // Empty string
    return F("\"\"");
  }
  const size_t val_length = value.length();

  if (val_length > 2) {
    // Check for JSON objects or arrays
    const char firstchar = value[0];
    const char lastchar = value[val_length - 1];
    if ((firstchar == '[' && lastchar == ']') ||
        (firstchar == '{' && lastchar == '}')) 
    {
      return value;
    }
  }


  if (wrapInQuotes || mustConsiderAsJSONString(value)) {
    // Is not a numerical value, or BIN/HEX notation, thus wrap with quotes

    // First we check for not allowed special characters.

    // Most frequently found in a string
    const bool backslash_or_doubleQuote_found = 
      value.indexOf('\\') != -1 ||
      value.indexOf('"') != -1;
    
    for (size_t i = 0; i < val_length; ++i) {
      const char c = value[i];
      // Special characters not allowed in JSON:
      if (backslash_or_doubleQuote_found ||
          (c >= '\b' && c <= '\r')
/*
          c == '\b'|| //  \b  Backspace (ascii code 08)
          c == '\t'|| //  \t  Tab
          c == '\n'|| //  \n  New line
          c == '\f'|| //  \f  Form feed (ascii code 0C)
          c == '\r'|| //  \r  Carriage return
*/
          ) {
        // Must replace characters, so make a deepcopy
        String tmpValue(value);
        tmpValue.replace('\b', '^');  //  \b  Backspace (ascii code 08)
        tmpValue.replace('\t', ' ');  //  \t  Tab
        tmpValue.replace('\n', '^');  //  \n  New line
        tmpValue.replace('\f', '^');  //  \f  Form feed (ascii code 0C)
        tmpValue.replace('\r', '^');  //  \r  Carriage return
        tmpValue.replace('\\', '^');  //  Backslash
        tmpValue.replace('"',  '\''); //  Double quote
        return wrap_String(tmpValue, '"');
      }
    }
    return wrap_String(value, '"');
  } 
  // It is a numerical
  return value;
}


/*********************************************************************************************\
   Strip wrapping chars (e.g. quotes)
\*********************************************************************************************/
String stripWrappingChar(const String& text, char wrappingChar) {
  const unsigned int length = text.length();

  if ((length >= 2) && stringWrappedWithChar(text, wrappingChar)) {
    return move_special(text.substring(1, length - 1));
  }
  return text;
}

bool stringWrappedWithChar(const String& text, char wrappingChar) {
  const unsigned int length = text.length();

  if (length < 2) { return false; }
  return (text.charAt(0) == wrappingChar) && 
         (text.charAt(length - 1) == wrappingChar);
}

bool isQuoteChar(char c) {
  return c == '\'' || c == '"' || c == '`';
}

bool findUnusedQuoteChar(const String& text, char& quotechar) {
  quotechar = '_';
  if (text.indexOf('\'') == -1) quotechar = '\'';
  else if (text.indexOf('"') == -1) quotechar = '"';
  else if (text.indexOf('`') == -1) quotechar = '`';
  
  return isQuoteChar(quotechar);
}

bool isParameterSeparatorChar(char c) {
  return c == ',' || c == ' ';
}

bool stringContainsSeparatorChar(const String& text) {
  return text.indexOf(',') != -1 || text.indexOf(' ') != -1;
}

bool isWrappedWithQuotes(const String& text) {
  if (text.length() < 2) {
    return false;
  }
  const char quoteChar = text[0];
  return isQuoteChar(quoteChar) && stringWrappedWithChar(text, quoteChar);
}

String stripQuotes(const String& text) {
  if (text.length() >= 2) {
    char c = text.charAt(0);

    if (isQuoteChar(c)) {
      return stripWrappingChar(text, c);
    }
  }
  return text;
}

bool safe_strncpy(char         *dest,
                  const __FlashStringHelper * source,
                  size_t        max_size) 
{
  return safe_strncpy(dest, String(source), max_size);
}

bool safe_strncpy(char *dest, const String& source, size_t max_size) {
  return safe_strncpy(dest, source.c_str(), max_size);
}

bool safe_strncpy(char *dest, const char *source, size_t max_size) {
  if (max_size < 1) { return false; }

  if (dest == nullptr) { return false; }

  if (source == nullptr) { return false; }
  bool result = true;

  memset(dest, 0, max_size);
  size_t str_length = strlen_P(source);

  if (str_length >= max_size) {
    str_length = max_size;
    result     = false;
  }
  strncpy_P(dest, source, str_length);
  dest[max_size - 1] = 0;
  return result;
}

// Convert a string to lower case and replace spaces with underscores.
String to_internal_string(const String& input, char replaceSpace) {
  // Do not set to 2nd heap as it is only used temporarily so prefer speed over mem usage
  String result = input;

  result.trim();
  result.toLowerCase();
  result.replace(' ', replaceSpace);
  return result;
}

/*********************************************************************************************\
   Parse a string and get the xth command or parameter
   IndexFind = 1 => command.
    // FIXME TD-er: parseString* should use index starting at 0.
\*********************************************************************************************/
String parseString(const char * string, uint8_t indexFind, char separator, bool trimResult) {
  return parseString(String(string), indexFind, separator, trimResult);
}

String parseString(const String& string, uint8_t indexFind, char separator, bool trimResult) {
  String result = parseStringKeepCase(string, indexFind, separator, trimResult);

  result.toLowerCase();
  return result;
}

String parseStringKeepCaseNoTrim(const String& string, uint8_t indexFind, char separator) {
  return parseStringKeepCase(string, indexFind, separator, false);
}

String parseStringKeepCase(const String& string, uint8_t indexFind, char separator, bool trimResult) {
  String result;

  if (!GetArgv(string.c_str(), result, indexFind, separator)) {
    return String();
  }
  if (trimResult) {
    result.trim();
  }
  result = stripQuotes(result);
  return result;
}

String parseStringToEnd(const String& string, uint8_t indexFind, char separator, bool trimResult) {
  String result = parseStringToEndKeepCase(string, indexFind, separator, trimResult);

  result.toLowerCase();
  return result;
}

String parseStringToEndKeepCaseNoTrim(const String& string, uint8_t indexFind, char separator) {
  return parseStringToEndKeepCase(string, indexFind, separator, false);
}

String parseStringToEndKeepCase(const String& string, uint8_t indexFind, char separator, bool trimResult) {
  // Loop over the arguments to find the first and last pos of the arguments.
  int  pos_begin = string.length();
  int  pos_end = pos_begin;
  int  tmppos_begin, tmppos_end = -1;
  uint8_t nextArgument = indexFind;
  bool hasArgument  = false;

  while (GetArgvBeginEnd(string.c_str(), nextArgument, tmppos_begin, tmppos_end, separator))
  {
    hasArgument = true;

    if ((tmppos_begin < pos_begin) && (tmppos_begin >= 0)) {
      pos_begin = tmppos_begin;
    }

    if ((tmppos_end >= 0)) {
      pos_end = tmppos_end;
    }
    ++nextArgument;
  }

  if (!hasArgument || (pos_begin < 0) || (pos_begin == pos_end)) {
    return EMPTY_STRING;
  }

  String result;
  move_special(result, string.substring(pos_begin, pos_end));

  if (trimResult) {
    result.trim();
  }
  return stripQuotes(result);
}

String tolerantParseStringKeepCase(const char * string,
                                   uint8_t      indexFind,
                                   char         separator,
                                   bool         trimResult)
{
  return tolerantParseStringKeepCase(String(string), indexFind, separator, trimResult);
}


String tolerantParseStringKeepCase(const String& string, uint8_t indexFind, char separator, bool trimResult)
{
  if (Settings.TolerantLastArgParse()) {
    return parseStringToEndKeepCase(string, indexFind, separator, trimResult);
  }
  return parseStringKeepCase(string, indexFind, separator, trimResult);
}

/*****************************************************************************
 * handles: 0xXX,text,0xXX," more text ",0xXX starting from index 2 (1-based)
 ****************************************************************************/
String parseHexTextString(const String& argument, int index) {
  String result;
  result.reserve(argument.length()); // longer than needed, most likely

  // Ignore these characters when used as hex-byte separators (0x01ab 23-cd:45 -> 0x01,0xab,0x23,0xcd,0x45)
  const String skipChars = F(" -:,.;");
  int i      = index;
  String arg = parseStringKeepCase(argument, i, ',', false);

  while (!arg.isEmpty()) {
    if ((arg.startsWith(F("0x")) || arg.startsWith(F("0X")))) {
      size_t j = 2;

      while (j < arg.length()) {
        int32_t hex = -1;

        if (validIntFromString(concat(F("0x"), arg.substring(j, j + 2)), hex) && (hex > 0) && (hex < 256)) {
          result += char(hex);
        }
        j += 2;
        int c = skipChars.indexOf(arg.substring(j, j + 1));

        while (j < arg.length() && c > -1) {
          j++;
          c = skipChars.indexOf(arg.substring(j, j + 1));
        }
      }
    } else {
      result += arg;
    }
    i++;
    arg = parseStringKeepCase(argument, i, ',', false);
  }

  return result;
}

/*****************************************************************************
 * handles: 0xXX,text,0xXX," more text ",0xXX starting from index 2 (1-based)
 ****************************************************************************/
std::vector<uint8_t> parseHexTextData(const String& argument, int index) {
  std::vector<uint8_t> result;

  // Ignore these characters when used as hex-byte separators (0x01ab 23-cd:45 -> 0x01,0xab,0x23,0xcd,0x45)
  const String skipChars = F(" -:,.;");

  result.reserve(argument.length() / 2); // longer than needed, most likely

  int i      = index;
  String arg = parseStringKeepCase(argument, i, ',', false);

  while (!arg.isEmpty()) {
    if ((arg.startsWith(F("0x")) || arg.startsWith(F("0X")))) {
      size_t j = 2;

      while (j < arg.length()) {
        int32_t hex = -1;

        if (validIntFromString(concat(F("0x"), arg.substring(j, j + 2)), hex) && (hex > -1) && (hex < 256)) {
          result.push_back(char(hex));
        }
        j += 2;

        // Skip characters we need to ignore
        if ((j + 1 < arg.length()) && (skipChars.indexOf(arg[j + 1]) != -1)) {
          int c = -1;

          do {
            ++j;
            c = (j < arg.length()) ? skipChars.indexOf(arg[j]) : -1;
          } while (c > -1);
        }
      }
    } else {
      for (size_t s = 0; s < arg.length(); s++) {
        result.push_back(arg[s]);
      }
    }
    i++;
    arg = parseStringKeepCase(argument, i, ',', false);
  }

  return result;
}

/*********************************************************************************************\
   GetTextIndexed: Get text from large PROGMEM stored string
   Items are separated by a '|'
   Code (c) Tasmota:
   https://github.com/arendst/Tasmota/blob/293ae8064d753e6d38488b46d21cdc52a4a6e637/tasmota/tasmota_support/support.ino#L937
\*********************************************************************************************/
char* GetTextIndexed(char* destination, size_t destination_size, uint32_t index, const char* haystack)
{
  // Returns empty string if not found
  // Returns text of found
  char* write = destination;

  if (haystack != nullptr) {
    const char* read = haystack;
    index++;
    while (index--) {
      size_t size = destination_size -1;
      write = destination;
      char ch = '.';
      while ((ch != '\0') && (ch != '|')) {
        ch = pgm_read_byte(read++);
        if (size && (ch != '|'))  {
          *write++ = ch;
          size--;
        }
      }
      if (0 == ch) {
        if (index) {
          write = destination;
        }
        break;
      }
    }
  }
  *write = '\0';
  return destination;
}

/*********************************************************************************************\
   GetCommandCode: Find string in large PROGMEM stored string
   Items are separated by a '|'
   Code (c) Tasmota:
   https://github.com/arendst/Tasmota/blob/293ae8064d753e6d38488b46d21cdc52a4a6e637/tasmota/tasmota_support/support.ino#L967
\*********************************************************************************************/
int GetCommandCode(char* destination, size_t destination_size, const char* needle, const char* haystack)
{
  // Returns -1 of not found
  // Returns index and command if found
  int result = -1;
  if (haystack == nullptr) 
    return result;
  const char* read = haystack;
  char* write = destination;

  while (true) {
    result++;
    size_t size = destination_size -1;
    write = destination;
    char ch = '.';
    while ((ch != '\0') && (ch != '|')) {
      ch = pgm_read_byte(read++);
      if (size && (ch != '|'))  {
        *write++ = ch;
        size--;
      }
    }
    *write = '\0';
    if (!strcasecmp(needle, destination)) {
      break;
    }
    if (0 == ch) {
      result = -1;
      break;
    }
  }
  return result;
}

int GetCommandCode(const char* needle, const char* haystack)
{
  // Likely long enough to parse any command
  static char temp[32]{};
  temp[0] = '\0';
  return GetCommandCode(temp, sizeof(temp), needle, haystack);
}


// escapes special characters in strings for use in html-forms
bool htmlEscapeChar(char c, String& esc)
{
  const __FlashStringHelper * escaped = F("");
  switch (c)
  {
    case '&':  escaped = F("&amp;");  break;
    case '\"': escaped = F("&quot;"); break;
    case '\'': escaped = F("&#039;"); break;
    case '<':  escaped = F("&lt;");   break;
    case '>':  escaped = F("&gt;");   break;
    case '/':  escaped = F("&#047;"); break;
    default:
      return false;
  }

  esc = String(escaped);  
  return true;
}

void htmlEscape(String& html, char c)
{
  String repl;

  if (htmlEscapeChar(c, repl)) {
    html.replace(String(c), repl);
  }
}

void htmlEscape(String& html)
{
  htmlEscape(html, '&');
  htmlEscape(html, '\"');
  htmlEscape(html, '\'');
  htmlEscape(html, '<');
  htmlEscape(html, '>');
  htmlEscape(html, '/');
}

void htmlStrongEscape(String& html)
{
  String escaped;

  escaped.reserve(html.length());

  for (unsigned i = 0; i < html.length(); ++i)
  {
    if (isAlphaNumeric(html[i]))
    {
      escaped += html[i];
    }
    else
    {
      escaped += strformat(F("&#%03d;"), static_cast<int>(html[i]));
    }
  }
  html = escaped;
}

// ********************************************************************************
// URNEncode char string to string object
// ********************************************************************************
String URLEncode(const String& msg)
{
  // Only used for temporary strings, so keep on default heap for speed
  #ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  #endif
  String encodedMsg;

  const size_t msg_length = msg.length();

  encodedMsg.reserve(msg_length);

  for (size_t i = 0; i < msg_length; ++i) {
    const char ch = msg[i];
    if (isAlphaNumeric(ch)
        || ('-' == ch) || ('_' == ch)
        || ('.' == ch) || ('~' == ch)) {
      encodedMsg += ch;
    } else {
      encodedMsg += '%';
      appendHexChar(ch, encodedMsg);
    }
  }
  return encodedMsg;
}

bool repl(const __FlashStringHelper * key,
            const String& val,
            String      & s,
            bool       useURLencode)
{
  const char c = pgm_read_byte(key);
  if (s.indexOf(c) != -1) 
    return repl(String(key), val, s, useURLencode);
  return false;
}

bool repl(const __FlashStringHelper * key,
          const char* val,
          String      & s,
          bool       useURLencode)
{
  const char c = pgm_read_byte(key);
  if (s.indexOf(c) != -1) 
    return repl(String(key), String(val), s, useURLencode);
  return false;
}

bool repl(const __FlashStringHelper * key1,
           const __FlashStringHelper * key2,
           const char* val,
           String      & s,
           bool       useURLencode)
{
  bool somethingReplaced = false;
  if (repl(key1, val, s, useURLencode)) somethingReplaced = true;
  if (repl(key2, val, s, useURLencode)) somethingReplaced = true;
  return somethingReplaced;
}


bool repl(const String& key, const String& val, String& s, bool useURLencode)
{
  if (s.indexOf(key) == -1) { return false; }
  if (useURLencode) {
    s.replace(key, URLEncode(val));
  } else {
    s.replace(key, val);
  }
  return true;
}

void parseSpecialCharacters(String& s, bool useURLencode)
{
  const bool no_accolades   = s.indexOf('{') == -1 || s.indexOf('}') == -1;
  const bool no_html_entity = s.indexOf('&') == -1 || s.indexOf(';') == -1;

  if (no_accolades && no_html_entity) {
    return; // Nothing to replace
  }
  {
    // Degree
    const char degree[3]   = { 0xc2, 0xb0, 0 };       // Unicode degree symbol
    const char degreeC[4]  = { 0xe2, 0x84, 0x83, 0 }; // Unicode degreeC symbol
    const char degree_C[4] = { 0xc2, 0xb0, 'C', 0 };  // Unicode degree symbol + captial C
    if (!no_accolades)   repl(F("{D}"),   degree,   s, useURLencode);
    if (!no_html_entity) repl(F("&deg;"), degree,   s, useURLencode);
    repl(degreeC,    degree_C, s, useURLencode);
  }
  // Degree symbol is often used on displays, so still support that one.
#ifndef BUILD_NO_SPECIAL_CHARACTERS_STRINGCONVERTER
  {
    // Angle quotes
    const char laquo[3] = { 0xc2, 0xab, 0 }; // Unicode left angle quotes symbol
    const char raquo[3] = { 0xc2, 0xbb, 0 }; // Unicode right angle quotes symbol
    repl(F("{<<}"), F("&laquo;"), laquo, s, useURLencode);
    repl(F("{>>}"), F("&raquo;"), raquo, s, useURLencode);
  }
  {
    // Greek letter Mu
    const char mu[3] = { 0xc2, 0xb5, 0 }; // Unicode greek letter mu
    repl(F("{u}"), F("&micro;"), mu, s, useURLencode);
  }
  {
    // Currency
    const char euro[4]  = { 0xe2, 0x82, 0xac, 0 }; // Unicode euro symbol
    const char yen[3]   = { 0xc2, 0xa5, 0 };       // Unicode yen symbol
    const char pound[3] = { 0xc2, 0xa3, 0 };       // Unicode pound symbol
    const char cent[3]  = { 0xc2, 0xa2, 0 };       // Unicode cent symbol
    repl(F("{E}"), F("&euro;"),  euro,  s, useURLencode);
    repl(F("{Y}"), F("&yen;"),   yen,   s, useURLencode);
    repl(F("{P}"), F("&pound;"), pound, s, useURLencode);
    repl(F("{c}"), F("&cent;"),  cent,  s, useURLencode);
  }
  {
    // Math symbols
    const char sup1[3]   = { 0xc2, 0xb9, 0 }; // Unicode sup1 symbol
    const char sup2[3]   = { 0xc2, 0xb2, 0 }; // Unicode sup2 symbol
    const char sup3[3]   = { 0xc2, 0xb3, 0 }; // Unicode sup3 symbol
    const char frac14[3] = { 0xc2, 0xbc, 0 }; // Unicode frac14 symbol
    const char frac12[3] = { 0xc2, 0xbd, 0 }; // Unicode frac12 symbol
    const char frac34[3] = { 0xc2, 0xbe, 0 }; // Unicode frac34 symbol
    const char plusmn[3] = { 0xc2, 0xb1, 0 }; // Unicode plusmn symbol
    const char times[3]  = { 0xc3, 0x97, 0 }; // Unicode times symbol
    const char divide[3] = { 0xc3, 0xb7, 0 }; // Unicode divide symbol
    repl(F("{^1}"),  F("&sup1;"),   sup1,   s, useURLencode);
    repl(F("{^2}"),  F("&sup2;"),   sup2,   s, useURLencode);
    repl(F("{^3}"),  F("&sup3;"),   sup3,   s, useURLencode);
    repl(F("{1_4}"), F("&frac14;"), frac14, s, useURLencode);
    repl(F("{1_2}"), F("&frac12;"), frac12, s, useURLencode);
    repl(F("{3_4}"), F("&frac34;"), frac34, s, useURLencode);
    repl(F("{+-}"),  F("&plusmn;"), plusmn, s, useURLencode);
    repl(F("{x}"),   F("&times;"),  times,  s, useURLencode);
    repl(F("{..}"),  F("&divide;"), divide, s, useURLencode);
  }
#endif // ifndef BUILD_NO_SPECIAL_CHARACTERS_STRINGCONVERTER
}


/********************************************************************************************\
   replace other system variables like %sysname%, %systime%, %ip%
 \*********************************************************************************************/
void parseControllerVariables(String& s, struct EventStruct *event, bool useURLencode) {
  parseEventVariables(s, event, useURLencode);
  s = parseTemplate(s, useURLencode);
}

// FIXME TD-er: These macros really increase build size.
// Simple macro to create the replacement string only when needed.
#define SMART_REPL(T, S) \
  if (s.indexOf(T) != -1) { repl((T), (S), s, useURLencode); }

void parseSingleControllerVariable(String            & s,
                                   struct EventStruct* event,
                                   uint8_t             taskValueIndex,
                                   bool                useURLencode) {
  SMART_REPL(F("%valname%"), getTaskValueName(event->TaskIndex, taskValueIndex));
}

#if FEATURE_MQTT_DISCOVER
void parseDeviceClassVariable(String                   & s,
                              const __FlashStringHelper* devclass,
                              bool                       useURLencode) {
  SMART_REPL(F("%devclass%"), devclass);
}

void parseUniqueIdVariable(String      & s,
                           const String& uniqueId,
                           bool          useURLencode) {
  SMART_REPL(F("%unique_id%"), uniqueId);
}

void parseElementIdVariable(String     & s,
                           const String& elementId,
                           bool          useURLencode) {
  SMART_REPL(F("%element_id%"), elementId);
}
#endif

#if FEATURE_STRING_VARIABLES
void parseValNameVariable(String      & s,
                          const String& valname,
                          bool          useURLencode) {
  SMART_REPL(F("%valname%"), valname);
}
#endif // if FEATURE_STRING_VARIABLES

void parseSystemVariables(String& s, bool useURLencode)
{
  String MaskEscapedPercent;
  bool mustReplaceEscapedPercent = hasEscapedCharacter(s, '%');

  if (mustReplaceEscapedPercent) {
    MaskEscapedPercent = static_cast<char>(0x04); // ASCII 0x04 = End of transmit
    s.replace(F("\\%"), MaskEscapedPercent);
  }

  parseSpecialCharacters(s, useURLencode);

  SystemVariables::parseSystemVariables(s, useURLencode);

  if (mustReplaceEscapedPercent) {
    s.replace(MaskEscapedPercent, F("\\%"));
  }
}

void parseEventVariables(String& s, struct EventStruct *event, bool useURLencode)
{
  if (s.indexOf('%') == -1) {
    return;
  }
  repl(F("%id%"), String(event->idx), s, useURLencode);

  const bool val_found = s.indexOf(F("%val")) != -1;
  const bool vname_found = s.indexOf(F("%vname")) != -1;

  #if FEATURE_STRING_VARIABLES
  std::map<uint8_t, String> strVarNames;
  std::map<uint8_t, String> strVarValues;

  if (Settings.SendDerivedTaskValues(event->TaskIndex, event->ControllerIndex)) {
    String taskName = getTaskDeviceName(event->TaskIndex);
    taskName.toLowerCase();
    String postfix;
    const String search = getDerivedValueSearchAndPostfix(taskName, postfix);

    auto it = customStringVar.begin();
    uint8_t varNr = VARS_PER_TASK; // %val5% and %vname5% and incrementing the number
    while (it != customStringVar.end()) {
      if (it->first.startsWith(search) && it->first.endsWith(postfix)) {
        String valueName = it->first.substring(search.length(), it->first.indexOf('-'));
        const String vname2 = getDerivedValueName(taskName, valueName);
        if (!vname2.isEmpty()) {
          valueName = vname2;
        }
        if (!it->second.isEmpty()) {
          String value(it->second);
          value = parseTemplateAndCalculate(value);
          strVarNames.insert(std::pair<uint8_t, String>(varNr, valueName));
          strVarValues.insert(std::pair<uint8_t, String>(varNr, value));
          ++varNr; // increment after to keep the values & code below consistent
        }
      }
      else if (it->first.substring(0, search.length()).compareTo(search) > 0) {
        break;
      }
      ++it;
    }

  }
  #endif // if FEATURE_STRING_VARIABLES

  if (validTaskIndex(event->TaskIndex)) {
    if (val_found) {
      const uint8_t valueCount = (event->getSensorType() == Sensor_VType::SENSOR_TYPE_ULONG) ? 1 : getValueCountForTask(event->TaskIndex);
      for (uint8_t i = 0; i < valueCount; ++i) {
        String valstr = F("%val");
        valstr += (i + 1);
        valstr += '%';
        SMART_REPL(valstr, formatUserVarNoCheck(event, i));
      }

      #if FEATURE_STRING_VARIABLES
      auto it = strVarValues.begin();
      while (it != strVarValues.end()) {
        String valstr = F("%val");
        valstr += (it->first + 1);
        valstr += '%';
        SMART_REPL(valstr, it->second);
        ++it;
      }
      #endif // if FEATURE_STRING_VARIABLES
    }
  }

  SMART_REPL(F("%tskname%"), getTaskDeviceName(event->TaskIndex));

  if (vname_found) {
    const uint8_t valueCount = getValueCountForTask(event->TaskIndex);
    for (uint8_t i = 0; i < valueCount; ++i) {
      String vname = F("%vname");
      vname += (i + 1);
      vname += '%';

      SMART_REPL(vname, Cache.getTaskDeviceValueName(event->TaskIndex, i));
    }
    #if FEATURE_STRING_VARIABLES
    auto it = strVarNames.begin();
    while (it != strVarNames.end()) {
      String vname = F("%vname");
      vname += (it->first + 1);
      vname += '%';

      SMART_REPL(vname, it->second);
      ++it;
    }
    #endif // if FEATURE_STRING_VARIABLES
  }
}

#undef SMART_REPL

bool getConvertArgument(const __FlashStringHelper * marker, const String& s, float& argument, int& startIndex, int& endIndex) {
  String argumentString;

  if (getConvertArgumentString(marker, s, argumentString, startIndex, endIndex)) {
    return validFloatFromString(argumentString, argument);
  }
  return false;
}

bool getConvertArgument2(const __FlashStringHelper * marker, const String& s, float& arg1, float& arg2, int& startIndex, int& endIndex) {
  String argumentString;

  if (getConvertArgumentString(marker, s, argumentString, startIndex, endIndex)) {
    const int pos_comma = argumentString.indexOf(',');

    if (pos_comma == -1) { 
      return validFloatFromString(argumentString, arg1); // Accept single argument, with second argument 0
    }

    if (validFloatFromString(argumentString.substring(0, pos_comma), arg1)) {
      return validFloatFromString(argumentString.substring(pos_comma + 1), arg2);
    }
  }
  return false;
}

#if FEATURE_STRING_VARIABLES
bool getConvertArgumentStr(const __FlashStringHelper * marker, const String& s, String& argument, int& startIndex, int& endIndex) {
  return getConvertArgumentString(marker, s, argument, startIndex, endIndex);
}

#define _MAX_STRFORMAT_ARGUMENTS 2 // Max. 2 numeric arguments for now to avoid strformat() from crashing when reading the 3rd argument that's not there
bool getConvertArgumentStrFormat(const __FlashStringHelper * marker, const String& s, String& argStr, float& arg1, float& arg2, int& startIndex, int& endIndex) {
  String argumentString;

  if (getConvertArgumentString(marker, s, argumentString, startIndex, endIndex)) {
    const int pos_comma = argumentString.indexOf(',');
    
    if (pos_comma == -1) { return false; } // At least 2 arguments required
    
    argStr = getCustomStringVar(argumentString.substring(0, pos_comma)); // Get String variable content

    if (argStr.isEmpty()) { // Not found or invalid variable name: probably provided a formatstring (not quoted, no commas!)
      argStr = argumentString.substring(0, pos_comma);
    }

    int pos_dollar = argStr.indexOf('$');
    int dollarCount{};
    while (pos_dollar != -1) {
      ++dollarCount;
      ++pos_dollar;
      pos_dollar = argStr.indexOf('$', pos_dollar);
    }

    // We support, 0, 1 or 2, $ formatting characters to avoid crashes and confusion with shorthand variables
    if (dollarCount > _MAX_STRFORMAT_ARGUMENTS) { return false; }

    argStr.replace('$', '%'); // Change to regular strformat() format specifiers

    const int pos_comma2 = argumentString.indexOf(',', pos_comma + 1);

    if (pos_comma2 == -1) {
      return validFloatFromString(argumentString.substring(pos_comma + 1), arg1);
    }

    return validFloatFromString(argumentString.substring(pos_comma + 1, pos_comma2), arg1)
           && validFloatFromString(argumentString.substring(pos_comma2 + 1), arg2);
  }
  return false;
}
#undef _MAX_STRFORMAT_ARGUMENTS
#endif // if FEATURE_STRING_VARIABLES

bool getConvertArgumentString(const __FlashStringHelper * marker, const String& s, String& argumentString, int& startIndex, int& endIndex) {
  return getConvertArgumentString(String(marker), s, argumentString, startIndex, endIndex);
}

bool getConvertArgumentString(const String& marker,
                              const String& s,
                              String      & argumentString,
                              int         & startIndex,
                              int         & endIndex) {


  startIndex = s.indexOf(marker);

  if (startIndex == -1) { return false; }

  int startIndexArgument = startIndex + marker.length();

  if (s.charAt(startIndexArgument) != '(') {
    return false;
  }
  ++startIndexArgument;
  endIndex = s.indexOf(')', startIndexArgument);

  if (endIndex == -1) { return false; }

  argumentString = s.substring(startIndexArgument, endIndex);

  if (argumentString.isEmpty()) { return false; }
  ++endIndex; // Must also strip ')' from the original string.
  return true;
}


// FIXME TD-er: These macros really increase build size
struct ConvertArgumentData {
  ConvertArgumentData(String& s, bool useURLencode) 
    : str(s), arg1(0.0f), arg2(0.0f), startIndex(0), endIndex(0),
      URLencode(useURLencode) {}

  ConvertArgumentData() = delete;

  String& str;
  String str1;
  float arg1, arg2;
  int   startIndex;
  int   endIndex;
  bool  URLencode;
};

bool repl(ConvertArgumentData& data, const String& repl_str) {
  return repl(data.str.substring(data.startIndex, data.endIndex), repl_str, data.str, data.URLencode);
}

bool getConvertArgument(const __FlashStringHelper * marker, ConvertArgumentData& data) {
  return getConvertArgument(marker, data.str, data.arg1, data.startIndex, data.endIndex);
}

bool getConvertArgument2(const __FlashStringHelper * marker, ConvertArgumentData& data) {
  return getConvertArgument2(marker, data.str, data.arg1, data.arg2, data.startIndex, data.endIndex);
}

bool getConvertArgumentStr(const __FlashStringHelper * marker, ConvertArgumentData& data) {
  return getConvertArgumentStr(marker, data.str, data.str1, data.startIndex, data.endIndex);
}

#if FEATURE_STRING_VARIABLES
bool getConvertArgumentStrFormat(const __FlashStringHelper * marker, ConvertArgumentData& data) {
  return getConvertArgumentStrFormat(marker, data.str, data.str1, data.arg1, data.arg2, data.startIndex, data.endIndex);
}
#endif // if FEATURE_STRING_VARIABLES

// Parse conversions marked with "%conv_marker%(float)"
// Must be called last, since all sensor values must be converted, processed, etc.
void parseStandardConversions(String& s, bool useURLencode) {
  if (s.indexOf(F("%c_")) == -1) {
    return; // Nothing to replace
  }

  ConvertArgumentData data(s, useURLencode);

  // These replacements should be done in a while loop per marker,
  // since they also replace the numerical parameter.
  // The marker may occur more than once per string, but with different parameters.
  #define SMART_CONV(T, FUN) \
  while (getConvertArgument((T), data)) { repl(data, (FUN)); }
  SMART_CONV(F("%c_w_dir%"),  getBearing(data.arg1))
  SMART_CONV(F("%c_c2f%"),    toString(CelsiusToFahrenheit(data.arg1), 2))
  SMART_CONV(F("%c_ms2Bft%"), String(m_secToBeaufort(data.arg1)))
  SMART_CONV(F("%c_cm2imp%"), centimeterToImperialLength(data.arg1))
  SMART_CONV(F("%c_mm2imp%"), millimeterToImperialLength(data.arg1))
  SMART_CONV(F("%c_m2day%"),  toString(minutesToDay(data.arg1), 2))
  SMART_CONV(F("%c_m2dh%"),   minutesToDayHour(data.arg1))
  SMART_CONV(F("%c_m2dhm%"),  minutesToDayHourMinute(data.arg1))
  SMART_CONV(F("%c_m2hcm%"),  minutesToHourColonMinute(data.arg1))
  SMART_CONV(F("%c_s2dhms%"), secondsToDayHourMinuteSecond(data.arg1))
  #if FEATURE_ESPEASY_P2P
  SMART_CONV(F("%c_uname%"),  getNameForUnit(data.arg1))
  SMART_CONV(F("%c_uage%"),   String(static_cast<int32_t>(getAgeForUnit(data.arg1) / 1000)))
  SMART_CONV(F("%c_ubuild%"), String(getBuildnrForUnit(data.arg1)))
  SMART_CONV(F("%c_ubuildstr%"), formatSystemBuildNr(getBuildnrForUnit(data.arg1)))
  SMART_CONV(F("%c_uload%"),  toString(getLoadForUnit(data.arg1)))
  SMART_CONV(F("%c_utype%"),  String(getTypeForUnit(data.arg1)))
  SMART_CONV(F("%c_utypestr%"), getTypeStringForUnit(data.arg1))
  #endif // if FEATURE_ESPEASY_P2P
  #if FEATURE_STRING_VARIABLES
  SMART_CONV(F("%c_ts2wday%"),  get_weekday_from_timestamp(static_cast<uint32_t>(data.arg1)))
  #endif // if FEATURE_STRING_VARIABLES
  #ifndef LIMIT_BUILD_SIZE
  SMART_CONV(F("%c_d2r%"),    doubleToString(radians(data.arg1)))
  SMART_CONV(F("%c_r2d%"),    doubleToString(degrees(data.arg1)))
  #endif // ifndef LIMIT_BUILD_SIZE
  #undef SMART_CONV

  // Conversions with 2 parameters
  #define SMART_CONV(T, FUN) \
  while (getConvertArgument2((T), data)) { repl(data, (FUN)); }
  SMART_CONV(F("%c_dew_th%"), toString(compute_dew_point_temp(data.arg1, data.arg2), 2))
  SMART_CONV(F("%c_2hex%"),   formatToHex_no_prefix(data.arg1, data.arg2))
  #if FEATURE_ESPEASY_P2P
  SMART_CONV(F("%c_u2ip%"),   formatUnitToIPAddress(data.arg1, data.arg2))
  #endif
  SMART_CONV(F("%c_alt_pres_sea%"), toString(altitudeFromPressure(data.arg1, data.arg2), 2))
  SMART_CONV(F("%c_sea_pres_alt%"), toString(pressureElevation(data.arg1, data.arg2), 2))
  #if FEATURE_STRING_VARIABLES
  SMART_CONV(F("%c_ts2date%"),      get_date_time_from_timestamp(static_cast<uint32_t>(data.arg1), !essentiallyZero(data.arg2), false))
  SMART_CONV(F("%c_ts2isodate%"),   get_date_time_from_timestamp(static_cast<uint32_t>(data.arg1), !essentiallyZero(data.arg2), true))
  #endif // if FEATURE_STRING_VARIABLES

  #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
  SMART_CONV(F("%c_random%"), doubleToString(HwRandom_f(data.arg1, data.arg2), 3, true))
  #else
  SMART_CONV(F("%c_random%"), floatToString(HwRandom_f(data.arg1, data.arg2), 3, true))
  #endif
  #undef SMART_CONV
  
  #if FEATURE_STRING_VARIABLES
  double tmp{};
  #define SMART_CONV(T, FUN) \
  while (getConvertArgumentStr((T), data)) { repl(data, (FUN)); }
  SMART_CONV(F("%c_isnum%"),  String(validDoubleFromString(getCustomStringVar(data.str1), tmp) ? 1 : 0))
  #undef SMART_CONV
  // 1 string and 1 or 2 numeric arguments
  #define SMART_CONV(T, FUN) \
  while (getConvertArgumentStrFormat((T), data)) { repl(data, (FUN)); }
  SMART_CONV(F("%c_strf%"),   strformat(data.str1, data.arg1, data.arg2))
  #undef SMART_CONV
  #endif // if FEATURE_STRING_VARIABLES
}

#if FEATURE_STRING_VARIABLES
String get_date_time_from_timestamp(time_t unix_timestamp, bool am_pm, bool iso_format) {
  struct tm ts;
  ts = *localtime(&unix_timestamp);

  return formatDateTimeString(ts, '-', ':', iso_format ? 'T' : ' ', am_pm && !iso_format)
          + (iso_format && am_pm ? node_time.getTimeZoneOffsetString() : F("Z"));
}

String get_weekday_from_timestamp(time_t unix_timestamp) {
  struct tm ts;
  ts = *localtime(&unix_timestamp);

  return String(ts.tm_wday);
}
#endif // if FEATURE_STRING_VARIABLES

/********************************************************************************************\
   Find positional parameter in a char string
 \*********************************************************************************************/
bool HasArgv(const char *string, unsigned int argc) {
  String argvString;

  return GetArgv(string, argvString, argc);
}

bool GetArgv(const char *string, String& argvString, unsigned int argc, char separator) {
  int  pos_begin, pos_end;
  bool hasArgument = GetArgvBeginEnd(string, argc, pos_begin, pos_end, separator);

  free_string(argvString);

  if (!hasArgument) { return false; }

  if ((pos_begin >= 0) && (pos_end >= 0) && (pos_end > pos_begin)) {
    argvString.reserve(pos_end - pos_begin);
    argvString.concat(string + pos_begin, pos_end - pos_begin);
    argvString.trim();
    argvString = stripQuotes(argvString);
  }
  return true;
}

bool GetArgvBeginEnd(const char *string, const unsigned int argc, int& pos_begin, int& pos_end, char separator) {
  pos_begin = -1;
  pos_end   = -1;
  if (string == nullptr) {
    return false;
  }
  size_t string_len = strlen(string);
  unsigned int string_pos = 0, argc_pos = 0;
  bool parenthesis          = false;
  char matching_parenthesis = '"';

  while (string_pos < string_len)
  {
    char c, d, e; // c = current char, d,e = next char (if available)
    c = string[string_pos];
    d = 0;
    e = 0;

    if ((string_pos + 1) < string_len) {
      d = string[string_pos + 1];
    }
    if ((string_pos + 2) < string_len) {
      e = string[string_pos + 2];
    }

    if  (!parenthesis && (((c == ' ') && (d == ' ')) || 
                          ((c == separator) && (d == ' ')))) {
      // Consider multiple consecutive spaces as one.
    }
    else if  (!parenthesis && ((d == ' ') && (e == separator))) {
      // Skip the space.      
    }
    else
    {
      // Found the start of the new argument.
      if (pos_begin == -1 && !parenthesis && !((c == separator) || isParameterSeparatorChar(c))) {
        pos_begin = string_pos;
        pos_end   = string_pos;
      }
      if (pos_end != -1) {
        ++pos_end;
      }

      // Check if we're in a set of parenthesis (any quote char or [])
      if (!parenthesis && (isQuoteChar(c) || (c == '['))) {
        parenthesis          = true;
        matching_parenthesis = c;

        if (c == '[') {
          matching_parenthesis = ']';
        }
      } else if (parenthesis && (c == matching_parenthesis)) {
        parenthesis = false;
      }

      if (!parenthesis && (isParameterSeparatorChar(d) || (d == separator) || (d == 0))) // end of word
      {
        argc_pos++;
        if (argc_pos == argc)
        {
          return true;
        }
        // new Argument separator found
        pos_begin = -1;
        pos_end   = -1;
      }
    }
    string_pos++;
  }
  return false;
}

#include "../Helpers/Scheduler.h"

#include "../../ESPEasy-Globals.h"
#include "../../_Plugin_Helper.h"

#include "../DataStructs/Scheduler_IntendedRebootTimerID.h"
#include "../DataStructs/TimingStats.h"

#include "../ESPEasyCore/ESPEasyRules.h"

#include "../Globals/RTC.h"

#include "../Helpers/ESPEasyRTC.h"


void ESPEasy_Scheduler::markIntendedReboot(IntendedRebootReason_e reason) {
  const IntendedRebootTimerID id(reason);

  RTC.lastMixedSchedulerId = id.mixed_id;
  saveToRTC();
}

/*********************************************************************************************\
* Generic Timer functions.
\*********************************************************************************************/
void ESPEasy_Scheduler::setNewTimerAt(SchedulerTimerID id, unsigned long timer) {
  START_TIMER;
  msecTimerHandler.registerAt(id.mixed_id, timer);
  STOP_TIMER(SET_NEW_TIMER);
}

/*********************************************************************************************\
* Handle scheduled timers.
\*********************************************************************************************/
void ESPEasy_Scheduler::handle_schedule() {
  START_TIMER
  unsigned long timer    = 0;
  unsigned long mixed_id = 0;

  if (timePassedSince(last_system_event_run) < 500) {
    // Make sure system event queue will be looked at every now and then.
    mixed_id = msecTimerHandler.getNextId(timer);
  }

  if (RTC.lastMixedSchedulerId != mixed_id) {
    RTC.lastMixedSchedulerId = mixed_id;
    saveToRTC();
  }

  if (mixed_id == 0) {
    // No id ready to run right now.
    // Events are not that important to run immediately.
    // Make sure normal scheduled jobs run at higher priority.
    // backgroundtasks();
    process_system_event_queue();

    // System events may have added one or more rule events, try to process those
    processNextEvent();
    last_system_event_run = millis();
    STOP_TIMER(HANDLE_SCHEDULER_IDLE);
    return;
  }

  const SchedulerTimerID timerID(mixed_id);

  delay(0); // See: https://github.com/letscontrolit/ESPEasy/issues/1818#issuecomment-425351328

  switch (timerID.getTimerType()) {
    case SchedulerTimerType_e::ConstIntervalTimer:
      process_interval_timer(timerID, timer);
      break;
    case SchedulerTimerType_e::PLUGIN_TASKTIMER_IN_e:
      process_plugin_task_timer(timerID);
      break;
    case SchedulerTimerType_e::PLUGIN_DEVICETIMER_IN_e:
      process_plugin_timer(timerID);
      break;
    case SchedulerTimerType_e::RulesTimer:
      process_rules_timer(timerID, timer);
      break;
    case SchedulerTimerType_e::TaskDeviceTimer:
      process_task_device_timer(timerID, timer);
      break;
    case SchedulerTimerType_e::GPIO_timer:
      process_gpio_timer(timerID, timer);
      break;

    case SchedulerTimerType_e::SystemEventQueue:
    case SchedulerTimerType_e::IntendedReboot:
      // TD-er: Not really something that needs to be processed here.
      // - SystemEventQueue has its own ScheduledEventQueue which isn't time based.
      // - IntendedReboot is just used to mark the intended reboot reason in RTC.
      break;
  }
  STOP_TIMER(HANDLE_SCHEDULER_TASK);
}

String ESPEasy_Scheduler::getQueueStats() {
  return msecTimerHandler.getQueueStats();
}

void ESPEasy_Scheduler::updateIdleTimeStats() {
  msecTimerHandler.updateIdleTimeStats();
}

float ESPEasy_Scheduler::getIdleTimePct() const {
  return msecTimerHandler.getIdleTimePct();
}

void ESPEasy_Scheduler::setEcoMode(bool enabled) {
  msecTimerHandler.setEcoMode(enabled);
}

#include "../Helpers/_Plugin_Helper_webform.h"

#include "../Globals/GlobalMapPortStatus.h"

#include "../Helpers/PortStatus.h"

#include "../WebServer/Markup.h"
#include "../WebServer/Markup_Forms.h"


#define SWITCH_DOUBLECLICK_MIN_INTERVAL      1000
#define SWITCH_DOUBLECLICK_MAX_INTERVAL      3000
#define SWITCH_LONGPRESS_MIN_INTERVAL        500
#define SWITCH_LONGPRESS_MAX_INTERVAL        5000


void SwitchWebformLoad(
  const int16_t& bootState,
  const float  & debounce_ms,
  const int16_t& doubleClickEvent,
  float        & doubleClickMaxInterval,
  const int16_t& longPressEvent,
  float        & longPressMinInterval_ms,
  const float  & useSafeButton)
{
  addFormCheckBox(F("Send Boot state"), F("sw_boot"),
                  bootState);

  addFormSubHeader(F("Advanced event management"));

  addFormNumericBox(
    F("De-bounce (ms)"),
    F("sw_debounce"),
    static_cast<int>(debounce_ms),
    0, 250);

  // set minimum value for doubleclick MIN max speed
  if (doubleClickMaxInterval < SWITCH_DOUBLECLICK_MIN_INTERVAL) {
    doubleClickMaxInterval = SWITCH_DOUBLECLICK_MIN_INTERVAL;
  }

  {
    const __FlashStringHelper *buttonDC[] = {
      F("Disabled"),
      F("Active only on LOW (EVENT=3)"),
      F("Active only on HIGH (EVENT=3)"),
      F("Active on LOW & HIGH (EVENT=3)")
    };
    const int buttonDCValues[] = {
      SWITCH_DC_DISABLED,
      SWITCH_DC_LOW,
      SWITCH_DC_HIGH,
      SWITCH_DC_BOTH };

    const FormSelectorOptions selector(NR_ELEMENTS(buttonDCValues),
      buttonDC,
      buttonDCValues);
    selector.addFormSelector(
      F("Doubleclick event"),
      F("sw_dc"),
      doubleClickEvent);
  }

  addFormNumericBox(F("Doubleclick max. interval (ms)"),
                    F("sw_dcmaxinterval"),
                    lround(doubleClickMaxInterval),
                    SWITCH_DOUBLECLICK_MIN_INTERVAL,
                    SWITCH_DOUBLECLICK_MAX_INTERVAL);

  // set minimum value for longpress MIN max speed
  if (longPressMinInterval_ms < SWITCH_LONGPRESS_MIN_INTERVAL) {
    longPressMinInterval_ms = SWITCH_LONGPRESS_MIN_INTERVAL;
  }

  {
    const __FlashStringHelper *buttonLP[] = {
      F("Disabled"),
      F("Active only on LOW (EVENT= 10 [NORMAL] or 11 [INVERSED])"),
      F("Active only on HIGH (EVENT= 11 [NORMAL] or 10 [INVERSED])"),
      F("Active on LOW & HIGH (EVENT= 10 or 11)")
    };
    const int buttonLPValues[] = {
      SWITCH_LONGPRESS_DISABLED,
      SWITCH_LONGPRESS_LOW,
      SWITCH_LONGPRESS_HIGH,
      SWITCH_LONGPRESS_BOTH };

    const FormSelectorOptions selector(
      NR_ELEMENTS(buttonLPValues),
      buttonLP,
      buttonLPValues);
    selector.addFormSelector(
      F("Longpress event"),
      F("sw_lp"),
      longPressEvent);
  }

  addFormNumericBox(F("Longpress min. interval (ms)"),
                    F("sw_lpmininterval"),
                    lround(longPressMinInterval_ms),
                    SWITCH_LONGPRESS_MIN_INTERVAL,
                    SWITCH_LONGPRESS_MAX_INTERVAL);

  addFormCheckBox(F("Use Safe Button (slower)"), F("sw_sb"), lround(useSafeButton));

  // TO-DO: add Extra-Long Press event
  // addFormCheckBox(F("Extra-Longpress event (20 & 21)"), F("sw_elp"), PCONFIG_LONG(1));
  // addFormNumericBox(F("Extra-Longpress min. interval (ms)"), F("sw_elpmininterval"), PCONFIG_LONG(2), 500, 2000);
}

void SwitchWebformSave(
  taskIndex_t TaskIndex,
  pluginID_t  pluginID,
  int16_t   & bootState,
  float     & debounce_ms,
  int16_t   & doubleClickEvent,
  float     & doubleClickMaxInterval,
  int16_t   & longPressEvent,
  float     & longPressMinInterval_ms,
  float     & useSafeButton)
{
  bootState = isFormItemChecked(F("sw_boot"));

  debounce_ms = getFormItemInt(F("sw_debounce"));

  doubleClickEvent       = getFormItemInt(F("sw_dc"));
  doubleClickMaxInterval = getFormItemInt(F("sw_dcmaxinterval"));

  doubleClickMaxInterval  = constrain(doubleClickMaxInterval, SWITCH_DOUBLECLICK_MIN_INTERVAL, SWITCH_DOUBLECLICK_MAX_INTERVAL);
  longPressMinInterval_ms = constrain(longPressMinInterval_ms, SWITCH_LONGPRESS_MIN_INTERVAL, SWITCH_LONGPRESS_MAX_INTERVAL);

  longPressEvent          = getFormItemInt(F("sw_lp"));
  longPressMinInterval_ms = getFormItemInt(F("sw_lpmininterval"));

  useSafeButton = isFormItemChecked(F("sw_sb"));

  // TO-DO: add Extra-Long Press event
  // PCONFIG_LONG(1) = isFormItemChecked(F("sw_elp"));
  // PCONFIG_LONG(2) = getFormItemInt(F("sw_elpmininterval"));

  // check if a task has been edited and remove 'task' bit from the previous pin
  for (auto it = globalMapPortStatus.begin(); it != globalMapPortStatus.end(); ++it) {
    if ((it->second.previousTask == TaskIndex) && (getPluginFromKey(it->first) == pluginID)) {
      globalMapPortStatus[it->first].previousTask = -1;
      removeTaskFromPort(it->first);
      break;
    }
  }
}

#include "../Helpers/Scheduler.h"


#include "../DataStructs/Scheduler_TaskDeviceTimerID.h"
#include "../DataStructs/TimingStats.h"
#include "../ESPEasyCore/Controller.h"
#include "../Globals/Settings.h"
#include "../Helpers/DeepSleep.h"

/*********************************************************************************************\
* Task Device Timer
* This is the interval set in a plugin to get a new reading.
* These timers will re-schedule themselves as long as the plugin task is enabled.
* When the plugin task is initialized, a call to schedule_task_device_timer_at_init
* will bootstrap this sequence.
\*********************************************************************************************/
void ESPEasy_Scheduler::schedule_task_device_timer_at_init(unsigned long task_index) {
  unsigned long runAt = millis();

  if (!isDeepSleepEnabled()) {
    // Deepsleep is not enabled, add some offset based on the task index
    // to make sure not all are run at the same time.
    // This scheduled time may be overriden by the plugin's own init.
    runAt += (task_index * 37) + 100;
  } else {
    runAt += (task_index * 11) + 10;
  }
  schedule_task_device_timer(task_index, runAt);
}

// Typical use case is to run this when all needed connections are made.
void ESPEasy_Scheduler::schedule_all_task_device_timers() {
  for (taskIndex_t task = 0; task < TASKS_MAX; task++) {
    schedule_task_device_timer_at_init(task);
  }
}

void ESPEasy_Scheduler::schedule_task_device_timer(unsigned long task_index, unsigned long runAt) {
  /*
     String log = F("schedule_task_device_timer: task: ");
     log += task_index;
     log += F(" @ ");
     log += runAt;
     if (Settings.TaskDeviceEnabled[task_index]) {
      log += F(" (enabled)");
     }
     addLog(LOG_LEVEL_INFO, log);
   */

  if (!validTaskIndex(task_index)) { return; }

  const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(task_index);

  if (!validDeviceIndex(DeviceIndex)) { return; }

  // TD-er: Tasks without a timer or optional timer set to 0 should still be able to call PLUGIN_READ
  // For example to schedule a read from the PLUGIN_TEN_PER_SECOND when a new value is ready.

  /*
     if (!Device[DeviceIndex].TimerOption) { return; }

     if (Device[DeviceIndex].TimerOptional && (Settings.TaskDeviceTimer[task_index] == 0)) {
      return;
     }
   */

  if (Settings.TaskDeviceEnabled[task_index]) {
    const TaskDeviceTimerID timerID(task_index);
    setNewTimerAt(timerID, runAt);
  }
}

void ESPEasy_Scheduler::reschedule_task_device_timer(unsigned long task_index, unsigned long lasttimer) {
  if (!validTaskIndex(task_index)) { return; }
  unsigned long newtimer = Settings.TaskDeviceTimer[task_index];

  if (newtimer != 0) {
    newtimer = lasttimer + (newtimer * 1000);
    schedule_task_device_timer(task_index, newtimer);
  }
}

void ESPEasy_Scheduler::process_task_device_timer(SchedulerTimerID timerID, unsigned long lasttimer) {
  const TaskDeviceTimerID *tmp = reinterpret_cast<const TaskDeviceTimerID *>(&timerID);
  const taskIndex_t task_index = tmp->getTaskIndex();

  if (!validTaskIndex(task_index)) { return; }
  struct EventStruct TempEvent(task_index);
  SensorSendTask(&TempEvent, 0, lasttimer);
}

#include "../Helpers/OLed_helper.h"

/**************************************************************************
 * Select controller type, SSD1306 or SH1106
 *************************************************************************/
void OLedFormController(const __FlashStringHelper *id,
                        const int                 *values,
                        uint8_t                    selectedIndex) {
  const __FlashStringHelper *controllerOptions[] = {
    F("SSD1306 (128x64 dot controller)"),
    F("SH1106 (132x64 dot controller)")
  };
  const int controllerValues[] = { 1, 2 };
  const FormSelectorOptions selector(
    NR_ELEMENTS(controllerOptions),
    controllerOptions, 
    values == nullptr ? controllerValues : values);
  selector.addFormSelector(F("Controller"), id, selectedIndex);
}

/**************************************************************************
 * Select OLed rotation, normal or rotated (180 degrees)
 *************************************************************************/
void OLedFormRotation(const __FlashStringHelper *id,
                      uint8_t                    selectedIndex) {
  const __FlashStringHelper *rotationOptions[] = {
    F("Normal"),
    F("Rotated") };
  const int rotationValues[] = {
    1,
    2 };
  const FormSelectorOptions selector(
    NR_ELEMENTS(rotationOptions),
    rotationOptions, rotationValues);
  selector.addFormSelector(F("Rotation"), id, selectedIndex);
}

/**************************************************************************
 * Select contrast setting, default = high
 *************************************************************************/
void OLedFormContrast(const __FlashStringHelper *id,
                      uint8_t                    selectedIndex) {
  const __FlashStringHelper *contrastOptions[3] = {
    F("Low"),
    F("Medium"),
    F("High") };
  const int contrastValues[3] = {
    OLED_CONTRAST_LOW,
    OLED_CONTRAST_MED,
    OLED_CONTRAST_HIGH };
  const FormSelectorOptions selector(
    NR_ELEMENTS(contrastOptions),
    contrastOptions, contrastValues);

  selector.addFormSelector(
    F("Contrast"), 
    id, 
    selectedIndex == 0 ? OLED_CONTRAST_HIGH : selectedIndex);
}

/**************************************************************************
 * Select one of available sizes, values are different per plugin...
 *************************************************************************/
void OLedFormSizes(const __FlashStringHelper *id,
                   const int                 *values,
                   uint8_t                    selectedIndex,
                   bool                       reloadOnChange) {
  const __FlashStringHelper *options3[] = {
    F("128x64"),
    F("128x32"),
    F("64x48") };
  FormSelectorOptions selector(NR_ELEMENTS(options3), options3, values);
  selector.reloadonchange = reloadOnChange;
  selector.addFormSelector(
    F("Display Size"), 
    id,
    selectedIndex);
}

/**************************************************************************
 * Set the contrast of the provided OLed display, does null-check on display object
 *************************************************************************/
void OLedSetContrast(OLEDDisplay   *_display,
                     const uint8_t& OLED_contrast) {
  if (_display == nullptr) { // Sanity check
    return;
  }
  char contrast  = 100;
  char precharge = 241;
  char comdetect = 64;

  switch (OLED_contrast) {
    case OLED_CONTRAST_OFF:
      _display->displayOff();
      return; // Done
    case OLED_CONTRAST_LOW:
      contrast = 10; precharge = 5; comdetect = 0;
      break;
    case OLED_CONTRAST_MED:
      contrast = OLED_CONTRAST_MED; precharge = 0x1F;
      break;
    case OLED_CONTRAST_HIGH:
    default:
      contrast = OLED_CONTRAST_HIGH;
      break;
  }

  _display->displayOn();
  _display->setContrast(contrast, precharge, comdetect);
}

/**************************************************************************
 * Handle PLUGIN_WEBFORM_SHOW_I2C_PARAMS and PLUGIN_HAS_I2C_ADDRESS plugin events
 *************************************************************************/
bool OLedI2CAddressCheck(uint8_t                    function,
                         int                        checkI2cAddress,
                         const __FlashStringHelper *id,
                         int8_t                     deviceAddress) {
  bool success                     = false;
  const uint8_t i2cAddressValues[] = { 0x3c, 0x3d };

  if (function == PLUGIN_WEBFORM_SHOW_I2C_PARAMS) {
    addFormSelectorI2C(id, 2, i2cAddressValues, deviceAddress);
  } else {
    success = intArrayContains(2, i2cAddressValues, checkI2cAddress);
  }
  return success;
}

#include "../Helpers/PortStatus.h"

#include "../DataStructs/PinMode.h"
#include "../Globals/GlobalMapPortStatus.h"

#include "../Helpers/StringConverter.h"

#include "../../ESPEasy-Globals.h"


#ifdef ESP32

#include "../Helpers/Hardware_PWM.h"

void checkAndClearPWM(uint32_t key) {
  if (existPortStatus(key)) {
    switch (globalMapPortStatus[key].mode) {
      case PIN_MODE_PWM:
      case PIN_MODE_SERVO:
        {
          const uint16_t port = getPortFromKey(key);
          analogWriteESP32(port, 0);
        }
        break;
    }
  }
}

#endif


/**********************************************************
*                                                         *
* Helper Functions for managing the status data structure *
*                                                         *
**********************************************************/
void savePortStatus(uint32_t key, struct portStatusStruct& tempStatus) {
  // FIXME TD-er: task and monitor are unsigned, should we only check for == ????
  if ((tempStatus.task <= 0) && (tempStatus.monitor <= 0) && (tempStatus.command <= 0)) {
    #ifdef ESP32
    checkAndClearPWM(key);
    #endif

    globalMapPortStatus.erase(key);
  }
  else {
    #ifdef ESP32
    switch (tempStatus.mode) {
      case PIN_MODE_PWM:
      case PIN_MODE_SERVO:
        break;
      default:
        checkAndClearPWM(key);
        break;
    }
    #endif

    globalMapPortStatus[key] = tempStatus;
  }
}

bool existPortStatus(uint32_t key) {
  return globalMapPortStatus.find(key) != globalMapPortStatus.end();
}

void removeTaskFromPort(uint32_t key) {
  const auto it = globalMapPortStatus.find(key);
  if (it != globalMapPortStatus.end()) {
    (it->second.task > 0) ? it->second.task-- : it->second.task = 0;

    if ((it->second.task <= 0) && (it->second.monitor <= 0) && (it->second.command <= 0) &&
        (it->second.init <= 0)) {
      // erase using the key, so the iterator can be const
      #ifdef ESP32
      checkAndClearPWM(key);
      #endif

      globalMapPortStatus.erase(key);
    }
  }
}

void removeMonitorFromPort(uint32_t key) {
  const auto it = globalMapPortStatus.find(key);
  if (it != globalMapPortStatus.end()) {
    it->second.monitor = 0;

    if ((it->second.task <= 0) && (it->second.monitor <= 0) && (it->second.command <= 0) &&
        (it->second.init <= 0)) {
      // erase using the key, so the iterator can be const
      #ifdef ESP32
      checkAndClearPWM(key);
      #endif

      globalMapPortStatus.erase(key);
    }
  }
}

void addMonitorToPort(uint32_t key) {
  globalMapPortStatus[key].monitor = 1;
}

uint32_t createKey(pluginID_t pluginNumber, uint16_t portNumber) {
  return ((uint32_t)pluginNumber.value << 16) | portNumber;
}

pluginID_t getPluginFromKey(uint32_t key) {
  return pluginID_t::toPluginID((key >> 16) & 0xFFFF);
}

uint16_t getPortFromKey(uint32_t key) {
  return static_cast<uint16_t>(key & 0xFFFF);
}




/*********************************************************************************************\
   set pin mode & state (info table)
\*********************************************************************************************/
/*
   void setPinState(uint8_t plugin, uint8_t index, uint8_t mode, uint16_t value)
   {
   // plugin number and index form a unique key
   // first check if this pin is already known
   bool reUse = false;
   for (uint8_t x = 0; x < PINSTATE_TABLE_MAX; x++)
    if ((pinStates[x].plugin == plugin) && (pinStates[x].index == index))
    {
      pinStates[x].mode = mode;
      pinStates[x].value = value;
      reUse = true;
      break;
    }

   if (!reUse)
   {
    for (uint8_t x = 0; x < PINSTATE_TABLE_MAX; x++)
      if (pinStates[x].plugin == 0)
      {
        pinStates[x].plugin = plugin;
        pinStates[x].index = index;
        pinStates[x].mode = mode;
        pinStates[x].value = value;
        break;
      }
   }
   }
 */

/*********************************************************************************************\
   get pin mode & state (info table)
\*********************************************************************************************/

/*
   bool getPinState(uint8_t plugin, uint8_t index, uint8_t *mode, uint16_t *value)
   {
   for (uint8_t x = 0; x < PINSTATE_TABLE_MAX; x++)
    if ((pinStates[x].plugin == plugin) && (pinStates[x].index == index))
    {
 * mode = pinStates[x].mode;
 * value = pinStates[x].value;
      return true;
    }
   return false;
   }

 */
/*********************************************************************************************\
   check if pin mode & state is known (info table)
\*********************************************************************************************/
/*
   bool hasPinState(uint8_t plugin, uint8_t index)
   {
   for (uint8_t x = 0; x < PINSTATE_TABLE_MAX; x++)
    if ((pinStates[x].plugin == plugin) && (pinStates[x].index == index))
    {
      return true;
    }
   return false;
   }

 */


/*********************************************************************************************\
   report pin mode & state (info table) using json
\*********************************************************************************************/
String getPinStateJSON(bool search, uint32_t key, const String& log, int16_t noSearchValue)
{
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("getPinStateJSON"));
  #endif
  printToWebJSON = true;
  uint8_t mode     = PIN_MODE_INPUT;
  int16_t value = noSearchValue;
  bool    found = false;

  const auto it = globalMapPortStatus.find(key);
  if (it != globalMapPortStatus.end()) {
    found = true;
    // update mode even if search = false, otherwise it will print mode assigned above
    mode  = it->second.mode;
    // update value only if search = true, otherwise use noSearchValue
    if(search)
      value = it->second.getValue();
  }

  if (!search || found)
  {
    String reply;
    reserve_special(reply, 128);
    reply += F("{\n\"log\": \"");
    {
      // truncate to 25 chars, max MQTT message size = 128 including header...
      int colonPos = 1 + log.indexOf(':');

      String tmp = log.substring(colonPos, colonPos + 25);
      tmp.trim();      
      reply += tmp;
    }
    reply += F("\",\n\"plugin\": ");
    reply += getPluginFromKey(key).value;
    reply += F(",\n\"pin\": ");
    reply += getPortFromKey(key);
    reply += F(",\n\"mode\": \"");
    reply += getPinModeString(mode);
    reply += F("\",\n\"state\": ");
    reply += value;
    reply += F("\n}\n");
    return reply;
  }
  return "";
}

const __FlashStringHelper * getPinModeString(uint8_t mode) {
  switch (mode)
  {
    case PIN_MODE_UNDEFINED:    return F("undefined");
    case PIN_MODE_INPUT:        return F("input");
    case PIN_MODE_INPUT_PULLUP: return F("input pullup");
    case PIN_MODE_INPUT_PULLDOWN: return F("input pulldown");
    case PIN_MODE_OFFLINE:      return F("offline");
    case PIN_MODE_OUTPUT:       return F("output");
    case PIN_MODE_PWM:          return F("PWM");
    case PIN_MODE_SERVO:        return F("servo");
    default:
      break;
  }
  return F("ERROR: Not Defined");
}
#include "../Helpers/MDNS_Helper.h"

#if FEATURE_MDNS

#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../ESPEasyCore/ESPEasyEth.h"
#include "../ESPEasyCore/ESPEasyNetwork.h"

#include "../Globals/ESPEasyWiFiEvent.h"
#include "../Globals/NetworkState.h"
#include "../Globals/Services.h"
#include "../Globals/Settings.h"

#include "../Helpers/StringProvider.h"
#endif

void set_mDNS() {
  #if FEATURE_MDNS

  if (!WiFiEventData.WiFiServicesInitialized()) { return; }

  update_mDNS();
  #endif // if FEATURE_MDNS
}

void update_mDNS() {
  #if FEATURE_MDNS
  if (mDNS_init) {
    MDNS.end();
    mDNS_init = false;
  }
  if (webserverRunning) {
    if (!mDNS_init) {
      addLog(LOG_LEVEL_INFO, F("mDNS : Starting mDNS..."));
      mDNS_init = MDNS.begin(NetworkGetHostname().c_str());
      MDNS.setInstanceName(NetworkGetHostname()); // Needed for when the hostname has changed.

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        String log = F("mDNS : ");

        if (mDNS_init) {
          log += F("Started, with name: ");
          log += getValue(LabelType::M_DNS);
        }
        else {
          log += F("Failed");
        }
        addLogMove(LOG_LEVEL_INFO, log);
      }
      if (mDNS_init) {
        MDNS.addService(F("http"), F("tcp"), Settings.WebserverPort);
      }
    }
  } else {
    #ifdef ESP8266
    if (mDNS_init) {
      MDNS.close();
    }
    mDNS_init = false;
    #endif
  }
  #endif
}
#include "../Helpers/SystemVariables.h"


#include "../../ESPEasy_common.h"

#include "../../ESPEasy-Globals.h"

#include "../CustomBuild/CompiletimeDefines.h"

#include "../DataStructs/TimingStats.h"

#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../ESPEasyCore/ESPEasyNetwork.h"

#include "../Globals/CRCValues.h"
#include "../Globals/ESPEasy_time.h"
#include "../Globals/ESPEasyWiFiEvent.h"
#if FEATURE_MQTT
# include "../Globals/MQTT.h"
#endif // if FEATURE_MQTT
#include "../Globals/NetworkState.h"
#include "../Globals/RulesCalculate.h"
#include "../Globals/RuntimeData.h"
#include "../Globals/Settings.h"
#include "../Globals/Statistics.h"

#include "../Helpers/Convert.h"
#include "../Helpers/Hardware_device_info.h"
#include "../Helpers/Misc.h"
#include "../Helpers/Numerical.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringProvider.h"

#ifndef LIMIT_BUILD_SIZE
#include <math.h>
#include "../Helpers/StringConverter_Numerical.h"
#endif // ifndef LIMIT_BUILD_SIZE

#if defined(ESP8266)
  # include <ESP8266WiFi.h>
#endif // if defined(ESP8266)
#if defined(ESP32)
  # include <WiFi.h>
#endif // if defined(ESP32)


String getReplacementString(const String& format, const String& s) {
  int startpos = s.indexOf(format);
  int endpos   = s.indexOf('%', startpos + 1);
  if (endpos == -1) {
    addLog(LOG_LEVEL_ERROR, concat(F("SunTime syntax error: "), format));
    return format;
  }
  String R     = s.substring(startpos, endpos + 1);


#ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLogMove(LOG_LEVEL_DEBUG, strformat(
      F("ReplacementString SunTime: %s offset: %d"), 
      R.c_str(), 
      ESPEasy_time::getSecOffset(R)));
  }
#endif // ifndef BUILD_NO_DEBUG
  return R;
}

void replSunRiseTimeString(const String& format, String& s, boolean useURLencode) {
  const String R(getReplacementString(format, s));

  repl(R, node_time.getSunriseTimeString(':', ESPEasy_time::getSecOffset(R)), s, useURLencode);
}

void replSunSetTimeString(const String& format, String& s, boolean useURLencode) {
  const String R(getReplacementString(format, s));

  repl(R, node_time.getSunsetTimeString(':', ESPEasy_time::getSecOffset(R)), s, useURLencode);
}

String timeReplacement_leadZero(int value)
{
  char valueString[5] = { 0 };

  sprintf_P(valueString, PSTR("%02d"), value);
  return valueString;
}

// FIXME TD-er: Try to match these with  StringProvider::getValue
LabelType::Enum SystemVariables2LabelType(SystemVariables::Enum enumval) {
  LabelType::Enum label = LabelType::MAX_LABEL;

  switch (enumval)
  {
    case SystemVariables::IP:                label = LabelType::IP_ADDRESS; break;
#if FEATURE_USE_IPV6
    case SystemVariables::IP6_LOCAL:         label = LabelType::IP6_LOCAL; break;
#endif
    case SystemVariables::SUBNET:            label = LabelType::IP_SUBNET; break;
    case SystemVariables::DNS:               label = LabelType::DNS; break;
    case SystemVariables::DNS_1:             label = LabelType::DNS_1; break;
    case SystemVariables::DNS_2:             label = LabelType::DNS_2; break;
    case SystemVariables::GATEWAY:           label = LabelType::GATEWAY; break;
    case SystemVariables::CLIENTIP:          label = LabelType::CLIENT_IP; break;
    #if FEATURE_INTERNAL_TEMPERATURE
    case SystemVariables::INTERNAL_TEMPERATURE: label = LabelType::INTERNAL_TEMPERATURE; break;
    #endif // if FEATURE_INTERNAL_TEMPERATURE

    #if FEATURE_ETHERNET

    case SystemVariables::ETHWIFIMODE:       label = LabelType::ETH_WIFI_MODE; break; // 0=WIFI, 1=ETH
    case SystemVariables::ETHCONNECTED:      label = LabelType::ETH_CONNECTED; break; // 0=disconnected, 1=connected
    case SystemVariables::ETHDUPLEX:         label = LabelType::ETH_DUPLEX; break;
    case SystemVariables::ETHSPEED:          label = LabelType::ETH_SPEED; break;
    case SystemVariables::ETHSTATE:          label = LabelType::ETH_STATE; break;
    case SystemVariables::ETHSPEEDSTATE:     label = LabelType::ETH_SPEED_STATE; break;
    #endif // if FEATURE_ETHERNET
    case SystemVariables::LCLTIME:           label = LabelType::LOCAL_TIME; break;
    #if FEATURE_LAT_LONG_VAR_CMD
    case SystemVariables::LATITUDE:          label = LabelType::LATITUDE; break;
    case SystemVariables::LONGITUDE:         label = LabelType::LONGITUDE; break;
    #endif // if FEATURE_LAT_LONG_VAR_CMD
    case SystemVariables::MAC:               label = LabelType::STA_MAC; break;
    case SystemVariables::RSSI:              label = LabelType::WIFI_RSSI; break;
    case SystemVariables::SUNRISE_S:         label = LabelType::SUNRISE_S; break;
    case SystemVariables::SUNSET_S:          label = LabelType::SUNSET_S; break;
    case SystemVariables::SUNRISE_M:         label = LabelType::SUNRISE_M; break;
    case SystemVariables::SUNSET_M:          label = LabelType::SUNSET_M; break;
    case SystemVariables::SYSBUILD_DESCR:    label = LabelType::BUILD_DESC; break;
    case SystemVariables::SYSBUILD_FILENAME: label = LabelType::BINARY_FILENAME; break;
    case SystemVariables::SYSBUILD_GIT:      label = LabelType::GIT_BUILD; break;
    case SystemVariables::SYSSTACK:          label = LabelType::FREE_STACK; break;
    case SystemVariables::UNIT_sysvar:       label = LabelType::UNIT_NR; break;
    #if FEATURE_ZEROFILLED_UNITNUMBER
    case SystemVariables::UNIT_0_sysvar:     label = LabelType::UNIT_NR_0; break;
    #endif // FEATURE_ZEROFILLED_UNITNUMBER
    case SystemVariables::FLASH_FREQ:        label = LabelType::FLASH_CHIP_SPEED; break;
    case SystemVariables::FLASH_SIZE:        label = LabelType::FLASH_CHIP_REAL_SIZE; break;
    case SystemVariables::FLASH_CHIP_VENDOR: label = LabelType::FLASH_CHIP_VENDOR; break;
    case SystemVariables::FLASH_CHIP_MODEL:  label = LabelType::FLASH_CHIP_MODEL; break;
    case SystemVariables::FS_SIZE:           label = LabelType::FS_SIZE; break;
    case SystemVariables::FS_FREE:           label = LabelType::FS_FREE; break;

    case SystemVariables::ESP_CHIP_ID:       label = LabelType::ESP_CHIP_ID; break;
    case SystemVariables::ESP_CHIP_FREQ:     label = LabelType::ESP_CHIP_FREQ; break;
    case SystemVariables::ESP_CHIP_MODEL:    label = LabelType::ESP_CHIP_MODEL; break;
    case SystemVariables::ESP_CHIP_REVISION: label = LabelType::ESP_CHIP_REVISION; break;
    case SystemVariables::ESP_CHIP_CORES:    label = LabelType::ESP_CHIP_CORES; break;
    case SystemVariables::BOARD_NAME:        label = LabelType::BOARD_NAME; break;

    default:
      // No matching LabelType yet.
      break;
  }
  return label;
}

String SystemVariables::getSystemVariable(SystemVariables::Enum enumval) {
  const LabelType::Enum label = SystemVariables2LabelType(enumval);

  if (LabelType::MAX_LABEL != label) {
    return getValue(label);
  }
  constexpr int INT_NOT_SET = std::numeric_limits<int>::min();

  int intvalue = INT_NOT_SET;

  switch (enumval)
  {
    case BOOT_CAUSE:        intvalue = lastBootCause; break;                         // Integer value to be used in rules
    case BSSID:             return (WiFiEventData.WiFiDisconnected()) ? MAC_address().toString() : WiFi.BSSIDstr();
    case CR:                return String('\r');
    case IP4:               intvalue = static_cast<int>(NetworkLocalIP()[3]); break; // 4th IP octet
    case ISVAR_DOUBLE:      intvalue =
                            #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
                            1;
                            #else
                            0;
                            #endif
                            break;
    case ISLIMITED_BUILD:   intvalue =
                            #ifdef LIMIT_BUILD_SIZE
                            1;
                            #else
                            0;
                            #endif
                            break;
    case ISMQTT:            intvalue = 
    #if FEATURE_MQTT
        MQTTclient_connected ? 1 :
    #endif // if FEATURE_MQTT
        0; break;

    case ISMQTTIMP:         intvalue = 
    #ifdef USES_P037
        P037_MQTTImport_connected ? 1 :
    #endif // ifdef USES_P037
        0; break;

    case ISNTP:             intvalue = statusNTPInitialized ? 1 : 0; break;
    case ISWIFI:            intvalue = WiFiEventData.wifiStatus; break; // 0=disconnected, 1=connected, 2=got ip, 4=services
    // initialized
    case LCLTIME_AM:        return node_time.getDateTimeString_ampm('-', ':', ' ');
    case LF:                return String('\n');
    case MAC_INT:           intvalue = getChipId(); break; // Last 24 bit of MAC address as integer, to be used in rules.
    #ifndef LIMIT_BUILD_SIZE
    case S_PI:              {
                              constexpr ESPEASY_RULES_FLOAT_TYPE _pi = M_PI;
                              #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
                              return doubleToString(_pi, maxNrDecimals_fpType(_pi));
                              #else // if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
                              return toString(_pi, maxNrDecimals_fpType(_pi));
                              #endif // if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
                            }
    case S_E:               {
                              constexpr ESPEASY_RULES_FLOAT_TYPE _e = M_E;
                              #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
                              return doubleToString(_e, maxNrDecimals_fpType(_e));
                              #else // if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
                              return toString(_e, maxNrDecimals_fpType(_e));
                              #endif // if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
                            }
    #endif // ifndef LIMIT_BUILD_SIZE
    case SPACE:             return String(' ');
    case SSID:              return (WiFiEventData.WiFiDisconnected()) ? String(F("--")) : WiFi.SSID();
    case SYSBUILD_DATE:     return get_build_date();
    case SYSBUILD_TIME:     return get_build_time();
    case SYSDAY:            intvalue = node_time.day(); break;
    case SYSDAY_0:          return timeReplacement_leadZero(node_time.day());
    case SYSHEAP:           intvalue = ESP.getFreeHeap(); break;
    case SYSHOUR:           intvalue = node_time.hour(); break;
    case SYSHOUR_0:         return timeReplacement_leadZero(node_time.hour());
    case SYSLOAD:           return String(getCPUload(), 2);
    case SYSMIN:            intvalue = node_time.minute(); break;
    case SYSMIN_0:          return timeReplacement_leadZero(node_time.minute());
    case SYSMONTH:          intvalue = node_time.month(); break;
    case SYSMONTH_S:        return node_time.month_str();
    case SYSNAME:           return Settings.getHostname();
    case SYSSEC:            intvalue = node_time.second(); break;
    case SYSSEC_0:          return timeReplacement_leadZero(node_time.second());
    case SYSSEC_D:          intvalue = ((node_time.hour() * 60) + node_time.minute()) * 60 + node_time.second(); break;
    case SYSTIME:           return node_time.getTimeString(':');
    case SYSTIME_AM:        return node_time.getTimeString_ampm(':');
    case SYSTIME_AM_0:      return node_time.getTimeString_ampm(':', true, '0');
    case SYSTIME_AM_SP:     return node_time.getTimeString_ampm(':', true, ' ');
    case SYSTM_HM:          return node_time.getTimeString(':', false);
    case SYSTM_HM_0:        return node_time.getTimeString(':', false, '0');
    case SYSTM_HM_SP:       return node_time.getTimeString(':', false, ' ');
    case SYSTM_HM_AM:       return node_time.getTimeString_ampm(':', false);
    case SYSTM_HM_AM_0:     return node_time.getTimeString_ampm(':', false, '0');
    case SYSTM_HM_AM_SP:    return node_time.getTimeString_ampm(':', false, ' ');
    case SYSTZOFFSET:       return node_time.getTimeZoneOffsetString();
    #ifndef LIMIT_BUILD_SIZE
    case SYSTZOFFSET_S:     intvalue = static_cast<int>(static_cast<int64_t>(node_time.getLocalUnixTime()) - static_cast<int64_t>(node_time.getUnixTime())); break;
    #endif // ifndef LIMIT_BUILD_SIZE
    case SYSWEEKDAY:        intvalue = node_time.weekday(); break;
    case SYSWEEKDAY_S:      return node_time.weekday_str();
    case SYSYEAR_0:
    case SYSYEAR:           intvalue = node_time.year(); break;
    case SYSYEARS:          return timeReplacement_leadZero(node_time.year() % 100);
    case SYSMONTH_0:       return timeReplacement_leadZero(node_time.month());
    case S_CR:              return F("\\r");
    case S_LF:              return F("\\n");
    case UNIXDAY:           intvalue = node_time.getUnixTime() / 86400; break;
    case UNIXDAY_SEC:       intvalue = node_time.getUnixTime() % 86400; break;
    case UNIXTIME:          return String(node_time.getUnixTime());
    #ifndef LIMIT_BUILD_SIZE
    case UNIXTIME_LCL:      return String(node_time.getLocalUnixTime());
    #endif // ifndef LIMIT_BUILD_SIZE
    case UPTIME:            intvalue = getUptimeMinutes(); break;
    case UPTIME_MS:         return ull2String(getMicros64() / 1000);
    #if FEATURE_ADC_VCC
    case VCC:               return String(vcc);
    #else // if FEATURE_ADC_VCC
    case VCC:               intvalue = -1; break;
    #endif // if FEATURE_ADC_VCC
    case WI_CH:             intvalue = (WiFiEventData.WiFiDisconnected()) ? 0 : WiFi.channel(); break;

    default:
      // Already handled above.
      return EMPTY_STRING;
  }

  if (intvalue != INT_NOT_SET) {
    return String(intvalue);
  }

  return EMPTY_STRING;
}

/*
#define SMART_REPL_T(T, S) \
  while (s.indexOf(T) != -1) { (S((T), s, useURLencode)); }
*/

#define SMART_REPL_T(T, S) \
  const String T_str(T); int __pos__ = s.indexOf(T_str); \
  while (__pos__ != -1) { (S((T_str), s, useURLencode)); __pos__ = s.indexOf(T_str, __pos__ + 1);}

// Parse %vN% to replace ESPEasy variables
bool parse_pct_v_num_pct(String& s, boolean useURLencode, int start_pos)
{
  const String key_prefix = F("%v");
  int v_index = s.indexOf(key_prefix, start_pos);

  bool somethingReplaced = false;

  while ((v_index != -1)) {
    // Exclude "%valname% or %value%"
    // FIXME TD-er: Must find a more elegant way to fix this
    if (!isalpha(s.charAt(v_index + 2))) {
      // Check for:
      // - Calculations indicated with leading '='
      // - nested indirections like %v%v1%% or %v%v_bla%% or %v_%v_2%%
      const bool isv_ = s.charAt(v_index + 2) == '_';
      if ((s.charAt(v_index + 2) == '=') ||
          (s.charAt(v_index + 2) == '%' && s.charAt(v_index + 3) == 'v') ||
          (isv_ && (s.charAt(v_index + 3) == '%' && s.charAt(v_index + 4) == 'v'))) {
        // FIXME TD-er: This may lead to stack overflow if we do an awful lot of nested user variables
        if (parse_pct_v_num_pct(s, useURLencode, v_index + 2 + (isv_ ? 1 : 0))) {
          somethingReplaced = true;
        }
      }

      // variable index may contain a calculation
      // Calculations are enforced by a leading '='
      // like: %v=1+%v2%%
      String value;
      const int pos_closing_pct = s.indexOf('%', v_index + 1);
      const String arg = s.substring(v_index + 2 + (isv_ ? 1 : 0), pos_closing_pct);
      String valArg(arg);
      if (!isv_) {
        constexpr int64_t errorvalue = -1;
        const int64_t i = CalculateParam(arg, errorvalue);
        // addLog(LOG_LEVEL_INFO, strformat(F("s: '%s', calc parse: %s => %d"), s.c_str(), arg.c_str(), i));
        if (i != errorvalue) { // We're calculating a numeric index like %v=1+%v2%%, so have to use the result for the value
          valArg = ll2String(i);
        }
      }

      // Need to replace the entire arg
      const String key = strformat(F("%%%s%s%%"), FsP(isv_ ? F("v_") : F("v")), arg.c_str());
      // addLog(LOG_LEVEL_INFO, strformat(F("parsed, key: %s (valArg: %s)"), key.c_str(), valArg.c_str()));

      if (s.indexOf(key) != -1) {
        const bool trimTrailingZeros = true;
        const ESPEASY_RULES_FLOAT_TYPE floatvalue = getCustomFloatVar(valArg);
        const unsigned char nr_decimals = maxNrDecimals_fpType(floatvalue);
        #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
        value = doubleToString(floatvalue, nr_decimals, trimTrailingZeros);
        #else // if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
        value = floatToString(floatvalue, nr_decimals, trimTrailingZeros);
        #endif // if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE

        if (repl(key, value, s, useURLencode)) {
          somethingReplaced = true;
        }
      }
    }
    v_index = s.indexOf(key_prefix, v_index + 1); // Find next occurance
    //addLog(LOG_LEVEL_INFO, strformat(F("parse: %s"), s.c_str()));
  }
  return somethingReplaced;
}

void SystemVariables::parseSystemVariables(String& s, boolean useURLencode)
{
  START_TIMER

  if (s.indexOf('%') == -1) {
    STOP_TIMER(PARSE_SYSVAR_NOCHANGE);
    return;
  }

  bool somethingReplaced = false;

  // Parse ESPEasy user variables first as they might be combined 
  // as arument or index for other variables
  parse_pct_v_num_pct(s, useURLencode, 0);

  do {
    int last_percent_pos = -1;
    somethingReplaced = false;
    SystemVariables::Enum enumval = static_cast<SystemVariables::Enum>(0);
    do {
      enumval = SystemVariables::nextReplacementEnum(s, enumval, last_percent_pos);

      switch (enumval)
      {
        case SUNRISE: {
          SMART_REPL_T(SystemVariables::toString(enumval), replSunRiseTimeString);
          somethingReplaced = true;
          break;
        }
        case SUNSET: {
          SMART_REPL_T(SystemVariables::toString(enumval), replSunSetTimeString);
          somethingReplaced = true;
          break;
        }
        case VARIABLE:
        {
          // Should not be present anymore, but just in case...
          if (parse_pct_v_num_pct(s, useURLencode, 0))
            somethingReplaced = true;
    
          break;
        }
        case UNKNOWN:

          // Do not replace
          break;
        default:
        {
          const String sysvar_str(SystemVariables::toString(enumval));
          if (s.indexOf(sysvar_str) != -1) {
            if (repl(
              sysvar_str, 
              getSystemVariable(enumval), 
              s, 
              useURLencode))
              somethingReplaced = true;
          }
          break;
        }
      }
    }
    while (enumval != SystemVariables::Enum::UNKNOWN);
  }
  while (somethingReplaced);

  STOP_TIMER(PARSE_SYSVAR);
}

#undef SMART_REPL_T


SystemVariables::Enum SystemVariables::nextReplacementEnum(const String& str, SystemVariables::Enum last_tested, int& last_percent_pos)
{
  SystemVariables::Enum nextTested;
  int percent_pos = last_percent_pos;

  do {
    // Find first position in string which might be a good candidate to look for a system variable.
    // Look for "%N" where 'N' is the first letter of a variable name we support.
    percent_pos = str.indexOf('%', percent_pos + 1);

    if (percent_pos == -1) {
      return Enum::UNKNOWN;
    }

    nextTested = SystemVariables::startIndex_beginWith(str.c_str() + percent_pos + 1);
  } while (Enum::UNKNOWN == nextTested);

  if (last_percent_pos < percent_pos) {
    last_percent_pos = percent_pos;
    last_tested      = nextTested;
  }

  if (last_tested > nextTested) {
    // Iterate over the possible system variables
    nextTested = static_cast<SystemVariables::Enum>(last_tested + 1);
    const char firstChar_nextTested = static_cast<char>(pgm_read_byte(SystemVariables::toFlashString(nextTested)));
    const char firstChar_expected = str[percent_pos + 1];

    if (firstChar_nextTested != firstChar_expected) {
      nextTested = Enum::UNKNOWN;
    }
  }

  if (nextTested >= Enum::UNKNOWN) {
    // We have tested all possible system variables
    // Skip unsupported ones or maybe it is just a single percentage symbol in a string.
    percent_pos = str.indexOf('%', percent_pos + 1);

    if (percent_pos == -1) {
      return Enum::UNKNOWN;
    }
    last_percent_pos = percent_pos;
    return SystemVariables::startIndex_beginWith(str.c_str() + percent_pos + 1);
  }

  const __FlashStringHelper *fstr_sysvar = SystemVariables::toFlashString(nextTested);
  String str_prefix        = strformat(F("%%%c"), static_cast<char>(pgm_read_byte(fstr_sysvar)));
  bool   str_prefix_exists = str.indexOf(str_prefix) != -1;

  for (int i = nextTested; i < Enum::UNKNOWN; ++i) {
    SystemVariables::Enum enumval = static_cast<SystemVariables::Enum>(i);
    fstr_sysvar = SystemVariables::toFlashString(enumval);
    const String new_str_prefix = strformat(F("%%%c"), static_cast<char>(pgm_read_byte(fstr_sysvar)));

    if ((str_prefix == new_str_prefix) && !str_prefix_exists) {
      // Just continue
    } else {
      str_prefix        = new_str_prefix;
      str_prefix_exists = str.indexOf(str_prefix) != -1;

      if (str_prefix_exists) {
        if (str.indexOf(SystemVariables::toString(enumval)) != -1) {
          return enumval;
        }
      }
    }
  }

  return Enum::UNKNOWN;
}

String SystemVariables::toString(Enum enumval)
{
  if ((enumval == Enum::SUNRISE) || (enumval == Enum::SUNSET) || enumval == Enum::VARIABLE) {
    // These need variables, so only prepend a %, not wrap.
    return String('%') + SystemVariables::toFlashString(enumval);
  }

  return wrap_String(SystemVariables::toFlashString(enumval), '%');
}

SystemVariables::Enum SystemVariables::startIndex_beginWith(const char* beginchar)
{
  if (beginchar == nullptr || *beginchar == '\0')
  {
    return Enum::UNKNOWN;
  }

  const char ch = tolower(*beginchar);
#ifndef LIMIT_BUILD_SIZE
  // Speedup search for some variables starting with "%sys"
  // Not needed if build size really matters
  if (ch == 's') {
    // Lots of system variables start with "%sys"
    const char sys_str[] = {'s', 'y', 's'};
    bool found = true;
    const char* cur = beginchar;
    for (size_t i = 0; found && i < NR_ELEMENTS(sys_str); ++i, ++cur) {
      if (tolower(*cur) != sys_str[i]) {
        found = false;
      }
    }
    if (found) {
      const char next_ch = tolower(*cur);
      if (next_ch != '\0') {
        switch (next_ch) {
          case 'b': return Enum::SYSBUILD_DATE;
          case 'd': return Enum::SYSDAY;
          case 'h': return Enum::SYSHEAP;
          case 'l': return Enum::SYSLOAD;
          case 'm': return Enum::SYSMIN;
          case 'n': return Enum::SYSNAME;
          case 's': return Enum::SYSSEC;
          case 't': return Enum::SYSTIME;
          case 'w': return Enum::SYSWEEKDAY;
          case 'y': return Enum::SYSYEAR;
        }
      }
    }
  }
#endif
  switch (ch)
  {
    case 'b': return Enum::BOARD_NAME;
    case 'c': return Enum::CLIENTIP;
    case 'd': return Enum::DNS;
#if FEATURE_ETHERNET
    case 'e': return Enum::ETHCONNECTED;
#else // if FEATURE_ETHERNET
#ifndef LIMIT_BUILD_SIZE
    case 'e': return Enum::S_E;
#endif // ifndef LIMIT_BUILD_SIZE
#endif // if FEATURE_ETHERNET
    case 'f': return Enum::FLASH_CHIP_MODEL;
    case 'g': return Enum::GATEWAY;
#if FEATURE_INTERNAL_TEMPERATURE
    case 'i': return Enum::INTERNAL_TEMPERATURE;
#else // if FEATURE_INTERNAL_TEMPERATURE
    case 'i': return Enum::IP4;
#endif // if FEATURE_INTERNAL_TEMPERATURE
    case 'l': return Enum::LCLTIME;
    case 'm': return Enum::SUNRISE_M;
    case 'n': return Enum::S_LF;
#ifndef LIMIT_BUILD_SIZE
    case 'p': return Enum::S_PI;
#endif // ifndef LIMIT_BUILD_SIZE
    case 'r': return Enum::S_CR;
    case 's': return Enum::SPACE;
    case 'u': return Enum::UNIT_sysvar;
    // case 'v': return Enum::VARIABLE; // Can not be the first 'v' variable, as the name is only 1 character long
    case 'v': return Enum::VCC;
    case 'w': return Enum::WI_CH;
  }

  return Enum::UNKNOWN;
}

const __FlashStringHelper * SystemVariables::toFlashString(SystemVariables::Enum enumval)
{
  switch (enumval) {
    case Enum::BOARD_NAME:         return F("board_name");
    case Enum::BOOT_CAUSE:         return F("bootcause");
    case Enum::BSSID:              return F("bssid");
    case Enum::CLIENTIP:           return F("clientip");
    case Enum::CR:                 return F("CR");
    case Enum::ESP_CHIP_CORES:     return F("cpu_cores");
    case Enum::ESP_CHIP_FREQ:      return F("cpu_freq");
    case Enum::ESP_CHIP_ID:        return F("cpu_id");
    case Enum::ESP_CHIP_MODEL:     return F("cpu_model");
    case Enum::ESP_CHIP_REVISION:  return F("cpu_rev");
    case Enum::DNS:                return F("dns");
    case Enum::DNS_1:              return F("dns1");
    case Enum::DNS_2:              return F("dns2");
    #ifndef LIMIT_BUILD_SIZE
    case Enum::S_E:                return F("e");
    #endif // ifndef LIMIT_BUILD_SIZE
#if FEATURE_ETHERNET
    case Enum::ETHCONNECTED:       return F("ethconnected");
    case Enum::ETHDUPLEX:          return F("ethduplex");
    case Enum::ETHSPEED:           return F("ethspeed");
    case Enum::ETHSPEEDSTATE:      return F("ethspeedstate");
    case Enum::ETHSTATE:           return F("ethstate");
    case Enum::ETHWIFIMODE:        return F("ethwifimode");
#endif // if FEATURE_ETHERNET

    case Enum::FLASH_CHIP_MODEL:   return F("flash_chip_model");
    case Enum::FLASH_CHIP_VENDOR:  return F("flash_chip_vendor");
    case Enum::FLASH_FREQ:         return F("flash_freq");
    case Enum::FLASH_SIZE:         return F("flash_size");
    case Enum::FS_FREE:            return F("fs_free");
    case Enum::FS_SIZE:            return F("fs_size");
    case Enum::GATEWAY:            return F("gateway");
#if FEATURE_INTERNAL_TEMPERATURE
    case Enum::INTERNAL_TEMPERATURE: return F("inttemp");
#endif // if FEATURE_INTERNAL_TEMPERATURE

    case Enum::IP4:                return F("ip4");
    case Enum::IP:                 return F("ip");
#if FEATURE_USE_IPV6
    case Enum::IP6_LOCAL:          return F("ipv6local");
#endif
    case Enum::ISVAR_DOUBLE:       return F("isvar_double");
    case Enum::ISLIMITED_BUILD:    return F("islimited_build");
    case Enum::ISMQTT:             return F("ismqtt");
    case Enum::ISMQTTIMP:          return F("ismqttimp");
    case Enum::ISNTP:              return F("isntp");
    case Enum::ISWIFI:             return F("iswifi");
    case Enum::LCLTIME:            return F("lcltime");
    case Enum::LCLTIME_AM:         return F("lcltime_am");
    case Enum::LF:                 return F("LF");
    #if FEATURE_LAT_LONG_VAR_CMD
    case Enum::LATITUDE:           return F("latitude");
    case Enum::LONGITUDE:          return F("longitude");
    #endif // if FEATURE_LAT_LONG_VAR_CMD
    case Enum::SUNRISE_M:          return F("m_sunrise");
    case Enum::SUNSET_M:           return F("m_sunset");
    case Enum::MAC:                return F("mac");
    case Enum::MAC_INT:            return F("mac_int");
    case Enum::S_LF:               return F("N");
    #ifndef LIMIT_BUILD_SIZE
    case Enum::S_PI:               return F("pi");
    #endif // ifndef LIMIT_BUILD_SIZE
    case Enum::S_CR:               return F("R");
    case Enum::RSSI:               return F("rssi");
    case Enum::SPACE:              return F("SP");
    case Enum::SSID:               return F("ssid");
    case Enum::SUBNET:             return F("subnet");
    case Enum::SUNRISE:            return F("sunrise");
    case Enum::SUNRISE_S:          return F("s_sunrise");
    case Enum::SUNSET:             return F("sunset");
    case Enum::SUNSET_S:           return F("s_sunset");
    case Enum::SYSBUILD_DATE:      return F("sysbuild_date");
    case Enum::SYSBUILD_DESCR:     return F("sysbuild_desc");
    case Enum::SYSBUILD_FILENAME:  return F("sysbuild_filename");
    case Enum::SYSBUILD_GIT:       return F("sysbuild_git");
    case Enum::SYSBUILD_TIME:      return F("sysbuild_time");
    case Enum::SYSDAY:             return F("sysday");
    case Enum::SYSDAY_0:           return F("sysday_0");
    case Enum::SYSHEAP:            return F("sysheap");
    case Enum::SYSHOUR:            return F("syshour");
    case Enum::SYSHOUR_0:          return F("syshour_0");
    case Enum::SYSLOAD:            return F("sysload");
    case Enum::SYSMIN:             return F("sysmin");
    case Enum::SYSMIN_0:           return F("sysmin_0");
    case Enum::SYSMONTH:           return F("sysmonth");
    case Enum::SYSMONTH_S:         return F("sysmonth_s");
    case Enum::SYSMONTH_0:         return F("sysmonth_0");
    case Enum::SYSNAME:            return F("sysname");
    case Enum::SYSSEC:             return F("syssec");
    case Enum::SYSSEC_0:           return F("syssec_0");
    case Enum::SYSSEC_D:           return F("syssec_d");
    case Enum::SYSSTACK:           return F("sysstack");
    case Enum::SYSTIME:            return F("systime");
    case Enum::SYSTIME_AM:         return F("systime_am");
    case Enum::SYSTIME_AM_0:       return F("systime_am_0");
    case Enum::SYSTIME_AM_SP:      return F("systime_am_sp");
    case Enum::SYSTM_HM:           return F("systm_hm");
    case Enum::SYSTM_HM_0:         return F("systm_hm_0");
    case Enum::SYSTM_HM_AM:        return F("systm_hm_am");
    case Enum::SYSTM_HM_AM_0:      return F("systm_hm_am_0");
    case Enum::SYSTM_HM_AM_SP:     return F("systm_hm_am_sp");
    case Enum::SYSTM_HM_SP:        return F("systm_hm_sp");
    case Enum::SYSTZOFFSET:        return F("systzoffset");
    #ifndef LIMIT_BUILD_SIZE
    case Enum::SYSTZOFFSET_S:      return F("systzoffset_s");
    #endif // ifndef LIMIT_BUILD_SIZE
    case Enum::SYSWEEKDAY:         return F("sysweekday");
    case Enum::SYSWEEKDAY_S:       return F("sysweekday_s");
    case Enum::SYSYEAR:            return F("sysyear");
    case Enum::SYSYEARS:           return F("sysyears");
    case Enum::SYSYEAR_0:          return F("sysyear_0");
    case Enum::UNIT_sysvar:        return F("unit");
#if FEATURE_ZEROFILLED_UNITNUMBER
    case Enum::UNIT_0_sysvar:      return F("unit_0");
#endif // FEATURE_ZEROFILLED_UNITNUMBER
    case Enum::UNIXDAY:            return F("unixday");
    case Enum::UNIXDAY_SEC:        return F("unixday_sec");
    case Enum::UNIXTIME:           return F("unixtime");
    #ifndef LIMIT_BUILD_SIZE
    case Enum::UNIXTIME_LCL:       return F("unixtime_lcl");
    #endif // ifndef LIMIT_BUILD_SIZE
    case Enum::UPTIME:             return F("uptime");
    case Enum::UPTIME_MS:          return F("uptime_ms");
    case Enum::VCC:                return F("vcc");
    case Enum::VARIABLE:           return F("v"); // Can not be the first 'v' variable, as the name is only 1 character long
    case Enum::WI_CH:              return F("wi_ch");

    case Enum::UNKNOWN: break;
  }
  return F("Unknown");
}

#include "../Helpers/Hardware_device_info.h"

#ifdef ESP32C6

# include <soc/soc.h>
# include <soc/efuse_reg.h>
# include <soc/spi_reg.h>
# include <soc/spi_pins.h>
# include <soc/rtc.h>
# include <esp_chip_info.h>
# include <bootloader_common.h>


bool isFlashInterfacePin_ESPEasy(int gpio) {
  // FIXME TD-er: Must know whether we have internal or external flash

  // For chip variants with an in-package flash, this pin can not be used.

  // FIXME TD-er: No idea where these pins come from. Disabled for now.
  // See: https://github.com/letscontrolit/ESPEasy/issues/5220

  /*
     if ((gpio == 10) || (gpio == 11)) {
     return true;
     }
   */

  // For chip variants without an in-package flash, this pin can not be used.
  //  if (gpio == 14)
  //    return true;

  // GPIO-27: Flash voltage selector
  // GPIO-24 ... 30: Connected to internal flash (might be available when using external flash???)

  switch (gpio) {
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 0)
    case SPI_IOMUX_PIN_NUM_CS:
    case SPI_IOMUX_PIN_NUM_CLK:
    case SPI_IOMUX_PIN_NUM_MOSI:
    case SPI_IOMUX_PIN_NUM_MISO:
    case SPI_IOMUX_PIN_NUM_WP:
    case SPI_IOMUX_PIN_NUM_HD:
#else
    case MSPI_IOMUX_PIN_NUM_HD:
    case MSPI_IOMUX_PIN_NUM_WP:
    case MSPI_IOMUX_PIN_NUM_CS0:
    case MSPI_IOMUX_PIN_NUM_CLK:
    case MSPI_IOMUX_PIN_NUM_MOSI:
    case MSPI_IOMUX_PIN_NUM_MISO:
#endif
      return true;
  }
  return false;

  //  return (gpio) >= 24 && (gpio) <= 30 && gpio != 27;
}

bool flashVddPinCanBeUsedAsGPIO()
{
  return false;
}

int32_t getEmbeddedFlashSize()
{
  // See: framework-arduinoespressif32\tools\esp32-arduino-libs\esp32c6\include\soc\esp32c6\include\soc\efuse_reg.h
  const uint32_t flash_cap = REG_GET_FIELD(EFUSE_RD_MAC_SPI_SYS_4_REG, EFUSE_FLASH_CAP);

  // FIXME TD-er: No idea about meaning of values
  switch (flash_cap) {
    case 0: return 0;
    case 1: return 4;
    case 2: return 2;
    case 3: return 1;
    case 4: return 8;
  }

  // Unknown value, thus mark as negative value
  return -1 *  static_cast<int32_t>(flash_cap);
}

int32_t getEmbeddedPSRAMSize()
{
  // Doesn't have PSRAM
  return 0;
}

# ifndef isPSRAMInterfacePin
bool isPSRAMInterfacePin(int gpio) {
  return false;
}

# endif // ifndef isPSRAMInterfacePin


const __FlashStringHelper* getChipModel(uint32_t chip_model, uint32_t chip_revision, uint32_t pkg_version, bool single_core)
{
  if ((7 == chip_model) ||  // ESP32-C6(beta)
      (13 == chip_model)) { // ESP32-C6
    /* esptool:
        def get_pkg_version(self):
            num_word = 3
            block1_addr = self.EFUSE_BASE + 0x044
            word3 = self.read_reg(block1_addr + (4 * num_word))
            pkg_version = (word3 >> 21) & 0x0F
            return pkg_version
     */
    uint32_t chip_ver = REG_GET_FIELD(EFUSE_RD_MAC_SPI_SYS_3_REG, EFUSE_PKG_VERSION);
    pkg_version = chip_ver & 0x7;

    //    pkg_version = esp_efuse_get_pkg_ver();

    //    AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("HDW: ESP32 Model %d, Revision %d, Core %d, Package %d"), chip_info.model, chip_revision,
    // chip_info.cores, chip_ver);

    switch (pkg_version) {
      case 0: return F("ESP32-C6");
      case 1: return F("ESP32-C6FH4 (QFN32)");
    }

    return F("ESP32-C6");
  }

  return F("Unknown");
}

#endif // ifdef ESP32C6

#include "../Helpers/WebServer_commandHelper.h"

#include "../../ESPEasy-Globals.h"
#include "../Commands/ExecuteCommand.h"
#include "../Commands/InternalCommands_decoder.h"
#include "../Globals/EventQueue.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringParser.h"
#include "../WebServer/AccessControl.h"


HandledWebCommand_result handle_command_from_web(EventValueSource::Enum source, String& webrequest)
{
  if (!clientIPallowed()) { return HandledWebCommand_result::IP_not_allowed; }
  webrequest.trim();
  if (webrequest.isEmpty()) { return HandledWebCommand_result::NoCommand; }

  addLogMove(LOG_LEVEL_INFO,  concat(F("HTTP: "), webrequest));
  webrequest = parseTemplate(webrequest);
#ifndef BUILD_NO_DEBUG
  addLogMove(LOG_LEVEL_DEBUG, concat(F("HTTP after parseTemplate: "), webrequest));
#endif // ifndef BUILD_NO_DEBUG

  bool handledCmd = false;
  bool sendOK     = false;
  free_string(printWebString);
  printToWeb     = false;
  printToWebJSON = false;

  // in case of event, store to buffer and return...
  const String command = parseString(webrequest, 1);

  const ESPEasy_cmd_e command_e = match_ESPEasy_internal_command(command);

  if (command_e == ESPEasy_cmd_e::NotMatched) {
    // For sure not an internal command, try plugin or remote config
    printToWeb = true;
    handledCmd = ExecuteCommand_plugin_config({source, webrequest.c_str()});
    sendOK     = false;
  } else {
    if ((command_e == ESPEasy_cmd_e::event) || (command_e == ESPEasy_cmd_e::asyncevent))
    {
      eventQueue.addMove(parseStringToEndKeepCase(webrequest, 2));
      handledCmd = true;
      sendOK     = true;
    } else {
      switch (command_e) {
        case ESPEasy_cmd_e::taskrun:
        case ESPEasy_cmd_e::taskrunat:
        case ESPEasy_cmd_e::scheduletaskrun:
        case ESPEasy_cmd_e::taskvalueset:
        case ESPEasy_cmd_e::taskvaluesetandrun:
        case ESPEasy_cmd_e::taskvaluetoggle:
        case ESPEasy_cmd_e::let:
        #if FEATURE_STRING_VARIABLES
        case ESPEasy_cmd_e::letstr:
        #endif // if FEATURE_STRING_VARIABLES
#ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
        case ESPEasy_cmd_e::logportstatus:
#endif
        case ESPEasy_cmd_e::logentry:
        case ESPEasy_cmd_e::rules:
          sendOK = true;
          break;
#ifndef BUILD_NO_DIAGNOSTIC_COMMANDS
        case ESPEasy_cmd_e::jsonportstatus:
          sendOK         = true;
          printToWebJSON = true;
          break;
#endif
#if FEATURE_USE_IPV6
        case ESPEasy_cmd_e::ip6:
          sendOK         = true;
          printToWebJSON = true;
          break;
#endif
        default:
          sendOK = false;
          break;
      }

      // handledCmd = true;
    } 
    if (!handledCmd) {
      printToWeb = true;
      handledCmd = ExecuteCommand_internal({source, webrequest.c_str()});
    }
  }

  if (handledCmd) {
    if (sendOK) {
      String reply = printWebString.isEmpty() ? F("OK") : printWebString;
      if (printToWebJSON) {
        removeChar(reply, '\n'); // Don't use newline in JSON.
        // Format return string of command to JSON format
        printWebString = strformat(
          F("{\"return\": %s,\"command\": %s}"),
          to_json_value(reply).c_str(),
          to_json_value(webrequest).c_str());
      } else {
        printWebString = reply;
      }
    }
    return HandledWebCommand_result::CommandHandled;
  }

  if (printToWebJSON) {
    // Format error to JSON format
    printWebString = strformat(
      F("{\"return\": \"Unknown or restricted command\",\"command\": \"%s\"}"),
      webrequest.c_str());
  }
  return HandledWebCommand_result::Unknown_or_restricted_command;
}

#include "../Helpers/I2C_access.h"

#include "../DataStructs/TimingStats.h"
#include "../Globals/I2Cdev.h"
#include "../Globals/Settings.h"
#include "../Helpers/ESPEasy_time_calc.h"
#include "../Helpers/Hardware_I2C.h"
#include "../Helpers/StringConverter.h"

#if FEATURE_I2C_MULTIPLE
# include "../WebServer/Markup_Forms.h"
#endif // if FEATURE_I2C_MULTIPLE

enum class I2C_clear_bus_state {
  Start,
  Wait_SCL_become_high,     // Wait for 2.5 seconds for SCL to become high after enabling pull-up resistors
  Wait_SDA_become_high,
  Wait_SCL_SDA_become_high, // SDA is low, try to toggle SCL and wait for it to be freed.
};


// Code to clear I2C bus as described here:
// http://www.forward.com.au/pfod/ArduinoProgramming/I2C_ClearBus/index.html
// Changed into a state machine for use in a non blocking way to be used in ESPEasy.
I2C_bus_state I2C_check_bus(int8_t scl, int8_t sda) {
  static I2C_clear_bus_state clearing_state = I2C_clear_bus_state::Start;
  static unsigned long last_state_change    = 0;
  static int clockCount                     = 20; // > 2x9 clock


  switch (clearing_state) {
    case I2C_clear_bus_state::Start:
    {
      // FIXME TD-er: Check for proper I2C pins
      if ((sda < 0) || (scl < 0)) {
        last_state_change = 0;
        return I2C_bus_state::NotConfigured;
      }

      if ((digitalRead(scl) == HIGH) && (digitalRead(sda) == HIGH)) {
        last_state_change = 0;
        return I2C_bus_state::OK;
      }

      pinMode(sda, INPUT_PULLUP); // Make SDA (data) and SCL (clock) pins Inputs with pullup.
      pinMode(scl, INPUT_PULLUP);

      clockCount        = 20;
      clearing_state    = I2C_clear_bus_state::Wait_SCL_become_high;
      last_state_change = millis();
      break;
    }

    case I2C_clear_bus_state::Wait_SCL_become_high:
    {
      // Wait 2.5 secs. This is strictly only necessary on the first power
      // up of the DS3231 module to allow it to initialize properly,
      // but is also assists in reliable programming of FioV3 boards as it gives the
      // IDE a chance to start uploaded the program
      // before existing sketch confuses the IDE by sending Serial data.
      if (digitalRead(scl) == LOW) {
        if (timePassedSince(last_state_change) > 2500) {
          clearing_state = I2C_clear_bus_state::Start;
          return I2C_bus_state::SCL_Low; // I2C bus error. Could not clear SCL clock line held low
        }
        return I2C_bus_state::ClearingProcessActive;
      }
      clearing_state    = I2C_clear_bus_state::Wait_SDA_become_high;
      last_state_change = millis();
      break;
    }

    case I2C_clear_bus_state::Wait_SDA_become_high:
    {
      boolean SDA_LOW = (digitalRead(sda) == LOW); // vi. Check SDA input.

      while (SDA_LOW && (clockCount > 0)) {        //  vii. If SDA is Low,
        clockCount--;

        // Note: I2C bus is open collector so do NOT drive SCL or SDA high.
        pinMode(scl, INPUT);        // release SCL pullup so that when made output it will be LOW
        pinMode(scl, OUTPUT);       // then clock SCL Low
        delayMicroseconds(10);      //  for >5uS
        pinMode(scl, INPUT);        // release SCL LOW
        pinMode(scl, INPUT_PULLUP); // turn on pullup resistors again
        // do not force high as slave may be holding it low for clock stretching.
        delayMicroseconds(10);      //  for >5uS

        // The >5uS is so that even the slowest I2C devices are handled.
        if (digitalRead(scl) == LOW) {
          //  loop waiting for SCL to become High only wait 2sec.
          clearing_state    = I2C_clear_bus_state::Wait_SCL_SDA_become_high;
          last_state_change = millis();
          return I2C_bus_state::ClearingProcessActive;
        }
        SDA_LOW = (digitalRead(sda) == LOW);     //   and check SDA input again and loop
      }

      if (SDA_LOW) {                             // still low
        clearing_state = I2C_clear_bus_state::Start;
        return I2C_bus_state::SDA_Low_20_clocks; // I2C bus error. Could not clear. SDA data line held low
      }

      // else pull SDA line low for Start or Repeated Start
      pinMode(sda, INPUT);              // remove pullup.
      pinMode(sda, OUTPUT);             // and then make it LOW i.e. send an I2C Start or Repeated start control.
      // When there is only one I2C master a Start or Repeat Start has the same function as a Stop and clears the bus.
      // A Repeat Start is a Start occurring after a Start with no intervening Stop.
      delayMicroseconds(10);            // wait >5uS
      pinMode(sda, INPUT);              // remove output low
      pinMode(sda, INPUT_PULLUP);       // and make SDA high i.e. send I2C STOP control.
      delayMicroseconds(10);            // x. wait >5uS
      pinMode(sda, INPUT);              // and reset pins as tri-state inputs which is the default state on reset
      pinMode(scl, INPUT);
      clearing_state = I2C_clear_bus_state::Start;
      return I2C_bus_state::BusCleared; // all ok
    }

    case I2C_clear_bus_state::Wait_SCL_SDA_become_high:
    {
      if (digitalRead(scl) == LOW) {
        if (timePassedSince(last_state_change) > 2000) {
          // I2C bus error. Could not clear.
          // SCL clock line held low by slave clock stretch for >2sec
          clearing_state = I2C_clear_bus_state::Start;
          return I2C_bus_state::SDA_Low_over_2_sec;
        }
        return I2C_bus_state::ClearingProcessActive;
      }
      clearing_state    = I2C_clear_bus_state::Wait_SDA_become_high;
      last_state_change = millis();
      break;
    }
  }

  if (timePassedSince(last_state_change) > 5000) {
    // Just to prevent infinite loop
    // Should not be needed.
    clearing_state    = I2C_clear_bus_state::Start;
    last_state_change = millis();
  }
  return I2C_bus_state::ClearingProcessActive;
}

// **************************************************************************/
// Central functions for I2C data transfers
// **************************************************************************/
bool I2C_read_bytes(uint8_t i2caddr, I2Cdata_bytes& data) {
  const uint8_t size = data.getSize();

  return size == i2cdev.readBytes(i2caddr, data.getRegister(), size, data.get());
}

bool I2C_read_words(uint8_t i2caddr, I2Cdata_words& data) {
  const uint8_t size = data.getSize();

  return size == i2cdev.readWords(i2caddr, data.getRegister(), size, data.get());
}

// See https://github.com/platformio/platform-espressif32/issues/126
#ifdef ESP32

// ESP32: uint8_t TwoWire::endTransmission(bool sendStop)
  # define END_TRANSMISSION_FLAG true
#else // ifdef ESP32
// ESP8266: uint8_t TwoWire::endTransmission(uint8_t sendStop)
  # define END_TRANSMISSION_FLAG 0
#endif // ifdef ESP32

// **************************************************************************/
// Wake up I2C device
// **************************************************************************/
unsigned char I2C_wakeup(uint8_t i2caddr) {
  Wire.beginTransmission(i2caddr);
  return Wire.endTransmission();
}

// **************************************************************************/
// Writes an 8 bit value over I2C
// **************************************************************************/
bool I2C_write8(uint8_t i2caddr, uint8_t value) {
  Wire.beginTransmission(i2caddr);
  Wire.write((uint8_t)value);
  return Wire.endTransmission() == 0;
}

// **************************************************************************/
// Writes an 8 bit value over I2C to a register
// **************************************************************************/
bool I2C_write8_reg(uint8_t i2caddr, uint8_t reg, uint8_t value) {
  Wire.beginTransmission(i2caddr);
  Wire.write((uint8_t)reg);
  Wire.write((uint8_t)value);
  return Wire.endTransmission() == 0;
}

// **************************************************************************/
// Writes an 8 bit value over I2C to a 16 bit register
// **************************************************************************/
bool I2C_write8_reg16(uint8_t i2caddr, uint16_t reg, uint8_t value) {
  Wire.beginTransmission(i2caddr);
  Wire.write((uint8_t)(reg >> 8));
  Wire.write((uint8_t)reg);
  Wire.write((uint8_t)value);
  return Wire.endTransmission() == 0;
}

// **************************************************************************/
// Writes an 16 bit value over I2C
// **************************************************************************/
bool I2C_write16(uint8_t i2caddr, uint16_t value) {
  Wire.beginTransmission(i2caddr);
  Wire.write((uint8_t)(value >> 8));
  Wire.write((uint8_t)value);
  return Wire.endTransmission() == 0;
}

// **************************************************************************/
// Writes an 24 bit value over I2C
// **************************************************************************/
bool I2C_write24(uint8_t i2caddr, uint32_t value) {
  Wire.beginTransmission(i2caddr);
  Wire.write((uint8_t)(value >> 16));
  Wire.write((uint8_t)(value >> 8));
  Wire.write((uint8_t)value);
  return Wire.endTransmission() == 0;
}

// **************************************************************************/
// Writes an 32 bit value over I2C
// **************************************************************************/
bool I2C_write32(uint8_t i2caddr, uint32_t value) {
  Wire.beginTransmission(i2caddr);
  Wire.write((uint8_t)(value >> 24));
  Wire.write((uint8_t)(value >> 16));
  Wire.write((uint8_t)(value >> 8));
  Wire.write((uint8_t)value);
  return Wire.endTransmission() == 0;
}

// **************************************************************************/
// Writes an 16 bit LE value over I2C
// **************************************************************************/
bool I2C_write16_LE(uint8_t i2caddr, uint16_t value) {
  return I2C_write16(i2caddr, (value << 8) | (value >> 8));
}

// **************************************************************************/
// Writes an 16 bit value over I2C to a register
// **************************************************************************/
bool I2C_write16_reg(uint8_t i2caddr, uint8_t reg, uint16_t value) {
  Wire.beginTransmission(i2caddr);
  Wire.write((uint8_t)reg);
  Wire.write((uint8_t)(value >> 8));
  Wire.write((uint8_t)value);
  return Wire.endTransmission() == 0;
}

// **************************************************************************/
// Writes a 24 bit value over I2C to a register
// **************************************************************************/
bool I2C_write24_reg(uint8_t i2caddr, uint8_t reg, uint32_t value) {
  Wire.beginTransmission(i2caddr);
  Wire.write((uint8_t)reg);
  Wire.write((uint8_t)(value >> 16));
  Wire.write((uint8_t)(value >> 8));
  Wire.write((uint8_t)value);
  return Wire.endTransmission() == 0;
}

// **************************************************************************/
// Writes a 32 bit value over I2C to a register
// **************************************************************************/
bool I2C_write32_reg(uint8_t i2caddr, uint8_t reg, uint32_t value) {
  Wire.beginTransmission(i2caddr);
  Wire.write((uint8_t)reg);
  Wire.write((uint8_t)(value >> 24));
  Wire.write((uint8_t)(value >> 16));
  Wire.write((uint8_t)(value >> 8));
  Wire.write((uint8_t)value);
  return Wire.endTransmission() == 0;
}

// **************************************************************************/
// Writes an 16 bit value over I2C to a register
// **************************************************************************/
bool I2C_write16_LE_reg(uint8_t i2caddr, uint8_t reg, uint16_t value) {
  return I2C_write16_reg(i2caddr, reg, (value << 8) | (value >> 8));
}

// **************************************************************************/
// Writes length bytes over I2C to a register
// **************************************************************************/
bool I2C_writeBytes_reg(uint8_t i2caddr, uint8_t reg, uint8_t *buffer, uint8_t length) {
  Wire.beginTransmission(i2caddr);
  Wire.write(reg);

  for (int i = 0; i < length; i++) {
    Wire.write(*(buffer + i));
  }
  return Wire.endTransmission() == 0;
}

// **************************************************************************/
// Helper functions for read functions
// **************************************************************************/
bool I2C_setRegister(uint8_t i2caddr, uint8_t reg, bool *is_ok) {
  Wire.beginTransmission(i2caddr);
  Wire.write((uint8_t)reg);

  const bool res = Wire.endTransmission(END_TRANSMISSION_FLAG) == 0;

  /*
     0:success
     1:data too long to fit in transmit buffer
     2:received NACK on transmit of address
     3:received NACK on transmit of data
     4:other error
     See https://www.arduino.cc/en/Reference/WireEndTransmission
   */
  if (is_ok != nullptr) {
    *is_ok = res;
  }
  return res;
}

bool I2C_requestFrom(uint8_t i2caddr, uint8_t nrBytes, bool *is_ok) {
  const bool res = Wire.requestFrom(i2caddr, nrBytes) == nrBytes;

  if (is_ok != nullptr) {
    *is_ok = res;
  }
  return res;
}

// **************************************************************************/
// Reads an 8 bit value over I2C
// **************************************************************************/
uint8_t I2C_read8(uint8_t i2caddr, bool *is_ok) {
  uint8_t value{};

  if (I2C_requestFrom(i2caddr, 1, is_ok)) {
    value = Wire.read();
  }

  return value;
}

uint16_t I2C_read16(uint8_t i2caddr, bool *is_ok) {
  uint16_t value{};

  if (I2C_requestFrom(i2caddr, 2, is_ok)) {
    value = (Wire.read() << 8) | Wire.read();
  }

  return value;
}

uint32_t I2C_read24(uint8_t i2caddr, bool *is_ok) {
  uint32_t value{};

  if (I2C_requestFrom(i2caddr, 3, is_ok)) {
    value = (Wire.read() << 16) | (Wire.read() << 8) | Wire.read();
  }

  return value;
}

uint32_t I2C_read32(uint8_t i2caddr, bool *is_ok) {
  uint32_t value{};

  if (I2C_requestFrom(i2caddr, 4, is_ok)) {
    value = (Wire.read() << 24) | (Wire.read() << 16) | (Wire.read() << 8) | Wire.read();
  }

  return value;
}

// **************************************************************************/
// Reads an 8 bit value from a register over I2C
// **************************************************************************/
uint8_t I2C_read8_reg(uint8_t i2caddr, uint8_t reg, bool *is_ok) {
  uint8_t value{};

  if (I2C_setRegister(i2caddr, reg, is_ok) && I2C_requestFrom(i2caddr, 1, is_ok)) {
    value = Wire.read();
  }

  return value;
}

// **************************************************************************/
// Reads a 16 bit value starting at a given register over I2C
// **************************************************************************/
uint16_t I2C_read16_reg(uint8_t i2caddr, uint8_t reg, bool *is_ok) {
  uint16_t value(0);

  if (I2C_setRegister(i2caddr, reg, is_ok) && I2C_requestFrom(i2caddr, 2, is_ok)) {
    value = (Wire.read() << 8) | Wire.read();
  }

  return value;
}

// **************************************************************************/
// Reads a 24 bit value starting at a given register over I2C
// **************************************************************************/
int32_t I2C_read24_reg(uint8_t i2caddr, uint8_t reg, bool *is_ok) {
  int32_t value{};

  if (I2C_setRegister(i2caddr, reg, is_ok) && I2C_requestFrom(i2caddr, 3, is_ok)) {
    value = (((int32_t)Wire.read()) << 16) | (Wire.read() << 8) | Wire.read();
  }

  return value;
}

// **************************************************************************/
// Reads a 32 bit value starting at a given register over I2C
// **************************************************************************/
int32_t I2C_read32_reg(uint8_t i2caddr, uint8_t reg, bool *is_ok) {
  int32_t value{};

  if (I2C_setRegister(i2caddr, reg, is_ok) && I2C_requestFrom(i2caddr, 4, is_ok)) {
    value = (((int32_t)Wire.read()) << 24) | (((uint32_t)Wire.read()) << 16) | (Wire.read() << 8) | Wire.read();
  }

  return value;
}

// **************************************************************************/
// Reads a 16 bit value starting at a given register over I2C
// **************************************************************************/
uint16_t I2C_read16_LE_reg(uint8_t i2caddr, uint8_t reg, bool *is_ok) {
  const uint16_t temp = I2C_read16_reg(i2caddr, reg, is_ok);

  return (temp >> 8) | (temp << 8);
}

// **************************************************************************/
// Reads a signed 16 bit value starting at a given register over I2C
// **************************************************************************/
int16_t I2C_readS16_reg(uint8_t i2caddr, uint8_t reg, bool *is_ok) {
  return (int16_t)I2C_read16_reg(i2caddr, reg, is_ok);
}

int16_t I2C_readS16_LE_reg(uint8_t i2caddr, uint8_t reg, bool *is_ok) {
  return (int16_t)I2C_read16_LE_reg(i2caddr, reg, is_ok);
}

// *************************************************************************/
// Checks if a device is responding on the address
// Should be used in any I2C plugin case PLUGIN_INIT: before any initialization
// Can be used in any I2C plugin case PLUGIN_READ: to check if the device is still connected/responding
// if (!I2C_deviceCheck(configured_I2C_address)) {
//   break; // Will return the default false for success
// }
// *************************************************************************/

#if FEATURE_I2C_DEVICE_CHECK
static uint8_t deviceCheckI2C[TASKS_MAX]{};

bool I2C_deviceCheck(uint8_t     i2caddr,
                     taskIndex_t taskIndex,
                     uint8_t     maxRetries,
                     uint8_t     function) {
  if (!Settings.CheckI2Cdevice()) { return true; } // Check disabled, continue

  bool retval = true;

  if (validTaskIndex(taskIndex)) {
    if (Settings.TaskDeviceDataFeed[taskIndex] != 0) { return true; } // Remote device can't be checked

    START_TIMER;
    Wire.beginTransmission(i2caddr);

    retval = 0 == Wire.endTransmission(); // Only 0 indicates Success

    if (retval) {
      deviceCheckI2C[taskIndex] = 0;
    } else {
      if (maxRetries > 0) {
        deviceCheckI2C[taskIndex]++;

        if (deviceCheckI2C[taskIndex] >= maxRetries) {
          // Disable temporarily as device check failed
          // FIXME TD-er: Should reschedule call to PLUGIN_INIT????
          Settings.TaskDeviceEnabled[taskIndex] = false; // If the number of retries is reached, disable the device
          # ifndef BUILD_NO_DEBUG
          addLog(LOG_LEVEL_ERROR, concat(F("I2C  : Device doesn't respond for task: "), static_cast<int>(taskIndex + 1)));
          # endif // ifndef BUILD_NO_DEBUG
        }
      }
    }

    if (function != 0) {
      STOP_TIMER_TASK(getDeviceIndex_from_TaskIndex(taskIndex), function);
    }
  }
  return retval;
}

#endif // if FEATURE_I2C_DEVICE_CHECK

#undef END_TRANSMISSION_FLAG

#if FEATURE_I2C_MULTIPLE
void I2CInterfaceSelector(String  label,
                          String  id,
                          uint8_t choice,
                          bool    reloadWhenNeeded) {
  const uint8_t i2cMaxBusCount = (getI2CBusCount() > 1
                                  ? ((Settings.isI2CEnabled(1) ? 1 : 0)
                                    # if FEATURE_I2C_INTERFACE_3
                                     + (Settings.isI2CEnabled(2) ? 1 : 0)
                                    # endif // if FEATURE_I2C_INTERFACE_3
                                     )
                                  : 0) + (Settings.isI2CEnabled(0) ? 1 : 0);

  if (i2cMaxBusCount > 1) {
    static uint8_t i2cBusCount = 0;
    static int     i2cBusNumbers[3];

    if (i2cBusCount != i2cMaxBusCount) {
      i2cBusCount                = 0;
      i2cBusNumbers[i2cBusCount] = 0;
      ++i2cBusCount;

      if ((getI2CBusCount() > 1) && Settings.isI2CEnabled(1)) {
        i2cBusNumbers[i2cBusCount] = 1;
        ++i2cBusCount;
      }
      # if FEATURE_I2C_INTERFACE_3

      if ((getI2CBusCount() > 2) && Settings.isI2CEnabled(2)) {
        i2cBusNumbers[i2cBusCount] = 2;
        ++i2cBusCount;
      }
      # endif // if FEATURE_I2C_INTERFACE_3
    }
    FormSelectorOptions selector(i2cBusCount,
                                 i2cBusNumbers);
    selector.default_index = 0;
    bool reloadOnChange = false;

    # if FEATURE_I2CMULTIPLEXER

    if (reloadWhenNeeded) {
      // Only use reloadOnChange if current I2C Bus has multiplexer availability different than the other I2C Bus(s)
      bool hasMultiplexer = false;

      for (uint8_t i2cBus = 0; i2cBus < getI2CBusCount(); ++i2cBus) {
        if (i2cBus != choice) {
          hasMultiplexer |= (Settings.isI2CEnabled(i2cBus) && isI2CMultiplexerEnabled(i2cBus));
        }
      }
      reloadOnChange = (hasMultiplexer != isI2CMultiplexerEnabled(choice));
    }
    # endif // if FEATURE_I2CMULTIPLEXER
    selector.reloadonchange = reloadOnChange;
    selector.addFormSelector(label, id, choice);
  }
}

#endif // if FEATURE_I2C_MULTIPLE

#include "../Helpers/_CPlugin_Helper_webform.h"

#include "../../ESPEasy_common.h"

#include "../DataStructs/ESPEasy_EventStruct.h"
#include "../DataTypes/ESPEasy_plugin_functions.h"
#include "../Globals/CPlugins.h"
#include "../Globals/SecuritySettings.h"
#include "../Globals/Settings.h"
#include "../Helpers/_CPlugin_Helper.h"
#include "../Helpers/Networking.h"
#include "../WebServer/ESPEasy_WebServer.h"
#include "../WebServer/Markup.h"
#include "../WebServer/Markup_Forms.h"

/*********************************************************************************************\
* Functions to load and store controller settings on the web page.
\*********************************************************************************************/
const __FlashStringHelper* toString(ControllerSettingsStruct::VarType parameterIdx, bool displayName)
{
  switch (parameterIdx) {
    case ControllerSettingsStruct::CONTROLLER_USE_DNS:                  return F("Locate Controller");
    case ControllerSettingsStruct::CONTROLLER_HOSTNAME:                 return F("Controller Hostname");
    case ControllerSettingsStruct::CONTROLLER_IP:                       return F("Controller IP");
    case ControllerSettingsStruct::CONTROLLER_PORT:                     return F("Controller Port");
#if FEATURE_MQTT_TLS
    case ControllerSettingsStruct::CONTROLLER_MQTT_TLS_TYPE:            return F("Use TLS");
    case ControllerSettingsStruct::CONTROLLER_MQTT_TLS_STORE_FINGERPRINT: return F("Store Fingerprint");
    case ControllerSettingsStruct::CONTROLLER_MQTT_TLS_STORE_CERT:      return F("Store Certificate");
    case ControllerSettingsStruct::CONTROLLER_MQTT_TLS_STORE_CACERT:    return F("Store CA Certificate");
#endif // if FEATURE_MQTT_TLS

    case ControllerSettingsStruct::CONTROLLER_USER:                     return F("Controller User");
    case ControllerSettingsStruct::CONTROLLER_PASS:                     return F("Controller Password");

    case ControllerSettingsStruct::CONTROLLER_MIN_SEND_INTERVAL:        return F("Minimum Send Interval");
    case ControllerSettingsStruct::CONTROLLER_MAX_QUEUE_DEPTH:          return F("Max Queue Depth");
    case ControllerSettingsStruct::CONTROLLER_MAX_RETRIES:              return F("Max Retries");
    case ControllerSettingsStruct::CONTROLLER_FULL_QUEUE_ACTION:        return F("Full Queue Action");
    case ControllerSettingsStruct::CONTROLLER_ALLOW_EXPIRE:             return F("Allow Expire");
    case ControllerSettingsStruct::CONTROLLER_DEDUPLICATE:              return F("De-duplicate");
    case ControllerSettingsStruct::CONTROLLER_USE_LOCAL_SYSTEM_TIME:    return F("Use Local System Time");

    case ControllerSettingsStruct::CONTROLLER_CHECK_REPLY:              return F("Check Reply");

    case ControllerSettingsStruct::CONTROLLER_CLIENT_ID:                return F("Controller Client ID");
#if FEATURE_MQTT
    case ControllerSettingsStruct::CONTROLLER_UNIQUE_CLIENT_ID_RECONNECT: return F("Unique Client ID on Reconnect");
    case ControllerSettingsStruct::CONTROLLER_RETAINFLAG:               return F("Publish Retain Flag");
#endif // if FEATURE_MQTT
    case ControllerSettingsStruct::CONTROLLER_SUBSCRIBE:                return F("Controller Subscribe");
    case ControllerSettingsStruct::CONTROLLER_PUBLISH:                  return F("Controller Publish");
#if FEATURE_MQTT
    case ControllerSettingsStruct::CONTROLLER_LWT_TOPIC:                return F("Controller LWT Topic");
    case ControllerSettingsStruct::CONTROLLER_LWT_CONNECT_MESSAGE:      return F("LWT Connect Message");
    case ControllerSettingsStruct::CONTROLLER_LWT_DISCONNECT_MESSAGE:   return F("LWT Disconnect Message");
    case ControllerSettingsStruct::CONTROLLER_SEND_LWT:                 return F("Send LWT to broker");
    case ControllerSettingsStruct::CONTROLLER_WILL_RETAIN:              return F("Will Retain");
    case ControllerSettingsStruct::CONTROLLER_CLEAN_SESSION:            return F("Clean Session");
    case ControllerSettingsStruct::CONTROLLER_KEEP_ALIVE_TIME:          return F("Keep Alive Time");
#endif // if FEATURE_MQTT
    case ControllerSettingsStruct::CONTROLLER_USE_EXTENDED_CREDENTIALS: return F("Use Extended Credentials");
    case ControllerSettingsStruct::CONTROLLER_SEND_BINARY:              return F("Send Binary");
    case ControllerSettingsStruct::CONTROLLER_TIMEOUT:                  return F("Client Timeout");
    case ControllerSettingsStruct::CONTROLLER_SAMPLE_SET_INITIATOR:     return F("Sample Set Initiator");
    # if FEATURE_MQTT && FEATURE_MQTT_DISCOVER
    case ControllerSettingsStruct::CONTROLLER_AUTO_DISCOVERY_OPTION:    return F("Enable Auto Discovery");
    case ControllerSettingsStruct::CONTROLLER_AUTO_DISCOVERY_TRIGGER:   return F("Discovery Trigger topic");
    case ControllerSettingsStruct::CONTROLLER_AUTO_DISCOVERY_TOPIC:     return F("Auto Discovery topic");
    case ControllerSettingsStruct::CONTROLLER_AUTO_DISCOVERY_CONFIG:    return F("Config suffix|online message (/config|online)");
    case ControllerSettingsStruct::CONTROLLER_RETAINED_DISCOVERY_OPTION: return F("Retain Discovery");
    # endif // if FEATURE_MQTT && FEATURE_MQTT_DISCOVER

    case ControllerSettingsStruct::CONTROLLER_ENABLED:

      if (displayName) { return F("Enabled"); }
      else {             return F("controllerenabled"); }


    default:
      return F("Undefined");
  }
}

String getControllerParameterName(protocolIndex_t                   ProtocolIndex,
                                  ControllerSettingsStruct::VarType parameterIdx,
                                  bool                              displayName,
                                  bool                            & isAlternative) {
  String name;

  if (displayName) {
    EventStruct tmpEvent;
    tmpEvent.idx = parameterIdx;

    if (CPluginCall(ProtocolIndex, CPlugin::Function::CPLUGIN_GET_PROTOCOL_DISPLAY_NAME, &tmpEvent, name)) {
      // Found an alternative name for it.
      isAlternative = true;
      return name;
    }
  }
  isAlternative = false;

  name = toString(parameterIdx, displayName);

  if (!displayName) {
    // Change name to lower case and remove spaces to make it an internal name.
    name.toLowerCase();
    removeChar(name, ' ');
  }
  return name;
}

String getControllerParameterInternalName(protocolIndex_t ProtocolIndex, ControllerSettingsStruct::VarType parameterIdx) {
  bool isAlternative; // Dummy, not needed for internal name
  bool displayName = false;

  return getControllerParameterName(ProtocolIndex, parameterIdx, displayName, isAlternative);
}

String getControllerParameterDisplayName(protocolIndex_t ProtocolIndex, ControllerSettingsStruct::VarType parameterIdx, bool& isAlternative) {
  bool displayName = true;

  return getControllerParameterName(ProtocolIndex, parameterIdx, displayName, isAlternative);
}

void addControllerEnabledForm(controllerIndex_t controllerindex) {
  protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(controllerindex);

  if (!validProtocolIndex(ProtocolIndex)) {
    return;
  }

  ControllerSettingsStruct::VarType varType = ControllerSettingsStruct::CONTROLLER_ENABLED;

  bool isAlternativeDisplayName = false;
  const String displayName      = getControllerParameterDisplayName(ProtocolIndex, varType, isAlternativeDisplayName);
  const String internalName     = getControllerParameterInternalName(ProtocolIndex, varType);
  addFormCheckBox(displayName, internalName, Settings.ControllerEnabled[controllerindex]);
}

#if FEATURE_MQTT_TLS
void addCertificateFileNote(const ControllerSettingsStruct& ControllerSettings, const String& description, TLS_types tls_type) {
  const String certFile = ControllerSettings.getCertificateFilename(tls_type);

  if (!certFile.isEmpty())
  {
    String note = description;
    note += F(" <tt>");
    note += certFile;
    note += F("</tt> ");

    if (fileExists(certFile)) {
      note += F("(File exists)");
    } else {
      note += F("(Not found)");
    }
    addFormNote(note);
  }
}

#endif // if FEATURE_MQTT_TLS

void addControllerParameterForm(const ControllerSettingsStruct  & ControllerSettings,
                                controllerIndex_t                 controllerindex,
                                ControllerSettingsStruct::VarType varType) {
  protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(controllerindex);

  if (!validProtocolIndex(ProtocolIndex)) {
    return;
  }

  bool isAlternativeDisplayName = false;
  const String displayName      = getControllerParameterDisplayName(ProtocolIndex, varType, isAlternativeDisplayName);
  const String internalName     = getControllerParameterInternalName(ProtocolIndex, varType);

  switch (varType) {
    case ControllerSettingsStruct::CONTROLLER_USE_DNS:
    {
      const __FlashStringHelper *options[] = {
        F("Use IP address"),
        F("Use Hostname")
      };
      FormSelectorOptions selector(NR_ELEMENTS(options), options);
      selector.reloadonchange = true;
      selector.addFormSelector(displayName, internalName, ControllerSettings.UseDNS);
      break;
    }
    case ControllerSettingsStruct::CONTROLLER_HOSTNAME:
    {
      addFormTextBox(displayName, internalName, ControllerSettings.HostName, sizeof(ControllerSettings.HostName) - 1);
      break;
    }
    case ControllerSettingsStruct::CONTROLLER_IP:
    {
      addFormIPBox(displayName, internalName, ControllerSettings.IP);
      break;
    }
    case ControllerSettingsStruct::CONTROLLER_PORT:
    {
      addFormNumericBox(displayName, internalName, ControllerSettings.Port, 1, 65535);
      break;
    }
#if FEATURE_MQTT_TLS || FEATURE_HTTP_TLS
    case ControllerSettingsStruct::CONTROLLER_MQTT_TLS_TYPE:
    {
      const int choice                     = static_cast<int>(ControllerSettings.TLStype());
      const __FlashStringHelper *options[]{
        toString(TLS_types::NoTLS),

        //       toString(TLS_types::TLS_PSK),

        /*
                // FIXME TD-er: Disabled for now, will add selection later
                toString(TLS_types::TLS_CA_CERT),
                toString(TLS_types::TLS_FINGERPRINT),
         */
        toString(TLS_types::TLS_insecure)
      };
      constexpr int indices[] = {
        static_cast<int>(TLS_types::NoTLS),

        //        static_cast<int>(TLS_types::TLS_PSK),

        /*
              // FIXME TD-er: Disabled for now, will add selection later
              static_cast<int>(TLS_types::TLS_CA_CERT),
              static_cast<int>(TLS_types::TLS_FINGERPRINT),
         */
        static_cast<int>(TLS_types::TLS_insecure)
      };
      constexpr int nrOptions = NR_ELEMENTS(indices);
      FormSelectorOptions selector(nrOptions, options, indices);
      selector.reloadonchange = true;
      selector.addFormSelector(displayName, internalName, choice);
      addCertificateFileNote(ControllerSettings,
                             F("Certificate or FingerPrint must be stored on the filesystem in"),
                             ControllerSettings.TLStype());
      break;
    }
    case ControllerSettingsStruct::CONTROLLER_MQTT_TLS_STORE_FINGERPRINT:
    {
      /*
            // FIXME TD-er: Disabled for now, will add selection later
            const bool saveDisabled = fileExists(ControllerSettings.getCertificateFilename(TLS_types::TLS_FINGERPRINT));
            addFormCheckBox(displayName, internalName, false, saveDisabled);
            addCertificateFileNote(ControllerSettings, F("Store fingerprint in"), TLS_types::TLS_FINGERPRINT);
       */
      break;
    }
    case ControllerSettingsStruct::CONTROLLER_MQTT_TLS_STORE_CERT:
    // fall through
    case ControllerSettingsStruct::CONTROLLER_MQTT_TLS_STORE_CACERT:
    {
      /*
          // FIXME TD-er: Disabled for now, will add selection later
          //   const TLS_types tls_type =  (varType == ControllerSettingsStruct::CONTROLLER_MQTT_TLS_STORE_CACERT) ?
          //         TLS_types::TLS_CA_CERT : TLS_types::TLS_CERT;
          const bool saveDisabled = fileExists(ControllerSettings.getCertificateFilename(TLS_types::TLS_CA_CERT));
          addFormCheckBox(displayName, internalName, false, saveDisabled);

          if (saveDisabled) {
            addUnit(F("File Exists"));
          }
          addCertificateFileNote(ControllerSettings, F("Store CA Certificate in"), TLS_types::TLS_CA_CERT);
       */
      break;
    }
#endif // if FEATURE_MQTT_TLS || FEATURE_HTTP_TLS
    case ControllerSettingsStruct::CONTROLLER_USER:
    {
      const size_t fieldMaxLength =
        ControllerSettings.useExtendedCredentials() ? EXT_SECURITY_MAX_USER_LENGTH : sizeof(SecuritySettings.ControllerUser[0]) - 1;
      addFormTextBox(displayName,
                     internalName,
                     getControllerUser(controllerindex, ControllerSettings, false),
                     fieldMaxLength);
      break;
    }
    case ControllerSettingsStruct::CONTROLLER_PASS:
    {
      const size_t fieldMaxLength =
        ControllerSettings.useExtendedCredentials() ? EXT_SECURITY_MAX_PASS_LENGTH : sizeof(SecuritySettings.ControllerPassword[0]) - 1;

      if (isAlternativeDisplayName) {
        // It is not a regular password, thus use normal text field.
        addFormTextBox(displayName, internalName,
                       getControllerPass(controllerindex, ControllerSettings),
                       fieldMaxLength);
      } else {
        addFormPasswordBox(displayName, internalName,
                           getControllerPass(controllerindex, ControllerSettings),
                           fieldMaxLength);
      }
      break;
    }
    case ControllerSettingsStruct::CONTROLLER_MIN_SEND_INTERVAL:
    {
      addFormNumericBox(displayName, internalName, ControllerSettings.MinimalTimeBetweenMessages, 1, CONTROLLER_DELAY_QUEUE_DELAY_MAX);
      addUnit(F("ms"));
      break;
    }
    case ControllerSettingsStruct::CONTROLLER_MAX_QUEUE_DEPTH:
    {
      addFormNumericBox(displayName, internalName, ControllerSettings.MaxQueueDepth, 1, CONTROLLER_DELAY_QUEUE_DEPTH_MAX);
      break;
    }
    case ControllerSettingsStruct::CONTROLLER_MAX_RETRIES:
    {
      addFormNumericBox(displayName, internalName, ControllerSettings.MaxRetry, 1, CONTROLLER_DELAY_QUEUE_RETRY_MAX);
      break;
    }
    case ControllerSettingsStruct::CONTROLLER_FULL_QUEUE_ACTION:
    {
      const __FlashStringHelper *options[] {
        F("Ignore New"),
        F("Delete Oldest")
      };
      const FormSelectorOptions selector(NR_ELEMENTS(options), options);
      selector.addFormSelector(displayName, internalName, ControllerSettings.DeleteOldest);
      break;
    }
    case ControllerSettingsStruct::CONTROLLER_ALLOW_EXPIRE:
      addFormCheckBox(displayName, internalName, ControllerSettings.allowExpire());
      break;
    case ControllerSettingsStruct::CONTROLLER_DEDUPLICATE:
      addFormCheckBox(displayName, internalName, ControllerSettings.deduplicate());
      break;
    case ControllerSettingsStruct::CONTROLLER_USE_LOCAL_SYSTEM_TIME:
      addFormCheckBox(displayName, internalName, ControllerSettings.useLocalSystemTime());
      break;
    case ControllerSettingsStruct::CONTROLLER_CHECK_REPLY:
    {
      const __FlashStringHelper *options[] = {
        F("Ignore Acknowledgement"),
        F("Check Acknowledgement")
      };
      const FormSelectorOptions selector(NR_ELEMENTS(options), options);
      selector.addFormSelector(displayName, internalName, ControllerSettings.MustCheckReply);
      break;
    }
    case ControllerSettingsStruct::CONTROLLER_CLIENT_ID:
      addFormTextBox(displayName, internalName, ControllerSettings.ClientID, sizeof(ControllerSettings.ClientID) - 1);
      break;
#if FEATURE_MQTT
    case ControllerSettingsStruct::CONTROLLER_UNIQUE_CLIENT_ID_RECONNECT:
      addFormCheckBox(displayName, internalName, ControllerSettings.mqtt_uniqueMQTTclientIdReconnect());
      break;
    case ControllerSettingsStruct::CONTROLLER_RETAINFLAG:
      addFormCheckBox(displayName, internalName, ControllerSettings.mqtt_retainFlag());
      break;
#endif // if FEATURE_MQTT
    case ControllerSettingsStruct::CONTROLLER_SUBSCRIBE:
      addFormTextBox(displayName, internalName, ControllerSettings.Subscribe, sizeof(ControllerSettings.Subscribe) - 1);
      break;
    case ControllerSettingsStruct::CONTROLLER_PUBLISH:
      addFormTextBox(displayName, internalName, ControllerSettings.Publish,   sizeof(ControllerSettings.Publish) - 1);
      break;
#if FEATURE_MQTT
    case ControllerSettingsStruct::CONTROLLER_LWT_TOPIC:
      addFormTextBox(displayName, internalName, ControllerSettings.MQTTLwtTopic,         sizeof(ControllerSettings.MQTTLwtTopic) - 1);
      break;
    case ControllerSettingsStruct::CONTROLLER_LWT_CONNECT_MESSAGE:
      addFormTextBox(displayName, internalName, ControllerSettings.LWTMessageConnect,    sizeof(ControllerSettings.LWTMessageConnect) - 1);
      break;
    case ControllerSettingsStruct::CONTROLLER_LWT_DISCONNECT_MESSAGE:
      addFormTextBox(displayName, internalName, ControllerSettings.LWTMessageDisconnect, sizeof(ControllerSettings.LWTMessageDisconnect) - 1);
      break;
    case ControllerSettingsStruct::CONTROLLER_SEND_LWT:
      addFormCheckBox(displayName, internalName, ControllerSettings.mqtt_sendLWT());
      break;
    case ControllerSettingsStruct::CONTROLLER_WILL_RETAIN:
      addFormCheckBox(displayName, internalName, ControllerSettings.mqtt_willRetain());
      break;
    case ControllerSettingsStruct::CONTROLLER_CLEAN_SESSION:
      addFormCheckBox(displayName, internalName, ControllerSettings.mqtt_cleanSession());
      break;
    case ControllerSettingsStruct::CONTROLLER_KEEP_ALIVE_TIME:
      addFormNumericBox(displayName, internalName, ControllerSettings.KeepAliveTime, 0, CONTROLLER_KEEP_ALIVE_TIME_MAX);
      addUnit(F("sec"));
      break;
    # if FEATURE_MQTT_DISCOVER
    case ControllerSettingsStruct::CONTROLLER_AUTO_DISCOVERY_OPTION:
      addFormCheckBox(displayName, internalName, ControllerSettings.mqtt_autoDiscovery());
      break;
    case ControllerSettingsStruct::CONTROLLER_AUTO_DISCOVERY_TRIGGER:
      addFormTextBox(displayName, internalName, ControllerSettings.MqttAutoDiscoveryTrigger,
                     sizeof(ControllerSettings.MqttAutoDiscoveryTrigger) - 1);
      break;
    case ControllerSettingsStruct::CONTROLLER_AUTO_DISCOVERY_TOPIC:
      addFormTextBox(displayName, internalName, ControllerSettings.MqttAutoDiscoveryTopic,
                     sizeof(ControllerSettings.MqttAutoDiscoveryTopic) - 1);
      break;
    case ControllerSettingsStruct::CONTROLLER_AUTO_DISCOVERY_CONFIG:
      addFormTextBox(displayName, internalName, ControllerSettings.MqttAutoDiscoveryConfig,
                     sizeof(ControllerSettings.MqttAutoDiscoveryConfig) - 1);
      break;
    case ControllerSettingsStruct::CONTROLLER_RETAINED_DISCOVERY_OPTION:
      addFormCheckBox(displayName, internalName, ControllerSettings.mqtt_retainDiscovery());
      break;
    # endif // if FEATURE_MQTT_DISCOVER
#endif // if FEATURE_MQTT
    case ControllerSettingsStruct::CONTROLLER_USE_EXTENDED_CREDENTIALS:
      addFormCheckBox(displayName, internalName, ControllerSettings.useExtendedCredentials());
      break;
    case ControllerSettingsStruct::CONTROLLER_SEND_BINARY:
      addFormCheckBox(displayName, internalName, ControllerSettings.sendBinary());
      break;
    case ControllerSettingsStruct::CONTROLLER_TIMEOUT:
      addFormNumericBox(displayName, internalName, ControllerSettings.ClientTimeout, 10, CONTROLLER_CLIENTTIMEOUT_MAX);
      addUnit(F("ms"));
      break;
    case ControllerSettingsStruct::CONTROLLER_SAMPLE_SET_INITIATOR:
      addTaskSelectBox(displayName, internalName, ControllerSettings.SampleSetInitiator);
      break;
    case ControllerSettingsStruct::CONTROLLER_ENABLED:
      addFormCheckBox(displayName, internalName, Settings.ControllerEnabled[controllerindex]);
      break;
  }
}

void saveControllerParameterForm(ControllerSettingsStruct        & ControllerSettings,
                                 controllerIndex_t                 controllerindex,
                                 ControllerSettingsStruct::VarType varType) {
  const protocolIndex_t ProtocolIndex =
    getProtocolIndex_from_ControllerIndex(controllerindex);

  if (!validProtocolIndex(ProtocolIndex)) {
    return;
  }
  const String internalName = getControllerParameterInternalName(ProtocolIndex, varType);

  switch (varType) {
    case ControllerSettingsStruct::CONTROLLER_USE_DNS:  ControllerSettings.UseDNS = getFormItemInt(internalName); break;
    case ControllerSettingsStruct::CONTROLLER_HOSTNAME:

      if (ControllerSettings.UseDNS)
      {
        strncpy_webserver_arg(ControllerSettings.HostName, internalName);
        IPAddress IP;
        resolveHostByName(ControllerSettings.HostName, IP, ControllerSettings.ClientTimeout);

        for (uint8_t x = 0; x < 4; x++) {
          ControllerSettings.IP[x] = IP[x];
        }
      }
      break;
    case ControllerSettingsStruct::CONTROLLER_IP:

      if (!ControllerSettings.UseDNS)
      {
        str2ip(webArg(internalName), ControllerSettings.IP);
      }
      break;
    case ControllerSettingsStruct::CONTROLLER_PORT:
      ControllerSettings.Port = getFormItemInt(internalName, ControllerSettings.Port);
      break;
#if FEATURE_MQTT_TLS || FEATURE_HTTP_TLS
    case ControllerSettingsStruct::CONTROLLER_MQTT_TLS_TYPE:
    {
      const int current        = static_cast<int>(ControllerSettings.TLStype());
      const TLS_types tls_type = static_cast<TLS_types>(getFormItemInt(internalName, current));
      ControllerSettings.TLStype(tls_type);
      break;
    }

    case ControllerSettingsStruct::CONTROLLER_MQTT_TLS_STORE_FINGERPRINT:
    {
      if (isFormItemChecked(internalName)) {
        String fingerprint;

        if (GetTLSfingerprint(fingerprint)) {
          if (ControllerSettings.UseDNS) {
            fingerprint += '\n';
            fingerprint += ControllerSettings.getHost();
          }
          SaveCertificate(ControllerSettings.getCertificateFilename(TLS_types::TLS_FINGERPRINT), fingerprint);
        }
      }
      break;
    }

    case ControllerSettingsStruct::CONTROLLER_MQTT_TLS_STORE_CERT:
    // fall through
    case ControllerSettingsStruct::CONTROLLER_MQTT_TLS_STORE_CACERT:
    {
      if (isFormItemChecked(internalName)) {
        String cacert;

        if (GetTLS_Certificate(cacert, true)) {
          SaveCertificate(ControllerSettings.getCertificateFilename(TLS_types::TLS_CA_CERT), cacert);
        }
      }
      break;
    }
#endif // if FEATURE_MQTT_TLS || FEATURE_HTTP_TLS
    case ControllerSettingsStruct::CONTROLLER_USER:
      setControllerUser(controllerindex, ControllerSettings, webArg(internalName));
      break;
    case ControllerSettingsStruct::CONTROLLER_PASS:
    {
      String password;

      if (getFormPassword(internalName, password)) {
        setControllerPass(controllerindex, ControllerSettings, password);
      }
      break;
    }

    case ControllerSettingsStruct::CONTROLLER_MIN_SEND_INTERVAL:
      ControllerSettings.MinimalTimeBetweenMessages = getFormItemInt(internalName, ControllerSettings.MinimalTimeBetweenMessages);
      break;
    case ControllerSettingsStruct::CONTROLLER_MAX_QUEUE_DEPTH:
      ControllerSettings.MaxQueueDepth = getFormItemInt(internalName, ControllerSettings.MaxQueueDepth);
      break;
    case ControllerSettingsStruct::CONTROLLER_MAX_RETRIES:
      ControllerSettings.MaxRetry = getFormItemInt(internalName, ControllerSettings.MaxRetry);
      break;
    case ControllerSettingsStruct::CONTROLLER_FULL_QUEUE_ACTION:
      ControllerSettings.DeleteOldest = getFormItemInt(internalName, ControllerSettings.DeleteOldest);
      break;
    case ControllerSettingsStruct::CONTROLLER_ALLOW_EXPIRE:
      ControllerSettings.allowExpire(isFormItemChecked(internalName));
      break;
    case ControllerSettingsStruct::CONTROLLER_DEDUPLICATE:
      ControllerSettings.deduplicate(isFormItemChecked(internalName));
      break;
    case ControllerSettingsStruct::CONTROLLER_USE_LOCAL_SYSTEM_TIME:
      ControllerSettings.useLocalSystemTime(isFormItemChecked(internalName));
      break;
    case ControllerSettingsStruct::CONTROLLER_CHECK_REPLY:
      ControllerSettings.MustCheckReply = getFormItemInt(internalName, ControllerSettings.MustCheckReply);
      break;
    case ControllerSettingsStruct::CONTROLLER_CLIENT_ID:
      strncpy_webserver_arg(ControllerSettings.ClientID, internalName);
      break;
#if FEATURE_MQTT
    case ControllerSettingsStruct::CONTROLLER_UNIQUE_CLIENT_ID_RECONNECT:
      ControllerSettings.mqtt_uniqueMQTTclientIdReconnect(isFormItemChecked(internalName));
      break;
    case ControllerSettingsStruct::CONTROLLER_RETAINFLAG:
      ControllerSettings.mqtt_retainFlag(isFormItemChecked(internalName));
      break;
#endif // if FEATURE_MQTT
    case ControllerSettingsStruct::CONTROLLER_SUBSCRIBE:
      strncpy_webserver_arg(ControllerSettings.Subscribe, internalName);
      break;
    case ControllerSettingsStruct::CONTROLLER_PUBLISH:
      strncpy_webserver_arg(ControllerSettings.Publish,   internalName);
      break;
#if FEATURE_MQTT
    case ControllerSettingsStruct::CONTROLLER_LWT_TOPIC:
      strncpy_webserver_arg(ControllerSettings.MQTTLwtTopic,         internalName);
      break;
    case ControllerSettingsStruct::CONTROLLER_LWT_CONNECT_MESSAGE:
      strncpy_webserver_arg(ControllerSettings.LWTMessageConnect,    internalName);
      break;
    case ControllerSettingsStruct::CONTROLLER_LWT_DISCONNECT_MESSAGE:
      strncpy_webserver_arg(ControllerSettings.LWTMessageDisconnect, internalName);
      break;
    case ControllerSettingsStruct::CONTROLLER_SEND_LWT:
      ControllerSettings.mqtt_sendLWT(isFormItemChecked(internalName));
      break;
    case ControllerSettingsStruct::CONTROLLER_WILL_RETAIN:
      ControllerSettings.mqtt_willRetain(isFormItemChecked(internalName));
      break;
    case ControllerSettingsStruct::CONTROLLER_CLEAN_SESSION:
      ControllerSettings.mqtt_cleanSession(isFormItemChecked(internalName));
      break;
    case ControllerSettingsStruct::CONTROLLER_KEEP_ALIVE_TIME:
      ControllerSettings.KeepAliveTime = getFormItemInt(internalName, ControllerSettings.KeepAliveTime);
      if (0 == ControllerSettings.KeepAliveTime) {
        ControllerSettings.KeepAliveTime = CONTROLLER_KEEP_ALIVE_TIME_DFLT;
      }
      break;
    # if FEATURE_MQTT_DISCOVER
    case ControllerSettingsStruct::CONTROLLER_AUTO_DISCOVERY_OPTION:
      ControllerSettings.mqtt_autoDiscovery(isFormItemChecked(internalName));
      break;
    case ControllerSettingsStruct::CONTROLLER_AUTO_DISCOVERY_TRIGGER:
      strncpy_webserver_arg(ControllerSettings.MqttAutoDiscoveryTrigger, internalName);
      break;
    case ControllerSettingsStruct::CONTROLLER_AUTO_DISCOVERY_TOPIC:
      strncpy_webserver_arg(ControllerSettings.MqttAutoDiscoveryTopic,   internalName);
      break;
    case ControllerSettingsStruct::CONTROLLER_AUTO_DISCOVERY_CONFIG:
      strncpy_webserver_arg(ControllerSettings.MqttAutoDiscoveryConfig,  internalName);
      break;
    case ControllerSettingsStruct::CONTROLLER_RETAINED_DISCOVERY_OPTION:
      ControllerSettings.mqtt_retainDiscovery(isFormItemChecked(internalName));
      break;
    # endif // if FEATURE_MQTT_DISCOVER
#endif // if FEATURE_MQTT
    case ControllerSettingsStruct::CONTROLLER_USE_EXTENDED_CREDENTIALS:
      ControllerSettings.useExtendedCredentials(isFormItemChecked(internalName));
      break;
    case ControllerSettingsStruct::CONTROLLER_SEND_BINARY:
      ControllerSettings.sendBinary(isFormItemChecked(internalName));
      break;
    case ControllerSettingsStruct::CONTROLLER_TIMEOUT:
      ControllerSettings.ClientTimeout = getFormItemInt(internalName, ControllerSettings.ClientTimeout);
      break;
    case ControllerSettingsStruct::CONTROLLER_SAMPLE_SET_INITIATOR:
      ControllerSettings.SampleSetInitiator = getFormItemInt(internalName, ControllerSettings.SampleSetInitiator);
      break;
    case ControllerSettingsStruct::CONTROLLER_ENABLED:
      Settings.ControllerEnabled[controllerindex] = isFormItemChecked(internalName);
      break;
  }
}

#include "../Helpers/Scheduler.h"


#include "../DataStructs/Scheduler_RulesTimerID.h"

#include "../ESPEasyCore/ESPEasyRules.h"

#include "../Globals/Settings.h"

#include "../Helpers/ESPEasy_time_calc.h"


/*********************************************************************************************\
* Rules Timer
\*********************************************************************************************/
static bool checkRulesTimerIndex(unsigned int timerIndex) {
  if ((timerIndex > RULES_TIMER_MAX) || (timerIndex == 0)) {
    if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
      String log = F("TIMER: invalid timer number ");
      log += timerIndex;
      addLogMove(LOG_LEVEL_ERROR, log);
    }
    return false;
  }
  return true;
}

bool ESPEasy_Scheduler::setRulesTimer(unsigned long msecFromNow, unsigned int timerIndex, int recurringCount, bool startImmediately) {
  if (!checkRulesTimerIndex(timerIndex)) { return false; }

  const RulesTimerID timerID(timerIndex);

  const systemTimerStruct timer_data(recurringCount, msecFromNow, timerIndex);

  systemTimers[timerID.mixed_id] = timer_data;
  setNewTimerAt(timerID, millis() + (startImmediately ? 10 : msecFromNow));
  return true;
}

void ESPEasy_Scheduler::process_rules_timer(SchedulerTimerID id, unsigned long lasttimer) {
  auto it = systemTimers.find(id.mixed_id);

  if (it == systemTimers.end()) { return; }

  if (it->second.isPaused()) {
    // Timer is paused.
    // Must keep this timer 'active' in the scheduler.
    // However it does not need to be looked at frequently as the resume function re-schedules it when needed.
    // Look for its state every second.
    setNewTimerAt(id, millis() + 1000);
    return;
  }

  // Create a deep copy of the timer data as we may delete it from the map before sending the event.
  const int loopCount  = it->second.getLoopCount();
  const int timerIndex = it->second.getTimerIndex();

  // Reschedule before sending the event, as it may get rescheduled in handling the timer event.
  if (it->second.isRecurring()) {
    // Recurring timer
    unsigned long newTimer = lasttimer;
    setNextTimeInterval(newTimer, it->second.getInterval());
    setNewTimerAt(id, newTimer);
    it->second.markNextRecurring();
  } else {
    systemTimers.erase(id.mixed_id);
  }

  if (loopCount > 0) {
    // Should be executed
    if (Settings.UseRules) {
      String event = F("Rules#Timer=");
      event += timerIndex;

      // Add count as 2nd eventvalue
      event += ',';
      event += loopCount;
      rulesProcessing(event); // TD-er: Do not add to the eventQueue, but execute right now.
    }
  }
}

bool ESPEasy_Scheduler::pause_rules_timer(unsigned long timerIndex) {
  if (!checkRulesTimerIndex(timerIndex)) { return false; }
  const RulesTimerID timerID(timerIndex);
  auto it = systemTimers.find(timerID.mixed_id);

  if (it == systemTimers.end()) {
    #ifndef BUILD_MINIMAL_OTA
    addLog(LOG_LEVEL_INFO, F("TIMER: no timer set"));
    #endif
    return false;
  }

  unsigned long timer;

  if (msecTimerHandler.getTimerForId(timerID.mixed_id, timer)) {
    if (it->second.isPaused()) {
      #ifndef BUILD_MINIMAL_OTA
      addLog(LOG_LEVEL_INFO, F("TIMER: already paused"));
      #endif
    } else {
      // Store remainder of interval
      const long timeLeft = timePassedSince(timer) * -1;

      if (timeLeft > 0) {
        it->second.setRemainder(timeLeft);
        return true;
      }
    }
  }
  return false;
}

bool ESPEasy_Scheduler::resume_rules_timer(unsigned long timerIndex) {
  if (!checkRulesTimerIndex(timerIndex)) { return false; }
  const RulesTimerID timerID(timerIndex);
  auto it = systemTimers.find(timerID.mixed_id);

  if (it == systemTimers.end()) { return false; }

  unsigned long timer;

  if (msecTimerHandler.getTimerForId(timerID.mixed_id, timer)) {
    if (it->second.isPaused()) {
      // Reschedule timer with remainder of interval
      setNewTimerAt(timerID, millis() + it->second.getRemainder());
      it->second.setRemainder(0);
      return true;
    }
  }
  return false;
}

#include "../Helpers/Improv_Helper.h"

#if FEATURE_IMPROV
# include "../CustomBuild/CompiletimeDefines.h"
# include "../ESPEasyCore/ESPEasyNetwork.h"
# include "../Globals/SecuritySettings.h"
# include "../Helpers/ESPEasy_Storage.h"
# include "../Helpers/Hardware_device_info.h"
# include "../Helpers/StringConverter.h"
# include "../Helpers/StringGenerator_System.h"


void OnImprovError(ImprovTypes::Error error)
{
  if ((error != ImprovTypes::Error::ERROR_NONE) &&
      loglevelActiveFor(LOG_LEVEL_ERROR)) {
    String log = F("IMPROV : ");

    switch (error) {
      case ImprovTypes::Error::ERROR_NONE: return;
      case ImprovTypes::Error::ERROR_INVALID_RPC:       log += F("Invalid RPC"); break;
      case ImprovTypes::Error::ERROR_UNKNOWN_RPC:       log += F("Unkown RPC"); break;
      case ImprovTypes::Error::ERROR_UNABLE_TO_CONNECT: log += F("Unable to connect"); break;
      case ImprovTypes::Error::ERROR_NOT_AUTHORIZED:    log += F("Not Authorized"); break;
      case ImprovTypes::Error::ERROR_INVALID_CHECKSUM:  log += F("Invalid Checksum"); break;
      case ImprovTypes::Error::ERROR_EMPTY_SSID:        log += F("Empty SSID"); break;
      case ImprovTypes::Error::ERROR_UNKNOWN:           log += F("Unknown"); break;
    }
    addLogMove(LOG_LEVEL_ERROR, log);
  }
}

void OnImprovConnected(const char *ssid, const char *password)
{
  safe_strncpy(
    SecuritySettings.WifiSSID,
    ssid,
    sizeof(SecuritySettings.WifiSSID));
  safe_strncpy(
    SecuritySettings.WifiKey,
    password,
    sizeof(SecuritySettings.WifiKey));
  SaveSecuritySettings();
}

bool OnImprovESPEasyConnectWiFi(const char *ssid, const char *password)
{
//  addLog(LOG_LEVEL_INFO, strformat(F("IMPROV WiFi connect: SSID: %s, Pass: %s"), ssid, password));
/*
  safe_strncpy(
    SecuritySettings.WifiSSID,
    ssid,
    sizeof(SecuritySettings.WifiSSID));
  safe_strncpy(
    SecuritySettings.WifiKey,
    password,
    sizeof(SecuritySettings.WifiKey));
  */

  return false;
}

Improv_Helper_t::Improv_Helper_t() {}

void Improv_Helper_t::init()
{
  // Set callback functions
  _improv.onImprovError(OnImprovError);
  _improv.onImprovConnected(OnImprovConnected);

  // FIXME TD-er: Implement callback to use ESPEasy functions to connect to WiFi
//  _improv.setCustomTryConnectToWiFi(OnImprovESPEasyConnectWiFi);

  String firmwareName      = get_binary_filename();
  const String buildString = getSystemBuildString();

  firmwareName.replace(F("ESP_Easy_mega_"), F(""));
  firmwareName.replace(buildString,         F(""));
  firmwareName.replace('_',                 ' ');

  // Remove chip variant from the name
  const int pos = firmwareName.indexOf(F("ESP"));

  if (pos != -1) {
    const int endpos = firmwareName.indexOf(' ', pos);

    if (endpos != -1) {
      firmwareName.replace(firmwareName.substring(pos, endpos), F(""));
    }
  }
  firmwareName.replace(F("  "), F(" "));
  firmwareName.trim();

  _improv.setDeviceInfo(
    firmwareName.c_str(),
    buildString.c_str(),
    NetworkGetHostNameFromSettings().c_str());

  String chipModel = getChipModel();

  //  chipModel += F(" rev ");
  //  chipModel += getChipRevision();
  chipModel += '/';
  chipModel += (getFlashRealSizeInBytes() >> 20);
  chipModel += 'M';

  // TD-er: Removed chip features description as it is just too much info.

  /*
   # ifdef ESP32
     chipModel += ' ';
     chipModel += getChipFeaturesString();
   # endif // ifdef ESP32
   */
  _improv.setDeviceChipInfo(chipModel.c_str());
}

bool Improv_Helper_t::handle(uint8_t b, Stream *serialForWrite)
{
  #ifdef USE_SECOND_HEAP
  // Do not store in 2nd heap, std::list cannot handle 2nd heap well
  HeapSelectDram ephemeral;
  #endif // ifdef USE_SECOND_HEAP

  _tmpbuffer.push_back(b);

  const ImprovTypes::ParseState state = _improv.handleSerial(b, serialForWrite);

#ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG) &&
      state != ImprovTypes::ParseState::VALID_INCOMPLETE)
  {
    String log = F("IMPROVDEBUG: ");
    for (auto it = _tmpbuffer.begin(); it != _tmpbuffer.end(); ++it) {
      if (isAlphaNumeric(*it))
        log += static_cast<char>(*it);
      else {
        log += strformat(F("_%d_"), *it);
      }          
    }
    if (state == ImprovTypes::ParseState::INVALID) {
      log += F(" (invalid)");
    }
    addLog(LOG_LEVEL_DEBUG, log);
  }
#endif

  switch (state) {
    case ImprovTypes::ParseState::VALID_INCOMPLETE:
      _mustDumpBuffer = false;
      return true;
    case ImprovTypes::ParseState::VALID_COMPLETE:
      _tmpbuffer.clear();
      _mustDumpBuffer = false;
      return true;
    case ImprovTypes::ParseState::INVALID:
      //   _mustDumpBuffer = true;
      break;
  }
  _mustDumpBuffer = true;
  return false;
}

bool Improv_Helper_t::getFromBuffer(uint8_t& b)
{
  if (available() == 0) { return false; }
  b = _tmpbuffer.front();
  _tmpbuffer.pop_front();

  if (_tmpbuffer.size() == 0) {
    _mustDumpBuffer = false;
  }
  return true;
}

size_t Improv_Helper_t::available() const
{
  if (_mustDumpBuffer) { return _tmpbuffer.size(); }
  return 0u;
}

#endif // if FEATURE_IMPROV

#include "../Helpers/Modbus_RTU.h"

#if FEATURE_MODBUS

#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../Helpers/ESPEasy_time_calc.h"
#include "../Helpers/StringConverter.h"


ModbusRTU_struct::~ModbusRTU_struct() {
  if (easySerial != nullptr) {
    delete easySerial;
    easySerial = nullptr;
  }
}

void ModbusRTU_struct::reset() {
  if (easySerial != nullptr) {
    delete easySerial;
    easySerial = nullptr;
  }
  free_string(detected_device_description);

  for (int i = 0; i < 8; ++i) {
    _sendframe[i] = 0;
  }
  _sendframe_used = 0;

  for (int i = 0; i < MODBUS_RECEIVE_BUFFER; ++i) {
    _recv_buf[i] = 0xff;
  }
  _recv_buf_used    = 0;
  _modbus_address   = MODBUS_BROADCAST_ADDRESS;
  _reads_pass       = 0;
  _reads_crc_failed = 0;
  _reads_nodata     = 0;
}

bool ModbusRTU_struct::init(const ESPEasySerialPort port, const int16_t serial_rx, const int16_t serial_tx, int16_t baudrate, uint8_t address) {
  return init(port, serial_rx, serial_tx, baudrate, address, -1);
}

bool ModbusRTU_struct::init(const ESPEasySerialPort port, const int16_t serial_rx, const int16_t serial_tx, int16_t baudrate, uint8_t address, int8_t dere_pin) {
  if ((serial_rx < 0) || (serial_tx < 0)) {
    return false;
  }
  reset();
  {
    # ifdef USE_SECOND_HEAP
    HeapSelectDram ephemeral;
    # endif // ifdef USE_SECOND_HEAP

    easySerial = new (std::nothrow) ESPeasySerial(port, serial_rx, serial_tx);
  }
  if (easySerial == nullptr) { return false; }
  easySerial->begin(baudrate);

  if (!isInitialized()) { return false; }
  _modbus_address = address;
  _dere_pin       = dere_pin;

  if (_dere_pin != -1) { // set output pin mode for DE/RE pin when used (for control MAX485)
    pinMode(_dere_pin, OUTPUT);
  }

  detected_device_description = getDevice_description(_modbus_address);

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    String log; // = F("Modbus detected: ");
    log += detected_device_description;
    addLogMove(LOG_LEVEL_INFO, log);
    modbus_log_MEI(_modbus_address);
  }
  return true;
}

bool ModbusRTU_struct::isInitialized() const {
  return easySerial != nullptr;
}

void ModbusRTU_struct::getStatistics(uint32_t& pass, uint32_t& fail, uint32_t& nodata) const {
  pass   = _reads_pass;
  fail   = _reads_crc_failed;
  nodata = _reads_nodata;
}

void ModbusRTU_struct::setModbusTimeout(uint16_t timeout) {
  _modbus_timeout = timeout;
}

uint16_t ModbusRTU_struct::getModbusTimeout() const {
  return _modbus_timeout;
}

String ModbusRTU_struct::getDevice_description(uint8_t slaveAddress) {
  bool more_follows     = true;
  uint8_t next_object_id   = 0;
  uint8_t conformity_level = 0;
  unsigned int object_value_int;
  String description;
  String obj_text;

  for (uint8_t object_id = 0; object_id < 0x84; ++object_id) {
    if (object_id == 6) {
      object_id = 0x82; // Skip to the serialnr/sensor type
    }
    int result = modbus_get_MEI(slaveAddress, object_id, obj_text,
                                object_value_int, next_object_id,
                                more_follows, conformity_level);
    String label;

    switch (object_id) {
      case 0x01:

        if (result == 0) { label = F("Pcode"); }
        break;
      case 0x02:

        if (result == 0) { label = F("Rev"); }
        break;
      case 0x82:
      {
        if (result != 0) {
          uint32_t sensorId = readSensorId();
          obj_text = String(sensorId, HEX);
          result   = 0;
        }

        label = F("S/N");
        break;
      }
      case 0x83:
      {
        if (result != 0) {
          uint32_t sensorId = readTypeId();
          obj_text = String(sensorId, HEX);
          result   = 0;
        }

        label = F("Type");
        break;
      }
      default:
        break;
    }

    if (result == 0) {
      if (label.length() > 0) {
        // description += MEI_objectid_to_name(object_id);
        description += label;
        description += F(": ");
      }

      if (obj_text.length() > 0) {
        description += obj_text;
        description += F(" - ");
      }
    }
  }
  return description;
}

// Read from RAM or EEPROM
void ModbusRTU_struct::buildRead_RAM_EEPROM(uint8_t slaveAddress, uint8_t functionCode,
                                            short startAddress, uint8_t number_bytes) {
  _sendframe[0]   = slaveAddress;
  _sendframe[1]   = functionCode;
  _sendframe[2]   = (uint8_t)(startAddress >> 8);
  _sendframe[3]   = (uint8_t)(startAddress & 0xFF);
  _sendframe[4]   = number_bytes;
  _sendframe_used = 5;
}

// Write to the Special Control Register (SCR)
void ModbusRTU_struct::buildWriteCommandRegister(uint8_t slaveAddress, uint8_t value) {
  _sendframe[0]   = slaveAddress;
  _sendframe[1]   = MODBUS_CMD_WRITE_RAM;
  _sendframe[2]   = 0;    // Address-Hi SCR  (0x0060)
  _sendframe[3]   = 0x60; // Address-Lo SCR
  _sendframe[4]   = 1;    // Count
  _sendframe[5]   = value;
  _sendframe_used = 6;
}

void ModbusRTU_struct::buildWriteMult16bRegister(uint8_t slaveAddress, uint16_t startAddress, uint16_t value) {
  _sendframe[0]   = slaveAddress;
  _sendframe[1]   = MODBUS_WRITE_MULTIPLE_REGISTERS;
  _sendframe[2]   = (uint8_t)(startAddress >> 8);
  _sendframe[3]   = (uint8_t)(startAddress & 0xFF);
  _sendframe[4]   = 0; // nr reg hi
  _sendframe[5]   = 1; // nr reg lo
  _sendframe[6]   = 2; // nr bytes to follow (2 bytes per register)
  _sendframe[7]   = (uint8_t)(value >> 8);
  _sendframe[8]   = (uint8_t)(value & 0xFF);
  _sendframe_used = 9;
}

void ModbusRTU_struct::buildFrame(uint8_t slaveAddress, uint8_t functionCode,
                                  short startAddress, short parameter) {
  _sendframe[0]   = slaveAddress;
  _sendframe[1]   = functionCode;
  _sendframe[2]   = (uint8_t)(startAddress >> 8);
  _sendframe[3]   = (uint8_t)(startAddress & 0xFF);
  _sendframe[4]   = (uint8_t)(parameter >> 8);
  _sendframe[5]   = (uint8_t)(parameter & 0xFF);
  _sendframe_used = 6;
}

void ModbusRTU_struct::build_modbus_MEI_frame(uint8_t slaveAddress, uint8_t device_id,
                                              uint8_t object_id) {
  _sendframe[0] = slaveAddress;
  _sendframe[1] = 0x2B;
  _sendframe[2] = 0x0E;

  // The parameter "Read Device ID code" allows to define four access types :
  // 01: request to get the basic device identification (stream access)
  // 02: request to get the regular device identification (stream access)
  // 03: request to get the extended device identification (stream access)
  // 04: request to get one specific identification object (individual access)
  _sendframe[3]   = device_id;
  _sendframe[4]   = object_id;
  _sendframe_used = 5;
}

String ModbusRTU_struct::MEI_objectid_to_name(uint8_t object_id) {
  String result;

  switch (object_id) {
    case 0:    result = F("VendorName");          break;
    case 1:    result = F("ProductCode");         break;
    case 2:    result = F("MajorMinorRevision");  break;
    case 3:    result = F("VendorUrl");           break;
    case 4:    result = F("ProductName");         break;
    case 5:    result = F("ModelName");           break;
    case 6:    result = F("UserApplicationName"); break;
    case 0x80: result = F("MemoryMapVersion");    break;
    case 0x81: result = F("Firmware Rev.");       break;
    case 0x82: result = F("Sensor S/N");          break;
    case 0x83: result = F("Sensor type");         break;
    default:
      result = formatToHex(object_id);
      break;
  }
  return result;
}

String ModbusRTU_struct::parse_modbus_MEI_response(unsigned int& object_value_int,
                                                   uint8_t        & next_object_id,
                                                   bool        & more_follows,
                                                   uint8_t        & conformity_level) {
  String result;

  if (_recv_buf_used < 8) {
    // Too small.
    addLog(LOG_LEVEL_INFO,
           concat(F("MEI response too small: "), _recv_buf_used));
    next_object_id = 0xFF;
    more_follows   = false;
    return result;
  }
  int pos = 4; // Data skipped: slave_address, FunctionCode, MEI type, ReadDevId
  // See http://www.modbus.org/docs/Modbus_Application_Protocol_V1_1b.pdf
  // Page 45
  conformity_level = _recv_buf[pos++];
  more_follows     = _recv_buf[pos++] != 0;
  next_object_id   = _recv_buf[pos++];
  const uint8_t number_objects = _recv_buf[pos++];
  uint8_t object_id            = 0;

  for (int i = 0; i < number_objects; ++i) {
    if ((pos + 3) < _recv_buf_used) {
      object_id = _recv_buf[pos++];
      const uint8_t object_length = _recv_buf[pos++];

      if ((pos + object_length) < _recv_buf_used) {
        String object_value;

        if (object_id < 0x80) {
          // Parse as type String
          object_value.reserve(object_length);
          object_value_int = static_cast<unsigned int>(-1);

          for (int c = 0; c < object_length; ++c) {
            object_value += char(_recv_buf[pos++]);
          }
        } else {
          object_value.reserve(2 * object_length + 2);
          object_value_int = 0;

          for (int c = 0; c < object_length; ++c) {
            object_value_int =
              object_value_int << 8 | _recv_buf[pos++];
          }
          object_value += formatToHex(object_value_int);
        }

        if (i != 0) {
          // Append to existing description
          result += F(", ");
        }
        result += object_value;
      }
    }
  }
  return result;
}

void ModbusRTU_struct::logModbusException(uint8_t value) {
  if (value == 0) {
    return;
  }

  /*
     // Exception Response, see:
     // http://digital.ni.com/public.nsf/allkb/E40CA0CFA0029B2286256A9900758E06?OpenDocument
     String log = F("Modbus Exception - ");
     switch (value) {
     case MODBUS_EXCEPTION_ILLEGAL_FUNCTION: {
      // The function code received in the query is not an allowable action for
      // the slave.
      // If a Poll Program Complete command was issued, this code indicates that
      // no program function preceded it.
      log += F("Illegal Function (not allowed by client)");
      break;
     }
     case MODBUS_EXCEPTION_ILLEGAL_DATA_ADDRESS: {
      // The data address received in the query is not an allowable address for
      // the slave.
      log += F("Illegal Data Address");
      break;
     }
     case MODBUS_EXCEPTION_ILLEGAL_DATA_VALUE: {
      // A value contained in the query data field is not an allowable value for
      // the slave
      log += F("Illegal Data Value");
      break;
     }
     case MODBUS_EXCEPTION_SLAVE_OR_SERVER_FAILURE: {
      // An unrecoverable error occurred while the slave was attempting to perform
      // the requested action
      log += F("Slave Device Failure");
      break;
     }
     case MODBUS_EXCEPTION_ACKNOWLEDGE: {
      // The slave has accepted the request and is processing it, but a long
      // duration of time will be
      // required to do so. This response is returned to prevent a timeout error
      // from occurring in the master.
      // The master can next issue a Poll Program Complete message to determine if
      // processing is completed.
      log += F("Acknowledge");
      break; // Is this an error?
     }
     case MODBUS_EXCEPTION_SLAVE_OR_SERVER_BUSY: {
      // The slave is engaged in processing a long-duration program command.
      // The master should retransmit the message later when the slave is free.
      log += F("Slave Device Busy");
      break;
     }
     case MODBUS_EXCEPTION_NEGATIVE_ACKNOWLEDGE:
      log += F("Negative acknowledge");
      break;
     case MODBUS_EXCEPTION_MEMORY_PARITY:
      log += F("Memory parity error");
      break;
     case MODBUS_EXCEPTION_GATEWAY_PATH:
      log += F("Gateway path unavailable");
      break;
     case MODBUS_EXCEPTION_GATEWAY_TARGET:
      log += F("Target device failed to respond");
      break;
     case MODBUS_BADCRC:
      log += F("Invalid CRC");
      break;
     case MODBUS_BADDATA:
      log += F("Invalid data");
      break;
     case MODBUS_BADEXC:
      log += F("Invalid exception code");
      break;
     case MODBUS_MDATA:
      log += F("Too many data");
      break;
     case MODBUS_BADSLAVE:
      log += F("Response not from requested slave");
      break;
     case MODBUS_TIMEOUT:
      log += F("Modbus Timeout");
      break;
     case MODBUS_NODATA:
      log += F("Modbus No Data");
      break;
     default:
      log += concat(F("Unknown Exception code: "), value);
      break;
     }
     log += F(" - sent: ");
     log += log_buffer(_sendframe, _sendframe_used);
     log += F(" - received: ");
     log += log_buffer(_recv_buf, _recv_buf_used);
     addLog(LOG_LEVEL_DEBUG_MORE, log);
   */
}

/*
   String log_buffer(uint8_t *buffer, int length) {
    String log;
    log.reserve(3 * length + 5);
    for (int i = 0; i < length; ++i) {
      String hexvalue(buffer[i], HEX);
      hexvalue.toUpperCase();
      log += hexvalue;
      log += ' ';
    }
    log += '(';
    log += length;
    log += ')';
    return log;
   }
 */
uint8_t ModbusRTU_struct::processCommand() {
  // CRC-calculation
  unsigned int crc =
    ModRTU_CRC(_sendframe, _sendframe_used);

  // Note, this number has low and high bytes swapped, so use it accordingly (or
  // swap bytes)
  uint8_t checksumHi = (uint8_t)((crc >> 8) & 0xFF);
  uint8_t checksumLo = (uint8_t)(crc & 0xFF);

  _sendframe[_sendframe_used++] = checksumLo;
  _sendframe[_sendframe_used++] = checksumHi;

  int  nrRetriesLeft = 2;
  uint8_t return_value  = 0;

  while (nrRetriesLeft > 0) {
    return_value = 0;

    // Send the uint8_t array
    startWrite();
    easySerial->write(_sendframe, _sendframe_used);

    // sent all data from buffer
    easySerial->flush();
    startRead();

    // Read answer from sensor
    _recv_buf_used = 0;
    unsigned long timeout    = millis() + _modbus_timeout;
    bool validPacket         = false;
    bool invalidDueToTimeout = false;

    //  idx:    0,   1,   2,   3,   4,   5,   6,   7
    // send: 0x02,0x03,0x00,0x00,0x00,0x01,0x39,0x84
    // recv: 0x02,0x03,0x02,0x01,0x57,0xBC,0x2A

    while (!validPacket && !invalidDueToTimeout && _recv_buf_used < MODBUS_RECEIVE_BUFFER) {
      if (timeOutReached(timeout)) {
        invalidDueToTimeout = true;
      }

      while (!invalidDueToTimeout && easySerial->available() && _recv_buf_used < MODBUS_RECEIVE_BUFFER) {
        if (timeOutReached(timeout)) {
          invalidDueToTimeout = true;
        }
        _recv_buf[_recv_buf_used++] = easySerial->read();
      }

      if (_recv_buf_used > 2) {                                         // got length
        if (_recv_buf_used >= (3 + _recv_buf[2] + 2)) {                 // got whole pkt
          crc          = ModRTU_CRC(_recv_buf, _recv_buf_used);         // crc16 is 0 for whole valid pkt
          validPacket  = (crc == 0) && (_recv_buf[0] == _sendframe[0]); // check crc and address
          return_value = 0;                                             // reset return value
        }
      }
      delay(0);
    }

    // Check for MODBUS exception
    if (invalidDueToTimeout) {
      ++_reads_nodata;

      if (_recv_buf_used == 0) {
        return_value = MODBUS_NODATA;
      } else {
        return_value = MODBUS_TIMEOUT;
      }
    } else if (!validPacket) {
      ++_reads_crc_failed;
      return_value = MODBUS_BADCRC;
    } else {
      const uint8_t received_functionCode = _recv_buf[1];

      if ((received_functionCode & 0x80) != 0) {
        return_value = _recv_buf[2];
      }
      ++_reads_pass;
      _reads_nodata = 0;
    }

    switch (return_value) {
      case MODBUS_EXCEPTION_ACKNOWLEDGE:
      case MODBUS_EXCEPTION_SLAVE_OR_SERVER_BUSY:
      case MODBUS_BADCRC:
      case MODBUS_TIMEOUT:

        // Bad communication, makes sense to retry.
        break;
      default:
        nrRetriesLeft = 0; // When not supported, does not make sense to retry.
        break;
    }
    --nrRetriesLeft;
  }
  _last_error = return_value;
  return return_value;
}

uint32_t ModbusRTU_struct::read_32b_InputRegister(short address) {
  uint32_t result = 0;
  uint8_t     errorcode;
  int idHigh = readInputRegister(address, errorcode);

  if (errorcode != 0) { return result; }
  int idLow = readInputRegister(address + 1, errorcode);

  if (errorcode == 0) {
    result  = idHigh;
    result  = result << 16;
    result += idLow;
  }
  return result;
}

uint32_t ModbusRTU_struct::read_32b_HoldingRegister(short address) {
  uint32_t result = 0;

  process_32b_register(_modbus_address, MODBUS_READ_HOLDING_REGISTERS, address, result);
  return result;
}

float ModbusRTU_struct::read_float_HoldingRegister(short address) {
  const uint32_t ival = read_32b_HoldingRegister(address);
  float    fval{};
  memcpy(&fval, &ival, sizeof(ival));
  return fval;

  //    uint32_t ival = read_32b_HoldingRegister(address);
  //    float fval = *reinterpret_cast<float*>(&ival);
  //    return fval;
}

int ModbusRTU_struct::readInputRegister(short address, uint8_t& errorcode) {
  // Only read 1 register
  return process_16b_register(_modbus_address, MODBUS_READ_INPUT_REGISTERS, address, 1, errorcode);
}

int ModbusRTU_struct::readHoldingRegister(short address, uint8_t& errorcode) {
  // Only read 1 register
  return process_16b_register(
    _modbus_address, MODBUS_READ_HOLDING_REGISTERS, address, 1, errorcode);
}

// Write to holding register.
int ModbusRTU_struct::writeSingleRegister(short address, short value) {
  // No check for the specific error code.
  uint8_t errorcode = 0;

  return writeSingleRegister(address, value, errorcode);
}

int ModbusRTU_struct::writeSingleRegister(short address, short value, uint8_t& errorcode) {
  // GN: Untested, will probably not work
  return process_16b_register(
    _modbus_address, MODBUS_WRITE_SINGLE_REGISTER, address, value, errorcode);
}

// Function 16 (0x10) "Write Multiple Registers" to write to a single holding register
int ModbusRTU_struct::writeMultipleRegisters(short address, short value) {
  return preset_mult16b_register(
    _modbus_address, address, value);
}

uint8_t ModbusRTU_struct::modbus_get_MEI(uint8_t slaveAddress, uint8_t object_id,
                                      String& result, unsigned int& object_value_int,
                                      uint8_t& next_object_id, bool& more_follows,
                                      uint8_t& conformity_level) {
  // Force device_id to 4 = individual access (reading one ID object per call)
  build_modbus_MEI_frame(slaveAddress, 4, object_id);
  const uint8_t process_result = processCommand();

  if (process_result == 0) {
    result = parse_modbus_MEI_response(object_value_int,
                                       next_object_id, more_follows,
                                       conformity_level);
  } else {
    more_follows = false;
  }
  return process_result;
}

void ModbusRTU_struct::modbus_log_MEI(uint8_t slaveAddress) {
  // Iterate over all Device identification items, using
  // Modbus command (0x2B / 0x0E) Read Device Identification
  // And add to log.
  bool more_follows     = true;
  uint8_t conformity_level = 0;
  uint8_t object_id        = 0;
  uint8_t next_object_id   = 0;

  while (more_follows) {
    String result;
    unsigned int object_value_int;
    const uint8_t   process_result = modbus_get_MEI(
      slaveAddress, object_id, result, object_value_int, next_object_id,
      more_follows, conformity_level);

    if (process_result == 0) {
      if (result.length() > 0) {
        String log = MEI_objectid_to_name(object_id);
        log += F(": ");
        log += result;
        addLogMove(LOG_LEVEL_INFO, log);
      }
    } else {
      switch (process_result) {
        case MODBUS_EXCEPTION_ILLEGAL_DATA_ADDRESS:

          // No need to log this exception when scanning.
          break;
        default:
          logModbusException(process_result);
          break;
      }
    }

    // If more parts are needed, collect them or iterate over the known list.
    // For example with "individual access" a new request has to be sent for each single item
    if (more_follows) {
      object_id = next_object_id;
    } else if (object_id < 0x84) {
      // Allow for scanning only the usual object ID's
      // This range is vendor specific
      more_follows = true;
      object_id++;

      if (object_id == 7) {
        // Skip range 0x07...0x7F
        object_id = 0x80;
      }
    }
  }
}

int ModbusRTU_struct::process_16b_register(uint8_t slaveAddress, uint8_t functionCode,
                                           short startAddress, short parameter,
                                           uint8_t& errorcode) {
  buildFrame(slaveAddress, functionCode, startAddress, parameter);
  errorcode = processCommand();

  if (errorcode == 0) {
    return (_recv_buf[3] << 8) | (_recv_buf[4]);
  }
  logModbusException(errorcode);
  return -1;
}

// Still writing single register, but calling it using "Preset Multiple Registers" function (FC=16)
int ModbusRTU_struct::preset_mult16b_register(uint8_t slaveAddress, uint16_t startAddress, uint16_t value) {
  buildWriteMult16bRegister(slaveAddress, startAddress, value);
  const uint8_t process_result = processCommand();

  if (process_result == 0) {
    return (_recv_buf[4] << 8) | (_recv_buf[5]);
  }
  logModbusException(process_result);
  return -1 * process_result;
}

bool ModbusRTU_struct::process_32b_register(uint8_t slaveAddress, uint8_t functionCode,
                                            short startAddress, uint32_t& result) {
  buildFrame(slaveAddress, functionCode, startAddress, 2);
  const uint8_t process_result = processCommand();

  if (process_result == 0) {
    result = 0;

    for (uint8_t i = 0; i < 4; ++i) {
      result  = result << 8;
      result += _recv_buf[i + 3];
    }
    return true;
  }
  logModbusException(process_result);
  return false;
}

int ModbusRTU_struct::writeSpecialCommandRegister(uint8_t command) {
  buildWriteCommandRegister(_modbus_address, command);
  const uint8_t process_result = processCommand();

  if (process_result == 0) {
    return 0;
  }
  logModbusException(process_result);
  return -1 * process_result;
}

unsigned int ModbusRTU_struct::read_RAM_EEPROM(uint8_t command, uint8_t startAddress,
                                               uint8_t nrBytes,
                                               uint8_t& errorcode) {
  buildRead_RAM_EEPROM(_modbus_address, command,
                       startAddress, nrBytes);
  errorcode = processCommand();

  if (errorcode == 0) {
    unsigned int result = 0;

    for (int i = 0; i < _recv_buf[2]; ++i) {
      // Most significant uint8_t at lower address
      result = (result << 8) | _recv_buf[i + 3];
    }
    return result;
  }
  logModbusException(errorcode);
  return 0;
}

// Compute the MODBUS RTU CRC
unsigned int ModbusRTU_struct::ModRTU_CRC(uint8_t *buf, int len) {
  unsigned int crc = 0xFFFF;

  for (int pos = 0; pos < len; pos++) {
    crc ^= (unsigned int)buf[pos]; // XOR uint8_t into least sig. uint8_t of crc

    for (int i = 8; i != 0; i--) { // Loop over each bit
      if ((crc & 0x0001) != 0) {   // If the LSB is set
        crc >>= 1;                 // Shift right and XOR 0xA001
        crc  ^= 0xA001;
      } else {                     // Else LSB is not set
        crc >>= 1;                 // Just shift right
      }
    }
  }
  return crc;
}

uint32_t ModbusRTU_struct::readTypeId() {
  return read_32b_InputRegister(25);
}

uint32_t ModbusRTU_struct::readSensorId() {
  return read_32b_InputRegister(29);
}

uint8_t ModbusRTU_struct::getLastError() const {
  return _last_error;
}

uint32_t ModbusRTU_struct::getFailedReadsSinceLastValid() const {
  return _reads_nodata;
}

void ModbusRTU_struct::startWrite() {
  // transmit to device  -> DE Enable, /RE Disable (for control MAX485)
  if ((_dere_pin == -1) || !isInitialized()) { return; }
  digitalWrite(_dere_pin, HIGH);
  delay(2); // Switching may take some time
}

void ModbusRTU_struct::startRead() {
  if (!isInitialized()) { return; }
  easySerial->flush(); // clear out tx buffer

  // receive from device -> DE Disable, /RE Enable (for control MAX485)
  if (_dere_pin != -1) {
    digitalWrite(_dere_pin, LOW);
  }
}

#endif
#include "../PluginStructs/P132_data_struct.h"

#ifdef USES_P132

# if FEATURE_MQTT_DISCOVER
int Plugin_132_QueryVType(uint8_t value_nr) {
  if (value_nr < 6u) {
    const bool odd = value_nr % 2;
    return static_cast<int>(odd ? Sensor_VType::SENSOR_TYPE_CURRENT_ONLY : Sensor_VType::SENSOR_TYPE_VOLTAGE_ONLY);
  }
  return static_cast<int>(Sensor_VType::SENSOR_TYPE_NONE);
}

# endif // if FEATURE_MQTT_DISCOVER

// **************************************************************************/
// Constructor
// **************************************************************************/
P132_data_struct::P132_data_struct(struct EventStruct *event) {
  _i2c_address = P132_I2C_ADDR;
  setCalibration_INA3221(event);
}

// **************************************************************************/
// Gets the raw bus voltage  (7FF8 / 32760) LSB 8mV
// **************************************************************************/
int16_t P132_data_struct::getBusVoltage_raw(byte reg) {
  uint16_t value = I2C_read16_reg(_i2c_address, reg);

  // Shift to the right 3 to drop CNVR and OVF and multiply by LSB 8 mV
  # ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLog(LOG_LEVEL_DEBUG,
           strformat(F("INA3221: get raw bus %d reg - %d"),
                     value, reg));
  }
  # endif // ifndef BUILD_NO_DEBUG
  return (int16_t)((value >> 3) * 8);
}

// **************************************************************************/
// Gets the raw shunt voltage (integer, so +-32760) LSB 40 uV
// **************************************************************************/
int16_t P132_data_struct::getShuntVoltage_raw(byte reg) {
  uint16_t value = I2C_read16_reg(_i2c_address, reg);

  # ifndef BUILD_NO_DEBUG
  String log = strformat(F("INA3221: get raw shunt voltage %d value2 - "), value);
  # endif // ifndef BUILD_NO_DEBUG

  // Shift to the right 3 to drop CNVR and OVF and multiply by LSB
  if (value > 32767) {               // check value is negative
    //		value = 0;  // no negative measure
    value = ((value >> 3) | 0xE000); // correct int16_t value
    # ifndef BUILD_NO_DEBUG
    log += concat(F(" value_neg - "), value);
    # endif // ifndef BUILD_NO_DEBUG
  } else {
    value = (value >> 3);
    # ifndef BUILD_NO_DEBUG
    log += concat(F(" value_pos - "), value);
    # endif // ifndef BUILD_NO_DEBUG
  }
  # ifndef BUILD_NO_DEBUG
  log += concat(F(" reg - "), reg);
  addLog(LOG_LEVEL_DEBUG, log);
  # endif // ifndef BUILD_NO_DEBUG
  return value;
}

// **************************************************************************/
// Gets the shunt voltage in mV (32760 so +-163.8 mV) 7ff8 LSB 40uV
// **************************************************************************/
float P132_data_struct::getShuntVoltage_mV(byte reg) {
  int16_t value = getShuntVoltage_raw(reg);

  # ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLog(LOG_LEVEL_DEBUG,
           strformat(F("INA3221: shunt voltage in mV * 0.04 %d reg - %d"),
                     value, reg));
  }
  # endif // ifndef BUILD_NO_DEBUG
  return value * 0.04f;
}

// **************************************************************************/
// Gets the Bus voltage in volts
// **************************************************************************/
float P132_data_struct::getBusVoltage_V(byte reg) {
  int16_t value = getBusVoltage_raw(reg);

  # ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLog(LOG_LEVEL_DEBUG,
           strformat(F("INA3221: get bus voltage %d reg - %d"),
                     value, reg));
  }
  # endif // ifndef BUILD_NO_DEBUG
  return value * 0.001f;
}

// **************************************************************************/
// Configures to INA3221
// **************************************************************************/
void P132_data_struct::setCalibration_INA3221(struct EventStruct *event) {
  // Set Config register
  uint32_t config = I2C_read16_reg(_i2c_address, 0x00); // read, chip default: 0x7127
  uint16_t mfgid  = I2C_read16_reg(_i2c_address, 0xFE); // read manufacturer ID, should be 0x5449

  set3BitToUL(config, INA3221_AVERAGE_BIT,          P132_GET_AVERAGE);
  set3BitToUL(config, INA3221_CONVERSION_BUS_BIT,   P132_GET_CONVERSION_B);
  set3BitToUL(config, INA3221_CONVERSION_SHUNT_BIT, P132_GET_CONVERSION_S);

  # ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO,
           strformat(F("INA3221: init I2C: 0x%02x mfg: 0x%x, config: 0x%x, 0b%s"),
                     _i2c_address, mfgid, config, String(config, BIN).c_str()));
  }
  # endif // ifndef BUILD_NO_DEBUG

  if (mfgid != 0x5449) {
    addLogMove(LOG_LEVEL_ERROR, F("INA3221: Invalid Manufacturer ID! (0x5449)"));
  }

  I2C_write16_reg(_i2c_address, 0x00, static_cast<uint16_t>(config));
}

#endif // ifdef USES_P132

#include "../PluginStructs/P067_data_struct.h"

#ifdef USES_P067
# include <GPIO_Direct_Access.h>

/****************************************************
* Convert a float to 2 ints
****************************************************/
void P067_float2int(float valFloat, int16_t *valInt0, int16_t *valInt1) {
  // FIXME TD-er: Casting from float* to integer* is not portable due to different binary data representations on different platforms.
  int16_t *fti = (int16_t *)&valFloat;

  *valInt0 = *fti++;
  *valInt1 = *fti;
}

/****************************************************
* Convert 2 ints to a float
****************************************************/
void P067_int2float(int16_t valInt0, int16_t valInt1, float *valFloat) {
  // FIXME TD-er: Casting from float* to integer* is not portable due to different binary data representations on different platforms.
  float offset = 0.0f; // Set to some value to prevent compiler warnings
  int16_t *itf = (int16_t *)&offset;

  *itf++    = valInt0;
  *itf      = valInt1;
  *valFloat = offset;
}

/**************************************************************************
* Constructor
**************************************************************************/
P067_data_struct::P067_data_struct(struct EventStruct *event,
                                   int8_t              pinSCL,
                                   int8_t              pinDOUT)
  : _pinSCL(pinSCL), _pinDOUT(pinDOUT)
{
  _modeChanA = P067_GET_CHANNEL_A_MODE_e;
  _modeChanB = P067_GET_CHANNEL_B_MODE_e;
  P067_int2float(P067_OFFSET_CHANNEL_A_1, P067_OFFSET_CHANNEL_A_2, &_offsetChanA);
  P067_int2float(P067_OFFSET_CHANNEL_B_1, P067_OFFSET_CHANNEL_B_2, &_offsetChanB);
}

/*****************************************************
* Destructor
*****************************************************/
P067_data_struct::~P067_data_struct() {}

/****************************************************
* Initialization
****************************************************/
bool P067_data_struct::init(struct EventStruct *event) {
  // Log anyway
  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO, strformat(F("HX711: GPIO: SCL=%d DOUT=%d"), _pinSCL, _pinDOUT));
  }
  UserVar.setFloat(event->TaskIndex, 0, 0.0f); // Reset output
  UserVar.setFloat(event->TaskIndex, 1, 0.0f);

  if (isInitialized()) {
    pinMode(_pinSCL, OUTPUT); // Keep regular pinMode functions for initialization
    digitalWrite(_pinSCL, LOW);

    pinMode(_pinDOUT, INPUT); // Checked, doesn't seem applicable: https://github.com/bogde/HX711/issues/222

    return true;
  }
  return false;
}

/*****************************************************
* plugin_read
*****************************************************/
bool P067_data_struct::plugin_read(struct EventStruct *event)           {
  bool success = false;

  if (isInitialized()) {
    if ((_modeChanA == P067_ChannelA_State_e::modeAoff) && (_modeChanB == P067_ChannelB_State_e::modeBoff)) {
      addLog(LOG_LEVEL_INFO, F("HX711: No channel selected"));
    }

    // Channel A activated?
    if (_modeChanA != P067_ChannelA_State_e::modeAoff) {
      String log = strformat(F("HX711: (%d) ChanA: "), (int)event->TaskIndex + 1);

      float value{};

      if (OversamplingChanA.get(value)) {
        UserVar.setFloat(event->TaskIndex, 2, value);
        UserVar.setFloat(event->TaskIndex, 0, UserVar.getFloat(event->TaskIndex, 2) + _offsetChanA); // Offset

        log += formatUserVarNoCheck(event, 0);

        if (P067_GET_CHANNEL_A_CALIB) { // Calibration channel A?
          int   adc1 = P067_CONFIG_CHANNEL_A_ADC1;
          int   adc2 = P067_CONFIG_CHANNEL_A_ADC2;
          float out1 = P067_CONFIG_CHANNEL_A_OUT1;
          float out2 = P067_CONFIG_CHANNEL_A_OUT2;

          if (adc1 != adc2) {
            const float normalized = static_cast<float>(UserVar[event->BaseVarIndex] - adc1) / static_cast<float>(adc2 - adc1);
            UserVar.setFloat(event->TaskIndex, 0, normalized * (out2 - out1) + out1);

            log += concat(F(" = "), formatUserVarNoCheck(event, 0));
          }
        }
      } else {
        log += F("NO NEW VALUE");
      }
      addLogMove(LOG_LEVEL_INFO, log);
      success = !firstRead;
    }

    // Channel B activated?
    if (_modeChanB != P067_ChannelB_State_e::modeBoff) {
      String log = strformat(F("HX711: (%d) ChanB: "), (int)event->TaskIndex + 1);

      float value{};

      if (OversamplingChanB.get(value)) {
        UserVar.setFloat(event->TaskIndex, 3, value);
        UserVar.setFloat(event->TaskIndex, 1, UserVar.getFloat(event->TaskIndex, 3) + _offsetChanB); // Offset

        log += formatUserVarNoCheck(event, 1);

        if (P067_GET_CHANNEL_B_CALIB) { // Calibration channel B?
          int   adc1 = P067_CONFIG_CHANNEL_B_ADC1;
          int   adc2 = P067_CONFIG_CHANNEL_B_ADC2;
          float out1 = P067_CONFIG_CHANNEL_B_OUT1;
          float out2 = P067_CONFIG_CHANNEL_B_OUT2;

          if (adc1 != adc2) {
            const float normalized = (UserVar[event->BaseVarIndex + 1] - adc1) / static_cast<float>(adc2 - adc1);
            UserVar.setFloat(event->TaskIndex, 1, normalized * (out2 - out1) + out1);

            log += concat(F(" = "), formatUserVarNoCheck(event, 1));
          }
        }
      } else {
        log += F("NO NEW VALUE");
      }
      addLogMove(LOG_LEVEL_INFO, log);
      success = !firstRead;
    }
    firstRead = false;
  }
  return success;
}

/*****************************************************
* plugin_fifty_per_second
*****************************************************/
bool P067_data_struct::plugin_fifty_per_second(struct EventStruct *event) {
  bool success = false;

  if (isInitialized() && isDataReady()) {
    int32_t value = readHX711();
    success = true;

    switch (_channelRead) {
      case P067_Channel_e::chanA64:   //
      case P067_Channel_e::chanA128:
      {
        if (!P067_GET_CHANNEL_A_OS) { // Oversampling on channel A?
          OversamplingChanA.reset();
        }

        if (OversamplingChanA.getCount() > 250) {
          OversamplingChanA.resetKeepLast();
        }
        OversamplingChanA.add(value);
        break;
      }
      case P067_Channel_e::chanB32:
      {
        if (!P067_GET_CHANNEL_B_OS) { // Oversampling on channel B?
          OversamplingChanB.reset();
        }

        if (OversamplingChanB.getCount() > 250) {
          OversamplingChanB.resetKeepLast();
        }
        OversamplingChanB.add(value);
        break;
      }
    }
  }

  return success;
}

/*****************************************************
* plugin_write
*****************************************************/
bool P067_data_struct::plugin_write(struct EventStruct *event,
                                    String            & string) {
  bool success = false;

  String command = parseString(string, 1);

  if (equals(command, F("tarechana"))) {
    P067_float2int(-UserVar[event->BaseVarIndex + 2], &P067_OFFSET_CHANNEL_A_1, &P067_OFFSET_CHANNEL_A_2);
    P067_int2float(P067_OFFSET_CHANNEL_A_1, P067_OFFSET_CHANNEL_A_2, &_offsetChanA);
    OversamplingChanA.reset();

    addLog(LOG_LEVEL_INFO, F("HX711: tare channel A"));
    success = true;
  } else if (equals(command, F("tarechanb"))) {
    P067_float2int(-UserVar[event->BaseVarIndex + 3], &P067_OFFSET_CHANNEL_B_1, &P067_OFFSET_CHANNEL_B_2);
    P067_int2float(P067_OFFSET_CHANNEL_B_1, P067_OFFSET_CHANNEL_B_2, &_offsetChanB);
    OversamplingChanB.reset();

    addLog(LOG_LEVEL_INFO, F("HX711: tare channel B"));
    success = true;
  }

  return success;
}

/****************************************************************************************
* Private stuff
****************************************************************************************/
/****************************************************
* Minimal viable settings: GPIO pins valid?
****************************************************/
bool P067_data_struct::isInitialized() {
  return validGpio(_pinSCL) && validGpio(_pinDOUT);
}

/****************************************************
* New data available?
****************************************************/
bool P067_data_struct::isDataReady() {
  if (isInitialized()) {
    return !DIRECT_pinRead(_pinDOUT);
  }
  return false;
}

/****************************************************
* Read data from the load sensor
****************************************************/
int32_t P067_data_struct::readHX711() {
  int32_t  value = 0;
  uint32_t mask  = 0x00800000;

  _channelRead = _nextChannel;

  // Both channels off
  if ((_modeChanA == P067_ChannelA_State_e::modeAoff) && (_modeChanB == P067_ChannelB_State_e::modeBoff)) {
    DIRECT_pinWrite(_pinSCL, HIGH);
    return 0;
  }

  // Both channels on
  if ((_modeChanA != P067_ChannelA_State_e::modeAoff) && (_modeChanB != P067_ChannelB_State_e::modeBoff)) {
    // Both channels are activated -> do interleaved measurement
    _channelToggle = !_channelToggle;

    // FIXME tonhuisman: Toggling doesn't work as intended
    if (_channelToggle) {
      if (_modeChanA == P067_ChannelA_State_e::modeA64) {
        _nextChannel = P067_Channel_e::chanA64;
      } else {
        _nextChannel = P067_Channel_e::chanA128;
      }
    } else {
      _nextChannel = P067_Channel_e::chanB32;
    }
  } else {
    // Only one channel is activated
    if (_modeChanA == P067_ChannelA_State_e::modeA64) {
      _nextChannel = P067_Channel_e::chanA64;
    } else if (_modeChanA == P067_ChannelA_State_e::modeA128) {
      _nextChannel = P067_Channel_e::chanA128;
    }

    if (_modeChanB == P067_ChannelB_State_e::modeB32) {
      _nextChannel = P067_Channel_e::chanB32;
    }
  }

  for (uint8_t i = 0; i < 24; i++) {
    DIRECT_pinWrite(_pinSCL, HIGH);
    delayMicroseconds(1);
    DIRECT_pinWrite(_pinSCL, LOW);

    if (DIRECT_pinRead(_pinDOUT)) {
      value |= mask;
    }
    delayMicroseconds(1);
    mask >>= 1;
  }

  for (uint8_t i = 0; i < (static_cast < uint8_t > (_nextChannel) + 1); i++) {
    DIRECT_pinWrite(_pinSCL, HIGH);
    delayMicroseconds(1);
    DIRECT_pinWrite(_pinSCL, LOW);
    delayMicroseconds(1);
  }

  if (value & 0x00800000) { // negative?
    value |= 0xFF000000;    // expand sign bit to 32 bit
  }
  return value;
}

#endif // ifdef USES_P067

#include "../PluginStructs/P053_data_struct.h"

#ifdef USES_P053

const __FlashStringHelper* toString(PMSx003_type sensorType) {
  switch (sensorType) {
    case PMSx003_type::PMS1003_5003_7003: return F("PMS1003 / PMS5003 / PMS7003");
    # ifdef PLUGIN_053_ENABLE_EXTRA_SENSORS
    case PMSx003_type::PMS2003_3003: return F("PMS2003 / PMS3003");
    case PMSx003_type::PMS5003_S:    return F("PMS5003S");
    case PMSx003_type::PMS5003_T:    return F("PMS5003T");
    case PMSx003_type::PMS5003_ST:   return F("PMS5003ST");
    # endif // ifdef PLUGIN_053_ENABLE_EXTRA_SENSORS
    # ifdef USES_P175
    case PMSx003_type::PMSA003i:   return F("PMSA003i");
    # endif // ifdef USES_P175
  }
  return F("Unknown");
}

const __FlashStringHelper* toString(PMSx003_output_selection selection) {
  switch (selection) {
    case PMSx003_output_selection::Particles_ug_m3: return F("Particles &micro;g/m3: pm1.0, pm2.5, pm10");
    case PMSx003_output_selection::PM2_5_TempHum_Formaldehyde:  return F("Particles &micro;g/m3: pm2.5; Other: Temp, Humi, HCHO (PMS5003ST)");
    case PMSx003_output_selection::ParticlesCount_100ml_cnt1_0_cnt2_5_cnt10:
      return F("Particles count/0.1L: cnt1.0, cnt2.5, cnt5, cnt10 (PMS1003/5003(ST)/7003"
               # ifdef USES_P175
               "/A003i"
               # endif // ifdef USES_P175
               ")");
    case PMSx003_output_selection::ParticlesCount_100ml_cnt0_3__cnt_2_5:
      return F("Particles count/0.1L: cnt0.3, cnt0.5, cnt1.0, cnt2.5 (PMS1003/5003(ST)/7003"
               # ifdef USES_P175
               "/A003i"
               # endif // ifdef USES_P175
               ")");
  }
  return F("Unknown");
}

const __FlashStringHelper* toString(PMSx003_event_datatype selection) {
  switch (selection) {
    case PMSx003_event_datatype::Event_None: return F("None");
    case PMSx003_event_datatype::Event_PMxx_TempHum_Formaldehyde: return F("Particles &micro;g/m3 and Temp/Humi/HCHO");
    case PMSx003_event_datatype::Event_All: return F("Particles &micro;g/m3, Temp/Humi/HCHO and Particles count/0.1L");
    case PMSx003_event_datatype::Event_All_count_bins: return F("Particles count/0.1L");
  }
  return F("Unknown");
}

P053_data_struct::P053_data_struct(
  taskIndex_t             TaskIndex,
  int8_t                  rxPin,
  int8_t                  txPin,
  const ESPEasySerialPort port,
  int8_t                  resetPin,
  int8_t                  pwrPin,
  PMSx003_type            sensortype,
  uint32_t                delay_read_after_wakeup_ms
  # ifdef                 PLUGIN_053_ENABLE_EXTRA_SENSORS
  ,
  bool                    oversample
  ,
  bool                    splitCntBins
  # endif // ifdef PLUGIN_053_ENABLE_EXTRA_SENSORS
  # ifdef USES_P175
  ,
  bool    P053_for_P175
  # endif // ifdef USES_P175
  )
  : _taskIndex(TaskIndex),
  _rxPin(rxPin),
  _txPin(txPin),
  _port(port),
  _sensortype(sensortype),
  # ifdef PLUGIN_053_ENABLE_EXTRA_SENSORS
  _oversample(oversample),
  _splitCntBins(splitCntBins),
  # endif // ifdef PLUGIN_053_ENABLE_EXTRA_SENSORS
  _delay_read_after_wakeup_ms(delay_read_after_wakeup_ms),
  _resetPin(resetPin), _pwrPin(pwrPin)
  # ifdef USES_P175
  , _P053_for_P175(P053_for_P175)
  # endif // ifdef USES_P175
{}

bool P053_data_struct::init() {
  # ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLogMove(LOG_LEVEL_DEBUG, strformat(F("PMSx003 : config %d %d %d %d"), _rxPin, _txPin, _resetPin, _pwrPin));
  }
  # endif // ifndef BUILD_NO_DEBUG

  # ifdef USES_P175

  if (!_P053_for_P175)
  # endif // ifdef USES_P175
  {
    if (_easySerial != nullptr) {
      delete _easySerial;
      _easySerial = nullptr;
    }

    _easySerial = new (std::nothrow) ESPeasySerial(_port, _rxPin, _txPin, false, 96); // 96 Bytes buffer, enough for up to 3 packets.
  }

  if ((_easySerial != nullptr)
      # ifdef USES_P175
      || _P053_for_P175
      # endif // ifdef USES_P175
      ) {
    # ifdef USES_P175

    if (_P053_for_P175) {
      // Initialize I2C sensor
      _i2c_init = true;

      if (I2C_wakeup(P175_I2C_ADDR) != 0) {
        addLog(LOG_LEVEL_INFO, F("PMSx003 : I2C sensor not found"));
        _i2c_init = false;
      }
    } else
    # endif // ifdef USES_P175
    {
      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLog(LOG_LEVEL_INFO, concat(F("PMSx003 : "), _easySerial->getLogString()));
      }

      _easySerial->begin(9600);
      _easySerial->flush();
    }

    wakeSensor();

    resetSensor();

    // Make sure to set the mode to active reading mode.
    // This is the default, but not sure if passive mode can be set persistant.
    setActiveReadingMode();
  }
  clearReceivedData();
  return initialized();
}

P053_data_struct::~P053_data_struct() {
  if (_easySerial != nullptr) {
    delete _easySerial;
    _easySerial = nullptr;
  }
}

bool P053_data_struct::initialized() const
{
  return _easySerial != nullptr
         # ifdef USES_P175
         || _i2c_init
         # endif // ifdef USES_P175
  ;
}

// Read 2 bytes from serial and make an uint16 of it. Additionally calculate
// checksum for PMSx003. Assumption is that there is data available, otherwise
// this function is blocking.
void P053_data_struct::PacketRead16(uint16_t& value, uint16_t *checksum)
{
  if (!initialized()) { return; }

  if (_packetPos > (PMSx003_PACKET_BUFFER_SIZE - 2)) { return; }
  const uint8_t data_high = _packet[_packetPos++];
  const uint8_t data_low  = _packet[_packetPos++];

  value  = data_low;
  value |= (data_high << 8);

  if (checksum != nullptr)
  {
    *checksum += data_high;
    *checksum += data_low;
  }

  # ifdef P053_LOW_LEVEL_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    // Low-level logging to see data from sensor
    addLog(LOG_LEVEL_INFO,
           strformat(F("PMSx003 : uint8_t high=0x%02x uint8_t low=0x%02x result=0x%04x"), data_high, data_low, value));
  }
  # endif // ifdef P053_LOW_LEVEL_DEBUG
}

void P053_data_struct::SerialFlush() {
  if (_easySerial != nullptr) {
    _easySerial->flush();
  }
}

uint8_t P053_data_struct::packetSize() const {
  switch (_sensortype) {
    case PMSx003_type::PMS1003_5003_7003:    return PMS1003_5003_7003_SIZE;
    # ifdef PLUGIN_053_ENABLE_EXTRA_SENSORS
    case PMSx003_type::PMS5003_S:    return PMS5003_S_SIZE;
    case PMSx003_type::PMS5003_T:    return PMS5003_T_SIZE;
    case PMSx003_type::PMS5003_ST:   return PMS5003_ST_SIZE;
    case PMSx003_type::PMS2003_3003: return PMS2003_3003_SIZE;
    # endif // ifdef PLUGIN_053_ENABLE_EXTRA_SENSORS
    # ifdef USES_P175
    case PMSx003_type::PMSA003i:     return PMSA003i_SIZE;
    # endif // ifdef USES_P175
  }
  return 0u;
}

bool P053_data_struct::packetAvailable()
{
  const uint8_t expectedSize = packetSize();

  if (expectedSize == 0) { return false; }

  if (_easySerial != nullptr)
  {
    if (_packetPos < expectedSize) {
      // When there is enough data in the buffer, search through the buffer to
      // find header (buffer may be out of sync)
      if (!_easySerial->available()) { return false; }

      if (_packetPos == 0) {
        while ((_easySerial->peek() != PMSx003_SIG1) && _easySerial->available()) {
          _easySerial->read(); // Read until the buffer starts with the
          // first uint8_t of a message, or buffer
          // empty.
        }

        if (_easySerial->peek() == PMSx003_SIG1) {
          _packet[_packetPos++] = _easySerial->read();
        }
      }

      if (_packetPos > 0) {
        while (_packetPos < expectedSize) {
          if (_easySerial->available() == 0) {
            return false;
          } else {
            _packet[_packetPos++] = _easySerial->read();
          }
        }
      }
    }
  }
  # ifdef USES_P175

  if (_i2c_init) {
    if (I2C_wakeup(P175_I2C_ADDR) == 0) {
      if (Wire.requestFrom((uint8_t)P175_I2C_ADDR, expectedSize) == expectedSize) {
        // Read all bytes into packet-buffer
        for (_packetPos = 0; _packetPos < expectedSize; ++_packetPos) {
          _packet[_packetPos] = Wire.read(); // Read data bytes
        }
      }
    }
  }
  # endif // ifdef USES_P175
  return _packetPos >= expectedSize;
}

# ifdef PLUGIN_053_ENABLE_EXTRA_SENSORS
void P053_data_struct::sendEvent(taskIndex_t TaskIndex,
                                 uint8_t     index) {
  float value = 0.0f;

  if (!getValue(index, value)) { return; }

  const unsigned char nrDecimals = getNrDecimals(index, _oversample);

  // Temperature
  eventQueue.add(TaskIndex, getEventString(index), toString(value, nrDecimals));
}

bool P053_data_struct::hasFormaldehyde() const {
  #  ifdef PLUGIN_053_ENABLE_EXTRA_SENSORS
  return _sensortype == PMSx003_type::PMS5003_S ||
         _sensortype == PMSx003_type::PMS5003_ST;
  #  else // ifdef PLUGIN_053_ENABLE_EXTRA_SENSORS
  return false;
  #  endif // ifdef PLUGIN_053_ENABLE_EXTRA_SENSORS
}

bool P053_data_struct::hasTempHum() const {
  #  ifdef PLUGIN_053_ENABLE_EXTRA_SENSORS
  return _sensortype == PMSx003_type::PMS5003_T ||
         _sensortype == PMSx003_type::PMS5003_ST;
  #  else // ifdef PLUGIN_053_ENABLE_EXTRA_SENSORS
  return false;
  #  endif // ifdef PLUGIN_053_ENABLE_EXTRA_SENSORS
}

# endif // ifdef PLUGIN_053_ENABLE_EXTRA_SENSORS

bool P053_data_struct::processData(struct EventStruct *event) {
  uint16_t checksum = 0, checksum2 = 0;
  uint16_t framelength   = 0;
  uint16_t packet_header = 0;
  uint16_t data[PMS_RECEIVE_BUFFER_SIZE]{}; // uint8_t data_low, data_high;

  _packetPos = 0;

  PacketRead16(packet_header, &checksum); // read PMSx003_SIG1 + PMSx003_SIG2

  if (packet_header != ((PMSx003_SIG1 << 8) | PMSx003_SIG2)) {
    // Not the start of the packet, stop reading.
    return false;
  }

  PacketRead16(framelength, &checksum);

  if ((framelength + 4) != packetSize()) {
    if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
      addLog(LOG_LEVEL_ERROR, concat(F("PMSx003 : invalid framelength - "), framelength));
    }
    return false;
  }

  uint8_t frameData = packetSize();

  if (frameData > 0u) {
    frameData /= 2; // Each value is 16 bits
    frameData -= 3; // start markers, length, checksum
  }

  for (uint8_t i = 0; i < frameData && i < PMS_RECEIVE_BUFFER_SIZE; ++i) {
    PacketRead16(data[i], &checksum);
  }

  # ifdef PLUGIN_053_ENABLE_EXTRA_SENSORS

  if (GET_PLUGIN_053_SENSOR_MODEL_SELECTOR == PMSx003_type::PMS5003_T) {
    data[PMS_Temp_C]  = data[PMS_T_Temp_C]; // Move data to the 'usual' location for Temp/Hum
    data[PMS_Hum_pct] = data[PMS_T_Hum_pct];
  }
  # endif // ifdef PLUGIN_053_ENABLE_EXTRA_SENSORS

  # ifndef BUILD_NO_DEBUG
  #  ifdef P053_LOW_LEVEL_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { // Available on all supported sensor models
    addLog(LOG_LEVEL_DEBUG,
           strformat(F("PMSx003 : pm1.0=%d, pm2.5=%d, pm10=%d, pm1.0a=%d, pm2.5a=%d, pm10a=%d"),
                     data[PMS_PM1_0_ug_m3_factory],
                     data[PMS_PM2_5_ug_m3_factory],
                     data[PMS_PM10_0_ug_m3_factory],
                     data[PMS_PM1_0_ug_m3_normal],
                     data[PMS_PM2_5_ug_m3_normal],
                     data[PMS_PM10_0_ug_m3_normal]));
  }

  #   ifdef PLUGIN_053_ENABLE_EXTRA_SENSORS

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)
      && (GET_PLUGIN_053_SENSOR_MODEL_SELECTOR != PMSx003_type::PMS2003_3003)) { // 'Count' values not available on
    // PMS2003/PMS3003 models
    // (handled as 1 model in code)
    addLog(LOG_LEVEL_DEBUG,
           strformat(F("PMSx003 : count/0.1L : 0.3um=%d, 0.5um=%d, 1.0um=%d, 2.5um=%d, 5.0um=%d, 10um=%d"),
                     data[PMS_cnt0_3_100ml],
                     data[PMS_cnt0_5_100ml],
                     data[PMS_cnt1_0_100ml],
                     data[PMS_cnt2_5_100ml],
                     data[PMS_cnt5_0_100ml],
                     data[PMS_cnt10_0_100ml]));
  }


  if (loglevelActiveFor(LOG_LEVEL_DEBUG)
      && ((GET_PLUGIN_053_SENSOR_MODEL_SELECTOR == PMSx003_type::PMS5003_ST)
          || (GET_PLUGIN_053_SENSOR_MODEL_SELECTOR == PMSx003_type::PMS5003_T))) { // Values only available on PMS5003ST & PMS5003T
    String log;

    if (log.reserve(45)) {
      log = strformat(F("PMSx003 : temp=%.2f, humi=%.2f"),
                      static_cast<float>(data[PMS_Temp_C]) / 10.0f,
                      static_cast<float>(data[PMS_Hum_pct]) / 10.0f);

      if (GET_PLUGIN_053_SENSOR_MODEL_SELECTOR == PMSx003_type::PMS5003_ST) {
        log += strformat(F(", hcho=%.4f"), static_cast<float>(data[PMS_Formaldehyde_mg_m3]) / 1000.0f);
      }
      addLogMove(LOG_LEVEL_DEBUG, log);
    }
  }
  #   endif // ifdef PLUGIN_053_ENABLE_EXTRA_SENSORS
  #  endif     // ifdef P053_LOW_LEVEL_DEBUG
  # endif      // ifndef BUILD_NO_DEBUG

  // Compare checksums
  PacketRead16(checksum2, nullptr);
  SerialFlush(); // Make sure no data is lost due to full buffer.

  if (checksum != checksum2) {
    addLog(LOG_LEVEL_ERROR, strformat(F("PMSx003 : Checksum error (0x%x expected: 0x%x)"), checksum, checksum2));
    return false;
  }

  if (_last_wakeup_moment.isSet() && !_last_wakeup_moment.timeReached()) {
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO,
             strformat(F("PMSx003 : Less than %d sec since sensor wakeup => Ignoring sample"),
                       _delay_read_after_wakeup_ms / 1000ul));
    }
    return false;
  }

  if (checksum == _last_checksum) {
    // Duplicate message
    # ifndef BUILD_NO_DEBUG

    addLog(LOG_LEVEL_DEBUG, F("PMSx003 : Duplicate message"));
    # endif // ifndef BUILD_NO_DEBUG
    return false;
  }
  # ifndef PLUGIN_053_ENABLE_EXTRA_SENSORS

  // Data is checked and good, fill in output
  UserVar.setFloat(event->TaskIndex, 0, data[PMS_PM1_0_ug_m3_normal]);
  UserVar.setFloat(event->TaskIndex, 1, data[PMS_PM2_5_ug_m3_normal]);
  UserVar.setFloat(event->TaskIndex, 2, data[PMS_PM10_0_ug_m3_normal]);
  _values_received = 1;
  # else // ifndef PLUGIN_053_ENABLE_EXTRA_SENSORS

  // Store in the averaging buffer to process later
  if (!_oversample) {
    clearReceivedData();
  }

  for (uint8_t i = 0; i < PMS_RECEIVE_BUFFER_SIZE; ++i) {
    _data[i] += data[i];
  }
  ++_values_received;
  # endif // ifndef PLUGIN_053_ENABLE_EXTRA_SENSORS


  // Store new checksum, to help detect duplicates.
  _last_checksum = checksum;

  return true;
}

bool P053_data_struct::checkAndClearValuesReceived(struct EventStruct *event) {
  if (_values_received == 0) { return false; }

  # ifdef PLUGIN_053_ENABLE_EXTRA_SENSORS

  // Data is checked and good, fill in output

  _value_mask = 0;

  switch (GET_PLUGIN_053_OUTPUT_SELECTOR) {
    case PMSx003_output_selection::Particles_ug_m3:
    {
      UserVar.setFloat(event->TaskIndex, 0, getValue(PMS_PM1_0_ug_m3_normal));
      UserVar.setFloat(event->TaskIndex, 1, getValue(PMS_PM2_5_ug_m3_normal));
      UserVar.setFloat(event->TaskIndex, 2, getValue(PMS_PM10_0_ug_m3_normal));
      UserVar.setFloat(event->TaskIndex, 3, 0.0f);
      break;
    }
    case PMSx003_output_selection::PM2_5_TempHum_Formaldehyde:
    {
      UserVar.setFloat(event->TaskIndex, 0, getValue(PMS_PM2_5_ug_m3_normal));
      UserVar.setFloat(event->TaskIndex, 1, getValue(PMS_Temp_C));
      UserVar.setFloat(event->TaskIndex, 2, getValue(PMS_Hum_pct));
      UserVar.setFloat(event->TaskIndex, 3, getValue(PMS_Formaldehyde_mg_m3));
      break;
    }
    case PMSx003_output_selection::ParticlesCount_100ml_cnt0_3__cnt_2_5:
    {
      UserVar.setFloat(event->TaskIndex, 0, getValue(PMS_cnt0_3_100ml));
      UserVar.setFloat(event->TaskIndex, 1, getValue(PMS_cnt0_5_100ml));
      UserVar.setFloat(event->TaskIndex, 2, getValue(PMS_cnt1_0_100ml));
      UserVar.setFloat(event->TaskIndex, 3, getValue(PMS_cnt2_5_100ml));
      break;
    }
    case PMSx003_output_selection::ParticlesCount_100ml_cnt1_0_cnt2_5_cnt10:
    {
      UserVar.setFloat(event->TaskIndex, 0, getValue(PMS_cnt1_0_100ml));
      UserVar.setFloat(event->TaskIndex, 1, getValue(PMS_cnt2_5_100ml));
      UserVar.setFloat(event->TaskIndex, 2, getValue(PMS_cnt5_0_100ml));
      UserVar.setFloat(event->TaskIndex, 3, getValue(PMS_cnt10_0_100ml));
      break;
    }
  }

  if (Settings.UseRules
      && (GET_PLUGIN_053_EVENT_OUT_SELECTOR != PMSx003_event_datatype::Event_None)
      && (GET_PLUGIN_053_SENSOR_MODEL_SELECTOR != PMSx003_type::PMS2003_3003)) {
    // Events not applicable to PMS2003 & PMS3003 models
    // Send out events for those values not present in the task output
    switch (GET_PLUGIN_053_EVENT_OUT_SELECTOR) {
      case PMSx003_event_datatype::Event_None: break;
      case PMSx003_event_datatype::Event_All:
      {
        // Send all remaining
        for (uint8_t i = PMS_PM1_0_ug_m3_normal; i < PMS_RECEIVE_BUFFER_SIZE; ++i) {
          sendEvent(event->TaskIndex, i);
        }
        break;
      }
      case PMSx003_event_datatype::Event_PMxx_TempHum_Formaldehyde:
      {
        const uint8_t indices[] =
        {
          PMS_PM1_0_ug_m3_normal,
          PMS_PM2_5_ug_m3_normal,
          PMS_PM10_0_ug_m3_normal,
          PMS_Temp_C,
          PMS_Hum_pct,
          PMS_Formaldehyde_mg_m3
        };

        for (uint8_t i = 0; i < 6; ++i) {
          sendEvent(event->TaskIndex, indices[i]);
        }
        break;
      }
      case PMSx003_event_datatype::Event_All_count_bins:
      {
        // Thexe values are sequential, so just use a simple for loop.
        for (uint8_t i = PMS_cnt0_3_100ml; i <= PMS_cnt10_0_100ml; ++i) {
          sendEvent(event->TaskIndex, i);
        }
        break;
      }
    }
  }

  if (_oversample) {
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO,
                 strformat(F("PMSx003: Oversampling using %d samples"), _values_received));
    }
  }
  # endif // ifdef PLUGIN_053_ENABLE_EXTRA_SENSORS

  clearReceivedData();
  return true;
}

bool P053_data_struct::resetSensor() {
  if (validGpio(_resetPin)) { // Reset if pin is configured
    // Toggle 'reset' to assure we start reading header
    addLog(LOG_LEVEL_INFO, F("PMSx003: resetting module"));
    pinMode(_resetPin, OUTPUT);
    digitalWrite(_resetPin, LOW);
    delay(250);
    digitalWrite(_resetPin, HIGH);
    pinMode(_resetPin, INPUT_PULLUP);
    return true;
  }
  return false;
}

bool P053_data_struct::wakeSensor() {
  if (!initialized()) {
    return false;
  }
  addLog(LOG_LEVEL_INFO, F("PMSx003: Wake sensor"));

  if (validGpio(_pwrPin)) {
    // Make sure the sensor is "on"
    pinMode(_pwrPin, OUTPUT);
    digitalWrite(_pwrPin, HIGH);
    pinMode(_pwrPin, INPUT_PULLUP);
  } else
  # ifdef USES_P175
  if (!_P053_for_P175)
  # endif // ifdef USES_P175
  {
    const uint8_t command[7] = { 0x42, 0x4D, 0xE4, 0x00, 0x01, 0x01, 0x74 };
    _easySerial->write(command, 7);
  }

  // No idea how old this data is, so clear it.
  // Otherwise oversampling may give weird results
  clearReceivedData();

  if (_delay_read_after_wakeup_ms != 0) {
    _last_wakeup_moment.setMillisFromNow(_delay_read_after_wakeup_ms);
  } else {
    _last_wakeup_moment.clear();
  }
  return true;
}

bool P053_data_struct::sleepSensor() {
  if (!initialized()) {
    return false;
  }

  // Put the sensor to sleep
  addLog(LOG_LEVEL_INFO, F("PMSx003: Sleep sensor"));

  if (_pwrPin >= 0) {
    pinMode(_pwrPin, OUTPUT);
    digitalWrite(_pwrPin, LOW);
  } else
  # ifdef USES_P175
  if (!_P053_for_P175)
  # endif // ifdef USES_P175
  {
    const uint8_t command[7] = { 0x42, 0x4D, 0xE4, 0x00, 0x00, 0x01, 0x73 };
    _easySerial->write(command, 7);
  }

  if (_values_received > 0) {
    // Going to sleep, so flush whatever is read.
    Scheduler.schedule_task_device_timer(_taskIndex, millis() + 10);
  }
  return true;
}

void P053_data_struct::setActiveReadingMode() {
  if (initialized()) {
    # ifdef USES_P175

    if (!_P053_for_P175)
    # endif // ifdef USES_P175
    {
      const uint8_t command[7] = { 0x42, 0x4D, 0xE1, 0x00, 0x01, 0x01, 0x71 };
      _easySerial->write(command, 7);
    }
    _activeReadingModeEnabled = true;
  }
}

void P053_data_struct::setPassiveReadingMode() {
  if (initialized()) {
    # ifdef USES_P175

    if (!_P053_for_P175)
    # endif // ifdef USES_P175
    {
      const uint8_t command[7] = { 0x42, 0x4D, 0xE1, 0x00, 0x00, 0x01, 0x70 };
      _easySerial->write(command, 7);
    }
    _activeReadingModeEnabled = false;
  }
}

void P053_data_struct::requestData() {
  if (initialized() && !_activeReadingModeEnabled
      # ifdef USES_P175
      && !_P053_for_P175
      # endif // ifdef USES_P175
      ) {
    const uint8_t command[7] = { 0x42, 0x4D, 0xE2, 0x00, 0x00, 0x01, 0x71 };
    _easySerial->write(command, 7);
  }
}

# ifdef PLUGIN_053_ENABLE_EXTRA_SENSORS
float P053_data_struct::getValue(uint8_t index) {
  float value = 0.0f;

  getValue(index, value);
  return value;
}

bool P053_data_struct::getValue(uint8_t index, float& value) {
  if (bitRead(_value_mask, index) || (index >= PMS_RECEIVE_BUFFER_SIZE)) {
    // Already read
    return false;
  }

  switch (index) {
    case PMS_PM1_0_ug_m3_factory:
    case PMS_PM2_5_ug_m3_factory:
    case PMS_PM10_0_ug_m3_factory:
    case PMS_FW_rev_error:
    case PMS_Reserved:
      return false;

    case PMS_Temp_C:
    case PMS_Hum_pct:

      if (!hasTempHum()) { return false; }
      value = _data[index] / 10.0f;
      break;
    case PMS_Formaldehyde_mg_m3:

      if (!hasFormaldehyde()) { return false; }
      value = _data[index] / 1000.0f;
      break;
    case PMS_cnt5_0_100ml:
    case PMS_cnt10_0_100ml: // this option was missing :-|

      if (_sensortype == PMSx003_type::PMS5003_T) { return false; } // else: fall through
    case PMS_cnt0_3_100ml:
    case PMS_cnt0_5_100ml:
    case PMS_cnt1_0_100ml:
    case PMS_cnt2_5_100ml:
      value = _data[index];

      if (_splitCntBins) {
        value -= _data[index + 1];

        if (value < 0.0f) { value = 0.0f; }
      }
      break;

    default:
      value = _data[index];
      break;
  }

  if (_values_received > 1) {
    value /= static_cast<float>(_values_received);
  }

  bitSet(_value_mask, index);
  return true;
}

# endif // ifdef PLUGIN_053_ENABLE_EXTRA_SENSORS

void P053_data_struct::clearReceivedData() {
  # ifdef PLUGIN_053_ENABLE_EXTRA_SENSORS

  for (uint8_t i = 0; i < PMS_RECEIVE_BUFFER_SIZE; ++i) {
    _data[i] = 0.0f;
  }
  # endif // ifdef PLUGIN_053_ENABLE_EXTRA_SENSORS
  _values_received = 0;
}

void P053_data_struct::clearPacket() {
  _packetPos = 0;
  ZERO_FILL(_packet);
}

const __FlashStringHelper * P053_data_struct::getEventString(uint8_t index) {
  switch (index) {
    // case PMS_PM1_0_ug_m3_factory   : return F("");
    // case PMS_PM2_5_ug_m3_factory   : return F("");
    // case PMS_PM10_0_ug_m3_factory  : return F("");
    case PMS_PM1_0_ug_m3_normal: return F("pm1.0");
    case PMS_PM2_5_ug_m3_normal: return F("pm2.5");
    case PMS_PM10_0_ug_m3_normal: return F("pm10");
    case PMS_cnt0_3_100ml: return F("cnt0.3");
    case PMS_cnt0_5_100ml: return F("cnt0.5");
    case PMS_cnt1_0_100ml: return F("cnt1.0");
    case PMS_cnt2_5_100ml: return F("cnt2.5");
    case PMS_cnt5_0_100ml: return F("cnt5.0");
    case PMS_cnt10_0_100ml: return F("cnt10");
    case PMS_Formaldehyde_mg_m3: return F("HCHO");
    case PMS_Temp_C: return F("Temp");
    case PMS_Hum_pct: return F("Humi");

      // case PMS_FW_rev_error          : return F("");
  }
  return F("Unknown");
}

void P053_data_struct::setTaskValueNames(ExtraTaskSettingsStruct& settings, const uint8_t indices[], uint8_t nrElements,
                                         bool oversample) {
  for (uint8_t i = 0; i < VARS_PER_TASK; ++i) {
    settings.TaskDeviceValueDecimals[i] = 0;

    if (i < nrElements) {
      safe_strncpy(
        settings.TaskDeviceValueNames[i],
        P053_data_struct::getEventString(indices[i]),
        sizeof(settings.TaskDeviceValueNames[i]));

      settings.TaskDeviceValueDecimals[i] = getNrDecimals(indices[i], oversample);
    } else {
      ZERO_FILL(settings.TaskDeviceValueNames[i]);
    }
  }
}

unsigned char P053_data_struct::getNrDecimals(uint8_t index, bool oversample) {
  switch (index) {
    case PMS_Temp_C:
    case PMS_Hum_pct:
      return 1;
    case PMS_Formaldehyde_mg_m3:
      return 3;
  }

  if (oversample) { return 1; }
  return 0;
}

#endif // ifdef USES_P053

#include "../PluginStructs/P153_data_struct.h"

#ifdef USES_P153

# include "../Helpers/CRC_functions.h"

/**************************************************************************
* Constructor
**************************************************************************/
P153_data_struct::P153_data_struct(uint8_t              address,
                                   float                tempOffset,
                                   P153_configuration_e startupConfiguration,
                                   P153_configuration_e normalConfiguration,
                                   uint16_t             intervalLoops) :
  _address(address), _tempOffset(tempOffset), _startupConfiguration(startupConfiguration),
  _normalConfiguration(normalConfiguration), _intervalLoops(intervalLoops), initialized(false)
{}

bool P153_data_struct::init() {
  // - Read sensor serial number
  if (I2C_wakeup(_address) == 0) {
    if (I2C_write8(_address, P153_SHT4X_RESET)) {
      delay(1);

      uint8_t data[6]{};

      if (I2C_write8(_address, P153_SHT4X_READ_SERIAL)) {
        delay(10);

        Wire.requestFrom(_address, (uint8_t)6);

        for (uint8_t d = 0; d < 6; d++) {
          data[d] = Wire.read();
        }

        if (calc_CRC8(data[0], data[1], data[2]) && calc_CRC8(data[3], data[4], data[5])) {
          serialNumber   = data[0];
          serialNumber <<= 8;
          serialNumber  |= data[1];
          serialNumber <<= 8;
          serialNumber  |= data[3];
          serialNumber <<= 8;
          serialNumber  |= data[4];
          addLog(LOG_LEVEL_INFO, concat(F("SHT4x: Serial number: "), formatToHex_decimal(serialNumber)));
          initialized = true;
        } else {
          // crc error
          addLog(LOG_LEVEL_ERROR, F("SHT4x: Error reading serial number."));
        }
      }
    }
  }

  return isInitialized();
}

/*****************************************************
* plugin_read
*****************************************************/
bool P153_data_struct::plugin_read(struct EventStruct *event)           {
  bool success = false;

  if (isInitialized()) {
    int16_t timeDelay = -1;

    // read in stages
    if (P153_read_mode_e::Idle == readMode) {
      // Determine delay per command

      switch ((_intervalLoops > 0) ? _startupConfiguration : _normalConfiguration) {
        case P153_configuration_e::LowResolution:
          timeDelay = P153_DELAY_LOW_RESOLUTION;
          break;
        case P153_configuration_e::MediumResolution:
          timeDelay = P153_DELAY_MEDIUM_RESOLUTION;
          break;
        case P153_configuration_e::HighResolution:
          timeDelay = P153_DELAY_HIGH_RESOLUTION;
          break;
        case P153_configuration_e::HighResolution200mW100msec:
        case P153_configuration_e::HighResolution110mW100msec:
        case P153_configuration_e::HighResolution20mW100msec:
          timeDelay = P153_DELAY_100MS_HEATER;
          break;
        case P153_configuration_e::HighResolution200mW1000msec:
        case P153_configuration_e::HighResolution110mW1000msec:
        case P153_configuration_e::HighResolution20mW1000msec:
          timeDelay = P153_DELAY_1S_HEATER;
          break;
      }

      // Start measurement
      if (!I2C_write8(_address, static_cast<uint8_t>((_intervalLoops > 0) ? _startupConfiguration : _normalConfiguration))) {
        timeDelay = -1; // Don't continue if writing command fails

        UserVar.setFloat(event->TaskIndex, 0, NAN);
        UserVar.setFloat(event->TaskIndex, 1, NAN);

        if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
          addLogMove(LOG_LEVEL_ERROR, strformat(F("%s: Error writing command to sensor"), getTaskDeviceName(event->TaskIndex).c_str()));
        }
      }
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_DEBUG, concat(F("P153 : READ delay: "), timeDelay));
      # endif // ifndef BUILD_NO_DEBUG

      measurementStart = millis();

      if (timeDelay > P153_DELAY_HIGH_RESOLUTION) { // Short delays are handled locally, longer uses the task device timer
        Scheduler.schedule_task_device_timer(event->TaskIndex, measurementStart + timeDelay);
      }
    }

    if ((P153_read_mode_e::Reading == readMode) || ((timeDelay >= 0) && (timeDelay <= P153_DELAY_HIGH_RESOLUTION))) {
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_DEBUG, F("P153 : READ execute Readmode = Reading"));
      # endif // ifndef BUILD_NO_DEBUG

      // Handle short delays immediately
      if ((timeDelay > 0) && (timeDelay <= P153_DELAY_HIGH_RESOLUTION)) {
        delay(timeDelay);
      }

      // Read the sensor data
      uint8_t data[6]{};

      Wire.requestFrom(_address, (uint8_t)6);

      if (Wire.available() == 6) {
        for (uint8_t d = 0; d < 6; d++) {
          data[d] = Wire.read();
        }
      } else {
        UserVar.setFloat(event->TaskIndex, 0, NAN); // Read error or I/O error
        UserVar.setFloat(event->TaskIndex, 1, NAN);

        if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
          String log;

          if (log.reserve(40)) {
            log  = getTaskDeviceName(event->TaskIndex);
            log += F(": Error reading sensor");
            addLogMove(LOG_LEVEL_ERROR, log);
          }
        }
      }

      // Data valid?
      if (calc_CRC8(data[0], data[1], data[2]) && calc_CRC8(data[3], data[4], data[5])) {
        const float temp = static_cast<float>(((uint16_t)data[0] << 8) | (uint16_t)data[1]);
        const float hum  = static_cast<float>(((uint16_t)data[3] << 8) | (uint16_t)data[4]);
        temperature = -45.0f + 175.0f * temp / 65535.0f;
        humidity    = -6.0f + 125.0f * hum / 65535.0f;

        if (definitelyLessThan(humidity, 0.0f)) { humidity = 0.0f; }

        if (definitelyGreaterThan(humidity, 100.0f)) { humidity = 100.0f; }

        UserVar.setFloat(event->TaskIndex, 0, temperature + _tempOffset); // Apply offset
        UserVar.setFloat(event->TaskIndex, 1, humidity);

        success    = true;
        errorCount = 0;

        if (_intervalLoops > 0) {
          _intervalLoops--;

          if ((_intervalLoops == 0) && (_startupConfiguration != _normalConfiguration)) {
            addLog(LOG_LEVEL_INFO, F("SHT4x: Switching from Startup to Normal Configuration."));
          }
        }

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          const String taskName = getTaskDeviceName(event->TaskIndex);
          addLogMove(LOG_LEVEL_INFO, strformat(F("%s: Temperature: %s"),
                                               taskName.c_str(),
                                               formatUserVarNoCheck(event, 0).c_str()));

          addLogMove(LOG_LEVEL_INFO, strformat(F("%s: Humidity: %s"),
                                               taskName.c_str(),
                                               formatUserVarNoCheck(event, 1).c_str()));
        }
      } else {
        UserVar.setFloat(event->TaskIndex, 0, NAN);
        UserVar.setFloat(event->TaskIndex, 1, NAN);
        addLog(LOG_LEVEL_ERROR, concat(F("SHT4x: READ CRC Error, data: 0x"), formatToHex_array(data, 6)));
        errorCount++;

        if (errorCount > P153_MAX_ERRORCOUNT) {
          I2C_write8(_address, P153_SHT4X_RESET);
          delay(1);
          _intervalLoops = 0;
          addLog(LOG_LEVEL_ERROR, F("SHT4x: READ Error count reached, reset to Normal Configuration."));
        }
      }

      Scheduler.reschedule_task_device_timer(event->TaskIndex, measurementStart); // Sync with schedule
      readMode  = P153_read_mode_e::Idle;
      timeDelay = -1;                                                             // Reset
    }

    if ((P153_read_mode_e::Idle == readMode) && (timeDelay >= 0)) {
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_DEBUG, F("P153 : READ set Readmode = Reading"));
      # endif // ifndef BUILD_NO_DEBUG
      readMode = P153_read_mode_e::Reading;
    }
  }

  return success;
}

/*****************************************************
* plugin_write
*****************************************************/
bool P153_data_struct::plugin_write(struct EventStruct *event,
                                    String            & string) {
  bool success = false;

  const String command = parseString(string, 1);

  if (equals(command, F("sht4x"))) {
    const String subCommand = parseString(string, 2);

    if (equals(subCommand, F("startup")) && (_startupConfiguration != _normalConfiguration) && (P153_INTERVAL_LOOPS > 0)) {
      _intervalLoops = P153_INTERVAL_LOOPS;
      success        = true;
    }
  }
  return success;
}

/*****************************************************
* plugin_get_config_value
*****************************************************/
bool P153_data_struct::plugin_get_config_value(struct EventStruct *event,
                                               String            & string) {
  bool success = false;

  const String var = parseString(string, 1);

  if (equals(var, F("serialnumber"))) { // [<taskname>#serialnumber] = the devices electronic serial number
    string  = serialNumber;
    success = true;
  }
  return success;
}

#endif // ifdef USES_P153

#include "../PluginStructs/P154_data_struct.h"

#if defined(USES_P154) || defined(USES_P172)

# define P154_BMP3_CHIP_ID     0x50
# define P154_BMP390_CHIP_ID   0x60


P154_data_struct::P154_data_struct(struct EventStruct *event) :
  i2cAddress(P154_I2C_ADDR),
  elevation(P154_ALTITUDE),
  csPin(PIN(0))
{}

bool P154_data_struct::begin(bool _i2cMode)
{
  i2cMode = _i2cMode;

  if (i2cMode && !bmp.begin_I2C(i2cAddress)) {
    return false;
  }

  if (!i2cMode && !bmp.begin_SPI(csPin)) {
    return false;
  }

  // Set up oversampling and filter initialization
  bmp.setTemperatureOversampling(BMP3_OVERSAMPLING_8X);
  bmp.setPressureOversampling(BMP3_OVERSAMPLING_4X);
  bmp.setIIRFilterCoeff(BMP3_IIR_FILTER_COEFF_3);
  bmp.setOutputDataRate(BMP3_ODR_50_HZ);

  // Perform a few reads, so the IIR filter does have a 'seed'
  for (int i = 0; i < 5; ++i) {
    bmp.performReading();
  }
  initialized = true;

  return true;
}

bool P154_data_struct::read(float& temp, float& pressure)
{
  if (!bmp.performReading()) {
    return false;
  }

  temp     = bmp.temperature;
  pressure = bmp.pressure / 100.0f; // hPa

  if (elevation != 0) {
    pressure = pressureElevation(pressure, elevation);
  }

  return true;
}

uint32_t P154_data_struct::chipID() {
  return bmp.chipID();
}

bool P154_data_struct::webformLoad(struct EventStruct *event,
                                   bool                _i2cMode)
{
  uint32_t chipID{};
  bool     chipIDvalid = _i2cMode;

  if (_i2cMode) {
    chipID = I2C_read8_reg(P154_I2C_ADDR, 0);
  # ifdef USES_P172
  } else {
    P154_data_struct *P154_P172_data =
      static_cast<P154_data_struct *>(getPluginTaskData(event->TaskIndex));

    if (nullptr != P154_P172_data) {
      chipID      = P154_P172_data->chipID();
      chipIDvalid = true;
    }
  # endif // ifdef USES_P172
  }

  if (chipIDvalid) {
    addRowLabel(F("Detected Sensor Type"));

    if (chipID == P154_BMP3_CHIP_ID) {
      addHtml(F("BMP38x"));
    } else if (chipID == P154_BMP390_CHIP_ID) {
      addHtml(F("BMP390"));
    } else {
      addHtmlInt(chipID);
    }
  }

  addFormNumericBox(F("Altitude"), F("elev"), P154_ALTITUDE);
  addUnit('m');
  return true;
}

bool P154_data_struct::webformSave(struct EventStruct *event)
{
  P154_I2C_ADDR = getFormItemInt(F("i2c_addr"));
  P154_ALTITUDE = getFormItemInt(F("elev"));
  return true;
}

#endif // if defined(USES_P154) || defined(USES_P172)

#include "../PluginStructs/P014_data_struct.h"

#ifdef USES_P014


P014_data_struct::P014_data_struct() {
  state         = P014_state::Uninitialized; // Force device setup next time
  last_measurement_time = 0;
}


bool P014_data_struct::startInit(uint8_t i2caddr)
{
  state = P014_state::Uninitialized;

  uint8_t ret = I2C_wakeup(i2caddr);
  if (ret){
    if (loglevelActiveFor(LOG_LEVEL_ERROR)){
      addLogMove(LOG_LEVEL_ERROR, strformat(F("SI70xx : Not available at address: %x"), i2caddr));
    }
    return false;
  }

  //reset the device
  if (!softReset(i2caddr)){
    if (loglevelActiveFor(LOG_LEVEL_ERROR)){
      addLogMove(LOG_LEVEL_ERROR, strformat(F("SI70xx : Not able to reset: %x"), i2caddr));
    }
    return false;
  }

  return true;
}


bool P014_data_struct::finalizeInit(uint8_t i2caddr, uint8_t resolution)
{
  //read the device ID and revision
  //SHT devices do not have this capability so we are continuing if not available 
  int8_t ret = readSerialNumber(i2caddr); //this will also set the device ID
  if (ret){
      if (loglevelActiveFor(LOG_LEVEL_ERROR)){
        addLogMove(LOG_LEVEL_ERROR, strformat(F("SI70xx : Not able to read SN: addr=0x%x err=%d"), i2caddr, ret));
      }
    //return false;
  }

  //at this point we know the chip_id
  if (loglevelActiveFor(LOG_LEVEL_INFO)){
    addLogMove(LOG_LEVEL_INFO, strformat(F("P014: chip_id=%d"), chip_id));
  }
  if (chip_id == CHIP_ID_SI7013){
    if (!I2C_write8_reg(i2caddr,SI7013_WRITE_REG2, SI7013_REG2_DEFAULT )){
      return false;
    }
    if (!enablePowerForADC(i2caddr)){
      return false;
    }
  }

  // Set the resolution we want
  if(!setResolution(i2caddr,resolution)){
    return false;
  }
  
  //SI7013 has ADC and we need to start the first measurement
  if (chip_id == CHIP_ID_SI7013){
    if (!requestADC(i2caddr))
      return false;
  }

  //we are now initialized
  return true;
}



//The main state machine
//Only perform the measurements with big interval to prevent the sensor from warming up.
//returns true when the state changes and false otherwise
bool P014_data_struct::update(uint8_t i2caddr, uint8_t resolution, uint8_t filter_power) {

  switch(state){
    case P014_state::Uninitialized:
        //make sure we do not try to initialize too often
        //someone could enable the plugin but does not have the hardware connected
        if (!timeOutReached(last_measurement_time + errCount*SI70xx_INIT_DELAY)) {
          return false;
        }
        last_measurement_time = millis();
        //we have to stop trying after a while
        if (errCount>P014_MAX_RETRY){
            state = P014_state::Error;
            return true;
        }

        if (I2C_wakeup(i2caddr)!=0){
          if (loglevelActiveFor(LOG_LEVEL_ERROR)){
            addLogMove(LOG_LEVEL_ERROR, strformat(F("SI70xx : Not available at address: %x"), i2caddr));
          }
          errCount++;
          return false;
        }

        if (startInit(i2caddr)){
          state                 = P014_state::Wait_for_reset;
          return true;
        }

        errCount++;
        return false;
    //break;

    case P014_state::Wait_for_reset:
      //we need to wait for the chip to reset
      if (!timeOutReached(last_measurement_time + SI70xx_RESET_DELAY)) {
        return false;
      }

      if (!finalizeInit(i2caddr,resolution)){
        state = P014_state::Uninitialized;
        errCount++;
        return true;
      }

      errCount = 0;
      state = P014_state::Initialized;
      return true;
    //break;      

    case P014_state::Initialized:
      if (chip_id == CHIP_ID_SI7013){
        //we need to wait for the read of ADC
        if (!timeOutReached(last_measurement_time + SI70xx_MEASUREMENT_DELAY)) {
          return false;
        }

        if ( !readADC(i2caddr,filter_power)){
          return false;
        }
        
        adc = adc << filter_power; //this is the first measurement
      } 
      last_measurement_time = millis();
      state = P014_state::Ready;
      return true;
    //break;

    case P014_state::Ready:
        if (!I2C_write8(i2caddr,SI70xx_CMD_MEASURE_HUM)) { //measure humidity and temperature at the same time
          addLog(LOG_LEVEL_ERROR,F("SI70xx: startConv Failed!"));
          return false;
        }
      
        last_measurement_time = millis();
        state = P014_state::Wait_for_humidity_samples;
      return true;
    //break;

    case P014_state::Wait_for_humidity_samples:
        //make sure we wait for the measurement to complete
        if (!timeOutReached(last_measurement_time + SI70xx_MEASUREMENT_DELAY)) {
          return false;
        }

        if (!readHumidity(i2caddr, resolution)) {
          state = P014_state::Ready; //we go back to request the reading again
          return true;
        }
        
        last_measurement_time = millis();
  
        if (chip_id == CHIP_ID_HTU21D){
          if (!I2C_write8(i2caddr,SI70xx_CMD_MEASURE_TEMP)) { //HTU21D can't read temperature from humidity measurement
            addLog(LOG_LEVEL_ERROR,F("SI70xx: startConv Failed!"));
            return false;
          }
          state = P014_state::Wait_for_temperature_samples;
          return false;
        }

        if (!readTemperatureFromHumidity(i2caddr,resolution)){
          state = P014_state::Ready; //we go back to request the reading again
          return true;
        }

        if (chip_id == CHIP_ID_SI7013){
          if(!enablePowerForADC(i2caddr)){
            state = P014_state::Ready; //we go back to request the reading again
            return true;
          }
          state = P014_state::RequestADC;
        }else{
          state = P014_state::New_Values_Available;
        }
        return true;
    //break;

    case P014_state::Wait_for_temperature_samples:
        if (!timeOutReached(last_measurement_time + SI70xx_MEASUREMENT_DELAY)) {
          return false;
        }

        if (!readTemperature(i2caddr,resolution)){
          state = P014_state::Ready; //we go back to request the reading again
          return true;
        }

        state = P014_state::New_Values_Available;
        return true;

    case P014_state::RequestADC:
        //make sure we wait for the power to stabilize
        if (!timeOutReached(last_measurement_time + SI70xx_MEASUREMENT_DELAY)) {
          return false;
        }

        if (!requestADC(i2caddr)){
          state = P014_state::Ready; //we go back to request the reading again
          return true;
        }
        
        state = P014_state::Wait_for_adc_samples;
        return true;
    //break;

    case P014_state::Wait_for_adc_samples:
        if (!readADC(i2caddr, filter_power)){
          state = P014_state::RequestADC; //we go back to request the reading again
          return false;
        }
        state = P014_state::New_Values_Available;
        return true;
    //break;

    case P014_state::Error:
    case P014_state::New_Values_Available:
      //this state is used outside so all we need is to stay here
      return false;
    break;


    default: //anything not defined above sends the device for initialization
      state = P014_state::Uninitialized;
      return true;
  } 

  return false;
}


 

//--------------- Supporting Functions -------------------


/* ======================================================================
   Function: checkCRC
   Purpose : check the CRC of received data
   Input   : value read from sensor
   Output  : CRC read from sensor
   Comments: 0 if okay
   ====================================================================== */
uint8_t P014_data_struct::checkCRC(uint16_t data, uint8_t check)
{
  uint32_t remainder, divisor;

  // Pad with 8 bits because we have to add in the check value
  remainder = (uint32_t)data << 8;

  // From: http://www.nongnu.org/avr-libc/user-manual/group__util__crc.html
  // POLYNOMIAL = 0x0131 = x^8 + x^5 + x^4 + 1 : http://en.wikipedia.org/wiki/Computation_of_cyclic_redundancy_checks
  // 0x988000 is the 0x0131 polynomial shifted to farthest left of three bytes
  divisor = (uint32_t)0x988000;

  // Add the check value
  remainder |= check;

  // Operate on only 16 positions of max 24.
  // The remaining 8 are our remainder and should be zero when we're done.
  for (uint8_t i = 0; i < 16; ++i) {
    // Check if there is a one in the left position
    if (remainder & (uint32_t)1 << (23 - i)) {
      remainder ^= divisor;
    }

    // Rotate the divisor max 16 times so that we have 8 bits left of a remainder
    divisor >>= 1;
  }
  return (uint8_t)remainder;
}





/* ======================================================================
Function: setResolution
Purpose : Sets the sensor resolution to one of four levels
Input   : see #define default is SI7013_RESOLUTION_14T_12RH
Output  : 0 if okay
Comments: -
====================================================================== */
bool P014_data_struct::setResolution(uint8_t i2caddr, uint8_t resolution)
{
  bool ok;
  uint8_t reg = I2C_read8_reg(i2caddr,SI70xx_CMD_READ_REG1,&ok);
  if (ok) {
    // remove resolution bits
    reg &= SI70xx_RESOLUTION_MASK ;
    // Write the new resolution bits but clear unused before
    ok = I2C_write8_reg(i2caddr,SI70xx_CMD_WRITE_REG1, reg | ( resolution &= ~SI70xx_RESOLUTION_MASK) );
  }

  if (!ok) {
      if (loglevelActiveFor(LOG_LEVEL_ERROR)){
        addLogMove(LOG_LEVEL_ERROR, strformat(F("SI70xx : Unable to set resolution: %x"), i2caddr));
      }
  }

  return ok;
}


inline bool P014_data_struct::softReset(uint8_t i2caddr){
  // Prepare to write to the register value
    bool ret = I2C_write8(i2caddr, SI70xx_CMD_SOFT_RESET);
    return ret;
}


#ifndef LIMIT_BUILD_SIZE
int8_t P014_data_struct::readRevision(uint8_t i2caddr) {
  Wire.beginTransmission(i2caddr);
  Wire.write((uint8_t)(SI70xx_CMD_FIRMREV >> 8));
  Wire.write((uint8_t)(SI70xx_CMD_FIRMREV & 0xFF));
  Wire.endTransmission();

  uint32_t start = millis(); // start timeout
  
  while (!timeOutReached(start + SI70xx_MEASUREMENT_DELAY)) {
    if (Wire.requestFrom(i2caddr, 2u) == 2) {
      uint8_t rev = Wire.read();
      Wire.read(); //ignore CRC

      if (loglevelActiveFor(LOG_LEVEL_INFO)){
        addLogMove(LOG_LEVEL_INFO, strformat(F("SI7013 : revision=%x"), rev));
      }
      
      return 0;
    }
    delay(2);
  }
  return -1; // Error timeout
}

#endif

/*!
 *  @brief  Reads serial number and stores It in sernum_a and sernum_b variable
 */
int8_t P014_data_struct::readSerialNumber(uint8_t i2caddr) {
  
  bool ok = I2C_write8_reg(i2caddr,(uint8_t)(SI70xx_CMD_ID1 >> 8),(uint8_t)(SI70xx_CMD_ID1 & 0xFF));
  if (!ok) return -1; //error on sending command 
  
  if (Wire.requestFrom(i2caddr, 8u) != 8u) {
    return -2; //time out on the SNA read
  }

  uint32_t sernum_a = Wire.read(); //SNA_3
  Wire.read(); //ignore CRC
  sernum_a <<= 8;
  sernum_a |= Wire.read(); //SNA_2
  Wire.read(); //ignore CRC
  sernum_a <<= 8;
  sernum_a |= Wire.read(); //SNA_1
  Wire.read(); //ignore CRC
  sernum_a <<= 8;
  sernum_a |= Wire.read(); //SNA_0
  Wire.read(); //ignore CRC

  ok = I2C_write8_reg(i2caddr,(uint8_t)(SI70xx_CMD_ID2 >> 8),(uint8_t)(SI70xx_CMD_ID2 & 0xFF));
  if (!ok) return -3; //error on sending command 

  if (Wire.requestFrom(i2caddr, 6u) != 6u) {
    return -4; //time out on the SNA read
  }

  uint32_t sernum_b = Wire.read(); //SNB_3 (Device ID)
  chip_id = sernum_b; //we save the chip identifier
  //Wire.read(); //ignore CRC
  sernum_b <<= 8;
  sernum_b |= Wire.read(); //SNB_2
  Wire.read(); //ignore CRC
  sernum_b <<= 8;
  sernum_b |= Wire.read(); //SNB_1
  //Wire.read(); //ignore CRC
  sernum_b <<= 8;
  sernum_b |= Wire.read(); //SNB_0
  Wire.read(); //ignore CRC

  if (loglevelActiveFor(LOG_LEVEL_INFO)){
    addLogMove(LOG_LEVEL_INFO, strformat(F("SI7013 : sn=%x-%x"), sernum_a, sernum_b));
  }
  return 0; //ok
}


bool P014_data_struct::enablePowerForADC(uint8_t i2caddr){
  //set VOUT
  //Get the current register value

  //we know the type of circuit used based on the i2c address 


  bool ok=false;
  uint8_t reg = I2C_read8_reg(i2caddr,SI7013_READ_REG2, &ok);

  if (!ok) {
    addLog(LOG_LEVEL_ERROR, F("SI7013: Could not read REG2!"));
    return false;
  }
    
  
  if (i2caddr == SI7013_I2C_ADDRESS_AD0_1){
    ok = I2C_write8_reg(i2caddr,SI7013_WRITE_REG2, (reg & 0b11111000) | (2+4+64) );//set last three bits (VIN bufered, Vref=VDD, VOUT=GND) and No-Hold for bit 6
  }else{
    ok = I2C_write8_reg(i2caddr,SI7013_WRITE_REG2,reg | (1+2+4+64) );//set last three bits to 1 (VIN bufered, Vref=VDD, VOUT=VDD) and No-Hold for bit 6
  }
  if (!ok){
    addLog(LOG_LEVEL_ERROR, F("SI7013: Could not write REG2!"));
    return false;
  }

  return true;
}

bool P014_data_struct::disablePowerForADC(uint8_t i2caddr){
  //set VOUT
  //Get the current register value

  //we know the type of circuit used based on the i2c address 


  bool ok=false;
  uint8_t reg = I2C_read8_reg(i2caddr,SI7013_READ_REG2, &ok);

  if (!ok) {
    addLog(LOG_LEVEL_ERROR, F("SI7013: Could not read REG2!"));
    return false;
  }
    
  
  if (i2caddr == SI7013_I2C_ADDRESS_AD0_1){
    ok = I2C_write8_reg(i2caddr,SI7013_WRITE_REG2,reg | (1+2+4+64) );//set last three bits to 1 (VIN bufered, Vref=VDD, VOUT=VDD) and No-Hold for bit 6
  }else{
    ok = I2C_write8_reg(i2caddr,SI7013_WRITE_REG2, (reg & 0b11111000) | (2+4+64) );//set last three bits (VIN bufered, Vref=VDD, VOUT=GND) and No-Hold for bit 6
  }
  if (!ok){
    addLog(LOG_LEVEL_ERROR, F("SI7013: Could not write REG2!"));
    return false;
  }

  return true;
}

bool P014_data_struct::requestADC(uint8_t i2caddr)
{
    //start ADC conversion
    if(!I2C_write8(i2caddr, SI7013_READ_ADC)){
      addLog(LOG_LEVEL_ERROR,F("SI7013: could not start ADC conversion"));
      return false;
    }
    //delay(10); //waiting for conversion to be done: in the specs is mentioned 7ms in normal mode
  return true;
}


bool P014_data_struct::readADC(uint8_t i2caddr, uint8_t filter_power)
{
  if ( Wire.requestFrom(i2caddr, 2u) < 2 ) {
    return false;
  }

  // Comes back in two bytes, data(MSB) / data(LSB) with no Checksum
  uint16_t raw  = ((uint16_t) Wire.read()) << 8;
  raw |= Wire.read();

  //Calculate Moving average where 2^filter_power is the moving window of points
  //MA*[i]= MA*[i-1] +X[i] - MA*[i-1]/N
  adc = adc + raw - (adc>>filter_power);
  
  //disable power for ADC
  return disablePowerForADC(i2caddr);
}



bool P014_data_struct::readHumidity(uint8_t i2caddr, uint8_t resolution)
{
    uint16_t raw;
    uint8_t bytes = Wire.requestFrom(i2caddr, 3u); //asking to read 3 bytes  
    if ( bytes < 3 ) {
      return false;
    }

    // Comes back in three bytes, data(MSB) / data(LSB) / Checksum
    raw  = ((uint16_t) Wire.read()) << 8;
    raw |= Wire.read(); 
    uint8_t checksum = Wire.read();


    // Check CRC of data received
    if(checkCRC(raw, checksum) != 0) {
      addLog(LOG_LEVEL_ERROR,F("SI70xx : checksum error!"));
      return false;
    }

    // Convert raw value to Humidity percent
    // pm-cz: it is possible to enable decimal places for humidity as well by multiplying the value in formula by 100
    int data = ((1250 * (long)raw) >> 16) - 60;

    // Datasheet says doing this check
    if (data>1000) data = 1000;
    if (data<0)   data = 0;

    //pm-cz: Let us make sure we have enough precision due to ADC bits
    if (resolution == SI70xx_RESOLUTION_12T_08RH) {
      data = (data + 5) / 10;
      data *= 10;
    }
    // save value
    humidity = data;

  return true;
}

bool P014_data_struct::readTemperature(uint8_t i2caddr, uint8_t resolution)
{
  uint16_t raw;
  uint8_t bytes = Wire.requestFrom(i2caddr, 3u); //asking to read 3 bytes  
  if ( bytes < 3 ) {
    return false;
  }

  // Comes back in three bytes, data(MSB) / data(LSB) / Checksum
  raw  = ((uint16_t) Wire.read()) << 8;
  raw |= Wire.read();
  
  uint8_t checksum = Wire.read();

  // Check CRC of data received
  if(checkCRC(raw, checksum) != 0) {
    addLog(LOG_LEVEL_ERROR,F("SI70xx : checksum error!"));
    return false;
  }

  temperature =  convertRawTemperature(raw & 0xFFFC,resolution);
  return true;
}

bool P014_data_struct::readTemperatureFromHumidity(uint8_t i2caddr, uint8_t resolution){
  // Temperature
  //Request a reading
  uint16_t   raw = I2C_read16_reg(i2caddr,SI70xx_CMD_READ_TEMP_FROM_HUM);

  // save value
  temperature =  convertRawTemperature(raw,resolution);
  
  return true;
}

int16_t P014_data_struct::convertRawTemperature(uint16_t raw, uint8_t resolution){
  // Convert raw value to Temperature (*100)
  // for 23.45C value will be 2345
  int16_t data =  ((17572 * (long)raw) >> 16) - 4685;

  // pm-cz: We should probably check for precision here as well
  if (resolution != SI70xx_RESOLUTION_14T_12RH) {
    if (data > 0) {
      data = (data + 5) / 10;
    } else {
      data = (data - 5) / 10;
    }
    data *= 10;
  }

  return data;
}

#endif // ifdef USES_P014

#include "../PluginStructs/P104_data_struct.h"

#ifdef USES_P104

# include "../Helpers/ESPEasy_Storage.h"
# include "../Helpers/Numerical.h"
# include "../WebServer/Markup_Forms.h"
# include "../WebServer/ESPEasy_WebServer.h"
# include "../WebServer/Markup.h"
# include "../WebServer/HTML_wrappers.h"
# include "../ESPEasyCore/ESPEasyRules.h"
# include "../Globals/ESPEasy_time.h"
# include "../Globals/RTC.h"

# include <vector>
# include <MD_Parola.h>
# include <MD_MAX72xx.h>

// Needed also here for PlatformIO's library finder as the .h file
// is in a directory which is excluded in the src_filter

# if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT)
void createHString(String& string); // Forward definition
# endif // if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT)
void reverseStr(String& str);       // Forward definition

/****************************************************************
 * Constructor
 ***************************************************************/
P104_data_struct::P104_data_struct(MD_MAX72XX::moduleType_t _mod,
                                   taskIndex_t              _taskIndex,
                                   int8_t                   _cs_pin,
                                   uint8_t                  _modules,
                                   uint8_t                  _zonesCount)
  : mod(_mod), taskIndex(_taskIndex), cs_pin(_cs_pin), modules(_modules), expectedZones(_zonesCount) {
  if (Settings.isSPI_valid()) {
    P = new (std::nothrow) MD_Parola(mod, cs_pin, modules);
  } else {
    addLog(LOG_LEVEL_ERROR, F("DOTMATRIX: Required SPI not enabled. Initialization aborted!"));
  }
}

/*******************************
 * Destructor
 ******************************/
P104_data_struct::~P104_data_struct() {
  # if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET)

  if (nullptr != pM) {
    pM = nullptr; // Not created here, only reset
  }
  # endif // if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET)

  if (nullptr != P) {
    // P->~MD_Parola(); // Call destructor directly, as delete of the object fails miserably
    // do not: delete P; // Warning: the MD_Parola object doesn't have a virtual destructor, and when changed,
    // a reboot uccurs when the object is deleted here!
    P = nullptr; // Reset only
  }
}

/*******************************
 * Initializer/starter
 ******************************/
bool P104_data_struct::begin() {
  if (!initialized) {
    loadSettings();
    initialized = true;
  }

  if ((P != nullptr) && validGpio(cs_pin)) {
    # ifdef P104_DEBUG
    addLog(LOG_LEVEL_INFO, F("dotmatrix: begin() called"));
    # endif // ifdef P104_DEBUG
    P->begin(expectedZones);
    # if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET)
    pM = P->getGraphicObject();
    # endif // if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET)
    return true;
  }
  return false;
}

# define P104_ZONE_SEP   '\x02'
# define P104_FIELD_SEP  '\x01'
# define P104_ZONE_DISP  ';'
# define P104_FIELD_DISP ','

# define P104_CONFIG_VERSION_V2  0xF000 // Marker in first uint16_t to to indicate second version config settings, anything else if first
                                        // version.
                                        // Any third version or later could use 0xE000, etc. The 'version' is stored in the first uint16_t
                                        // stored in the custom settings
# define P104_CONFIG_VERSION_V3  0xE000 // Marker to indicate we're using V3 of the settings, same base-format as V2, but using the
                                        // CustomTaskSettings Extension file only, by inserting an offset of DAT_TASKS_CUSTOM_SIZE
                                        // ATTENTION: V3 is _only_ activated for FEATURE_EXTENDED_CUSTOM_SETTINGS, ESP32 & USE_LITTLEFS !!!

/*
   Settings layout:
   Version 1:
   - uint16_t : size of the next blob holding all settings
   - char[x]  : Blob with settings, with csv-like strings, using P104_FIELD_SEP and P104_ZONE_SEP separators
   Version 2:
   - uint16_t : marker with content P104_CONFIG_VERSION_V2
   - uint16_t : size of next blob holding 1 zone settings string
   - char[y]  : Blob holding 1 zone settings string, with csv like string, using P104_FIELD_SEP separators
   - uint16_t : next size, if 0 then no more blobs
   - char[x]  : Blob
   - ...
   - Max. allowed total custom settings size = 1024
   Version 3:
   - uint16_t : marker with content P104_CONFIG_VERSION_V2
   - empty space, size of DAT_TASKS_CUSTOM_SIZE - 2 so the actual storage is in the extension file
   - uint16_t : size of next blob holding 1 zone settings string
   - char[y]  : Blob holding 1 zone settings string, with csv like string, using P104_FIELD_SEP separators
   - uint16_t : next size, if 0 then no more blobs
   - char[x]  : Blob
   - ...
   - Max. allowed total custom settings size = 4096
 */
/**************************************
 * loadSettings
 *************************************/
void P104_data_struct::loadSettings() {
  uint16_t bufferSize;
  char    *settingsBuffer;

  if (validTaskIndex(taskIndex)) {
    int loadOffset = 0;

    // Read size of the used buffer, could be the settings-version marker
    LoadFromFile(SettingsType::Enum::CustomTaskSettings_Type, taskIndex, (uint8_t *)&bufferSize, sizeof(bufferSize), loadOffset);
    bool settingsVersionV2  = (bufferSize == P104_CONFIG_VERSION_V2) || (bufferSize == 0u);
    bool settingsVersionV3  = (bufferSize == P104_CONFIG_VERSION_V3) || (bufferSize == 0u);
    uint16_t structDataSize = 0;
    uint16_t reservedBuffer = 0;

    if (!settingsVersionV2 && !settingsVersionV3) {
      reservedBuffer = bufferSize + 1;                         // just add 1 for storing a string-terminator
      addLog(LOG_LEVEL_INFO, F("dotmatrix: Reading Settings V1, will be stored as Settings V2/V3."));
    } else {
      reservedBuffer = P104_SETTINGS_BUFFER_V2 + 1;            // just add 1 for storing a string-terminator
    }
    reservedBuffer++;                                          // Add 1 for 0..size use
    settingsBuffer = new (std::nothrow)char[reservedBuffer](); // Allocate buffer and reset to all zeroes
    # if P104_FEATURE_STORAGE_V3

    if (settingsVersionV3) {
      loadOffset = DAT_TASKS_CUSTOM_SIZE; // Skip storage in config.dat
    } else {
      loadOffset += sizeof(bufferSize);
    }
    # else // if P104_FEATURE_STORAGE_V3
    loadOffset += sizeof(bufferSize);
    # endif // if P104_FEATURE_STORAGE_V3

    if (settingsVersionV2 || settingsVersionV3) {
      LoadFromFile(SettingsType::Enum::CustomTaskSettings_Type, taskIndex, (uint8_t *)&bufferSize, sizeof(bufferSize), loadOffset);
      loadOffset += sizeof(bufferSize); // Skip the size
    }
    structDataSize = bufferSize;
    # ifdef P104_DEBUG_DEV

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, strformat(F("P104: loadSettings stored Size: %d taskindex: %d"), structDataSize, taskIndex));
    }
    # endif // ifdef P104_DEBUG_DEV

    // Read actual data
    if (structDataSize > 0) {              // Reading 0 bytes logs an error, so lets avoid that
      LoadFromFile(SettingsType::Enum::CustomTaskSettings_Type, taskIndex, (uint8_t *)settingsBuffer, structDataSize, loadOffset);
    }
    settingsBuffer[bufferSize + 1] = '\0'; // Terminate string

    uint8_t zoneIndex = 0;

    {
      String buffer(settingsBuffer);
      # ifdef P104_DEBUG_DEV

      String log;

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        log = strformat(F("P104: loadSettings bufferSize: %d untrimmed: %d"), bufferSize, buffer.length());
      }
      # endif // ifdef P104_DEBUG_DEV
      buffer.trim();
      # ifdef P104_DEBUG_DEV

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        log += concat(F(" trimmed: "), buffer.length());
        addLogMove(LOG_LEVEL_INFO, log);
      }
      # endif // ifdef P104_DEBUG_DEV

      if (zones.size() > 0) {
        zones.clear();
      }
      zones.reserve(P104_MAX_ZONES);
      numDevices = 0;

      String   tmp;
      String   fld;
      int32_t  tmp_int;
      uint16_t prev2   = 0;
      int16_t  offset2 = buffer.indexOf(P104_ZONE_SEP);

      if ((offset2 == -1) && (buffer.length() > 0)) {
        offset2 = buffer.length();
      }

      while (offset2 > -1) {
        tmp = buffer.substring(prev2, offset2);
        # ifdef P104_DEBUG_DEV

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          log  = F("P104: reading string: ");
          log += tmp;
          log.replace(P104_FIELD_SEP, P104_FIELD_DISP);
          addLogMove(LOG_LEVEL_INFO, log);
        }
        # endif // ifdef P104_DEBUG_DEV

        zones.push_back(P104_zone_struct(zoneIndex + 1));

        tmp_int = 0;

        // WARNING: Order of parsing these values should match the numeric order of P104_OFFSET_* values
        for (uint8_t i = 0; i < P104_OFFSET_COUNT; ++i) {
          if (i == P104_OFFSET_TEXT) {
            zones[zoneIndex].text = parseStringKeepCaseNoTrim(tmp, 1 + P104_OFFSET_TEXT, P104_FIELD_SEP);
          } else {
            if (validIntFromString(parseString(tmp, 1 + i, P104_FIELD_SEP), tmp_int)) {
              zones[zoneIndex].setIntValue(i, tmp_int);
            }
          }
        }

        delay(0);

        numDevices += zones[zoneIndex].size + zones[zoneIndex].offset;

        if (!settingsVersionV2 && !settingsVersionV3) { // V1 check
          prev2   = offset2 + 1;
          offset2 = buffer.indexOf(P104_ZONE_SEP, prev2);
        } else {
          loadOffset    += bufferSize;
          structDataSize = sizeof(bufferSize);
          LoadFromFile(SettingsType::Enum::CustomTaskSettings_Type, taskIndex, (uint8_t *)&bufferSize, structDataSize, loadOffset);
          offset2 = bufferSize;  // Length

          if (bufferSize == 0) { // End of zones reached
            offset2 = -1;        // fall out of while loop
          } else {
            structDataSize = bufferSize;
            loadOffset    += sizeof(bufferSize);
            LoadFromFile(SettingsType::Enum::CustomTaskSettings_Type, taskIndex, (uint8_t *)settingsBuffer, structDataSize, loadOffset);
            settingsBuffer[bufferSize + 1] = '\0'; // Terminate string
            buffer                         = String(settingsBuffer);
          }
        }
        zoneIndex++;

        # ifdef P104_DEBUG

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          addLogMove(LOG_LEVEL_INFO, concat(F("dotmatrix: parsed zone: "), zoneIndex));
        }
        # endif // ifdef P104_DEBUG
      }

      free_string(buffer);   // Free some memory
    }

    delete[] settingsBuffer; // Release allocated buffer
    # ifdef P104_DEBUG_DEV

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, concat(F("P104: read zones from config: "), zoneIndex));
    }
    # endif // ifdef P104_DEBUG_DEV

    if (expectedZones == -1) { expectedZones = zoneIndex; }

    if (expectedZones == 0) { expectedZones++; } // Guarantee at least 1 zone to be displayed

    while (zoneIndex < expectedZones) {
      zones.push_back(P104_zone_struct(zoneIndex + 1));

      if (equals(zones[zoneIndex].text, F("\"\""))) { // Special case
        zones[zoneIndex].text.clear();
      }

      zoneIndex++;
      delay(0);
    }
    # ifdef P104_DEBUG_DEV

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, strformat(F("P104: total zones initialized: %d expected: %d"), zoneIndex, expectedZones));
    }
    # endif // ifdef P104_DEBUG_DEV
  }
}

/****************************************************
 * configureZones: initialize Zones setup
 ***************************************************/
void P104_data_struct::configureZones() {
  if (!initialized) {
    loadSettings();
    initialized = true;
  }

  uint8_t currentZone = 0;
  uint8_t zoneOffset  = 0;

  # ifdef P104_DEBUG_DEV

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO, concat(F("P104: configureZones to do: "), zones.size()));
  }
  # endif // ifdef P104_DEBUG_DEV

  if (nullptr == P) { return; }

  P->displayClear();

  for (auto it = zones.begin(); it != zones.end(); ++it) {
    if (it->zone <= expectedZones) {
      zoneOffset += it->offset;
      P->setZone(currentZone, zoneOffset, zoneOffset + it->size - 1);
      # if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET)
      it->_startModule = zoneOffset;
      P->getDisplayExtent(currentZone, it->_lower, it->_upper);
      # endif // if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET)
      zoneOffset += it->size;

      P->setCharSpacing(currentZone, P104_NORMAL_CHAR_SPACING); // Set default font spacing

      switch (it->font) {
        # ifdef P104_USE_NUMERIC_DOUBLEHEIGHT_FONT
        case P104_DOUBLE_HEIGHT_FONT_ID: {
          P->setFont(currentZone, numeric7SegDouble);
          P->setCharSpacing(currentZone, P104_DOUBLE_CHAR_SPACING); // double spacing as well
          break;
        }
        # endif // ifdef P104_USE_NUMERIC_DOUBLEHEIGHT_FONT
        # ifdef P104_USE_FULL_DOUBLEHEIGHT_FONT
        case P104_FULL_DOUBLEHEIGHT_FONT_ID: {
          P->setFont(currentZone, BigFont);
          P->setCharSpacing(currentZone, P104_DOUBLE_CHAR_SPACING); // double spacing as well
          break;
        }
        # endif // ifdef P104_USE_FULL_DOUBLEHEIGHT_FONT
        # ifdef P104_USE_VERTICAL_FONT
        case P104_VERTICAL_FONT_ID: {
          P->setFont(currentZone, _fontVertical);
          break;
        }
        # endif // ifdef P104_USE_VERTICAL_FONT
        # ifdef P104_USE_EXT_ASCII_FONT
        case P104_EXT_ASCII_FONT_ID: {
          P->setFont(currentZone, ExtASCII);
          break;
        }
        # endif // ifdef P104_USE_EXT_ASCII_FONT
        # ifdef P104_USE_ARABIC_FONT
        case P104_ARABIC_FONT_ID: {
          P->setFont(currentZone, fontArabic);
          break;
        }
        # endif // ifdef P104_USE_ARABIC_FONT
        # ifdef P104_USE_GREEK_FONT
        case P104_GREEK_FONT_ID: {
          P->setFont(currentZone, fontGreek);
          break;
        }
        # endif // ifdef P104_USE_GREEK_FONT
        # ifdef P104_USE_KATAKANA_FONT
        case P104_KATAKANA_FONT_ID: {
          P->setFont(currentZone, fontKatakana);
          break;
        }
        # endif // ifdef P104_USE_KATAKANA_FONT

        // Extend above this comment with more fonts if/when available,
        // case P104_DEFAULT_FONT_ID: and default: clauses should be the last options.
        // This should also make sure the default font is set if a no longer available font was selected
        case P104_DEFAULT_FONT_ID:
        default: {
          P->setFont(currentZone, nullptr); // default font
          break;
        }
      }

      // Inverted
      P->setInvert(currentZone, it->inverted);

      // Special Effects
      P->setZoneEffect(currentZone, (it->specialEffect & P104_SPECIAL_EFFECT_UP_DOWN) == P104_SPECIAL_EFFECT_UP_DOWN,       PA_FLIP_UD);
      P->setZoneEffect(currentZone, (it->specialEffect & P104_SPECIAL_EFFECT_LEFT_RIGHT) == P104_SPECIAL_EFFECT_LEFT_RIGHT, PA_FLIP_LR);

      // Brightness
      P->setIntensity(currentZone, it->brightness);

      # ifdef P104_DEBUG_DEV

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLogMove(LOG_LEVEL_INFO, strformat(F("P104: configureZones #%d/%d offset: %d"), currentZone + 1, expectedZones, zoneOffset));
      }
      # endif // ifdef P104_DEBUG_DEV

      delay(0);

      // Content == text && text != ""
      if (((it->content == P104_CONTENT_TEXT) ||
           (it->content == P104_CONTENT_TEXT_REV))
          && (!it->text.isEmpty())) {
        displayOneZoneText(currentZone, *it, it->text);
      }

      # ifdef P104_USE_BAR_GRAPH

      // Content == Bar-graph && text != ""
      if ((it->content == P104_CONTENT_BAR_GRAPH)
          && (!it->text.isEmpty())) {
        displayBarGraph(currentZone, *it, it->text);
      }
      # endif // ifdef P104_USE_BAR_GRAPH

      if (it->repeatDelay > -1) {
        it->_repeatTimer = millis();
      }
      currentZone++;
      delay(0);
    }
  }

  // Synchronize the start
  P->synchZoneStart();
}

/**********************************************************
 * Display the text with attributes for a specific zone
 *********************************************************/
void P104_data_struct::displayOneZoneText(uint8_t                 zone,
                                          const P104_zone_struct& zstruct,
                                          const String          & text) {
  if ((nullptr == P) || (zone >= P104_MAX_ZONES)) { return; } // double check
  sZoneInitial[zone].reserve(text.length());
  sZoneInitial[zone] = text; // Keep the original string for future use
  sZoneBuffers[zone].reserve(text.length());
  sZoneBuffers[zone] = text; // We explicitly want a copy here so it can be modified by parseTemplate()

  sZoneBuffers[zone] = parseTemplate(sZoneBuffers[zone]);

  # if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT)

  if (zstruct.layout == P104_LAYOUT_DOUBLE_UPPER) {
    createHString(sZoneBuffers[zone]);
  }
  # endif // if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT)

  if (zstruct.content == P104_CONTENT_TEXT_REV) {
    reverseStr(sZoneBuffers[zone]);
  }

  if (loglevelActiveFor(LOG_LEVEL_INFO) &&
      logAllText) {
    addLogMove(LOG_LEVEL_INFO,
               strformat(F("dotmatrix: ZoneText: %d, '%s' -> '%s'"), zone + 1, text.c_str(), sZoneBuffers[zone].c_str()));
  }

  P->displayZoneText(zone,
                     sZoneBuffers[zone].c_str(),
                     static_cast<textPosition_t>(zstruct.alignment),
                     zstruct.speed,
                     zstruct.pause,
                     static_cast<textEffect_t>(zstruct.animationIn),
                     static_cast<textEffect_t>(zstruct.animationOut));
}

/*********************************************
 * Update all or the specified zone
 ********************************************/
void P104_data_struct::updateZone(uint8_t                 zone,
                                  const P104_zone_struct& zstruct) {
  if (nullptr == P) { return; }

  if (zone == 0) {
    for (auto it = zones.begin(); it != zones.end(); ++it) {
      if ((it->zone > 0) &&
          ((it->content == P104_CONTENT_TEXT) ||
           (it->content == P104_CONTENT_TEXT_REV))) {
        displayOneZoneText(it->zone - 1, *it, sZoneInitial[it->zone - 1]); // Re-send last displayed text
        P->displayReset(it->zone - 1);
      }
      # ifdef P104_USE_BAR_GRAPH

      if ((it->zone > 0) &&
          (it->content == P104_CONTENT_BAR_GRAPH)) {
        displayBarGraph(it->zone - 1, *it, sZoneInitial[it->zone - 1]); // Re-send last displayed bar graph
      }
      # endif // ifdef P104_USE_BAR_GRAPH

      if ((zstruct.content == P104_CONTENT_TEXT)
          || zstruct.content == P104_CONTENT_TEXT_REV
          # ifdef P104_USE_BAR_GRAPH
          || zstruct.content == P104_CONTENT_BAR_GRAPH
          # endif // ifdef P104_USE_BAR_GRAPH
          ) {
        if (it->repeatDelay > -1) { // Restart repeat timer
          it->_repeatTimer = millis();
        }
      }
    }
  } else {
    if ((zstruct.zone > 0) &&
        ((zstruct.content == P104_CONTENT_TEXT) ||
         (zstruct.content == P104_CONTENT_TEXT_REV))) {
      displayOneZoneText(zstruct.zone - 1, zstruct, sZoneInitial[zstruct.zone - 1]); // Re-send last displayed text
      P->displayReset(zstruct.zone - 1);
    }
    # ifdef P104_USE_BAR_GRAPH

    if ((zstruct.zone > 0) &&
        (zstruct.content == P104_CONTENT_BAR_GRAPH)) {
      displayBarGraph(zstruct.zone - 1, zstruct, sZoneInitial[zstruct.zone - 1]); // Re-send last displayed bar graph
    }
    # endif // ifdef P104_USE_BAR_GRAPH

    // Repeat timer is/should be started elsewhere
  }
}

# if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET)

/***********************************************
 * Enable/Disable updating a range of modules
 **********************************************/
void P104_data_struct::modulesOnOff(uint8_t start, uint8_t end, MD_MAX72XX::controlValue_t on_off) {
  for (uint8_t m = start; m <= end; m++) {
    pM->control(m, MD_MAX72XX::UPDATE, on_off);
  }
}

# endif // if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET)

# ifdef P104_USE_BAR_GRAPH

/********************************************************
 * draw a single bar-graph, arguments already adjusted for direction
 *******************************************************/
void P104_data_struct::drawOneBarGraph(uint16_t lower,
                                       uint16_t upper,
                                       int16_t  pixBottom,
                                       int16_t  pixTop,
                                       uint16_t zeroPoint,
                                       uint8_t  barWidth,
                                       uint8_t  barType,
                                       uint8_t  row) {
  bool on_off;

  for (uint8_t r = 0; r < barWidth; r++) {
    for (uint8_t col = lower; col <= upper; col++) {
      on_off = (col >= pixBottom && col <= pixTop); // valid area

      if ((zeroPoint != 0) &&
          (barType == P104_BARTYPE_STANDARD) &&
          (barWidth > 2) &&
          ((r == 0) || (r == barWidth - 1)) &&
          (col == lower + zeroPoint)) {
        on_off = false; // when bar wider than 2, turn off zeropoint top and bottom led
      }

      if ((barType == P104_BARTYPE_SINGLE) && (r > 0)) {
        on_off = false; // barType 1 = only a single line is drawn, independent of the width
      }

      if ((barType == P104_BARTYPE_ALT_DOT) && (barWidth > 1) && on_off) {
        on_off = ((r % 2) == (col % 2)); // barType 2 = dotted line when bar is wider than 1 pixel
      }
      pM->setPoint(row + r, col, on_off);

      if (col % 16 == 0) { delay(0); }
    }
    delay(0); // Leave some breathingroom
  }
}

/********************************************************************
 * Process a graph-string to display in a zone, format:
 * value,max-value,min-value,direction,bartype|...
 *******************************************************************/
void P104_data_struct::displayBarGraph(uint8_t                 zone,
                                       const P104_zone_struct& zstruct,
                                       const String          & graph) {
  if ((nullptr == P) || (nullptr == pM) || graph.isEmpty()) { return; }
  sZoneInitial[zone] = graph; // Keep the original string for future use

  #  define NOT_A_COMMA 0x02  // Something else than a comma, or the parseString function will get confused
  String parsedGraph(graph);  // Extra copy created so we don't mess up the incoming String
  parsedGraph = parseTemplate(parsedGraph);
  parsedGraph.replace(',', NOT_A_COMMA);

  std::vector<P104_bargraph_struct> barGraphs;
  uint8_t currentBar = 0;
  bool    loop       = true;

  // Parse the graph-string
  while (loop && currentBar < 8) { // Maximum 8 valuesets possible
    String graphpart = parseString(parsedGraph, currentBar + 1, '|');
    graphpart.trim();
    graphpart.replace(NOT_A_COMMA, ',');

    if (graphpart.isEmpty()) {
      loop = false;
    } else {
      barGraphs.push_back(P104_bargraph_struct(currentBar));
    }

    if (loop && validDoubleFromString(parseString(graphpart, 1), barGraphs[currentBar].value)) { // value
      String datapart = parseString(graphpart, 2);                                               // max (default: 100.0)

      if (datapart.isEmpty()) {
        barGraphs[currentBar].max = 100.0;
      } else {
        validDoubleFromString(datapart, barGraphs[currentBar].max);
      }
      datapart = parseString(graphpart, 3); // min (default: 0.0)

      if (datapart.isEmpty()) {
        barGraphs[currentBar].min = 0.0;
      } else {
        validDoubleFromString(datapart, barGraphs[currentBar].min);
      }
      datapart = parseString(graphpart, 4); // direction

      if (datapart.isEmpty()) {
        barGraphs[currentBar].direction = 0;
      } else {
        int32_t value = 0;
        validIntFromString(datapart, value);
        barGraphs[currentBar].direction = value;
      }
      datapart = parseString(graphpart, 5); // barType

      if (datapart.isEmpty()) {
        barGraphs[currentBar].barType = 0;
      } else {
        int32_t value = 0;
        validIntFromString(datapart, value);
        barGraphs[currentBar].barType = value;
      }

      if (definitelyGreaterThan(barGraphs[currentBar].min, barGraphs[currentBar].max)) {
        std::swap(barGraphs[currentBar].min, barGraphs[currentBar].max);
      }
    }
    #  ifdef P104_DEBUG

    if (logAllText && loglevelActiveFor(LOG_LEVEL_INFO)) {
      String log;

      if (log.reserve(70)) {
        log = F("dotmatrix: Bar-graph: ");

        if (loop) {
          log += currentBar;
          log += F(" in: ");
          log += graphpart;
          log += F(" value: ");
          log += barGraphs[currentBar].value;
          log += F(" max: ");
          log += barGraphs[currentBar].max;
          log += F(" min: ");
          log += barGraphs[currentBar].min;
          log += F(" dir: ");
          log += barGraphs[currentBar].direction;
          log += F(" typ: ");
          log += barGraphs[currentBar].barType;
        } else {
          log += F(" bsize: ");
          log += barGraphs.size();
        }
        addLogMove(LOG_LEVEL_INFO, log);
      }
    }
    #  endif // ifdef P104_DEBUG
    currentBar++; // next
    delay(0);     // Leave some breathingroom
  }
  #  undef NOT_A_COMMA

  if (barGraphs.size() > 0) {
    uint8_t  barWidth = 8 / barGraphs.size(); // Divide the 8 pixel width per number of bars to show
    int16_t  pixTop, pixBottom;
    uint16_t zeroPoint;
    #  ifdef P104_DEBUG
    String log;

    if (logAllText &&
        loglevelActiveFor(LOG_LEVEL_INFO) &&
        log.reserve(64)) {
      log  = F("dotmatrix: bar Width: ");
      log += barWidth;
      log += F(" low: ");
      log += zstruct._lower;
      log += F(" high: ");
      log += zstruct._upper;
    }
    #  endif // ifdef P104_DEBUG
    modulesOnOff(zstruct._startModule, zstruct._startModule + zstruct.size - 1, MD_MAX72XX::MD_OFF); // Stop updates on modules
    P->setIntensity(zstruct.zone - 1, zstruct.brightness);                                           // don't forget to set the brightness
    uint8_t row = 0;

    if ((barGraphs.size() == 3) || (barGraphs.size() == 5) || (barGraphs.size() == 6)) {             // Center within the rows a bit
      for (; row < (barGraphs.size() == 5 ? 2 : 1); row++) {
        for (uint8_t col = zstruct._lower; col <= zstruct._upper; col++) {
          pM->setPoint(row, col, false);                                                             // all off

          if (col % 16 == 0) { delay(0); }
        }
        delay(0); // Leave some breathingroom
      }
    }

    for (auto it = barGraphs.begin(); it != barGraphs.end(); ++it) {
      if (essentiallyZero(it->min)) {
        pixTop    = zstruct._lower - 1 + (((zstruct._upper + 1) - zstruct._lower) / it->max) * it->value;
        pixBottom = zstruct._lower - 1;
        zeroPoint = 0;
      } else {
        if (definitelyLessThan(it->min, 0.0) &&
            definitelyGreaterThan(it->max,           0.0) &&
            definitelyGreaterThan(it->max - it->min, 0.01)) { // Zero-point is used
          zeroPoint = (it->min * -1.0) / ((it->max - it->min) / (1.0 * ((zstruct._upper + 1) - zstruct._lower)));
        } else {
          zeroPoint = 0;
        }
        pixTop    = zstruct._lower + zeroPoint + (((zstruct._upper + 1) - zstruct._lower) / (it->max - it->min)) * it->value;
        pixBottom = zstruct._lower + zeroPoint;

        if (definitelyLessThan(it->value, 0.0)) {
          std::swap(pixTop, pixBottom);
        }
      }

      if (it->direction == 1) { // Left to right display: Flip values within the lower/upper range
        pixBottom = zstruct._upper - (pixBottom - zstruct._lower);
        pixTop    = zstruct._lower + (zstruct._upper - pixTop);
        std::swap(pixBottom, pixTop);
        zeroPoint = zstruct._upper - zstruct._lower - zeroPoint + (zeroPoint == 0 ? 1 : 0);
      }
      #  ifdef P104_DEBUG_DEV

      if (logAllText && loglevelActiveFor(LOG_LEVEL_INFO)) {
        log += F(" B: ");
        log += pixBottom;
        log += F(" T: ");
        log += pixTop;
        log += F(" Z: ");
        log += zeroPoint;
      }
      #  endif // ifdef P104_DEBUG_DEV
      drawOneBarGraph(zstruct._lower, zstruct._upper, pixBottom, pixTop, zeroPoint, barWidth, it->barType, row);
      row += barWidth;                 // Next set of rows
      delay(0);                        // Leave some breathingroom
    }

    for (; row < 8; row++) {           // Clear unused rows
      for (uint8_t col = zstruct._lower; col <= zstruct._upper; col++) {
        pM->setPoint(row, col, false); // all off

        if (col % 16 == 0) { delay(0); }
      }
      delay(0); // Leave some breathingroom
    }
    #  ifdef P104_DEBUG

    if (logAllText && loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, log);
    }
    #  endif // ifdef P104_DEBUG
    modulesOnOff(zstruct._startModule, zstruct._startModule + zstruct.size - 1, MD_MAX72XX::MD_ON); // Continue updates on modules
  }
}

# endif // ifdef P104_USE_BAR_GRAPH

# ifdef P104_USE_DOT_SET
void P104_data_struct::displayDots(uint8_t                 zone,
                                   const P104_zone_struct& zstruct,
                                   const String          & dots) {
  if ((nullptr == P) || (nullptr == pM) || dots.isEmpty()) { return; }
  {
    uint8_t idx = 0;
    String  sRow;
    String  sCol;
    String  sOn_off;
    bool    on_off = true;
    modulesOnOff(zstruct._startModule, zstruct._startModule + zstruct.size - 1, MD_MAX72XX::MD_OFF); // Stop updates on modules
    P->setIntensity(zstruct.zone - 1, zstruct.brightness);                                           // don't forget to set the brightness
    sRow    = parseString(dots, idx + 1);
    sCol    = parseString(dots, idx + 2);
    sOn_off = parseString(dots, idx + 3);

    while (!sRow.isEmpty() && !sCol.isEmpty()) {
      on_off = true; // Default On

      int32_t row;
      int32_t col;

      if (validIntFromString(sRow, row) &&
          validIntFromString(sCol, col) &&
          (row > 0) && ((row - 1) < 8) &&
          (col > 0) && ((col - 1) <= (zstruct._upper - zstruct._lower))) { // Valid coordinates?
        if (equals(sOn_off, F("0"))) {                                     // Dot On is the default
          on_off = false;
          idx++;                                                           // 3rd argument used
        }
        pM->setPoint(row - 1, zstruct._upper - (col - 1), on_off);         // Reverse layout
      }
      idx += 2;                                                            // Skip to next argument set

      if (idx % 16 == 0) { delay(0); }
      sRow    = parseString(dots, idx + 1);
      sCol    = parseString(dots, idx + 2);
      sOn_off = parseString(dots, idx + 3);
    }

    modulesOnOff(zstruct._startModule, zstruct._startModule + zstruct.size - 1, MD_MAX72XX::MD_ON); // Continue updates on modules
  }
}

# endif // ifdef P104_USE_DOT_SET

/**************************************************
 * Check if an animation is available in the current build
 *************************************************/
bool isAnimationAvailable(uint8_t animation, bool noneIsAllowed = false) {
  textEffect_t selection = static_cast<textEffect_t>(animation);

  switch (selection) {
    case PA_NO_EFFECT:
    {
      return noneIsAllowed;
    }
    case PA_PRINT:
    case PA_SCROLL_UP:
    case PA_SCROLL_DOWN:
    case PA_SCROLL_LEFT:
    case PA_SCROLL_RIGHT:
    {
      return true;
    }
    # if ENA_SPRITE
    case PA_SPRITE:
    {
      return true;
    }
    # endif // ENA_SPRITE
    # if ENA_MISC
    case PA_SLICE:
    case PA_MESH:
    case PA_FADE:
    case PA_DISSOLVE:
    case PA_BLINDS:
    case PA_RANDOM:
    {
      return true;
    }
    # endif // ENA_MISC
    # if ENA_WIPE
    case PA_WIPE:
    case PA_WIPE_CURSOR:
    {
      return true;
    }
    # endif // ENA_WIPE
    # if ENA_SCAN
    case PA_SCAN_HORIZ:
    case PA_SCAN_HORIZX:
    case PA_SCAN_VERT:
    case PA_SCAN_VERTX:
    {
      return true;
    }
    # endif // ENA_SCAN
    # if ENA_OPNCLS
    case PA_OPENING:
    case PA_OPENING_CURSOR:
    case PA_CLOSING:
    case PA_CLOSING_CURSOR:
    {
      return true;
    }
    # endif // ENA_OPNCLS
    # if ENA_SCR_DIA
    case PA_SCROLL_UP_LEFT:
    case PA_SCROLL_UP_RIGHT:
    case PA_SCROLL_DOWN_LEFT:
    case PA_SCROLL_DOWN_RIGHT:
    {
      return true;
    }
    # endif // ENA_SCR_DIA
    # if ENA_GROW
    case PA_GROW_UP:
    case PA_GROW_DOWN:
    {
      return true;
    }
    # endif // ENA_GROW
    default:
      return false;
  }
}

const char p104_subcommands[] PROGMEM =
  "clear"
  "|update"

  "|txt"
  "|settxt"

# ifdef P104_USE_BAR_GRAPH
  "|bar"
  "|setbar"
# endif // ifdef P104_USE_BAR_GRAPH

# ifdef P104_USE_DOT_SET
  "|dot"
# endif // ifdef P104_USE_DOT_SET

# ifdef P104_USE_COMMANDS
  "|alignment"
  "|anim.in"
  "|anim.out"
  "|brightness"
  "|content"
  "|font"
  "|inverted"
#  if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT)
  "|layout"
#  endif // if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT)
  "|offset"
  "|pause"
  "|repeat"
  "|size"
  "|specialeffect"
  "|speed"
# endif // ifdef P104_USE_COMMANDS
;

// Subcommands prefixed by "dotmatrix,"
enum class p104_subcommands_e {
  clear,  // subcommand: clear,<zone> / clear[,all]
  update, // subcommand: update,<zone> / update[,all]

  txt,    // subcommand: [set]txt,<zone>,<text> (only
  settxt, // subcommand: settxt,<zone>,<text> (stores

# ifdef P104_USE_BAR_GRAPH
  bar,    // subcommand: [set]bar,<zone>,<graph-string> (only allowed for zones
  setbar, // subcommand: setbar,<zone>,<graph-string> (stores the graph-string
# endif // ifdef P104_USE_BAR_GRAPH

# ifdef P104_USE_DOT_SET
  dot, // subcommand: dot,<zone>,<r>,<c>[,0][,<r>,<c>[,0]...] to draw
# endif // ifdef P104_USE_DOT_SET

# ifdef P104_USE_COMMANDS
  alignment,     // subcommand: alignment,<zone>,<alignment> (0..3)
  anim_in,       // subcommand: anim.in,<zone>,<animation> (1..)
  anim_out,      // subcommand: anim.out,<zone>,<animation> (0..)
  brightness,    // subcommand: brightness,<zone>,<brightness> (0..15)
  content,       // subcommand: content,<zone>,<contenttype> (0..<P104_CONTENT_count>-1)
  font,          // subcommand: font,<zone>,<font id> (only for incuded font id's)
  inverted,      // subcommand: inverted,<zone>,<invertedstate> (disable/enable)
#  if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT)
  layout,        // subcommand: layout,<zone>,<layout> (0..2), only when double-height font is available
#  endif // if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT)
  offset,        // subcommand: offset,<zone>,<size> (0..<size>-1)
  pause,         // subcommand: pause,<zone>,<pause_ms> (0..P104_MAX_SPEED_PAUSE_VALUE)
  repeat,        // subcommand: repeat,<zone>,<repeat_sec> (-1..86400 = 24h)
  size,          // subcommand: size,<zone>,<size> (1..)
  specialeffect, // subcommand: specialeffect,<zone>,<effect> (0..3)
  speed,         // subcommand: speed,<zone>,<speed_ms> (0..P104_MAX_SPEED_PAUSE_VALUE)
# endif // ifdef P104_USE_COMMANDS
};

/*******************************************************
 * handlePluginWrite : process commands
 ******************************************************/
bool P104_data_struct::handlePluginWrite(taskIndex_t   taskIndex,
                                         const String& string) {
  # ifdef P104_USE_COMMANDS
  bool reconfigure = false;
  # endif // ifdef P104_USE_COMMANDS
  bool success         = false;
  const String command = parseString(string, 1);

  if ((nullptr != P) && equals(command, F("dotmatrix"))) { // main command: dotmatrix
    const String subCommand   = parseString(string, 2);
    const int    subCommand_i = GetCommandCode(subCommand.c_str(), p104_subcommands);

    if (subCommand_i != -1) {
      const p104_subcommands_e subcommands_e = static_cast<p104_subcommands_e>(subCommand_i);

      int32_t zoneIndex{};
      const String string4 = parseStringKeepCaseNoTrim(string, 4);
      # ifdef P104_USE_COMMANDS
      int32_t value4{};
      validIntFromString(string4, value4);
      # endif // ifdef P104_USE_COMMANDS

      // Global subcommands

      if ((subcommands_e == p104_subcommands_e::clear) && // subcommand: clear[,all]
          (string4.isEmpty() ||
           string4.equalsIgnoreCase(F("all")))) {
        P->displayClear();
        success = true;
      } else

      if ((subcommands_e == p104_subcommands_e::update) && // subcommand: update[,all]
          (string4.isEmpty() ||
           string4.equalsIgnoreCase(F("all")))) {
        updateZone(0, P104_zone_struct(0));
        success = true;
      }

      // Zone-specific subcommands
      if (validIntFromString(parseString(string, 3), zoneIndex) &&
          (zoneIndex > 0) &&
          (static_cast<size_t>(zoneIndex) <= zones.size())) {
        // subcommands are processed in the same order as they are presented in the UI
        for (auto it = zones.begin(); it != zones.end() && !success; ++it) {
          if ((it->zone == zoneIndex)) { // This zone
            switch (subcommands_e) {
              case p104_subcommands_e::clear:
                // subcommand: clear,<zone>
              {
                P->displayClear(zoneIndex - 1);
                success = true;
                break;
              }

              case p104_subcommands_e::update:
                // subcommand: update,<zone>
              {
                updateZone(zoneIndex, *it);
                success = true;
                break;
              }

              # ifdef P104_USE_COMMANDS

              case p104_subcommands_e::size:
                // subcommand: size,<zone>,<size> (1..)
              {
                if ((value4 > 0) &&
                    (value4 <= P104_MAX_MODULES_PER_ZONE))
                {
                  reconfigure = (it->size != value4);
                  it->size    = value4;
                  success     = true;
                }
                break;
              }
              # endif // ifdef P104_USE_COMMANDS

              case p104_subcommands_e::txt:                                  // subcommand: [set]txt,<zone>,<text> (only
              case p104_subcommands_e::settxt:                               // allowed for zones with Text content)
              {
                if ((it->content == P104_CONTENT_TEXT) ||
                    (it->content == P104_CONTENT_TEXT_REV)) {                // no length check, so longer than the UI allows is made
                                                                             // possible
                  if ((subcommands_e == p104_subcommands_e::settxt) &&       // subcommand: settxt,<zone>,<text> (stores
                      (string4.length() <= P104_MAX_TEXT_LENGTH_PER_ZONE)) { // the text in the settings, is not saved)
                    it->text = string4;                                      // Only if not too long, could 'blow up' the
                  }                                                          // settings when saved
                  displayOneZoneText(zoneIndex - 1, *it, string4);
                  success = true;
                }

                break;
              }

              # ifdef P104_USE_COMMANDS

              case p104_subcommands_e::content:
                // subcommand: content,<zone>,<contenttype> (0..<P104_CONTENT_count>-1)
              {
                if ((value4 >= 0) &&
                    (value4 < P104_CONTENT_count))
                {
                  reconfigure = (it->content != value4);
                  it->content = value4;
                  success     = true;
                }
                break;
              }

              case p104_subcommands_e::alignment:
                // subcommand: alignment,<zone>,<alignment> (0..3)
              {
                if ((value4 >= 0) &&
                    (value4 <= static_cast<int>(textPosition_t::PA_RIGHT))) // last item in the enum
                {
                  it->alignment = value4;
                  success       = true;
                }
                break;
              }

              case p104_subcommands_e::anim_in:
                // subcommand: anim.in,<zone>,<animation> (1..)
              {
                if (isAnimationAvailable(value4)) {
                  it->animationIn = value4;
                  success         = true;
                }
                break;
              }

              case p104_subcommands_e::speed:
                // subcommand: speed,<zone>,<speed_ms> (0..P104_MAX_SPEED_PAUSE_VALUE)
              {
                if ((value4 >= 0) &&
                    (value4 <= P104_MAX_SPEED_PAUSE_VALUE))
                {
                  it->speed = value4;
                  success   = true;
                }
                break;
              }

              case p104_subcommands_e::anim_out:
                // subcommand: anim.out,<zone>,<animation> (0..)
              {
                if (isAnimationAvailable(value4, true))
                {
                  it->animationOut = value4;
                  success          = true;
                }
                break;
              }

              case p104_subcommands_e::pause:
                // subcommand: pause,<zone>,<pause_ms> (0..P104_MAX_SPEED_PAUSE_VALUE)
              {
                if ((value4 >= 0) &&
                    (value4 <= P104_MAX_SPEED_PAUSE_VALUE))
                {
                  it->pause = value4;
                  success   = true;
                }
                break;
              }

              case p104_subcommands_e::font:
                // subcommand: font,<zone>,<font id> (only for incuded font id's)
              {
                if (
                  (value4 == 0)
                #  ifdef P104_USE_NUMERIC_DOUBLEHEIGHT_FONT
                  || (value4 == P104_DOUBLE_HEIGHT_FONT_ID)
                #  endif // ifdef P104_USE_NUMERIC_DOUBLEHEIGHT_FONT
                #  ifdef P104_USE_FULL_DOUBLEHEIGHT_FONT
                  || (value4 == P104_FULL_DOUBLEHEIGHT_FONT_ID)
                #  endif // ifdef P104_USE_FULL_DOUBLEHEIGHT_FONT
                #  ifdef P104_USE_VERTICAL_FONT
                  || (value4 == P104_VERTICAL_FONT_ID)
                #  endif // ifdef P104_USE_VERTICAL_FONT
                #  ifdef P104_USE_EXT_ASCII_FONT
                  || (value4 == P104_EXT_ASCII_FONT_ID)
                #  endif // ifdef P104_USE_EXT_ASCII_FONT
                #  ifdef P104_USE_ARABIC_FONT
                  || (value4 == P104_ARABIC_FONT_ID)
                #  endif // ifdef P104_USE_ARABIC_FONT
                #  ifdef P104_USE_GREEK_FONT
                  || (value4 == P104_GREEK_FONT_ID)
                #  endif // ifdef P104_USE_GREEK_FONT
                #  ifdef P104_USE_KATAKANA_FONT
                  || (value4 == P104_KATAKANA_FONT_ID)
                #  endif // ifdef P104_USE_KATAKANA_FONT
                  )
                {
                  reconfigure = (it->font != value4);
                  it->font    = value4;
                  success     = true;
                }
                break;
              }

              case p104_subcommands_e::inverted:
                // subcommand: inverted,<zone>,<invertedstate> (disable/enable)
              {
                if ((value4 >= 0) &&
                    (value4 <= 1))
                {
                  reconfigure  = (it->inverted != value4);
                  it->inverted = value4;
                  success      = true;
                }
                break;
              }

              #  if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT)

              case p104_subcommands_e::layout:
                // subcommand: layout,<zone>,<layout> (0..2), only when double-height font is available
              {
                if ((value4 >= 0) &&
                    (value4 <= P104_LAYOUT_DOUBLE_LOWER))
                {
                  reconfigure = (it->layout != value4);
                  it->layout  = value4;
                  success     = true;
                }
                break;
              }
              #  endif // if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT)

              case p104_subcommands_e::specialeffect:
                // subcommand: specialeffect,<zone>,<effect> (0..3)
              {
                if ((value4 >= 0) &&
                    (value4 <= P104_SPECIAL_EFFECT_BOTH))
                {
                  reconfigure       = (it->specialEffect != value4);
                  it->specialEffect = value4;
                  success           = true;
                }
                break;
              }

              case p104_subcommands_e::offset:
                // subcommand: offset,<zone>,<size> (0..<size>-1)
              {
                if ((value4 >= 0) &&
                    (value4 < P104_MAX_MODULES_PER_ZONE) &&
                    (value4 < it->size))
                {
                  reconfigure = (it->offset != value4);
                  it->offset  = value4;
                  success     = true;
                }
                break;
              }

              case p104_subcommands_e::brightness:
                // subcommand: brightness,<zone>,<brightness> (0..15)
              {
                if ((value4 >= 0) &&
                    (value4 <= P104_BRIGHTNESS_MAX))
                {
                  it->brightness = value4;
                  P->setIntensity(zoneIndex - 1, it->brightness); // Change brightness immediately
                  success = true;
                }
                break;
              }

              case p104_subcommands_e::repeat:
                // subcommand: repeat,<zone>,<repeat_sec> (-1..86400 = 24h)
              {
                if ((value4 >= -1) &&
                    (value4 <= P104_MAX_REPEATDELAY_VALUE))
                {
                  it->repeatDelay = value4;
                  success         = true;

                  if (it->repeatDelay > -1) {
                    it->_repeatTimer = millis();
                  }
                }
                break;
              }
              # endif // ifdef P104_USE_COMMANDS

              # ifdef P104_USE_BAR_GRAPH

              case p104_subcommands_e::bar:                                  // subcommand: [set]bar,<zone>,<graph-string> (only allowed for
              // zones
              case p104_subcommands_e::setbar:                               // with Bargraph content) no length check, so longer than the
                                                                             // UI allows is made possible
              {
                if (it->content == P104_CONTENT_BAR_GRAPH) {
                  if ((subcommands_e == p104_subcommands_e::setbar) &&       // subcommand: setbar,<zone>,<graph-string> (stores the
                                                                             // graph-string
                      (string4.length() <= P104_MAX_TEXT_LENGTH_PER_ZONE)) { // in the settings, is not saved)
                    it->text = string4;                                      // Only if not too long, could 'blow up' the settings when
                                                                             // saved
                  }
                  displayBarGraph(zoneIndex - 1, *it, string4);
                  success = true;
                }
                break;
              }
              # endif // ifdef P104_USE_BAR_GRAPH

              # ifdef P104_USE_DOT_SET

              case p104_subcommands_e::dot:
                // subcommand: dot,<zone>,<r>,<c>[,0][,<r>,<c>[,0]...] to draw
              {
                displayDots(zoneIndex - 1, *it, parseStringToEnd(string, 4)); // dots at row/column, add ,0 to turn a dot off
                success = true;
                break;
              }
              # endif // ifdef P104_USE_DOT_SET
            }

            // FIXME TD-er: success is always false here. Maybe this must be done outside the for-loop?
            if (success) { // Reset the repeat timer
              if (it->repeatDelay > -1) {
                it->_repeatTimer = millis();
              }
            }
          }
        }
      }
    }
  }

  # ifdef P104_USE_COMMANDS

  if (reconfigure) {
    configureZones(); // Re-initialize
    success = true;   // Successful
  }
  # endif // ifdef P104_USE_COMMANDS

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    String log;

    if (log.reserve(34 + string.length())) {
      log = F("dotmatrix: command ");

      if (!success) { log += F("NOT "); }
      log += F("succesful: ");
      log += string;
      addLogMove(LOG_LEVEL_INFO, log);
    }
  }

  return success; // Default: unknown command
}

int8_t P104_data_struct::getTime(char *psz,
                                 bool  seconds,
                                 bool  colon,
                                 bool  time12h,
                                 bool  timeAmpm) {
  uint16_t h;
  String   ampm;

  # ifdef P104_USE_DATETIME_OPTIONS

  if (time12h) {
    if (timeAmpm) {
      ampm = (node_time.hour() >= 12 ? F("p") : F("a"));
    }
    h = node_time.hour() % 12;

    if (h == 0) { h = 12; }
  } else
  # endif // ifdef P104_USE_DATETIME_OPTIONS
  {
    h = node_time.hour();
  }
  const uint16_t M = node_time.minute();

  if (!seconds) {
    sprintf_P(psz, PSTR("%02d%c%02d%s"), h, (colon ? ':' : ' '), M, ampm.c_str());
  } else {
    const uint16_t s = node_time.second();
    sprintf_P(psz, PSTR("%02d%c%02d %02d%s"), h, (colon ? ':' : ' '), M, s, ampm.c_str());
  }
  return M;
}

void P104_data_struct::getDate(char           *psz,
                               bool            showYear,
                               bool            fourDgt
                               # ifdef         P104_USE_DATETIME_OPTIONS
                               , const uint8_t dateFmt
                               , const uint8_t dateSep
                               # endif // ifdef P104_USE_DATETIME_OPTIONS
                               ) {
  uint16_t d, m, y;
  const uint16_t year = node_time.year() - (fourDgt ? 0 : 2000);

  # ifdef P104_USE_DATETIME_OPTIONS
  const String separators = F(" /-.");
  const char   sep        = separators[dateSep];
  # else // ifdef P104_USE_DATETIME_OPTIONS
  const char sep = ' ';
  # endif // ifdef P104_USE_DATETIME_OPTIONS

  d = node_time.day();
  m = node_time.month();
  y = year;
  # ifdef P104_USE_DATETIME_OPTIONS

  if (showYear) {
    switch (dateFmt) {
      case P104_DATE_FORMAT_US:
        d = node_time.month();
        m = node_time.day();
        y = year;
        break;
      case P104_DATE_FORMAT_JP:
        d = year;
        m = node_time.month();
        y = node_time.day();
        break;
    }
  } else {
    if ((dateFmt == P104_DATE_FORMAT_US) ||
        (dateFmt == P104_DATE_FORMAT_JP)) {
      std::swap(d, m);
    }
  }
  # endif // ifdef P104_USE_DATETIME_OPTIONS

  if (showYear) {
    sprintf_P(psz, PSTR("%02d%c%02d%c%02d"), d, sep, m, sep, y); // %02d will expand to 04 when needed
  } else {
    sprintf_P(psz, PSTR("%02d%c%02d"), d, sep, m);
  }
}

uint8_t P104_data_struct::getDateTime(char           *psz,
                                      bool            colon,
                                      bool            time12h,
                                      bool            timeAmpm,
                                      bool            fourDgt
                                      # ifdef         P104_USE_DATETIME_OPTIONS
                                      , const uint8_t dateFmt
                                      , const uint8_t dateSep
                                      # endif // ifdef P104_USE_DATETIME_OPTIONS
                                      ) {
  String   ampm;
  uint16_t d, y;
  uint8_t  h, m;
  const uint16_t year = node_time.year() - (fourDgt ? 0 : 2000);

  # ifdef P104_USE_DATETIME_OPTIONS
  const String separators = F(" /-.");
  const char   sep        = separators[dateSep];
  # else // ifdef P104_USE_DATETIME_OPTIONS
  const char sep = ' ';
  # endif // ifdef P104_USE_DATETIME_OPTIONS

  # ifdef P104_USE_DATETIME_OPTIONS

  if (time12h) {
    if (timeAmpm) {
      ampm = (node_time.hour() >= 12 ? F("p") : F("a"));
    }
    h = node_time.hour() % 12;

    if (h == 0) { h = 12; }
  } else
  # endif // ifdef P104_USE_DATETIME_OPTIONS
  {
    h = node_time.hour();
  }
  const uint16_t M = node_time.minute();

  # ifdef P104_USE_DATETIME_OPTIONS

  switch (dateFmt) {
    case P104_DATE_FORMAT_US:
      d = node_time.month();
      m = node_time.day();
      y = year;
      break;
    case P104_DATE_FORMAT_JP:
      d = year;
      m = node_time.month();
      y = node_time.day();
      break;
    default:
  # endif // ifdef P104_USE_DATETIME_OPTIONS
  d = node_time.day();
  m = node_time.month();
  y = year;
  # ifdef P104_USE_DATETIME_OPTIONS
}

  # endif // ifdef P104_USE_DATETIME_OPTIONS
  sprintf_P(psz, PSTR("%02d%c%02d%c%02d %02d%c%02d%s"), d, sep, m, sep, y, h, (colon ? ':' : ' '), M, ampm.c_str()); // %02d will expand to
                                                                                                                     // 04 when needed
  return M;
}

# if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT)
void P104_data_struct::createHString(String& string) {
  const uint16_t stringLen = string.length();

  for (uint16_t i = 0; i < stringLen; i++) {
    string[i] |= 0x80; // use 'high' part of the font, by adding 0x80
  }
}

# endif // if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT)

void P104_data_struct::reverseStr(String& str) {
  const uint16_t n = str.length();

  // Swap characters starting from two corners
  for (uint16_t i = 0; i < n / 2; i++) {
    std::swap(str[i], str[n - i - 1]);
  }
}

/************************************************************************
 * execute all PLUGIN_ONE_PER_SECOND tasks
 ***********************************************************************/
bool P104_data_struct::handlePluginOncePerSecond(struct EventStruct *event) {
  if (nullptr == P) { return false; }
  bool redisplay = false;
  bool success   = false;

  # ifdef P104_USE_DATETIME_OPTIONS
  const bool useFlasher = !bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_FLASH);
  const bool time12h    = bitRead(P104_CONFIG_DATETIME,  P104_CONFIG_DATETIME_12H);
  const bool timeAmpm   = bitRead(P104_CONFIG_DATETIME,  P104_CONFIG_DATETIME_AMPM);
  const bool year4dgt   = bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_YEAR4DGT);
  # else // ifdef P104_USE_DATETIME_OPTIONS
  constexpr bool useFlasher = true;
  constexpr bool time12h    = false;
  constexpr bool timeAmpm   = false;
  constexpr bool year4dgt   = false;
  # endif // ifdef P104_USE_DATETIME_OPTIONS
  const bool newFlasher     = !flasher && useFlasher;
  const bool currentFlasher = flasher;

  for (auto it = zones.begin(); it != zones.end(); ++it) {
    redisplay = false;

    if (P->getZoneStatus(it->zone - 1)) { // Animations done?
      switch (it->content) {
        case P104_CONTENT_TIME:           // time
        case P104_CONTENT_TIME_SEC:       // time sec
        {
          const bool   useSeconds = (it->content == P104_CONTENT_TIME_SEC);
          const int8_t m          = getTime(szTimeL, useSeconds, currentFlasher || !useFlasher, time12h, timeAmpm);
          flasher          = newFlasher;
          redisplay        = useFlasher || useSeconds || (it->_lastChecked != m);
          it->_lastChecked = m;
          break;
        }
        case P104_CONTENT_DATE4: // date/4
        case P104_CONTENT_DATE6: // date/6
        {
          if (it->_lastChecked != node_time.day()) {
            getDate(szTimeL,
                    it->content != P104_CONTENT_DATE4,
                    year4dgt
                    # ifdef P104_USE_DATETIME_OPTIONS
                    , get4BitFromUL(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_FORMAT)
                    , get4BitFromUL(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_SEP_CHAR)
                    # endif // ifdef P104_USE_DATETIME_OPTIONS
                    );
            redisplay        = true;
            it->_lastChecked = node_time.day();
          }
          break;
        }
        case P104_CONTENT_DATE_TIME: // date-time/9
        {
          const int8_t m = getDateTime(szTimeL,
                                       currentFlasher || !useFlasher,
                                       time12h,
                                       timeAmpm,
                                       year4dgt
                                       # ifdef P104_USE_DATETIME_OPTIONS
                                       , get4BitFromUL(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_FORMAT)
                                       , get4BitFromUL(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_SEP_CHAR)
                                       # endif // ifdef P104_USE_DATETIME_OPTIONS
                                       );
          flasher          = newFlasher;
          redisplay        = useFlasher || (it->_lastChecked != m);
          it->_lastChecked = m;
          break;
        }
        default:
          break;
      }

      if (redisplay) {
        displayOneZoneText(it->zone - 1, *it, String(szTimeL));
        P->displayReset(it->zone - 1);

        if (it->repeatDelay > -1) {
          it->_repeatTimer = millis();
        }
      }
    }
    delay(0); // Leave some breathingroom
  }

  if (redisplay) {
    // synchronise the start
    P->synchZoneStart();
  }
  return redisplay || success;
}

/***************************************************
 * restart a zone if the repeat delay (if any) has passed
 **************************************************/
void P104_data_struct::checkRepeatTimer(uint8_t z) {
  if (nullptr == P) { return; }
  bool handled = false;

  for (auto it = zones.begin(); it != zones.end() && !handled; ++it) {
    if (it->zone == z + 1) {
      handled = true;

      if ((it->repeatDelay > -1) && (timePassedSince(it->_repeatTimer) >= (it->repeatDelay - 1) * 1000)) { // Compensated for the '1' in
                                                                                                           // PLUGIN_ONE_PER_SECOND
        # ifdef P104_DEBUG

        if (logAllText && loglevelActiveFor(LOG_LEVEL_INFO)) {
          String log;
          log.reserve(51);
          log  = F("dotmatrix: Repeat zone: ");
          log += it->zone;
          log += F(" delay: ");
          log += it->repeatDelay;
          log += F(" (");
          log += (timePassedSince(it->_repeatTimer) / 1000.0f); // Decimals can be useful here
          log += ')';
          addLogMove(LOG_LEVEL_INFO, log);
        }
        # endif // ifdef P104_DEBUG

        if ((it->content == P104_CONTENT_TEXT) ||
            (it->content == P104_CONTENT_TEXT_REV)) {
          displayOneZoneText(it->zone - 1, *it, sZoneInitial[it->zone - 1]); // Re-send last displayed text
          P->displayReset(it->zone - 1);
        }

        if ((it->content == P104_CONTENT_TIME) ||
            (it->content == P104_CONTENT_TIME_SEC) ||
            (it->content == P104_CONTENT_DATE4) ||
            (it->content == P104_CONTENT_DATE6) ||
            (it->content == P104_CONTENT_DATE_TIME)) {
          it->_lastChecked = -1; // Invalidate so next run will re-display the date/time
        }
        # ifdef P104_USE_BAR_GRAPH

        if (it->content == P104_CONTENT_BAR_GRAPH) {
          displayBarGraph(it->zone - 1, *it, sZoneInitial[it->zone - 1]); // Re-send last displayed bar graph
        }
        # endif // ifdef P104_USE_BAR_GRAPH
        it->_repeatTimer = millis();
      }
    }
    delay(0); // Leave some breathingroom
  }
}

/***************************************
 * saveSettings gather the zones data from the UI and store in customsettings
 **************************************/
bool P104_data_struct::saveSettings() {
  free_string(error); // Clear

  # ifdef P104_DEBUG_DEV

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO, concat(F("P104: saving zones, count: "), expectedZones));
  }
  # endif // ifdef P104_DEBUG_DEV

  uint8_t index      = 0;
  uint8_t action     = P104_ACTION_NONE;
  uint8_t zoneIndex  = 0;
  int8_t  zoneOffset = 0;

  zones.clear(); // Start afresh

  for (uint8_t zCounter = 0; zCounter < expectedZones; zCounter++) {
    # ifdef P104_USE_ZONE_ACTIONS
    action = getFormItemIntCustomArgName(index + P104_OFFSET_ACTION);

    if (((action == P104_ACTION_ADD_ABOVE) && (zoneOrder == 0)) ||
        ((action == P104_ACTION_ADD_BELOW) && (zoneOrder == 1))) {
      zones.push_back(P104_zone_struct(0));
      zoneOffset++;
      #  ifdef P104_DEBUG_DEV

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLogMove(LOG_LEVEL_INFO, concat(F("P104: insert before zone: "), zoneIndex + 1));
      }
      #  endif // ifdef P104_DEBUG_DEV
    }
    # endif    // ifdef P104_USE_ZONE_ACTIONS
    zoneIndex = zCounter + zoneOffset;

    if (action == P104_ACTION_DELETE) {
      zoneOffset--;
    } else {
      # ifdef P104_DEBUG_DEV

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLogMove(LOG_LEVEL_INFO, concat(F("P104: read zone: "), zoneIndex + 1));
      }
      # endif // ifdef P104_DEBUG_DEV
      zones.push_back(P104_zone_struct(zoneIndex + 1));

      for (uint8_t i = 0; i < P104_OFFSET_COUNT; ++i) {
        // for newly added zone, use defaults
        const bool mustCheckSize =
          (i == P104_OFFSET_BRIGHTNESS) ||
          (i == P104_OFFSET_REPEATDELAY);

        if (!mustCheckSize || (zones[zoneIndex].size != 0)) {
          if (i == P104_OFFSET_TEXT) {
            zones[zoneIndex].text = wrapWithQuotes(webArg(getPluginCustomArgName(index + P104_OFFSET_TEXT)));
          } else {
            zones[zoneIndex].setIntValue(i, getFormItemIntCustomArgName(index + i));
          }
        }
      }
    }
    # ifdef P104_DEBUG_DEV

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, concat(F("P104: add zone: "), zoneIndex + 1));
    }
    # endif // ifdef P104_DEBUG_DEV

    # ifdef P104_USE_ZONE_ACTIONS

    if (((action == P104_ACTION_ADD_BELOW) && (zoneOrder == 0)) ||
        ((action == P104_ACTION_ADD_ABOVE) && (zoneOrder == 1))) {
      zones.push_back(P104_zone_struct(0));
      zoneOffset++;
      #  ifdef P104_DEBUG_DEV

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLogMove(LOG_LEVEL_INFO, concat(F("P104: insert after zone: "), zoneIndex + 2));
      }
      #  endif // ifdef P104_DEBUG_DEV
    }
    # endif    // ifdef P104_USE_ZONE_ACTIONS

    index += P104_OFFSET_COUNT;
    delay(0);
  }

  uint16_t bufferSize;
  int saveOffset = 0;

  numDevices = 0;                      // Count the number of connected display units

  # if P104_FEATURE_STORAGE_V3
  bufferSize = P104_CONFIG_VERSION_V3; // Save special marker that we're using V3 (extended) settings
  # else // if P104_FEATURE_STORAGE_V3
  bufferSize = P104_CONFIG_VERSION_V2; // Save special marker that we're using V2 settings
  # endif // if P104_FEATURE_STORAGE_V3

  // This write is counting
  error += SaveToFile(SettingsType::Enum::CustomTaskSettings_Type, taskIndex, (uint8_t *)&bufferSize, sizeof(bufferSize), saveOffset);
  # if P104_FEATURE_STORAGE_V3
  saveOffset = DAT_TASKS_CUSTOM_SIZE; // Start in the extension file
  # else // if P104_FEATURE_STORAGE_V3
  saveOffset += sizeof(bufferSize);
  # endif // if P104_FEATURE_STORAGE_V3

  String zbuffer;

  // 47 total + (max) 100 characters for it->text requires a buffer of ~150 (P104_SETTINGS_BUFFER_V2), but only the required length is
  // stored with the length prefixed
  if (zbuffer.reserve(P104_SETTINGS_BUFFER_V2 + 2)) {
    for (auto it = zones.begin(); it != zones.end() && error.length() == 0; ++it) {
      // WARNING: Order of values should match the numeric order of P104_OFFSET_* values
      zbuffer.clear();

      for (uint8_t i = 0; i < P104_OFFSET_COUNT; ++i) {
        if (i == P104_OFFSET_TEXT) {
          zbuffer += it->text;
          zbuffer += '\x01';
        } else {
          int32_t value{};

          if (it->getIntValue(i, value)) {
            zbuffer += value;
            zbuffer += '\x01';
          }
        }
      }

      numDevices += (it->size != 0 ? it->size : 1) + it->offset; // Count corrected for newly added zones

      ZERO_FILL(P104_storeThis);                                 // Clean previous data

      if (saveOffset + zbuffer.length() + (sizeof(P104_dataSize) * 2) >
          (
            # if !P104_FEATURE_STORAGE_V3       // Don't count the skipped storage
            DAT_TASKS_CUSTOM_SIZE +
            # endif // if !P104_FEATURE_STORAGE_V3
            DAT_TASKS_CUSTOM_EXTENSION_SIZE)) { // Detect ourselves if we've reached the
        error.reserve(55);                      // high-water mark
        error += F("Total combination of Zones & text too long to store.\n");
        addLogMove(LOG_LEVEL_ERROR, error);
      } else {
        // Store length of buffer
        P104_dataSize = zbuffer.length();
        safe_strncpy(P104_data, zbuffer.c_str(), P104_dataSize + 1);

        // As we write in parts, only count as single write.
        if (RTC.flashDayCounter > 0) {
          RTC.flashDayCounter--;
        }
        error += SaveToFile(SettingsType::Enum::CustomTaskSettings_Type,
                            taskIndex,
                            (uint8_t *)P104_storeThis,
                            P104_dataSize + sizeof(P104_dataSize),
                            saveOffset);
        saveOffset += P104_dataSize + sizeof(P104_dataSize);

        # ifdef P104_DEBUG_DEV

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          addLogMove(LOG_LEVEL_INFO, strformat(F("P104: saveSettings zone: %d bufferSize: %d offset: %d"),
                                               it->zone, bufferSize, saveOffset));
          zbuffer.replace(P104_FIELD_SEP, P104_FIELD_DISP);
          addLog(LOG_LEVEL_INFO, zbuffer);
        }
        # endif // ifdef P104_DEBUG_DEV
      }

      delay(0);
    }

    // Store an End-of-settings marker == 0
    bufferSize = 0u;

    // This write is counting
    SaveToFile(SettingsType::Enum::CustomTaskSettings_Type, taskIndex, (uint8_t *)&bufferSize, sizeof(bufferSize), saveOffset);

    if (numDevices > 255) {
      error += strformat(F("More than 255 modules configured (%u)\n"), numDevices);
    }
  } else {
    addLog(LOG_LEVEL_ERROR, F("DOTMATRIX: Can't allocate string for saving settings, insufficient memory!"));
    return false; // Don't continue
  }

  return error.isEmpty();
}

/**************************************************************
* webform_load
**************************************************************/
bool P104_data_struct::webform_load(struct EventStruct *event) {
  {                                       // Hardware types
    # define P104_hardwareTypeCount 8
    const __FlashStringHelper *hardwareTypes[P104_hardwareTypeCount] = {
      F("Generic (DR:0, CR:1, RR:0)"),    // 010
      F("Parola (DR:1, CR:1, RR:0)"),     // 110
      F("FC16 (DR:1, CR:0, RR:0)"),       // 100
      F("IC Station (DR:1, CR:1, RR:1)"), // 111
      F("Other 1 (DR:0, CR:0, RR:0)"),    // 000
      F("Other 2 (DR:0, CR:0, RR:1)"),    // 001
      F("Other 3 (DR:0, CR:1, RR:1)"),    // 011
      F("Other 4 (DR:1, CR:0, RR:1)")     // 101
    };
    constexpr int hardwareOptions[P104_hardwareTypeCount] = {
      static_cast<int>(MD_MAX72XX::moduleType_t::GENERIC_HW),
      static_cast<int>(MD_MAX72XX::moduleType_t::PAROLA_HW),
      static_cast<int>(MD_MAX72XX::moduleType_t::FC16_HW),
      static_cast<int>(MD_MAX72XX::moduleType_t::ICSTATION_HW),
      static_cast<int>(MD_MAX72XX::moduleType_t::DR0CR0RR0_HW),
      static_cast<int>(MD_MAX72XX::moduleType_t::DR0CR0RR1_HW),
      static_cast<int>(MD_MAX72XX::moduleType_t::DR0CR1RR1_HW),
      static_cast<int>(MD_MAX72XX::moduleType_t::DR1CR0RR1_HW)
    };
    const FormSelectorOptions selector(
      P104_hardwareTypeCount,
      hardwareTypes,
      hardwareOptions);
    selector.addFormSelector(F("Hardware type"), F("hardware"), P104_CONFIG_HARDWARETYPE);
    # ifdef P104_ADD_SETTINGS_NOTES
    addFormNote(F("DR = Digits as Rows, CR = Column Reversed, RR = Row Reversed; 0 = no, 1 = yes."));
    # endif // ifdef P104_ADD_SETTINGS_NOTES
  }

  {
    addFormCheckBox(F("Clear display on disable"), F("clrdsp"),
                    bitRead(P104_CONFIG_FLAGS, P104_CONFIG_FLAG_CLEAR_DISABLE));

    addFormCheckBox(F("Log all displayed text (info)"),
                    F("logtxt"),
                    bitRead(P104_CONFIG_FLAGS, P104_CONFIG_FLAG_LOG_ALL_TEXT));
  }

  # ifdef P104_USE_DATETIME_OPTIONS
  {
    addFormSubHeader(F("Content options"));

    addFormCheckBox(F("Clock with flashing colon"), F("clkflash"), !bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_FLASH));
    addFormCheckBox(F("Clock 12h display"),         F("clk12h"),   bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_12H));
    addFormCheckBox(F("Clock 12h AM/PM indicator"), F("clkampm"),  bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_AMPM));
  }
  { // Date format
    const __FlashStringHelper *dateFormats[] = {
      F("Day Month [Year]"),
      F("Month Day [Year] (US-style)"),
      F("[Year] Month Day (Japanese-style)")
    };
    constexpr int dateFormatOptions[] = {
      P104_DATE_FORMAT_EU,
      P104_DATE_FORMAT_US,
      P104_DATE_FORMAT_JP
    };
    const FormSelectorOptions selector(3, dateFormats, dateFormatOptions);
    selector.addFormSelector(F("Date format"), F("datefmt"),
                             get4BitFromUL(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_FORMAT));
  }
  { // Date separator
    const __FlashStringHelper *dateSeparators[] = {
      F("Space"),
      F("Slash /"),
      F("Dash -"),
      F("Dot <b>.</b>")
    };
    constexpr int dateSeparatorOptions[] = {
      P104_DATE_SEPARATOR_SPACE,
      P104_DATE_SEPARATOR_SLASH,
      P104_DATE_SEPARATOR_DASH,
      P104_DATE_SEPARATOR_DOT
    };
    const FormSelectorOptions selector(4, dateSeparators, dateSeparatorOptions);
    selector.addFormSelector(F("Date separator"), F("datesep"),
                             get4BitFromUL(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_SEP_CHAR));

    addFormCheckBox(F("Year uses 4 digits"), F("year4dgt"), bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_YEAR4DGT));
  }
  # endif // ifdef P104_USE_DATETIME_OPTIONS

  addFormSubHeader(F("Zones"));

  { // Zones
    String zonesList[P104_MAX_ZONES];
    int    zonesOptions[P104_MAX_ZONES];

    for (uint8_t i = 0; i < P104_MAX_ZONES; i++) {
      zonesList[i]    = i + 1;
      zonesOptions[i] = i + 1; // No 0 needed or wanted
    }
    # if defined(P104_USE_TOOLTIPS) || defined(P104_ADD_SETTINGS_NOTES)

    const String zonetip = F("Select between 1 and " STRINGIFY(P104_MAX_ZONES) " zones, changing"
                             #  ifdef P104_USE_ZONE_ORDERING
                             " Zones or Zone order"
                             #  endif // ifdef P104_USE_ZONE_ORDERING
                             " will save and reload the page.");
    # endif    // if defined(P104_USE_TOOLTIPS) || defined(P104_ADD_SETTINGS_NOTES)

    FormSelectorOptions selector(P104_MAX_ZONES, zonesList, zonesOptions);
    selector.reloadonchange = true;
    # ifdef P104_USE_TOOLTIPS
    selector.tooltip = zonetip;
    # endif // ifdef P104_USE_TOOLTIPS
    selector.addFormSelector(F("Zones"), F("zonecnt"), P104_CONFIG_ZONE_COUNT);

    # ifdef P104_USE_ZONE_ORDERING
    const String orderTypes[] = {
      F("Numeric order (1..n)"),
      F("Display order (n..1)")
    };
    const int    orderOptions[] = { 0, 1 };
    FormSelectorOptions selector_zoneordering(2, orderTypes, orderOptions);
    selector.reloadonchange = true;
    #  ifdef P104_USE_TOOLTIPS
    selector.tooltip = zonetip;
    #  endif // ifdef P104_USE_TOOLTIPS
    selector_zoneordering.addFormSelector(F("Zone order"), F("zoneorder"),
                                          bitRead(P104_CONFIG_FLAGS, P104_CONFIG_FLAG_ZONE_ORDER) ? 1 : 0);
    selector_zoneordering.reloadonchange = true;
    # endif // ifdef P104_USE_ZONE_ORDERING
    # ifdef P104_ADD_SETTINGS_NOTES
    addFormNote(zonetip);
    # endif // ifdef P104_ADD_SETTINGS_NOTES
  }
  expectedZones = P104_CONFIG_ZONE_COUNT;

  if (expectedZones == 0) { expectedZones++; } // Minimum of 1 zone

  { // Optionlists and zones table
    const __FlashStringHelper *alignmentTypes[3] = {
      F("Left"),
      F("Center"),
      F("Right")
    };
    const int alignmentOptions[3] = {
      static_cast<int>(textPosition_t::PA_LEFT),
      static_cast<int>(textPosition_t::PA_CENTER),
      static_cast<int>(textPosition_t::PA_RIGHT)
    };


    // Append the numeric value as a reference for the 'anim.in' and 'anim.out' subcommands
    const __FlashStringHelper *animationTypes[] {
      F("None (0)")
      , F("Print (1)")
      , F("Scroll up (2)")
      , F("Scroll down (3)")
      , F("Scroll left * (4)")
      , F("Scroll right * (5)")
    # if ENA_SPRITE
      , F("Sprite (6)")
    # endif // ENA_SPRITE
    # if ENA_MISC
      , F("Slice * (7)")
      , F("Mesh (8)")
      , F("Fade (9)")
      , F("Dissolve (10)")
      , F("Blinds (11)")
      , F("Random (12)")
    # endif // ENA_MISC
    # if ENA_WIPE
      , F("Wipe (13)")
      , F("Wipe w. cursor (14)")
    # endif // ENA_WIPE
    # if ENA_SCAN
      , F("Scan horiz. (15)")
      , F("Scan horiz. cursor (16)")
      , F("Scan vert. (17)")
      , F("Scan vert. cursor (18)")
    # endif // ENA_SCAN
    # if ENA_OPNCLS
      , F("Opening (19)")
      , F("Opening w. cursor (20)")
      , F("Closing (21)")
      , F("Closing w. cursor (22)")
    # endif // ENA_OPNCLS
    # if ENA_SCR_DIA
      , F("Scroll up left * (23)")
      , F("Scroll up right * (24)")
      , F("Scroll down left * (25)")
      , F("Scroll down right * (26)")
    # endif // ENA_SCR_DIA
    # if ENA_GROW
      , F("Grow up (27)")
      , F("Grow down (28)")
    # endif // ENA_GROW
    };

    const int animationOptions[] = {
      static_cast<int>(textEffect_t::PA_NO_EFFECT)
      , static_cast<int>(textEffect_t::PA_PRINT)
      , static_cast<int>(textEffect_t::PA_SCROLL_UP)
      , static_cast<int>(textEffect_t::PA_SCROLL_DOWN)
      , static_cast<int>(textEffect_t::PA_SCROLL_LEFT)
      , static_cast<int>(textEffect_t::PA_SCROLL_RIGHT)
    # if ENA_SPRITE
      , static_cast<int>(textEffect_t::PA_SPRITE)
    # endif // ENA_SPRITE
    # if ENA_MISC
      , static_cast<int>(textEffect_t::PA_SLICE)
      , static_cast<int>(textEffect_t::PA_MESH)
      , static_cast<int>(textEffect_t::PA_FADE)
      , static_cast<int>(textEffect_t::PA_DISSOLVE)
      , static_cast<int>(textEffect_t::PA_BLINDS)
      , static_cast<int>(textEffect_t::PA_RANDOM)
    # endif // ENA_MISC
    # if ENA_WIPE
      , static_cast<int>(textEffect_t::PA_WIPE)
      , static_cast<int>(textEffect_t::PA_WIPE_CURSOR)
    # endif // ENA_WIPE
    # if ENA_SCAN
      , static_cast<int>(textEffect_t::PA_SCAN_HORIZ)
      , static_cast<int>(textEffect_t::PA_SCAN_HORIZX)
      , static_cast<int>(textEffect_t::PA_SCAN_VERT)
      , static_cast<int>(textEffect_t::PA_SCAN_VERTX)
    # endif // ENA_SCAN
    # if ENA_OPNCLS
      , static_cast<int>(textEffect_t::PA_OPENING)
      , static_cast<int>(textEffect_t::PA_OPENING_CURSOR)
      , static_cast<int>(textEffect_t::PA_CLOSING)
      , static_cast<int>(textEffect_t::PA_CLOSING_CURSOR)
    # endif // ENA_OPNCLS
    # if ENA_SCR_DIA
      , static_cast<int>(textEffect_t::PA_SCROLL_UP_LEFT)
      , static_cast<int>(textEffect_t::PA_SCROLL_UP_RIGHT)
      , static_cast<int>(textEffect_t::PA_SCROLL_DOWN_LEFT)
      , static_cast<int>(textEffect_t::PA_SCROLL_DOWN_RIGHT)
    # endif // ENA_SCR_DIA
    # if ENA_GROW
      , static_cast<int>(textEffect_t::PA_GROW_UP)
      , static_cast<int>(textEffect_t::PA_GROW_DOWN)
    # endif // ENA_GROW
    };

    constexpr int animationCount = NR_ELEMENTS(animationOptions);

    delay(0);

    const __FlashStringHelper *fontTypes[] = {
      F("Default (0)")
    # ifdef P104_USE_NUMERIC_DOUBLEHEIGHT_FONT
      , F("Numeric, double height (1)")
    # endif // ifdef P104_USE_NUMERIC_DOUBLEHEIGHT_FONT
    # ifdef P104_USE_FULL_DOUBLEHEIGHT_FONT
      , F("Full, double height (2)")
    # endif // ifdef P104_USE_FULL_DOUBLEHEIGHT_FONT
    # ifdef P104_USE_VERTICAL_FONT
      , F("Vertical (3)")
    # endif // ifdef P104_USE_VERTICAL_FONT
    # ifdef P104_USE_EXT_ASCII_FONT
      , F("Extended ASCII (4)")
      # endif // ifdef P104_USE_EXT_ASCII_FONT
    # ifdef P104_USE_ARABIC_FONT
      , F("Arabic (5)")
    # endif // ifdef P104_USE_ARABIC_FONT
    # ifdef P104_USE_GREEK_FONT
      , F("Greek (6)")
    # endif // ifdef P104_USE_GREEK_FONT
    # ifdef P104_USE_KATAKANA_FONT
      , F("Katakana (7)")
    # endif // ifdef P104_USE_KATAKANA_FONT
    };
    const int fontOptions[] = {
      P104_DEFAULT_FONT_ID
    # ifdef P104_USE_NUMERIC_DOUBLEHEIGHT_FONT
      , P104_DOUBLE_HEIGHT_FONT_ID
    # endif // ifdef P104_USE_NUMERIC_DOUBLEHEIGHT_FONT
    # ifdef P104_USE_FULL_DOUBLEHEIGHT_FONT
      , P104_FULL_DOUBLEHEIGHT_FONT_ID
    # endif // ifdef P104_USE_FULL_DOUBLEHEIGHT_FONT
    # ifdef P104_USE_VERTICAL_FONT
      , P104_VERTICAL_FONT_ID
    # endif // ifdef P104_USE_VERTICAL_FONT
    # ifdef P104_USE_EXT_ASCII_FONT
      , P104_EXT_ASCII_FONT_ID
      # endif // ifdef P104_USE_EXT_ASCII_FONT
    # ifdef P104_USE_ARABIC_FONT
      , P104_ARABIC_FONT_ID
    # endif // ifdef P104_USE_ARABIC_FONT
    # ifdef P104_USE_GREEK_FONT
      , P104_GREEK_FONT_ID
    # endif // ifdef P104_USE_GREEK_FONT
    # ifdef P104_USE_KATAKANA_FONT
      , P104_KATAKANA_FONT_ID
    # endif // ifdef P104_USE_KATAKANA_FONT
    };
    constexpr int fontCount = NR_ELEMENTS(fontTypes);

    const __FlashStringHelper *layoutTypes[] = {
      F("Standard")
    # if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT)
      , F("Double, upper")
      , F("Double, lower")
    # endif // if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT)
    };
    const int layoutOptions[] = {
      P104_LAYOUT_STANDARD
    # if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT)
      , P104_LAYOUT_DOUBLE_UPPER
      , P104_LAYOUT_DOUBLE_LOWER
    # endif // if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT)
    };
    constexpr int layoutCount = NR_ELEMENTS(layoutTypes);

    const __FlashStringHelper *specialEffectTypes[] = {
      F("None"),
      F("Flip up/down"),
      F("Flip left/right *"),
      F("Flip u/d &amp; l/r *")
    };
    const int specialEffectOptions[] = {
      P104_SPECIAL_EFFECT_NONE,
      P104_SPECIAL_EFFECT_UP_DOWN,
      P104_SPECIAL_EFFECT_LEFT_RIGHT,
      P104_SPECIAL_EFFECT_BOTH
    };
    constexpr int specialEffectCount = NR_ELEMENTS(specialEffectTypes);

    const __FlashStringHelper *contentTypes[] = {
      F("Text"),
      F("Text reverse"),
      F("Clock (4 mod)"),
      F("Clock sec (6 mod)"),
      F("Date (4 mod)"),
      F("Date yr (6/7 mod)"),
      F("Date/time (9/13 mod)"),
      # ifdef P104_USE_BAR_GRAPH
      F("Bar graph"),
      # endif // ifdef P104_USE_BAR_GRAPH
    };
    const int contentOptions[] {
      P104_CONTENT_TEXT,
      P104_CONTENT_TEXT_REV,
      P104_CONTENT_TIME,
      P104_CONTENT_TIME_SEC,
      P104_CONTENT_DATE4,
      P104_CONTENT_DATE6,
      P104_CONTENT_DATE_TIME,
      # ifdef P104_USE_BAR_GRAPH
      P104_CONTENT_BAR_GRAPH,
      # endif // ifdef P104_USE_BAR_GRAPH
    };
    const __FlashStringHelper *invertedTypes[3] = {
      F("Normal"),
      F("Inverted")
    };
    const int invertedOptions[] = {
      0,
      1
    };
    constexpr int invertedCount = NR_ELEMENTS(invertedOptions);
    # ifdef P104_USE_ZONE_ACTIONS
    uint8_t actionCount = 0;
    const __FlashStringHelper *actionTypes[4];
    int actionOptions[4];
    actionTypes[actionCount]   = F("None");
    actionOptions[actionCount] = P104_ACTION_NONE;
    actionCount++;

    if (zones.size() < P104_MAX_ZONES) {
      actionTypes[actionCount]   = F("New above");
      actionOptions[actionCount] = P104_ACTION_ADD_ABOVE;
      actionCount++;
      actionTypes[actionCount]   = F("New below");
      actionOptions[actionCount] = P104_ACTION_ADD_BELOW;
      actionCount++;
    }
    actionTypes[actionCount]   = F("Delete");
    actionOptions[actionCount] = P104_ACTION_DELETE;
    actionCount++;
    # endif // ifdef P104_USE_ZONE_ACTIONS

    delay(0);

    addFormSubHeader(F("Zone configuration"));

    {
      html_table(EMPTY_STRING); // Sub-table

      const __FlashStringHelper *headers[] = {
        F("Zone #&nbsp;"),
        F("Modules"),
        F("Text"),
        F("Content"),
        F("Alignment"),
        F("Animation In/Out"),               // 1st and 2nd row title
        F("Speed/Pause"),                    // 1st and 2nd row title
        F("Font/Layout"),                    // 1st and 2nd row title
        F("Inverted/ Special&nbsp;Effects"), // 1st and 2nd row title
        F("Offset"),
        F("Brightness"),
        F("Repeat (sec)")
      };

      constexpr unsigned nrHeaders = NR_ELEMENTS(headers);

      for (unsigned i = 0; i < nrHeaders; ++i) {
        int width = 0;

        if (i == 2) {
          // "Text" needs a width
          width = 180;
        }
        html_table_header(headers[i], width);
      }
      # ifdef P104_USE_ZONE_ACTIONS
      html_table_header(F(""),       15); // Spacer
      html_table_header(F("Action"), 45);
      # endif // ifdef P104_USE_ZONE_ACTIONS
    }

    uint16_t index;
    int16_t  startZone, endZone;
    int8_t   incrZone = 1;
    # ifdef P104_USE_ZONE_ACTIONS
    uint8_t currentRow = 0;
    # endif // ifdef P104_USE_ZONE_ACTIONS

    # ifdef P104_USE_ZONE_ORDERING

    if (bitRead(P104_CONFIG_FLAGS, P104_CONFIG_FLAG_ZONE_ORDER)) {
      startZone = zones.size() - 1;
      endZone   = -1;
      incrZone  = -1;
    } else
    # endif // ifdef P104_USE_ZONE_ORDERING
    {
      startZone = 0;
      endZone   = zones.size();
    }

    for (int8_t zone = startZone; zone != endZone; zone += incrZone) {
      if (zones[zone].zone <= expectedZones) {
        index = (zones[zone].zone - 1) * P104_OFFSET_COUNT;

        html_TR_TD(); // All columns use max. width available
        addHtml(F("&nbsp;"));
        addHtmlInt(zones[zone].zone);

        html_TD(); // Modules
        addNumericBox(getPluginCustomArgName(index + P104_OFFSET_SIZE), zones[zone].size, 1, P104_MAX_MODULES_PER_ZONE);

        html_TD(); // Text
        addTextBox(getPluginCustomArgName(index + P104_OFFSET_TEXT),
                   zones[zone].text,
                   P104_MAX_TEXT_LENGTH_PER_ZONE,
                   F(""));
        {
          html_TD(); // Content
          FormSelectorOptions selector(
            P104_CONTENT_count, contentTypes, contentOptions);
          selector.clearClassName();
          selector.addSelector(
            getPluginCustomArgName(index + P104_OFFSET_CONTENT),
            zones[zone].content);
        }
        {
          html_TD(); // Alignment
          FormSelectorOptions selector(3, alignmentTypes, alignmentOptions);
          selector.clearClassName();
          selector.addSelector(
            getPluginCustomArgName(index + P104_OFFSET_ALIGNMENT),
            zones[zone].alignment);
        }
        {
          html_TD(); // Animation In (without None by passing the second element index)
          FormSelectorOptions selector(
            animationCount - 1,
            &animationTypes[1],
            &animationOptions[1]);
          selector.clearClassName();
          # ifdef P104_USE_TOOLTIPS
          selector.tooltip = F("Animation In");
          # endif // ifdef P104_USE_TOOLTIPS
          selector.addSelector(
            getPluginCustomArgName(index + P104_OFFSET_ANIM_IN),
            zones[zone].animationIn);
        }

        html_TD();                 // Speed In
        addNumericBox(getPluginCustomArgName(index + P104_OFFSET_SPEED), zones[zone].speed, 0, P104_MAX_SPEED_PAUSE_VALUE
                      # ifdef P104_USE_TOOLTIPS
                      , F("")      // classname
                      , F("Speed") // title
                      # endif // ifdef P104_USE_TOOLTIPS
                      );
        {
          html_TD(); // Font
          FormSelectorOptions selector(
            fontCount,
            fontTypes,
            fontOptions);
          selector.clearClassName();
          # ifdef P104_USE_TOOLTIPS
          selector.tooltip = F("Font");
          # endif // ifdef P104_USE_TOOLTIPS

          selector.addSelector(
            getPluginCustomArgName(index + P104_OFFSET_FONT),
            zones[zone].font);
        }
        {
          html_TD(); // Inverted
          FormSelectorOptions selector(
            invertedCount,
            invertedTypes,
            invertedOptions);
          selector.clearClassName();
          # ifdef P104_USE_TOOLTIPS
          selector.tooltip = F("Inverted");
          # endif // ifdef P104_USE_TOOLTIPS

          selector.addSelector(
            getPluginCustomArgName(index + P104_OFFSET_INVERTED),
            zones[zone].inverted);
        }
        html_TD(3); // Fill columns
        # ifdef P104_USE_ZONE_ACTIONS

        html_TD();  // Spacer
        addHtml('|');

        if (currentRow < 2) {
          addHtml(F("<TD style=\"text-align:center;font-size:90%\">")); // Action column, text centered and font-size 90%
        } else {
          html_TD();
        }

        if (currentRow == 0) {
          addHtml(F("(applied immediately!)"));
        } else if (currentRow == 1) {
          addHtml(F("(Delete can't be undone!)"));
        }
        currentRow++;
        # endif // ifdef P104_USE_ZONE_ACTIONS

        // Split here
        html_TR_TD(); // Start new row
        html_TD(4);   // Start with some blank columns

        {
          html_TD();  // Animation Out
          FormSelectorOptions selector(
            animationCount,
            animationTypes,
            animationOptions);
          selector.clearClassName();
          # ifdef P104_USE_TOOLTIPS
          selector.tooltip = F("Animation Out");
          # endif // ifdef P104_USE_TOOLTIPS

          selector.addSelector(
            getPluginCustomArgName(index + P104_OFFSET_ANIM_OUT),
            zones[zone].animationOut);
        }

        html_TD();                 // Pause after Animation In
        addNumericBox(getPluginCustomArgName(index + P104_OFFSET_PAUSE), zones[zone].pause, 0, P104_MAX_SPEED_PAUSE_VALUE
                      # ifdef P104_USE_TOOLTIPS
                      , F("")      // classname
                      , F("Pause") // title
                      # endif // ifdef P104_USE_TOOLTIPS
                      );
        {
          html_TD(); // Layout
          FormSelectorOptions selector(
            layoutCount,
            layoutTypes,
            layoutOptions);
          selector.clearClassName();
          # ifdef P104_USE_TOOLTIPS
          selector.tooltip = F("Layout");
          # endif // ifdef P104_USE_TOOLTIPS

          selector.addSelector(
            getPluginCustomArgName(index + P104_OFFSET_LAYOUT),
            zones[zone].layout);
        }
        {
          html_TD(); // Special effects
          FormSelectorOptions selector(
            specialEffectCount,
            specialEffectTypes,
            specialEffectOptions);
          selector.clearClassName();
          # ifdef P104_USE_TOOLTIPS
          selector.tooltip = F("Special Effects");
          # endif // ifdef P104_USE_TOOLTIPS

          selector.addSelector(
            getPluginCustomArgName(index + P104_OFFSET_SPEC_EFFECT),
            zones[zone].specialEffect);
        }
        html_TD(); // Offset
        addNumericBox(getPluginCustomArgName(index + P104_OFFSET_OFFSET), zones[zone].offset, 0, 254);

        html_TD(); // Brightness

        if (zones[zone].brightness == -1) { zones[zone].brightness = P104_BRIGHTNESS_DEFAULT; }
        addNumericBox(getPluginCustomArgName(index + P104_OFFSET_BRIGHTNESS), zones[zone].brightness, 0, P104_BRIGHTNESS_MAX);

        html_TD(); // Repeat (sec)
        addNumericBox(getPluginCustomArgName(index + P104_OFFSET_REPEATDELAY),
                      zones[zone].repeatDelay,
                      -1,
                      P104_MAX_REPEATDELAY_VALUE                     // max delay 86400 sec. = 24 hours
                      # ifdef P104_USE_TOOLTIPS
                      , F("")                                        // classname
                      , F("Repeat after this delay (sec), -1 = off") // tooltip
                      # endif // ifdef P104_USE_TOOLTIPS
                      );

        # ifdef P104_USE_ZONE_ACTIONS
        html_TD();   // Spacer
        addHtml('|');
        {
          html_TD(); // Action
          FormSelectorOptions selector(
            actionCount,
            actionTypes,
            actionOptions);
          selector.classname      = F("");
          selector.reloadonchange = true;
          selector.addSelector(
            getPluginCustomArgName(index + P104_OFFSET_ACTION),
            P104_ACTION_NONE); // Always start with None
        }
        # endif // ifdef P104_USE_ZONE_ACTIONS

        delay(0);
      }
    }
    html_end_table();
  }

  # ifdef P104_ADD_SETTINGS_NOTES
  addFormNote(concat(F("- Maximum nr. of modules possible (Zones * Size + Offset) = 255. Last saved: "), numDevices));
  addFormNote(F("- 'Animation In' or 'Animation Out' and 'Special Effects' marked with <b>*</b> should <b>not</b> be combined in a Zone."));
  #  if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) && !defined(P104_USE_FULL_DOUBLEHEIGHT_FONT)
  addFormNote(F("- 'Layout' 'Double upper' and 'Double lower' are only supported for numeric 'Content' types like 'Clock' and 'Date'."));
  #  endif // if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) && !defined(P104_USE_FULL_DOUBLEHEIGHT_FONT)
  # endif    // ifdef P104_ADD_SETTINGS_NOTES

  return true;
}

/**************************************************************
* webform_save
**************************************************************/
bool P104_data_struct::webform_save(struct EventStruct *event) {
  P104_CONFIG_ZONE_COUNT   = getFormItemInt(F("zonecnt"));
  P104_CONFIG_HARDWARETYPE = getFormItemInt(F("hardware"));

  bitWrite(P104_CONFIG_FLAGS, P104_CONFIG_FLAG_CLEAR_DISABLE, isFormItemChecked(F("clrdsp")));
  bitWrite(P104_CONFIG_FLAGS, P104_CONFIG_FLAG_LOG_ALL_TEXT,  isFormItemChecked(F("logtxt")));

  # ifdef P104_USE_ZONE_ORDERING
  zoneOrder = getFormItemInt(F("zoneorder")); // Is used in saveSettings()
  bitWrite(P104_CONFIG_FLAGS, P104_CONFIG_FLAG_ZONE_ORDER, zoneOrder == 1);
  # endif // ifdef P104_USE_ZONE_ORDERING

  # ifdef P104_USE_DATETIME_OPTIONS
  uint32_t ulDateTime = 0;
  bitWrite(ulDateTime, P104_CONFIG_DATETIME_FLASH,    !isFormItemChecked(F("clkflash"))); // Inverted flag
  bitWrite(ulDateTime, P104_CONFIG_DATETIME_12H,      isFormItemChecked(F("clk12h")));
  bitWrite(ulDateTime, P104_CONFIG_DATETIME_AMPM,     isFormItemChecked(F("clkampm")));
  bitWrite(ulDateTime, P104_CONFIG_DATETIME_YEAR4DGT, isFormItemChecked(F("year4dgt")));
  set4BitToUL(ulDateTime, P104_CONFIG_DATETIME_FORMAT,   getFormItemInt(F("datefmt")));
  set4BitToUL(ulDateTime, P104_CONFIG_DATETIME_SEP_CHAR, getFormItemInt(F("datesep")));
  P104_CONFIG_DATETIME = ulDateTime;
  # endif // ifdef P104_USE_DATETIME_OPTIONS

  previousZones = expectedZones;
  expectedZones = P104_CONFIG_ZONE_COUNT;

  bool result = saveSettings();         // Determines numDevices and re-fills zones list

  P104_CONFIG_ZONE_COUNT  = zones.size();
  P104_CONFIG_TOTAL_UNITS = numDevices; // Store counted number of devices

  zones.clear();                        // Free some memory (temporarily)

  return result;
}

P104_zone_struct::P104_zone_struct(uint8_t _zone)
  :  text(F("\"\"")), zone(_zone) {}


bool P104_zone_struct::getIntValue(uint8_t offset, int32_t& value) const
{
  switch (offset) {
    case P104_OFFSET_SIZE:          value = size;           break;
    case P104_OFFSET_TEXT:          return false;
    case P104_OFFSET_CONTENT:       value = content;        break;
    case P104_OFFSET_ALIGNMENT:     value = alignment;      break;
    case P104_OFFSET_ANIM_IN:       value = animationIn;    break;
    case P104_OFFSET_SPEED:         value = speed;          break;
    case P104_OFFSET_ANIM_OUT:      value = animationOut;   break;
    case P104_OFFSET_PAUSE:         value = pause;          break;
    case P104_OFFSET_FONT:          value = font;           break;
    case P104_OFFSET_LAYOUT:        value = layout;         break;
    case P104_OFFSET_SPEC_EFFECT:   value = specialEffect;  break;
    case P104_OFFSET_OFFSET:        value = offset;         break;
    case P104_OFFSET_BRIGHTNESS:    value = brightness;     break;
    case P104_OFFSET_REPEATDELAY:   value = repeatDelay;    break;
    case P104_OFFSET_INVERTED:      value = inverted;       break;

    default:
      return false;
  }
  return true;
}

bool P104_zone_struct::setIntValue(uint8_t offset, int32_t value)
{
  switch (offset) {
    case P104_OFFSET_SIZE:          size = value; break;
    case P104_OFFSET_TEXT:          return false;
    case P104_OFFSET_CONTENT:       content       = value; break;
    case P104_OFFSET_ALIGNMENT:     alignment     = value; break;
    case P104_OFFSET_ANIM_IN:       animationIn   = value; break;
    case P104_OFFSET_SPEED:         speed         = value; break;
    case P104_OFFSET_ANIM_OUT:      animationOut  = value; break;
    case P104_OFFSET_PAUSE:         pause         = value; break;
    case P104_OFFSET_FONT:          font          = value; break;
    case P104_OFFSET_LAYOUT:        layout        = value; break;
    case P104_OFFSET_SPEC_EFFECT:   specialEffect = value; break;
    case P104_OFFSET_OFFSET:        offset        = value; break;
    case P104_OFFSET_BRIGHTNESS:    brightness    = value; break;
    case P104_OFFSET_REPEATDELAY:   repeatDelay   = value; break;
    case P104_OFFSET_INVERTED:      inverted      = value; break;

    default:
      return false;
  }
  return true;
}

#endif // ifdef USES_P104

#include "../PluginStructs/_StatsOnly_data_struct.h"


#if FEATURE_PLUGIN_STATS

_StatsOnly_data_struct::_StatsOnly_data_struct()
{
    _baseClassOnly = true;

}

#endif // if FEATURE_PLUGIN_STATS
#include "../PluginStructs/P077_data_struct.h"

#ifdef USES_P077

# include <ESPeasySerial.h>


const __FlashStringHelper* Plugin_077_valuename(P077_query value_nr, bool displayString)
{
  const __FlashStringHelper *strings[] = {
    F("Voltage"),        F("V"),
    F("Active Power"),   F("W"),
    F("Current"),        F("A"),
    F("Pulses"),         F("pulses"),
    F("Energy"),         F("kWh"),
    F("Apparent Power"), F("VA"),
    F("Power Factor"),   F("pf"),
    F("Reactive Power"), F("VAR")
  };
  constexpr size_t nrStrings = NR_ELEMENTS(strings);
  const size_t     index     = static_cast<size_t>(value_nr) * 2 + (displayString ? 0 : 1);

  if (index < nrStrings) {
    return strings[index];
  }
  return F("");
}

# if FEATURE_MQTT_DISCOVER
int Plugin_077_QueryVType(uint8_t value_nr) {
  const Sensor_VType vtypes[] = {
    Sensor_VType::SENSOR_TYPE_VOLTAGE_ONLY,
    Sensor_VType::SENSOR_TYPE_POWER_USG_ONLY,
    Sensor_VType::SENSOR_TYPE_CURRENT_ONLY,
    Sensor_VType::SENSOR_TYPE_NONE,
    Sensor_VType::SENSOR_TYPE_ENERGY,
    Sensor_VType::SENSOR_TYPE_APPRNT_POWER_USG_ONLY,
    Sensor_VType::SENSOR_TYPE_POWER_FACT_ONLY,
    Sensor_VType::SENSOR_TYPE_REACTIVE_POWER_ONLY,
  };
  constexpr uint8_t  valueCount = NR_ELEMENTS(vtypes);
  Sensor_VType result           = Sensor_VType::SENSOR_TYPE_NONE;

  if (value_nr < valueCount) {
    result = vtypes[value_nr];
  }
  return static_cast<int>(result);
}

# endif // if FEATURE_MQTT_DISCOVER

P077_query Plugin_077_from_valuename(const String& valuename)
{
  for (uint8_t query = 0; query < static_cast<uint8_t>(P077_query::P077_QUERY_NR_OUTPUT_OPTIONS); ++query) {
    if (valuename.equalsIgnoreCase(Plugin_077_valuename(static_cast<P077_query>(query), false))) {
      return static_cast<P077_query>(query);
    }
  }
  return P077_query::P077_QUERY_NR_OUTPUT_OPTIONS;
}

P077_data_struct::~P077_data_struct() {
  delete easySerial;
  easySerial = nullptr;
}

void P077_data_struct::reset() {
  delete easySerial;
  easySerial = nullptr;
}

bool P077_data_struct::isInitialized() const {
  return easySerial != nullptr;
}

bool P077_data_struct::init(ESPEasySerialPort port, const int16_t serial_rx, const int16_t serial_tx, unsigned long baudrate,
                            uint8_t config) {
  if (serial_rx < 0) {
    return false;
  }
  reset();
  easySerial = new (std::nothrow) ESPeasySerial(port, serial_rx, serial_tx);

  if (isInitialized()) {
    # if defined(ESP8266)
    easySerial->begin(baudrate, (SerialConfig)config);
    # elif defined(ESP32)
    easySerial->begin(baudrate, config);
    # endif // if defined(ESP8266)
    return true;
  }
  return false;
}

uint32_t P077_data_struct::get_24bit_value(uint8_t offset) const {
  uint32_t res{};
  constexpr size_t bufsize = NR_ELEMENTS(serial_in_buffer);

  if ((offset + 2u) < bufsize) {
    res = serial_in_buffer[offset] << 16 |
          serial_in_buffer[offset + 1] << 8 |
          serial_in_buffer[offset + 2];
  }
  return res;
}

bool P077_data_struct::processCseReceived(struct EventStruct *event) {
  const uint8_t header = serial_in_buffer[0];

  const bool voltage_cycle_exceeds_range = (header & 0xF8) == 0xF8;
  const bool current_cycle_exceeds_range = (header & 0xF4) == 0xF4;
  const bool power_cycle_exceeds_range   = (header & 0xF2) == 0xF2;


  if (header != 0x55) {
    if ((header & 0xF0) == 0xF0) {
      // Header1 = FxH
      if (voltage_cycle_exceeds_range || current_cycle_exceeds_range) {
        // Abnormal external circuit or chip damage
        return false;
      }
    } else {
      return false;
    }
  }

  // Get chip calibration data (coefficients) and use as initial defaults
  long voltage_coefficient = 191200;  // uSec
  long current_coefficient = 16140;   // uSec
  long power_coefficient   = 5364000; // uSec

  // V1R = shunt resistor
  // V2R = Voltage resistor
  float V1R = 1.0f; // With 1mR manganin resistor at V1P and V1N, V1R=1
  float V2R = 1.0f; // With 1M resistor at V2P, V2R=1


  if (CSE_NOT_CALIBRATED != header) {
    voltage_coefficient = get_24bit_value(2);
    current_coefficient = get_24bit_value(8);
    power_coefficient   = get_24bit_value(14);
  }

  if (voltage_coefficient != 0) {
    if ((CSE_UREF_PULSE == P077_UREF) || (P077_UREF == 0)) {
      P077_UREF = voltage_coefficient / CSE_UREF;
      V2R       = 1.0f;
    } else {
      V2R = static_cast<float>(P077_UREF * CSE_UREF) / voltage_coefficient;
    }
  }

  if (current_coefficient != 0) {
    if ((CSE_IREF_PULSE == P077_IREF) || (P077_IREF == 0)) {
      P077_IREF = current_coefficient;
      V1R       = 1.0f;
    } else {
      V1R = static_cast<float>(P077_IREF) / current_coefficient;
    }
  }

  if ((CSE_PREF_PULSE == P077_PREF) || (P077_PREF == 0)) {
    P077_PREF = (V1R * V2R * power_coefficient) / CSE_PREF;
  }

  if (P077_PREF != 0) {
    cf_frequency = (1e9f) / (P077_PREF * CSE_PREF);
  }


  adjustment = serial_in_buffer[20];

  const bool voltage_valid = bitRead(adjustment, 6);
  const bool current_valid = bitRead(adjustment, 5);
  const bool power_valid   = bitRead(adjustment, 4);

  // const bool cf_pulse_overflow = bitRead(adjustment, 7);


  if (voltage_valid) {
    const uint32_t voltage_cycle = get_24bit_value(5);

    if (voltage_cycle != 0) {
      setOutputValue(event, P077_query::P077_QUERY_VOLTAGE, static_cast<float>(P077_UREF * CSE_UREF) / static_cast<float>(voltage_cycle));
      newValue = true;
    }
  }

  if (power_valid) {
    const uint32_t cur_millis = millis();

    // Only use CF pulses when a power cycle has completed
    const uint32_t cur_cf_pulses = serial_in_buffer[21] << 8 | serial_in_buffer[22];

    if (last_cf_pulses == 0) {
      last_cf_pulses_moment = cur_millis;
      last_cf_pulses        = cur_cf_pulses;
    }

    uint32_t diff{};

    if (cur_cf_pulses < last_cf_pulses) {
      diff = cur_cf_pulses + (0xFFFF - last_cf_pulses) + 1;
    } else {
      diff = cur_cf_pulses - last_cf_pulses;
    }

    last_cf_pulses = cur_cf_pulses;

    cf_pulses += diff;

    float activePower{};

    if (!power_cycle_exceeds_range) {
      const long power_cycle = get_24bit_value(17);

      if (0 == power_cycle_first) {
        power_cycle_first = power_cycle; // Skip first incomplete power_cycle
      }

      if (power_cycle_first != power_cycle) {
        power_cycle_first = -1;

        if (power_cycle != 0) {
          activePower = static_cast<float>(P077_PREF * CSE_PREF) / static_cast<float>(power_cycle);
        } else {
          if ((diff > 0) && (last_cf_pulses_moment != cur_millis)) {
            const float time_passed_diff_sec = timeDiff(last_cf_pulses_moment, cur_millis) / 1000.0f;
            activePower = (diff / time_passed_diff_sec) / cf_frequency;
          }
        }
        newValue = true;
      }
    }
    setOutputValue(event, P077_query::P077_QUERY_ACTIVE_POWER, activePower);
    last_cf_pulses_moment = cur_millis;
  } else {
    power_cycle_first = 0;

    // Powered on but no load
    setOutputValue(event, P077_query::P077_QUERY_ACTIVE_POWER, 0);
  }

  if (current_valid) {
    newValue = true;
    const uint32_t current_cycle = get_24bit_value(11);

    if (essentiallyZero(getValue(P077_query::P077_QUERY_ACTIVE_POWER)) || (current_cycle == 0)) {
      setOutputValue(event, P077_query::P077_QUERY_CURRENT, 0);
    } else {
      setOutputValue(event, P077_query::P077_QUERY_CURRENT, static_cast<float>(P077_IREF) / static_cast<float>(current_cycle));
    }
  }

  // Update derived values
  {
    const float pulsesPerKwh = cf_frequency * 3600;
    setOutputValue(event, P077_query::P077_QUERY_KWH,    cf_pulses / pulsesPerKwh);
    setOutputValue(event, P077_query::P077_QUERY_PULSES, cf_pulses);

    const float voltage  = getValue(P077_query::P077_QUERY_VOLTAGE);
    const float current  = getValue(P077_query::P077_QUERY_CURRENT);
    const float active   = getValue(P077_query::P077_QUERY_ACTIVE_POWER);
    const float apparent = voltage * current;
    setOutputValue(event, P077_query::P077_QUERY_VA, apparent);

    float reactivePower{};

    if (definitelyGreaterThan(apparent, active)) {
      reactivePower = sqrtf(apparent * apparent - active * active);
    }
    setOutputValue(event, P077_query::P077_QUERY_REACTIVE_POWER, reactivePower);

    float powerfactor = 1.0f;

    if (!essentiallyZero(voltage) && !essentiallyZero(current)) {
      powerfactor = active / voltage / current;
    }
    setOutputValue(event, P077_query::P077_QUERY_PF, powerfactor);
  }


  return true;
}

bool P077_data_struct::processSerialData() {
  long t_start = millis();
  bool found   = false;

  if (isInitialized()) {
    int available = easySerial->available();

    while ((available > 0) && !found) {
      uint8_t serial_in_byte = easySerial->read();
      --available;
      count_bytes++;
      memmove(serial_in_buffer, serial_in_buffer + 1,
              sizeof(serial_in_buffer) - 1); // scroll buffer
      serial_in_buffer[23] = serial_in_byte; // add new data

      if ((serial_in_buffer[1] == 0x5A)      // Packet header2 = 5AH？
          && checksumMatch()) {
        count_pkt++;
        found = true;
      }
    }
    long t_diff = timePassedSince(t_start);

    t_all += t_diff;

    if (count_pkt > 10) { // bypass first 10 pkts
      t_max = max(t_max, t_diff);
    }

    if (found) {
      count_max = max(count_max, count_bytes);
      t_pkt     = t_start - t_pkt_tmp;
      t_pkt_tmp = t_start;
    }
  }

  return found;
}

bool P077_data_struct::checksumMatch() const
{
  uint8_t checksum = 0;

  // Sum of all data, except for 2 byte header and tail (checksum)
  for (uint8_t i = 2; i < 23; i++) {
    checksum += serial_in_buffer[i];
  }
  return checksum == serial_in_buffer[23];
}

bool P077_data_struct::plugin_read(struct EventStruct *event) {
  constexpr uint8_t nrElements = NR_ELEMENTS(_cache);

  for (uint8_t i = 0; i < P077_NR_OUTPUT_VALUES; ++i) {
    const uint8_t pconfigIndex = i + P077_QUERY1_CONFIG_POS;
    const uint8_t index        = static_cast<uint8_t>(PCONFIG(pconfigIndex));

    if (index < nrElements) {
      float value{};

      if (_cache[index].peek(value)) {
        UserVar.setFloat(event->TaskIndex, i, value);
      }
    }
  }

  for (uint8_t i = 0; i < nrElements; ++i) {
    _cache[i].resetKeepLast();
  }
  const bool res = newValue;

  newValue = false;
  return res;
}

/**
 * plugin_write: Handle commands
 * csereset: reset calibration values
 * csecalibrate,[voltage],[current],[power]: set calibration values, 0 value(s) will be skipped
 * Saves (all) settings if a calibration value is updated, or csereset is used
 */
bool P077_data_struct::plugin_write(struct EventStruct *event,
                                    String              string) {
  const String cmd = parseString(string, 1);
  bool success     = false;
  bool changed     = false;

  if (equals(cmd, F("csereset"))) { // Reset to defaults
    P077_UREF = CSE_UREF_PULSE;
    P077_IREF = CSE_IREF_PULSE;
    P077_PREF = CSE_PREF_PULSE;
    success   = true;
    changed   = true;
  } else if (equals(cmd, F("cseclearpulses"))) {
    // Clear the pulses count
    cf_pulses = 0;
    setOutputValue(event, P077_query::P077_QUERY_KWH,    cf_pulses);
    setOutputValue(event, P077_query::P077_QUERY_PULSES, cf_pulses);
    Scheduler.schedule_task_device_timer(event->TaskIndex, millis() + 10);
    success = true;
  } else if (equals(cmd, F("csecalibrate"))) { // Set 1 or more calibration values, 0 will skip that value
    success = true;
    float CalibVolt  = 0.0f;
    float CalibCurr  = 0.0f;
    float CalibAcPwr = 0.0f;

    if (validFloatFromString(parseString(string, 2), CalibVolt)) {
      if (validFloatFromString(parseString(string, 3), CalibCurr)) {
        validFloatFromString(parseString(string, 4), CalibAcPwr);
      }
    }

    const float Volt        = getValue(P077_query::P077_QUERY_VOLTAGE);
    const float ActivePower = getValue(P077_query::P077_QUERY_ACTIVE_POWER);
    const float Current     = getValue(P077_query::P077_QUERY_CURRENT);

    const bool applyCalibVolt = definitelyGreaterThan(CalibVolt, 0.0f)  && !essentiallyZero(Volt);
    const bool applyCalibCurr = definitelyGreaterThan(CalibCurr, 0.0f)  && !essentiallyZero(Current);
    const bool applyCalibPwr  = definitelyGreaterThan(CalibAcPwr, 0.0f) && !essentiallyZero(ActivePower);


    if (applyCalibVolt) {
      P077_UREF = static_cast<uint16_t>(static_cast<float>(P077_UREF) *
                                        (CalibVolt / Volt));
      changed = true;
    }

    if (applyCalibCurr) {
      P077_IREF = static_cast<uint16_t>(static_cast<float>(P077_IREF) *
                                        (CalibCurr / Current));
      changed = true;
    }

    if (applyCalibPwr) {
      P077_PREF = static_cast<uint16_t>(static_cast<float>(P077_PREF) *
                                        (CalibAcPwr / ActivePower));
      changed = true;
    } else if (changed) {
      // Force reload of factory calibration of pwr corrected with offset for voltage/current
      P077_PREF = CSE_PREF_PULSE;
    }
  }

  if (changed) {
    SaveSettings(); // FIXME tonhuisman: Doubting if by default the changes should be saved
  }
  return success;
}

# ifndef BUILD_NO_DEBUG
int P077_data_struct::serial_Available() {
  if (isInitialized()) {
    return easySerial->available();
  }
  return 0;
}

# endif // ifndef BUILD_NO_DEBUG

void P077_data_struct::setOutputValue(struct EventStruct *event, P077_query outputType, float value) {
  const uint8_t index          = static_cast<uint8_t>(outputType);
  constexpr uint8_t nrElements = NR_ELEMENTS(_cache);

  if (index < nrElements) {
    _cache[index].add(value);

    for (uint8_t i = 0; i < P077_NR_OUTPUT_VALUES; ++i) {
      const uint8_t pconfigIndex = i + P077_QUERY1_CONFIG_POS;

      if (PCONFIG(pconfigIndex) == index) {
# if FEATURE_PLUGIN_STATS
        PluginStats *stats = getPluginStats(i);

        if (stats != nullptr) {
          stats->trackPeak(value);
        }
# endif // if FEATURE_PLUGIN_STATS

        // Set preliminary averaged value as task value.
        // This way we can see intermediate updates.
        _cache[index].peek(value);
        UserVar.setFloat(event->TaskIndex, i, value);
      }
    }
  }
}

float P077_data_struct::getValue(P077_query outputType) const
{
  const uint8_t index          = static_cast<uint8_t>(outputType);
  constexpr uint8_t nrElements = NR_ELEMENTS(_cache);

  float res{};

  if (index < nrElements) {
    _cache[index].peek(res);
  }
  return res;
}

#endif // ifdef USES_P077

///////////////////////////////////////////////////////////////////////////////////////////////////
// P167 device class for IKEA Vindstyrka SEN54 and Sensirion SEN5x temperature, humidity and air quality sensors
// See datasheet https://sensirion.com/media/documents/6791EFA0/62A1F68F/Sensirion_Datasheet_Environmental_Node_SEN5x.pdf
// and info about extra request https://sensirion.com/media/documents/2B6FC1F3/6409E74A/PS_AN_Read_RHT_VOC_and_NOx_RAW_signals_D1.pdf
// Based upon code from Rob Tillaart, Viktor Balint, https://github.com/RobTillaart/SHT2x
// Rewritten and adapted for ESPeasy by andibaciu and tonhuisman
// changelog in _P167_Vindstyrka.ino
//////////////////////////////////////////////////////////////////////////////////////////////////

#include "../PluginStructs/P167_data_struct.h"
#include "../ESPEasyCore/ESPEasyGPIO.h"
#include "../Helpers/CRC_functions.h"

#include <GPIO_Direct_Access.h>

#ifdef USES_P167


# define P167_START_MEAS                           0x0021 // Start measurement command
# define P167_START_MEAS_RHT_GAS                   0x0037 // Start measurement RHT/Gas command
# define P167_STOP_MEAS                            0x0104 // Stop measurement command
# define P167_READ_DATA_RDY_FLAG                   0x0202 // Read Data Ready Flag command
# define P167_READ_MEAS                            0x03C4 // Read measurement command
# define P167_R_W_TEMP_COMP_PARAM                  0x60B2 // Read/Write Temperature Compensation Parameters command
# define P167_R_W_TWARM_START_PARAM                0x60C6 // Read/Write Warm Start Parameters command
# define P167_R_W_VOC_ALG_PARAM                    0x60D0 // Read/Write VOC Algorithm Tuning Parameters command
# define P167_R_W_NOX_ALG_PARAM                    0x60E1 // Read/Write NOx Algorithm Tuning Parameters command
# define P167_R_W_RH_T_ACC_Mode                    0x60F7 // Read/Write RH/T Acceleration Mode command
# define P167_R_W_VOC_ALG_STATE                    0x6181 // Read/Write VOC Algorithm State command
# define P167_START_FAN_CLEAN                      0x5607 // Start fan cleaning command
# define P167_R_W_AUTOCLEN_PARAM                   0x8004 // Read/Write Autocleaning Interval Parameters command
# define P167_READ_PROD_NAME                       0xD014 // Read Product Name command
# define P167_READ_SERIAL_NO                       0xD033 // Read Serial Number command
# define P167_READ_FIRM_VER                        0xD100 // Read Firmware Version command
# define P167_READ_DEVICE_STATUS                   0xD206 // Read Device Status command
# define P167_CLEAR_DEVICE_STATUS                  0xD210 // Clear Device Status command
# define P167_RESET_DEVICE                         0xD304 // Reset Device command
# define P167_READ_RAW_MEAS                        0x03D2 // Read relative humidity and temperature
                                                          // which are not compensated for temperature offset, and the
                                                          // VOC and NOx raw signals (proportional to the logarithm of the
                                                          // resistance of the MOX layer). It returns 4x2 bytes (+ 1 CRC
                                                          // byte each) command (see second datasheet fron header for more info)
# define P167_READ_RAW_MYS_MEAS                    0x03F5 // Read relative humidity and temperature and MYSTERY word (probably signed offset
                                                          // temperature)


# define P167_START_MEAS_DELAY                     50     // Timeout value for start measurement command [ms]
# define P167_START_MEAS_RHT_GAS_DELAY             50     // Timeout value for start measurement RHT/Gas command [ms]
# define P167_STOP_MEAS_DELAY                      200    // Timeout value for start measurement command [ms]
# define P167_READ_DATA_RDY_FLAG_DELAY             20     // Timeout value for read data ready flag command [ms]
# define P167_READ_MEAS_DELAY                      20     // Timeout value for read measurement command [ms]
# define P167_R_W_TEMP_COMP_PARAM_DELAY            20     // Timeout value for read/write temperature compensation parameters command [ms]
# define P167_R_W_WARM_START_PARAM_DELAY           20     // Timeout value for read/write warm start parameters command [ms]
# define P167_R_W_VOC_ALG_PARAM_DELAY              20     // Timeout value for read/write VOC algorithm tuning parameters command [ms]
# define P167_R_W_NOX_ALG_PARAM_DELAY              20     // Timeout value for read/write NOx algorithm tuning parameters command [ms]
# define P167_R_W_RH_T_ACC_MODE_DELAY              20     // Timeout value for read/write RH/T acceleration mode command [ms]
# define P167_R_W_VOC_ALG_STATE_DELAY              20     // Timeout value for read/write VOC algorithm State command [ms]
# define P167_START_FAN_CLEAN_DELAY                20     // Timeout value for start fan cleaning command [ms]
# define P167_R_W_AUTOCLEN_PARAM_DELAY             20     // Timeout value for read/write autoclean interval parameters command [ms]
# define P167_READ_PROD_NAME_DELAY                 20     // Timeout value for read product name command [ms]
# define P167_READ_SERIAL_NO_DELAY                 20     // Timeout value for read serial number command [ms]
# define P167_READ_FIRM_VER_DELAY                  20     // Timeout value for read firmware version command [ms]
# define P167_READ_DEVICE_STATUS_DELAY             20     // Timeout value for read device status command [ms]
# define P167_CLEAR_DEVICE_STATUS_DELAY            20     // Timeout value for clear device status command [ms]
# define P167_RESET_DEVICE_DELAY                   100    // Timeout value for reset device command [ms]
# define P167_READ_RAW_MEAS_DELAY                  20     // Timeout value for read raw temp and humidity command [ms]

# define P167_MAX_RETRY                            250    // Give up after amount of retries befoe going to error


const __FlashStringHelper* toString(P167_model model) {
  switch (model) {
    case P167_model::Vindstyrka: return F("IKEA Vindstyrka");
    case P167_model::SEN54: return F("Sensirion SEN54");
    case P167_model::SEN55: return F("Sensirion SEN55");
  }
  return F("");
}

/// @brief
/// @param query
/// @return
const __FlashStringHelper* P167_getQueryString(uint8_t query) {
  switch (query) {
    case 0: return F("Temperature (C)");
    case 1: return F("Humidity (% RH)");
    case 2: return F("tVOC (VOC index)");
    case 3: return F("NOx (NOx index)");
    case 4: return F("PM 1.0 (ug/m3)");
    case 5: return F("PM 2.5 (ug/m3)");
    case 6: return F("PM 4.0 (ug/m3)");
    case 7: return F("PM 10.0 (ug/m3)");
    case 8: return F("DewPoint (C)");
  }
  return F("");
}

/// @brief
/// @param query
/// @return
const __FlashStringHelper* P167_getQueryValueString(uint8_t query) {
  switch (query) {
    case 0: return F("Temperature");
    case 1: return F("Humidity");
    case 2: return F("tVOC");
    case 3: return F("NOx");
    case 4: return F("PM1p0");
    case 5: return F("PM2p5");
    case 6: return F("PM4p0");
    case 7: return F("PM10p0");
    case 8: return F("DewPoint");
  }
  return F("");
}

# if FEATURE_MQTT_DISCOVER
int Plugin_167_QueryVType(uint8_t value_nr) {
  Sensor_VType result = Sensor_VType::SENSOR_TYPE_NONE;

  switch (value_nr) {
    case 0: // fall through
    case 8: result = Sensor_VType::SENSOR_TYPE_TEMP_ONLY; break;
    case 1: result = Sensor_VType::SENSOR_TYPE_HUM_ONLY; break;
    case 2: result = Sensor_VType::SENSOR_TYPE_TVOC_ONLY; break;
    case 3: result = Sensor_VType::SENSOR_TYPE_NONE; break; // FIXME
    case 4: result = Sensor_VType::SENSOR_TYPE_DUSTPM1_0_ONLY; break;
    case 5: result = Sensor_VType::SENSOR_TYPE_DUSTPM2_5_ONLY; break;
    case 6: result = Sensor_VType::SENSOR_TYPE_NONE; break; // FIXME
    case 7: result = Sensor_VType::SENSOR_TYPE_DUSTPM10_ONLY; break;
  }
  return static_cast<int>(result);
}

# endif // if FEATURE_MQTT_DISCOVER

//////////////////////////////////////////////////////////////////////////////////////////////////
//
//  PUBLIC
//
P167_data_struct::P167_data_struct() {
  //
}

P167_data_struct::~P167_data_struct() {
  //
}

// Initialize/setup device properties
// Must be called at least once before oP167::Wairperating the device
bool P167_data_struct::setupDevice(uint8_t i2caddr) {
  _i2caddr = i2caddr;

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, strformat(F("SEN5x: Setup with address= 0x%02x"), _i2caddr));
  }
  return true;
}

bool P167_data_struct::setupMonPin(int16_t monpin) {
  if (validGpio(monpin)) {
    _monpin = monpin;
    pinMode(_monpin, INPUT_PULLUP); // declare monitoring pin as input with pullup's
    attachInterruptArg(digitalPinToInterrupt(_monpin),
                       reinterpret_cast<void (*)(void *)>(Plugin_167_interrupt),
                       this,
                       RISING);

    # ifdef PLUGIN_167_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO, strformat(F("SEN5x: Setup I2C SCL monpin= %d"), _monpin));
    }
    # endif // ifdef PLUGIN_167_DEBUG
    return true;
  }
  return false;
}

void P167_data_struct::disableInterrupt_monpin(void) {
  detachInterrupt(digitalPinToInterrupt(_monpin));
}

// Initialize/setup device properties
// Must be called at least once before operating the device
bool P167_data_struct::setupModel(P167_model model) {
  _model = model;

  # ifdef PLUGIN_167_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, strformat(F("SEN5x: Setup model= %s"), String(toString(_model)).c_str()));
  }
  # endif // ifdef PLUGIN_167_DEBUG
  return true;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Evaluate FSM for data acquisition
// This is a state machine that is evaluated step by step by calling update() repetatively
// NOTE: Function is expected to run as critical section w.r.t. other provided functions
//       This is typically met in ESPeasy plugin context when called from within the plugin
bool P167_data_struct::update() {
  bool stable = false; // signals when a stable state is reached

  # ifdef PLUGIN_167_DEBUG
  P167_state oldState = _state;
  # endif // ifdef PLUGIN_167_DEBUG


  if (!statusMonitoring) {
    return stable;
  }


  switch (_state) {
    case P167_state::Uninitialized:

      // we have to stop trying after a while
      if (_errCount > P167_MAX_RETRY) {
        _state = P167_state::Error;
        stable = true;
      } else if (I2C_wakeup(_i2caddr) != 0) { // Try to access the I2C device
        if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
          addLog(LOG_LEVEL_ERROR, strformat(F("SEN5x: Not found at I2C address: 0x%02x"), _i2caddr));
        }
        _errCount++;
      } else if (_model == P167_model::Vindstyrka) {            // sensor is Vindstyrka and don't need to be reset
        _errCount = 0;                                          // Device is reachable and initialized, reset error counter

        if (writeCmd(P167_READ_FIRM_VER)) {                     // Issue a reset command
          _state               = P167_state::Read_firm_version; // Will take <20ms according to datasheet
          _last_action_started = millis();
        }
      }
      else if ((_model == P167_model::SEN54) || (_model == P167_model::SEN55)) {
        _errCount = 0;                                       // Device is reachable and initialized, reset error counter

        if (writeCmd(P167_RESET_DEVICE)) {                   // Issue a reset command
          _state               = P167_state::Wait_for_reset; // Will take <20ms according to datasheet
          _last_action_started = millis();
        }
      }
      break;

    case P167_state::Wait_for_reset:

      if (timeOutReached(_last_action_started + P167_RESET_DEVICE_DELAY)) { // we need to wait for the chip to reset
        if (I2C_wakeup(_i2caddr) != 0) {
          _errCount++;
          _state = P167_state::Uninitialized;                               // Retry
        } else {
          _errCount = 0;                                                    // Device is reachable and initialized, reset error counter

          if (writeCmd(P167_READ_FIRM_VER)) {
            _state               = P167_state::Read_firm_version;           // Will take <20ms according to datasheet
            _last_action_started = millis();
          }
        }
      }
      break;

    case P167_state::Read_firm_version:

      if (timeOutReached(_last_action_started + P167_READ_FIRM_VER_DELAY)) {
        // Start read flag
        if (!getFirmwareVersion()) {
          _errCount++;
          _state = P167_state::Uninitialized; // Retry
        } else if (!writeCmd(P167_READ_PROD_NAME)) {
          _errCount++;
          _state = P167_state::Uninitialized; // Retry
        } else {
          _last_action_started = millis();
          _state               = P167_state::Read_prod_name;
        }
      }
      break;

    case P167_state::Read_prod_name:

      if (timeOutReached(_last_action_started + P167_READ_PROD_NAME_DELAY)) {
        // Start read flag
        if (!getProductName()) {
          _errCount++;
          _state = P167_state::Uninitialized; // Retry
        } else if (!writeCmd(P167_READ_SERIAL_NO)) {
          _errCount++;
          _state = P167_state::Uninitialized; // Retry
        } else {
          _last_action_started = millis();
          _state               = P167_state::Read_serial_no;
        }
      }
      break;

    case P167_state::Read_serial_no:

      if (timeOutReached(_last_action_started + P167_READ_SERIAL_NO_DELAY)) {
        // Start read flag
        if (!getSerialNumber()) {
          _errCount++;
          _state = P167_state::Uninitialized;        // Retry
        } else if (!writeCmd(P167_READ_SERIAL_NO)) { // Read serialno again?
          _errCount++;
          _state = P167_state::Uninitialized;        // Retry
        } else {
          _last_action_started = millis();
          _state               = P167_state::Initialized;
        }
      }
      break;

    case P167_state::Write_user_reg:
      _state = P167_state::Initialized;
      break;

    case P167_state::Initialized:

      // Trigger the first read cycle automatically on regular SEN5x
      if (_model != P167_model::Vindstyrka) {
        _state = P167_state::Ready;
      }
      break;

    case P167_state::Ready:

      // Ready to execute a measurement cycle, for Vindstyrka we're eavesdropping so no command needed?
      if ((_model == P167_model::Vindstyrka) ||  (_model == P167_model::SEN54) || (_model == P167_model::SEN55)) {
        // Start measuring data
        if (!writeCmd(P167_START_MEAS)) {
          _errCount++;
          _state = P167_state::Uninitialized; // Retry
        } else {
          _last_action_started = millis();
          _state               = P167_state::Wait_for_start_meas;
        }
      }
      break;

    case P167_state::Wait_for_start_meas:

      if (timeOutReached(_last_action_started + P167_START_MEAS_DELAY)) {
        // Start read flag
        if (!writeCmd(P167_READ_DATA_RDY_FLAG)) {
          _errCount++;
          _state = P167_state::Uninitialized; // Retry
        } else {
          _last_action_started = millis();
          _state               = P167_state::Wait_for_read_flag;
        }
      }
      break;

    case P167_state::Wait_for_read_flag:

      if (timeOutReached(_last_action_started + P167_READ_DATA_RDY_FLAG_DELAY)) {
        if (readDataRdyFlag()) {
          // Ready to execute a measurement cycle
          if (!writeCmd(P167_READ_MEAS)) {
            _errCount++;
            _state = P167_state::Uninitialized; // Retry
          } else {
            _last_action_started = millis();
            _state               = P167_state::Wait_for_read_meas;
          }
        } else { // Ready Flag NOT ok, so send again Start Measurement
          // Start measuring data
          if (!writeCmd(P167_START_MEAS)) {
            _errCount++;
            _state = P167_state::Uninitialized; // Retry
          } else {
            _last_action_started = millis();
            _state               = P167_state::Wait_for_start_meas;
          }
        }
      }
      break;

    case P167_state::Wait_for_read_meas:

      if (timeOutReached(_last_action_started + P167_READ_MEAS_DELAY)) {
        if (!readMeasValue()) { // Read the previously measured temperature
          _errCount++;

          // _state = P167_state::Uninitialized; // Lost connection
          _state = P167_state::cmdSTARTmeas;
        } else {
          if (!writeCmd(P167_READ_RAW_MEAS)) {
            _errCount++;
            _state = P167_state::Uninitialized; // Retry
          } else {
            _last_action_started = millis();
            _state               = P167_state::Wait_for_read_raw_meas;
          }
        }
      }
      break;

    case P167_state::Wait_for_read_raw_meas:

      // make sure we wait for the measurement to complete
      if (timeOutReached(_last_action_started + P167_READ_RAW_MEAS_DELAY)) {
        if (!readMeasRawValue()) {
          _errCount++;

          // _state = P167_state::Uninitialized; // Lost connection
          _state = P167_state::cmdSTARTmeas;
        } else {
          if (!writeCmd(P167_READ_RAW_MYS_MEAS)) {
            _errCount++;
            _state = P167_state::Uninitialized; // Retry
          } else {
            _last_action_started = millis();
            _state               = P167_state::Wait_for_read_raw_MYS_meas;
          }
        }
      }
      break;

    case P167_state::Wait_for_read_raw_MYS_meas:

      // make sure we wait for the measurement to complete
      if (timeOutReached(_last_action_started + P167_READ_RAW_MEAS_DELAY)) {
        if (!readMeasRawMYSValue()) {
          _errCount++;

          // _state = P167_state::Uninitialized; // Lost connection
          _state = P167_state::cmdSTARTmeas;
        } else {
          if (!writeCmd(P167_READ_DEVICE_STATUS)) {
            _errCount++;
            _state = P167_state::Uninitialized; // Retry
          } else {
            _last_action_started = millis();
            _state               = P167_state::Wait_for_read_status;
          }
          calculateValue();
          stable = true;
        }
      }
      break;

    case P167_state::Wait_for_read_status:

      // make sure we wait for the measurement to complete
      if (timeOutReached(_last_action_started + P167_READ_DEVICE_STATUS_DELAY)) {
        if (!readDeviceStatus()) {
          _errCount++;

          _state = P167_state::cmdSTARTmeas;
        } else {
          _last_action_started = millis();
          _state               = P167_state::cmdSTARTmeas;
          stable               = true;
        }
      }
      break;

    case P167_state::cmdSTARTmeas:

      // Start measuring data
      if (_model == P167_model::Vindstyrka) {
        if (!writeCmd(P167_START_MEAS)) {
          _errCount++;
          _state = P167_state::Uninitialized; // Retry
        } else {
          _last_action_started = millis();
          _state               = P167_state::IDLE;
        }
      } else {
        _state = P167_state::IDLE;
      }
      break;

    case P167_state::IDLE:
      stepMonitoring      = 1;
      startMonitoringFlag = false;

      if (!_errmeas && !_errmeasraw && !_errmeasrawmys) {
        _state = P167_state::New_Values_Available;
      }
      stable = true;
      break;

    case P167_state::Error:
    case P167_state::New_Values_Available:
      // this state is used outside so all we need is to stay here
      stable = true;
      break;

      // Missing states (enum values) to be checked by the compiler
  } // switch

  # ifdef PLUGIN_167_DEBUG

  if (_state != oldState) {
    if (loglevelActiveFor(LOG_LEVEL_INFO) && _enableLogging) {
      addLog(LOG_LEVEL_INFO, strformat(F("SEN5x: State transition %d-->%d"), static_cast<int>(oldState), static_cast<int>(_state)));
    }
  }
  # endif // ifdef PLUGIN_167_DEBUG
  return stable;
}

bool P167_data_struct::monitorSCL() {
  if (_model == P167_model::Vindstyrka) {
    if (startMonitoringFlag) {
      if (stepMonitoring == 1) {
        lastSCLLowTransitionMonitoringTime = monpinLastTransitionTime / 1000;

        if (millis() - lastSCLLowTransitionMonitoringTime < 100) {
          statusMonitoring = false;
          return true;
        } else {
          lastSCLLowTransitionMonitoringTime = monpinLastTransitionTime / 1000;
          statusMonitoring                   = true;
          stepMonitoring++;
        }
      }

      if (stepMonitoring == 2) {
        if (millis() - lastSCLLowTransitionMonitoringTime < 100) {
          lastSCLLowTransitionMonitoringTime = monpinLastTransitionTime / 1000;
          statusMonitoring                   = false;
          stepMonitoring                     = 1;
          return true;
        } else if (millis() - lastSCLLowTransitionMonitoringTime > 700) {
          statusMonitoring    = false;
          stepMonitoring      = 1;
          startMonitoringFlag = false;

          // if _state not finish reading process then start from begining
          if ((_state >= P167_state::Wait_for_read_meas) && (_state < P167_state::New_Values_Available)) {
            _state = P167_state::Ready;
          }
          return true;
        } else {
          // processing
        }
      }
    }
    monpinValuelast = monpinValue;
  }

  if ((_model == P167_model::SEN54) || (_model == P167_model::SEN55)) {
    statusMonitoring    = true;
    startMonitoringFlag = false;
    stepMonitoring      = 0;
  }

  return true;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Returns the I2C connection state
// Note: based upon the FSM state without actual accessing the device
bool P167_data_struct::isConnected() const {
  switch (_state) {
    case  P167_state::Initialized:
    case  P167_state::Ready:
    case  P167_state::Wait_for_start_meas:
    case  P167_state::Wait_for_read_flag:
    case  P167_state::Wait_for_read_meas:
    case  P167_state::Wait_for_read_raw_meas:
    case  P167_state::Wait_for_read_raw_MYS_meas:
    case  P167_state::Wait_for_read_status:
    case  P167_state::cmdSTARTmeas:
    case  P167_state::New_Values_Available:
    case  P167_state::Read_firm_version:
    case  P167_state::Read_prod_name:
    case  P167_state::Read_serial_no:
    case  P167_state::Write_user_reg:
    case  P167_state::IDLE:
      return true;
      break;
    case  P167_state::Uninitialized:
    case  P167_state::Error:
    case  P167_state::Wait_for_reset:
      return false;
      break;

      // Missing states (enum values) to be checked by the compiler
  }
  return false;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Returns if the device communication is in error
// Note: based upon the FSM state without actual accessing the device
bool P167_data_struct::inError() const {
  return _state == P167_state::Error;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Returns if new acquired values are available
bool P167_data_struct::newValues() const {
  return _state == P167_state::New_Values_Available;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Restart the FSM used to access the device
bool P167_data_struct::reset() {
  startMonitoringFlag = true;
  stepMonitoring      = 1;
  _state              = P167_state::Uninitialized;
  return true;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Start a new measurement cycle
bool P167_data_struct::startMeasurements() {
  if ((_state == P167_state::New_Values_Available) ||
      (_state == P167_state::Initialized)  ||
      (_state == P167_state::IDLE)) {
    _state = P167_state::Ready;
  }
  startMonitoringFlag = true;
  stepMonitoring      = 1;
  return true;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Get the electronic idenfification data store in the device
// Note: The data is read from the device during initialization
bool P167_data_struct::getEID(String& eid_productname, String& eid_serialnumber, uint8_t& firmware) const {
  eid_productname  = _eid_productname;
  eid_serialnumber = _eid_serialnumber;
  firmware         = _firmware;
  return true;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Get the status informasion about different part of the sensor
// Note: The data is read from the device after every measurement read request
bool P167_data_struct::getStatusInfo(P167_statusinfo param) {
  switch (param) {
    case P167_statusinfo::sensor_speed:
      return _devicestatus.speed;

    case P167_statusinfo::sensor_autoclean:
      return _devicestatus.autoclean;

    case P167_statusinfo::sensor_gas:
      return _devicestatus.gas;

    case P167_statusinfo::sensor_rht:
      return _devicestatus.rht;

    case P167_statusinfo::sensor_laser:
      return _devicestatus.laser;

    case P167_statusinfo::sensor_fan:
      return _devicestatus.fan;
  }
  return true;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Return the previously measured raw humidity data [bits]
float P167_data_struct::getRequestedValue(uint8_t request) const {
  switch (request) {
    case 0:
    {
      if (_model == P167_model::Vindstyrka) {
        return _TemperatureX;
      } else {
        return _Temperature;
      }
    }
    case 1:
    {
      if (_model == P167_model::Vindstyrka) {
        return _HumidityX;
      } else {
        return _Humidity;
      }
    }
    case 2:   return _tVOC;
    case 3:   return _NOx;
    case 4:   return _PM1p0;
    case 5:   return _PM2p5;
    case 6:   return _PM4p0;
    case 7:   return _PM10p0;
    case 8:   return _DewPoint;
  }
  return -1.0f;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
//
//  PROTECTED
//
//////////////////////////////////////////////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////////////////////////////////////////////
// bool P167_data_struct::writeCmd(uint8_t cmd)
// {
//  return I2C_write8(_i2caddr, cmd);
// }

//////////////////////////////////////////////////////////////////////////////////////////////////
bool P167_data_struct::writeCmd(uint16_t cmd)
{
  Wire.beginTransmission(_i2caddr);
  Wire.write((uint8_t)(cmd >> 8));
  Wire.write((uint8_t)cmd);
  return Wire.endTransmission() == 0;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
bool P167_data_struct::writeCmd(uint16_t cmd, uint8_t value) {
  Wire.beginTransmission(_i2caddr);
  Wire.write((uint8_t)(cmd >> 8));
  Wire.write((uint8_t)cmd);
  Wire.write((uint8_t)value);
  return Wire.endTransmission() == 0;
}

bool P167_data_struct::writeCmd(uint16_t cmd, uint8_t length, uint8_t *buffer) {
  Wire.beginTransmission(_i2caddr);
  Wire.write((uint8_t)(cmd >> 8));
  Wire.write((uint8_t)cmd);

  for (int i = 0; i < length; ++i) {
    Wire.write(*(buffer + i));
  }
  return Wire.endTransmission() == 0;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
bool P167_data_struct::readBytes(uint8_t n, uint8_t *val, uint8_t maxDuration) {
  // TODO check if part can be delegated to the I2C_access libraray from ESPeasy
  Wire.requestFrom(_i2caddr, (uint8_t)n);
  uint32_t start = millis();

  while (Wire.available() < n) {
    if (timePassedSince(start) > maxDuration) {
      return false;
    }
    yield();
  }

  for (uint8_t i = 0; i < n; i++) {
    val[i] = Wire.read();
  }
  return true;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Read data ready flag from device
bool P167_data_struct::readDataRdyFlag() {
  uint8_t value = 0;
  uint8_t buffer[3];

  if (!readBytes(3, (uint8_t *)&buffer[0], P167_READ_DATA_RDY_FLAG_DELAY)) {
    return false;
  }

  if (calc_CRC8(&buffer[0], 2) == buffer[2]) {
    value += buffer[1];
  }
  return value;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Read measurement values results from device
bool P167_data_struct::readMeasValue() {
  uint16_t value     = 0;
  int16_t  valuesign = 0;
  uint8_t  buffer[24];
  bool     condition = true;

  _errmeas = false;

  if (!readBytes(24, (uint8_t *)&buffer[0], P167_READ_MEAS_DELAY)) {
    _errmeas = true;
    return false;
  }

  String log = F("SEN5x: Measured value ");

  for (int xx = 0; xx < 24; ++xx) {
    log += buffer[xx];
    log += ' ';
  }

  if ((_model == P167_model::Vindstyrka) || (_model == P167_model::SEN54)) {
    condition = (buffer[0] == 0xFF && buffer[1] == 0xFF) || (buffer[3] == 0xFF && buffer[4] == 0xFF) ||
                (buffer[6] == 0xFF && buffer[7] == 0xFF) || (buffer[9] == 0xFF && buffer[10] == 0xFF) ||
                (buffer[12] == 0xFF && buffer[13] == 0xFF) ||
                (buffer[15] == 0xFF && buffer[16] == 0xFF) || (buffer[18] == 0xFF && buffer[19] == 0xFF);
  }

  if (_model == P167_model::SEN55) {
    condition = (buffer[0] == 0xFF && buffer[1] == 0xFF) || (buffer[3] == 0xFF && buffer[4] == 0xFF) ||
                (buffer[6] == 0xFF && buffer[7] == 0xFF) || (buffer[9] == 0xFF && buffer[10] == 0xFF) ||
                (buffer[12] == 0xFF && buffer[13] == 0xFF) ||
                (buffer[15] == 0xFF && buffer[16] == 0xFF) || (buffer[18] == 0xFF && buffer[19] == 0xFF) ||
                (buffer[21] == 0xFF && buffer[22] == 0xFF);
  }

  if (condition) {
    log += F("- error");

    if (_enableLogging) {
      addLog(LOG_LEVEL_ERROR, log);
    }
    _errmeas = true;
    _readingerrcount++;
    return false;
  } else {
    for (int xx = 0; xx < 8; ++xx) {
      if ((calc_CRC8(&buffer[xx * 3], 2) == buffer[xx * 3 + 2]) && ((buffer[xx * 3] != 0xFF) || (buffer[xx * 3 + 1] != 0xFF))) {
        value      = buffer[xx * 3] << 8;
        value     += buffer[xx * 3 + 1];
        valuesign  = buffer[xx * 3] << 8;
        valuesign += buffer[xx * 3 + 1];

        if (xx == 0) {
          _PM1p0 = value / 10.0f;
        }

        if (xx == 1) {
          _PM2p5 = value / 10.0f;
        }

        if (xx == 2) {
          _PM4p0 = value / 10.0f;
        }

        if (xx == 3) {
          _PM10p0 = value / 10.0;
        }

        if (xx == 4) {
          _Humidity = valuesign / 100.0f;
        }

        if (xx == 5) {
          _Temperature = valuesign / 200.0f;
        }

        if (xx == 6) {
          _tVOC = valuesign / 10.0f;
        }

        if (xx == 7)
        {
          if (_model == P167_model::SEN55) {
            _NOx = valuesign / 10.0f;
          } else {
            _NOx = 0.0f;
          }
        }
      } else {
        _errmeasrawmys = true;
      }
    }

    if (_errmeas) {
      log += F("- crc error");
      _readingerrcount++;
    } else {
      log += F("- pass");
      _readingsuccesscount++;

      if (_enableLogging) {
        addLog(LOG_LEVEL_INFO, log);
      }
      return !_errmeas;
    }
  }

  return true;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Read measurement values results from device
bool P167_data_struct::readMeasRawValue() {
  uint16_t value     = 0;
  int16_t  valuesign = 0;
  uint8_t  buffer[12];
  bool     condition = true;

  _errmeasraw = false;

  if (!readBytes(12, &buffer[0], P167_READ_RAW_MEAS_DELAY)) {
    _errmeasraw = true;
    return false;
  }

  String log = F("SEN5x: Measured RAW value 0x");

  for (int xx = 0; xx < 12; xx++) {
    log += strformat(F("%02x "), buffer[xx]);
  }

  if ((_model == P167_model::Vindstyrka) || (_model == P167_model::SEN54)) {
    condition = (buffer[0] == 0xFF && buffer[1] == 0xFF) || (buffer[3] == 0xFF && buffer[4] == 0xFF) ||
                (buffer[6] == 0xFF && buffer[7] == 0xFF); // || (buffer[9] == 0xFF && buffer[10] == 0xFF))
  }

  if (_model == P167_model::SEN55) {
    condition = (buffer[0] == 0xFF && buffer[1] == 0xFF) || (buffer[3] == 0xFF && buffer[4] == 0xFF) ||
                (buffer[6] == 0xFF && buffer[7] == 0xFF) || (buffer[9] == 0xFF && buffer[10] == 0xFF);
  }

  if (condition) {
    log += F("- error");

    if (_enableLogging) {
      addLog(LOG_LEVEL_ERROR, log);
    }
    _errmeasraw = true;
    _readingerrcount++;
    return false;
  } else {
    for (int xx = 0; xx < 4; ++xx) {
      if ((calc_CRC8(&buffer[xx * 3], 2) == buffer[xx * 3 + 2]) && ((buffer[xx * 3] != 0xFF) || (buffer[xx * 3 + 1] != 0xFF))) {
        value      = buffer[xx * 3] << 8;
        value     += buffer[xx * 3 + 1];
        valuesign  = buffer[xx * 3] << 8;
        valuesign += buffer[xx * 3 + 1];

        if (xx == 0) {
          _rawHumidity = valuesign / 100.0f;
        } else

        if (xx == 1) {
          _rawTemperature = valuesign / 200.0f;
        } else

        if (xx == 2) {
          _rawtVOC = value / 10.0f;
        } else

        if (xx == 3) {
          if (_model == P167_model::SEN55) {
            _rawNOx = value / 10.0f;
          } else {
            _rawNOx = 0.0f;
          }
        }
      } else {
        _errmeasrawmys = true;
      }
    }

    if (_errmeasraw) {
      log += F("- crc error");
      _readingerrcount++;
    } else {
      log += F("- pass");
      _readingsuccesscount++;
    }

    if (_enableLogging) {
      addLog(LOG_LEVEL_INFO, log);
    }
    return !_errmeasraw;
  }


  return true;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Read measurement values results from device
bool P167_data_struct::readMeasRawMYSValue() {
  int16_t valuesign = 0;
  uint8_t buffer[9];

  _errmeasrawmys = false;

  if (!readBytes(9, (uint8_t *)&buffer[0], P167_READ_RAW_MEAS_DELAY)) {
    _errmeasrawmys = true;
    return false;
  }

  String log = F("SEN5x: Measured MYS value 0x");

  for (int xx = 0; xx < 9; ++xx) {
    log += strformat(F("%02x "), buffer[xx]);
  }

  if (((buffer[0] == 0xFF) && (buffer[1] == 0xFF)) ||
      ((buffer[3] == 0xFF) && (buffer[4] == 0xFF)) ||
      ((buffer[6] == 0xFF) && (buffer[7] == 0xFF))) {
    log += F("- error");

    if (_enableLogging) {
      addLog(LOG_LEVEL_ERROR, log);
    }
    _errmeasrawmys = true;
    _readingerrcount++;
    return false;
  } else {
    for (int xx = 0; xx < 3; ++xx) {
      if ((calc_CRC8(&buffer[xx * 3], 2) == buffer[xx * 3 + 2]) && ((buffer[xx * 3] != 0xFF) || (buffer[xx * 3 + 1] != 0xFF))) {
        valuesign  = buffer[xx * 3] << 8;
        valuesign += buffer[xx * 3 + 1];

        if (xx == 0) {
          _mysHumidity = valuesign / 100.0f;
        } else

        if (xx == 1) {
          _mysTemperature = valuesign / 200.0f;
        } else

        if (xx == 2) {
          _mysOffset = valuesign / 200.0f;
        }
      } else {
        _errmeasrawmys = true;
      }
    }

    if (_errmeasrawmys) {
      log += F("- crc error");
      _readingerrcount++;
    } else {
      log += F("- pass");
      _readingsuccesscount++;
    }

    if (_enableLogging) {
      addLog(LOG_LEVEL_INFO, log);
    }
    return !_errmeasrawmys;
  }


  return true;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
//  Calculate DewPoint, F Temp, F HUM
bool P167_data_struct::calculateValue() {
  if (_model == P167_model::Vindstyrka) {
    _TemperatureX = _mysTemperature + _mysOffset - 2.4f; // (2.4 - temperature offset because enclosure and esp8266 power disipation)


    // version formula with interpolation
    _HumidityX = _Humidity + (_TemperatureX - _Temperature) * ((_rawHumidity - _Humidity) / (_rawTemperature - _Temperature));

    _DewPoint = compute_dew_point_temp(_TemperatureX, _HumidityX);

    if (_HumidityX < 0.0f) {
      _HumidityX = 0.0f;
    }

    if (_HumidityX > 100.0f) {
      _HumidityX = 100.0f;
    }
  } else {
    _DewPoint = compute_dew_point_temp(_Temperature, _Humidity);
  }

  return true;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
//  Retrieve SEN5x identification code
//  Sensirion_SEN5x
bool P167_data_struct::getProductName() {
  String  prodname;
  uint8_t buffer[48];

  // writeCmd(P167_READ_PROD_NAME);
  if (!readBytes(48, (uint8_t *)buffer, P167_READ_PROD_NAME_DELAY)) {
    return false;
  }

  for (uint8_t i = 1; i <= 16; ++i) {
    if (calc_CRC8(&buffer[i * 3 - 3], 2) == buffer[i * 3 - 1]) {
      if (buffer[i * 3 - 3] < 32) {
        break;
      }
      prodname += char(buffer[i * 3 - 3]);

      if (buffer[i * 3 - 2] < 32) {
        break;
      }
      prodname += char(buffer[i * 3 - 2]);
    }
  }
  _eid_productname = prodname;

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, concat(F("SEN5x: Product name: "), prodname));
  }

  return true;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
//  Retrieve SEN54 Serial Number
bool P167_data_struct::getSerialNumber() {
  String  serno;
  uint8_t buffer[48];

  // writeCmd(P167_READ_SERIAL_NO);
  if (!readBytes(48, (uint8_t *)buffer, P167_READ_SERIAL_NO_DELAY)) {
    return false;
  }

  for (uint8_t i = 1; i <= 16; ++i) {
    if (calc_CRC8(&buffer[i * 3 - 3], 2) == buffer[i * 3 - 1]) {
      if (buffer[i * 3 - 3] < 32) {
        break;
      }
      serno += char(buffer[i * 3 - 3]);

      if (buffer[i * 3 - 2] < 32) {
        break;
      }
      serno += char(buffer[i * 3 - 2]);
    }
  }
  _eid_serialnumber = serno;

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, concat(F("SEN5x: Serial number: "), serno));
  }

  return true;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Retrieve SEN54 Firmware version from device
bool P167_data_struct::getFirmwareVersion() {
  uint8_t version = 0;
  uint8_t read_data[3];

  // writeCmd(P167_READ_FIRM_VER);
  if (!readBytes(3, (uint8_t *)&read_data, P167_READ_FIRM_VER_DELAY)) {
    return false;
  }

  if (read_data[2] == calc_CRC8(&read_data[0], 2)) {
    version = read_data[0];
  }
  _firmware = version;

  addLog(LOG_LEVEL_INFO, strformat(F("SEN5x: Firmware version: %d"), version));

  return true;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Retrieve SEN54 Device Status from device
bool P167_data_struct::readDeviceStatus() {
  uint32_t value = 0;
  uint8_t  bufferstatus[6];

  // writeCmd(P167_READ_FIRM_VER);
  _errdevicestatus = false;

  if (!readBytes(6, (uint8_t *)&bufferstatus, P167_READ_DEVICE_STATUS_DELAY)) {
    _errdevicestatus = true;
    return false;
  }

  String log = F("SEN5x: device status 0x");

  for (int xx = 0; xx < 6; ++xx) {
    log += strformat(F("%02x "), bufferstatus[xx]);
  }

  if ((calc_CRC8(&bufferstatus[0], 2) == bufferstatus[2]) && (calc_CRC8(&bufferstatus[3], 2) == bufferstatus[5])) {
    value             = bufferstatus[0] << 24;
    value            += bufferstatus[1] << 16;
    value            += bufferstatus[3] << 8;
    value            += bufferstatus[4] << 0;
    _devicestatus.val = value;
  } else {
    _errdevicestatus = true;
  }

  if (_errdevicestatus) {
    log += F("- crc error");
    _readingerrcount++;
  } else {
    log += strformat(F(", flags: sp:%d cln:%d gas:%d rht:%d las:%d fan:%d - pass"),
                     _devicestatus.speed,
                     _devicestatus.autoclean,
                     _devicestatus.gas,
                     _devicestatus.rht,
                     _devicestatus.laser,
                     _devicestatus.fan);
    _readingsuccesscount++;
  }

  if (_enableLogging) {
    addLog(LOG_LEVEL_INFO, log);
  }
  return !_errdevicestatus;
}

uint16_t P167_data_struct::getErrCode(bool _clear) {
  uint16_t _tmp = _readingerrcode;

  if (_clear == true) {
    clearErrCode();
  }
  return _tmp;
}

uint16_t P167_data_struct::getErrCount(bool _clear) {
  uint16_t _tmp = _readingerrcount;

  if (_clear == true) {
    clearErrCount();
  }
  return _tmp;
}

uint16_t P167_data_struct::getSuccCount(bool _clear) {
  uint16_t _tmp = _readingsuccesscount;

  if (_clear == true) {
    clearSuccCount();
  }
  return _tmp;
}

void P167_data_struct::clearErrCode() {
  _readingerrcode = VIND_ERR_NO_ERROR;
}

void P167_data_struct::clearErrCount() {
  _readingerrcount = 0;
}

void P167_data_struct::clearSuccCount() {
  _readingsuccesscount = 0;
}

void P167_data_struct::checkPin_interrupt() {
  monpinValue              = monpinValue + 1; // volatile
  monpinLastTransitionTime = getMicros64();

  // Mark pin value changed
  monpinChanged = false;

  if (monpinValue != monpinValuelast) {
    monpinChanged = true;
  }
}

void P167_data_struct::setLogging(bool logStatus) {
  _enableLogging = logStatus;
}

void P167_data_struct::startCleaning() {
  writeCmd(P167_START_FAN_CLEAN); // Don't wait for a response, as the command causes the fan to run for 10 seconds
}

// When using interrupts we have to call the library entry point
// whenever an interrupt is triggered
void P167_data_struct::Plugin_167_interrupt(P167_data_struct *self) {
  // addLog(LOG_LEVEL_ERROR, F("********* SEN5X: interrupt apear!"));
  if (self) {
    self->checkPin_interrupt();
  }
}

#endif // USES_P167

#include "../PluginStructs/P114_data_struct.h"

#ifdef USES_P114

// **************************************************************************/
// Constructor
// **************************************************************************/
P114_data_struct::P114_data_struct(uint8_t i2c_addr, uint8_t integration_time, bool highDensity)
  : i2cAddress(i2c_addr), IT(integration_time), HD(highDensity) {}


// **************************************************************************/
// Initialize sensor and read data from VEML6075
// **************************************************************************/
bool P114_data_struct::read_sensor(float& _UVA, float& _UVB, float& _UVIndex) {
  if (!initialised) {
    initialised = init_sensor(); // Check id device is present
  }


  # ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLogMove(LOG_LEVEL_DEBUG,
               strformat(F("VEML6075: i2caddress: 0x%02x, initialized: %s"),
                         i2cAddress,
                         String(initialised ? F("true") : F("false")).c_str()));
  }
  # endif // ifndef BUILD_NO_DEBUG

  if (initialised) {
    for (int j = 0; j < 5; ++j) {
      UVData[j] = I2C_read16_LE_reg(i2cAddress, VEML6075_UVA_DATA + j);
    }

    // Calculate the UV Index, valid in open air not behind glass!
    UVAComp  = (UVData[0] - UVData[1]) - ACoef * (UVData[3] - UVData[1]) - BCoef * (UVData[4] - UVData[1]);
    UVBComp  = (UVData[2] - UVData[1]) - CCoef * (UVData[3] - UVData[1]) - DCoef * (UVData[4] - UVData[1]);
    _UVIndex = ((UVBComp * UVBresponsivity) +  (UVAComp * UVAresponsivity)) / 2.0f;

    _UVA = static_cast<float>(UVData[0]) / static_cast<float>(1 << (IT - 1)); // UVA light sensitivity increases linear with integration
                                                                              // time
    _UVB = static_cast<float>(UVData[2]) / static_cast<float>(1 << (IT - 1)); // UVB light sensitivity increases linear with integration
                                                                              // time

    // float UVASensitivity = 0.93/(static_cast<float>(IT + 1)); // UVA light sensitivity increases with integration time
    // float UVBSensitivity = 2.10/(static_cast<float>(IT + 1)); // UVB light sensitivity increases with integration time
    # ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      addLogMove(LOG_LEVEL_DEBUG, strformat(F("VEML6075: IT raw: 0x%02x"), IT + 1));
    }
    # endif // ifndef BUILD_NO_DEBUG
    return true;
  }
  return false;
}

// **************************************************************************/
// Check VEML6075 presence and initialize
// **************************************************************************/
bool P114_data_struct::init_sensor() {
  uint16_t deviceID = I2C_readS16_LE_reg(i2cAddress, VEML6075_UV_ID);

  # ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    String log;
    log.reserve(60);
    log  = F("VEML6075: ID: 0x");
    log += String(deviceID, HEX);
    log += F(" / checked Address: 0x");
    log += String(i2cAddress, HEX);
    log += F(" / 0x");
    log += String(VEML6075_UV_ID, HEX);
    addLogMove(LOG_LEVEL_DEBUG, log);
  }
  # endif // ifndef BUILD_NO_DEBUG

  if (deviceID != 0x26) {
    if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
      String log;
      log.reserve(60);
      log  = F("VEML6075: wrong deviceID: ");
      log += String(deviceID, HEX);
      addLogMove(LOG_LEVEL_ERROR, log);
    }
    return false;
  } else {
    // log  = F("VEML6075: found deviceID: 0x");
    // log += String(deviceID, HEX);

    if (!I2C_write16_LE_reg(i2cAddress, VEML6075_UV_CONF, (IT << 4) | (HD << 3))) { // Bit 3 must be 0, bit 0 is 0 for run and 1 for
      // shutdown, LS Byte
      if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
        String log;
        log.reserve(60);
        log  = F("VEML6075: setup failed!!");
        log += F(" / CONF: ");
        log += String(static_cast<uint16_t>(IT << 4) | (HD << 3), BIN);
        addLogMove(LOG_LEVEL_ERROR, log);
      }
      return false;
    } else if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      String log;
      log  = F("VEML6075: sensor initialised / CONF: ");
      log += String((uint16_t)(IT << 4) | (HD << 3), BIN);
      addLogMove(LOG_LEVEL_INFO, log);
    }
  }
  return true;
}

#endif // ifdef USES_P114

#include "../PluginStructs/P096_data_struct.h"

#ifdef USES_P096


/****************************************************************************
 * EPD_type_toString: Display-value for the device selected
 ***************************************************************************/
const __FlashStringHelper* EPD_type_toString(EPD_type_e device) {
  switch (device) {
    case EPD_type_e::EPD_IL3897: return F("IL3897 (Lolin 250 x 122px)");
    case EPD_type_e::EPD_UC8151D: return F("UC8151D (212 x 104px)");
    case EPD_type_e::EPD_SSD1680: return F("SSD1680 (250 x 212px)");
    # if P096_USE_WAVESHARE_2IN7
    case EPD_type_e::EPD_WS2IN7: return F("Waveshare 2.7\" (264 x 176px)");
    # endif // if P096_USE_WAVESHARE_2IN7
    case EPD_type_e::EPD_MAX: break;
  }
  return F("Unsupported type!");
}

/****************************************************************************
 * P096_CommandTrigger_toString: return the command string selected
 ***************************************************************************/
const __FlashStringHelper* P096_CommandTrigger_toString(P096_CommandTrigger cmd) {
  switch (cmd) {
    case P096_CommandTrigger::eInk: return F("eink");
    case P096_CommandTrigger::ePaper: return F("epaper");
    case P096_CommandTrigger::il3897: return F("il3897");
    case P096_CommandTrigger::uc8151d: return F("uc8151d");
    case P096_CommandTrigger::ssd1680: return F("ssd1680");
    # if P096_USE_WAVESHARE_2IN7
    case P096_CommandTrigger::ws2in7: return F("ws2in7");
    # endif // if P096_USE_WAVESHARE_2IN7
    case P096_CommandTrigger::MAX: return F("None");
    case P096_CommandTrigger::epd: break;
  }
  return F("epd"); // Default command trigger
}

/****************************************************************************
 * EPD_type_toResolution: X and Y resolution for the selected type
 ***************************************************************************/
void EPD_type_toResolution(EPD_type_e device, uint16_t& x, uint16_t& y) {
  switch (device) {
    case EPD_type_e::EPD_IL3897:
    case EPD_type_e::EPD_SSD1680:
      x = 250;
      y = 122;
      break;
    case EPD_type_e::EPD_UC8151D:
      x = 212;
      y = 104;
      break;
    # if P096_USE_WAVESHARE_2IN7
    case EPD_type_e::EPD_WS2IN7:
      x = 264;
      y = 176;
      break;
    # endif // if P096_USE_WAVESHARE_2IN7
    case EPD_type_e::EPD_MAX:
      break;
  }
}

/****************************************************************************
 * Constructor
 ***************************************************************************/
P096_data_struct::P096_data_struct(EPD_type_e          display,
                                   # if !P096_USE_EXTENDED_SETTINGS
                                   uint16_t            width,
                                   uint16_t            height,
                                   # endif // if P096_USE_EXTENDED_SETTINGS
                                   uint8_t             rotation,
                                   uint8_t             fontscaling,
                                   AdaGFXTextPrintMode textmode,
                                   String              commandTrigger,
                                   uint16_t            fgcolor,
                                   uint16_t            bgcolor,
                                   AdaGFXColorDepth    colorDepth,
                                   bool                textBackFill)
  : _display(display),
  # if !P096_USE_EXTENDED_SETTINGS
  _xpix(width), _ypix(height),
  # endif // if !P096_USE_EXTENDED_SETTINGS
  _rotation(rotation), _fontscaling(fontscaling), _textmode(textmode), _commandTrigger(commandTrigger),
  _fgcolor(fgcolor), _bgcolor(bgcolor), _colorDepth(colorDepth), _textBackFill(textBackFill)
{
  _commandTrigger.toLowerCase();
  _commandTriggerCmd  = _commandTrigger;
  _commandTriggerCmd += F("cmd");
}

/****************************************************************************
 * Destructor
 ***************************************************************************/
P096_data_struct::~P096_data_struct() {
  if (nullptr != gfxHelper) {
    delete gfxHelper;
    gfxHelper = nullptr;
  }

  if (nullptr != eInkScreen) {
    delete eInkScreen;
    eInkScreen = nullptr;
  }
}

/****************************************************************************
 * plugin_init: Initialize display
 ***************************************************************************/
bool P096_data_struct::plugin_init(struct EventStruct *event) {
  # if P096_USE_EXTENDED_SETTINGS

  EPD_type_toResolution(_display, _xpix, _ypix);
  # endif // if P096_USE_EXTENDED_SETTINGS

  updateFontMetrics();

  bool success = false;

  if (nullptr == eInkScreen) {
    addLog(LOG_LEVEL_INFO, F("EPD  : Init start."));

    switch (_display) {
      case EPD_type_e::EPD_IL3897:
        eInkScreen = new (std::nothrow) LOLIN_IL3897(_xpix, _ypix, PIN(1), PIN(2), PIN(0), PIN(3));  // HSPI
        break;
      case EPD_type_e::EPD_UC8151D:
        eInkScreen = new (std::nothrow) LOLIN_UC8151D(_xpix, _ypix, PIN(1), PIN(2), PIN(0), PIN(3)); // HSPI
        break;
      case EPD_type_e::EPD_SSD1680:
        eInkScreen = new (std::nothrow) LOLIN_SSD1680(_xpix, _ypix, PIN(1), PIN(2), PIN(0), PIN(3)); // HSPI
        break;
      # if P096_USE_WAVESHARE_2IN7
      case EPD_type_e::EPD_WS2IN7:
        eInkScreen = new (std::nothrow) Waveshare_2in7(_xpix, _ypix, PIN(1), PIN(2), PIN(0), PIN(3)); // HSPI
        break;
      # endif // if P096_USE_WAVESHARE_2IN7
      case EPD_type_e::EPD_MAX:
        break;
    }
    plugin_096_sequence_in_progress = false;
    # ifdef P096_USE_ADA_GRAPHICS

    if (nullptr != eInkScreen) {
      gfxHelper = new (std::nothrow) AdafruitGFX_helper(eInkScreen,
                                                        _commandTrigger,
                                                        _xpix,
                                                        _ypix,
                                                        _colorDepth,
                                                        _textmode,
                                                        _fontscaling,
                                                        _fgcolor,
                                                        _bgcolor,
                                                        true,
                                                        _textBackFill
                                                        #  if ADAGFX_FONTS_INCLUDED
                                                        , P096_CONFIG_DEFAULT_FONT
                                                        #  endif // if ADAGFX_FONTS_INCLUDED
                                                        );
      #  if P096_USE_EXTENDED_SETTINGS

      if (nullptr != gfxHelper) {
        gfxHelper->initialize();
        gfxHelper->setRotation(_rotation);
        gfxHelper->setColumnRowMode(bitRead(P096_CONFIG_FLAGS, P096_CONFIG_FLAG_USE_COL_ROW));
        gfxHelper->setTxtfullCompensation(!bitRead(P096_CONFIG_FLAGS, P096_CONFIG_FLAG_COMPAT_P096) ? 0 : 1); // Inverted
      }
      #  endif // if P096_USE_EXTENDED_SETTINGS
    }
    updateFontMetrics();
    # endif // ifdef P096_USE_ADA_GRAPHICS

    # ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      String log;
      log.reserve(50);
      log += F("EPD  : Init done, address: 0x");
      log += String(reinterpret_cast<ulong>(eInkScreen), HEX);
      log += ' ';

      if (nullptr == eInkScreen) {
        log += F("in");
      }
      log += F("valid, commands: ");
      log += _commandTrigger;
      log += F(", display: ");
      log += EPD_type_toString(_display);
      addLog(LOG_LEVEL_INFO, log);
      log.clear();
      log += F("EPD  : Foreground: 0x");
      log += String(_fgcolor, HEX);
      log += F(", background: 0x");
      log += String(_bgcolor, HEX);
      addLogMove(LOG_LEVEL_INFO, log);
    }
    # endif // ifndef BUILD_NO_DEBUG

    if (nullptr != eInkScreen) {
      eInkScreen->begin(); // Start the device
      eInkScreen->clearBuffer();

      eInkScreen->setRotation(_rotation);
      eInkScreen->setTextColor(_fgcolor);
      eInkScreen->setTextSize(_fontscaling); // Handles 0 properly, text size, default 1 = very small
      eInkScreen->setCursor(0, 0);           // move cursor to position (0, 0) pixel

      if (!stringsLoaded) {
        LoadCustomTaskSettings(event->TaskIndex, strings, P096_Nlines, 0);
        stringsLoaded = true;

        for (uint8_t x = 0; x < P096_Nlines && !stringsHasContent; x++) {
          stringsHasContent = !strings[x].isEmpty();
        }
      }
    }

    success = true;
  } else {
    addLog(LOG_LEVEL_INFO, F("EPD  : No init?"));
  }

  return success;
}

/****************************************************************************
 * updateFontMetrics: recalculate x and y columns, based on font size and font scale
 ***************************************************************************/
void P096_data_struct::updateFontMetrics() {
  if (nullptr != gfxHelper) {
    gfxHelper->getTextMetrics(_textcols, _textrows, _fontwidth, _fontheight, _fontscaling, _heightOffset, _xpix, _ypix);
    gfxHelper->getColors(_fgcolor, _bgcolor);
  } else {
    if (_fontscaling == 0) { _fontscaling = 1; }
    _textcols = _xpix / (_fontwidth * _fontscaling);
    _textrows = _ypix / (_fontheight * _fontscaling);
  }
}

/****************************************************************************
 * plugin_exit: De-initialize before destruction
 ***************************************************************************/
bool P096_data_struct::plugin_exit(struct EventStruct *event) {
  addLog(LOG_LEVEL_INFO, F("EPD  : Exit."));

  # if P096_USE_EXTENDED_SETTINGS

  if (nullptr != gfxHelper) { delete gfxHelper; }
  gfxHelper = nullptr;

  # endif // if P096_USE_EXTENDED_SETTINGS

  if (nullptr != eInkScreen) { delete eInkScreen; }
  eInkScreen = nullptr; // Is used as a proxy only
  return true;
}

/****************************************************************************
 * plugin_read: Re-draw the default content
 ***************************************************************************/
bool P096_data_struct::plugin_read(struct EventStruct *event) {
  # if P096_USE_EXTENDED_SETTINGS

  if (nullptr != eInkScreen) {
    if (stringsHasContent) {
      gfxHelper->setColumnRowMode(false); // Turn off column mode

      eInkScreen->clearBuffer();

      int yPos = 0;

      for (uint8_t x = 0; x < P096_Nlines; x++) {
        String newString = AdaGFXparseTemplate(strings[x], _textcols, gfxHelper);

        #  if ADAGFX_PARSE_SUBCOMMAND
        updateFontMetrics();
        #  endif // if ADAGFX_PARSE_SUBCOMMAND

        if (yPos < _ypix) {
          gfxHelper->printText(newString.c_str(), 0, yPos, _fontscaling, _fgcolor, _bgcolor);
        }
        delay(0);
        yPos += (_fontheight * _fontscaling);
      }
      gfxHelper->setColumnRowMode(bitRead(P096_CONFIG_FLAGS, P096_CONFIG_FLAG_USE_COL_ROW)); // Restore column mode
      int16_t curX, curY;
      gfxHelper->getCursorXY(curX, curY);                                                    // Get current X and Y coordinates,
      UserVar.setFloat(event->TaskIndex, 0, curX);                                           // and put into Values
      UserVar.setFloat(event->TaskIndex, 1, curY);

      eInkScreen->display();
      eInkScreen->clearBuffer();
    }
  }
  # endif // if P096_USE_EXTENDED_SETTINGS
  return false; // Always return false, so no attempt to send to Controllers or generate events is started
}

/****************************************************************************
 * plugin_write: Handle commands
 ***************************************************************************/
bool P096_data_struct::plugin_write(struct EventStruct *event, const String& string) {
  bool   success = false;
  String cmd     = parseString(string, 1);

  if ((nullptr != eInkScreen) && cmd.equals(_commandTriggerCmd)) {
    String arg1 = parseString(string, 2);

    if (equals(arg1, F("off"))) { // Not supported 'on' and 'off' as commands
      success = false;
    }
    else if (equals(arg1, F("on"))) {
      success = false;
    }
    else if (equals(arg1, F("clear"))) {
      String arg2 = parseString(string, 3);

      eInkScreen->clearBuffer();

      if (!arg2.isEmpty()) {
        eInkScreen->fillScreen(AdaGFXparseColor(arg2, _colorDepth));
      } else {
        eInkScreen->fillScreen(_bgcolor);
      }
      eInkScreen->display();
      eInkScreen->clearBuffer();
      success = true;
    }
    else if (equals(arg1, F("backlight"))) { // not supported
      success = false;
    }
    else if (equals(arg1, F("deepsleep"))) {
      eInkScreen->deepSleep();
    }
    else if (equals(arg1, F("seq_start"))) {
      String arg2 = parseString(string, 3);

      eInkScreen->clearBuffer();
      const uint16_t fillColor =
        (arg2.isEmpty() ? static_cast<uint16_t>(AdaGFXMonoRedGreyscaleColors::ADAGFXEPD_BLACK)
        : AdaGFXparseColor(arg2, _colorDepth));
      eInkScreen->fillScreen(fillColor);
      plugin_096_sequence_in_progress = true;
      success                         = true;
    }
    else if (equals(arg1, F("seq_end"))) {
      // # ifndef BUILD_NO_DEBUG
      //             TimingStats s;
      //             const unsigned statisticsTimerStart(micros());
      // # endif // ifndef BUILD_NO_DEBUG
      eInkScreen->display();

      // # ifndef BUILD_NO_DEBUG
      //             s.add(usecPassedSince(statisticsTimerStart));
      //             tmpString += "<br/> Display timings = " + String(s.getAvg());
      // # endif // ifndef BUILD_NO_DEBUG
      eInkScreen->clearBuffer();
      plugin_096_sequence_in_progress = false;
      success                         = true;
    }
    else if (equals(arg1, F("inv"))) {
      const int nArg2 = event->Par2;

      if ((nArg2 >= 0) &&
          (nArg2 <= 1)) {
        eInkScreen->invertDisplay(nArg2);
        eInkScreen->display();
        success = true;
      }
    }
    else if (equals(arg1, F("rot"))) {
      ///control?cmd=epdcmd,rot,0
      // not working to verify
      const int nArg2 = event->Par2;

      if ((nArg2 >= 0)) {
        eInkScreen->setRotation(nArg2 % 4);
        eInkScreen->display();
        success = true;
      }
    } else {
      success = false;
    }
  }
  else if (eInkScreen && (cmd.equals(_commandTrigger) ||
                          (gfxHelper && gfxHelper->isAdaGFXTrigger(cmd)))) {
    success = true;

    // if (!bitRead(P096_CONFIG_FLAGS, P096_CONFIG_FLAG_NO_WAKE)) { // Wake display?
    //   displayOnOff(true, P096_CONFIG_BACKLIGHT_PIN, P096_CONFIG_BACKLIGHT_PERCENT, P096_CONFIG_DISPLAY_TIMEOUT);
    // }

    if (nullptr != gfxHelper) {
      String tmp = string;

      if (!plugin_096_sequence_in_progress) {
        eInkScreen->clearBuffer();
        eInkScreen->fillScreen(EPD_WHITE);
      }

      // Hand it over after replacing variables
      success = gfxHelper->processCommand(AdaGFXparseTemplate(tmp, _textcols, gfxHelper));

      if (success && !plugin_096_sequence_in_progress) {
        eInkScreen->display();

        // eInkScreen->clearBuffer();
      }

      updateFontMetrics(); // Font or color may have changed

      if (success) {
        int16_t curX, curY;
        gfxHelper->getCursorXY(curX, curY); // Get current X and Y coordinates, and put into Values
        UserVar.setFloat(event->TaskIndex, 0, curX);
        UserVar.setFloat(event->TaskIndex, 1, curY);
      }
    }
  }
  return success;
}

#endif // ifdef USES_P096

#include "../PluginStructs/P061_data_struct.h"

#if defined(USES_P061)

bool P061_data_struct::plugin_init(struct EventStruct *event) {
  switch (P061_CONFIG_KEYPAD_TYPE) {
    case 0: MCP23017_KeyPadMatrixInit(_i2c_addr); break;
    case 1: PCF8574_KeyPadMatrixInit(_i2c_addr); break;
    case 2: PCF8574_KeyPadDirectInit(_i2c_addr); break;
    case 3: MCP23017_KeyPadDirectInit(_i2c_addr); break;
    # ifdef P061_ENABLE_PCF8575
    case 4: PCF8575_KeyPadMatrixInit(_i2c_addr); break;
    case 5: PCF8575_KeyPadDirectInit(_i2c_addr); break;
    # endif // ifdef P061_ENABLE_PCF8575
  }

  return true;
}

bool P061_data_struct::plugin_fifty_per_second(struct EventStruct *event) {
  uint8_t actScanCode = 0;

  switch (P061_CONFIG_KEYPAD_TYPE) {
    case 0: actScanCode = MCP23017_KeyPadMatrixScan(_i2c_addr); break;
    case 1: actScanCode = PCF8574_KeyPadMatrixScan(_i2c_addr); break;
    case 2: actScanCode = PCF8574_KeyPadDirectScan(_i2c_addr); break;
    case 3: actScanCode = MCP23017_KeyPadDirectScan(_i2c_addr); break;
    # ifdef P061_ENABLE_PCF8575
    case 4: actScanCode = PCF8575_KeyPadMatrixScan(_i2c_addr); break;
    case 5: actScanCode = PCF8575_KeyPadDirectScan(_i2c_addr); break;
    # endif // ifdef P061_ENABLE_PCF8575
  }

  if (lastScanCode == actScanCode) {   // debounced? - two times the same value?
    if (sentScanCode != actScanCode) { // any change to last sent data?
      UserVar.setFloat(event->TaskIndex, 0, actScanCode);
      event->sensorType = Sensor_VType::SENSOR_TYPE_SWITCH;

      addLog(LOG_LEVEL_INFO, strformat(F("KPad : ScanCode=0x%x"), actScanCode));

      sendData(event);

      sentScanCode = actScanCode;
    }
  } else {
    lastScanCode = actScanCode;
  }

  return true;
}

void P061_data_struct::MCP23017_setReg(uint8_t addr, uint8_t reg, uint8_t data) {
  I2C_write8_reg(addr, reg, data);
}

uint8_t P061_data_struct::MCP23017_getReg(uint8_t addr, uint8_t reg) {
  bool success      = false;
  const uint8_t res = I2C_read8_reg(addr, reg, &success);

  return success ? res : 0xff;
}

void P061_data_struct::MCP23017_KeyPadMatrixInit(uint8_t addr) {
  MCP23017_setReg(addr, MCP23017_IODIRA, 0x00); // port A to output
  MCP23017_setReg(addr, MCP23017_GPIOA,  0x00); // port A to low
  MCP23017_setReg(addr, MCP23017_IODIRB, 0xFF); // port B to input
  MCP23017_setReg(addr, MCP23017_GPPUA,  0xFF); // port A pullup on
  MCP23017_setReg(addr, MCP23017_GPPUB,  0xFF); // port B pullup on
}

void P061_data_struct::MCP23017_KeyPadDirectInit(uint8_t addr) {
  MCP23017_setReg(addr, MCP23017_IODIRA, 0xFF); // port A to input
  MCP23017_setReg(addr, MCP23017_IODIRB, 0xFF); // port B to input
  MCP23017_setReg(addr, MCP23017_GPPUA,  0xFF); // port A pullup on
  MCP23017_setReg(addr, MCP23017_GPPUB,  0xFF); // port B pullup on
}

uint8_t P061_data_struct::MCP23017_KeyPadMatrixScan(uint8_t addr) {
  uint8_t rowMask = 1;
  uint8_t colData;

  colData = MCP23017_getReg(addr, MCP23017_GPIOB);
  # if P061_DEBUG_LOG

  if (loglevelActiveFor(LOG_LEVEL_INFO) && (millis() % 1000 < 10)) {
    addLog(LOG_LEVEL_INFO, strformat(F("P061 MCP23017 matrix, read data: 0x%x"), colData));
  }
  # endif // if P061_DEBUG_LOG

  if (colData == 0xFF) { // no key pressed?
    return 0;            // no key pressed!
  }

  for (uint8_t row = 0; row <= 8; ++row) {
    if (row == 0) {
      MCP23017_setReg(addr, MCP23017_IODIRA, 0xFF);     // no bit of port A to output
    } else {
      MCP23017_setReg(addr, MCP23017_IODIRA, ~rowMask); // one bit of port A to output 0
      rowMask <<= 1;
    }

    colData = MCP23017_getReg(addr, MCP23017_GPIOB);

    if (colData != 0xFF) { // any key pressed?
      uint8_t colMask = 1;

      for (uint8_t col = 1; col <= 8; ++col) {
        if ((colData & colMask) == 0) {                 // this key pressed?
          MCP23017_setReg(addr, MCP23017_IODIRA, 0x00); // port A to output 0
          return (row << 4) | col;
        }
        colMask <<= 1;
      }
    }
  }

  MCP23017_setReg(addr, MCP23017_IODIRA, 0x00); // port A to output 0
  return 0;                                     // no key pressed!
}

uint8_t P061_data_struct::MCP23017_KeyPadDirectScan(uint8_t addr) {
  uint16_t colData;

  colData  = (MCP23017_getReg(addr, MCP23017_GPIOB) << 8);
  colData |= MCP23017_getReg(addr, MCP23017_GPIOA);
  # if P061_DEBUG_LOG

  if (loglevelActiveFor(LOG_LEVEL_INFO) && (millis() % 1000 < 10)) {
    addLog(LOG_LEVEL_INFO, strformat(F("P061 MCP23017 direct, read data: 0x%x"), colData));
  }
  # endif // if P061_DEBUG_LOG

  if (colData == 0xFFFF) { // no key pressed?
    return 0;              // no key pressed!
  }
  uint16_t colMask = 0x01;

  for (uint8_t col = 1; col <= 16; ++col) {
    if ((colData & colMask) == 0) { // this key pressed?
      return col;
    }
    colMask <<= 1;
  }

  return 0; // no key pressed!
}

// PCF8574 Matrix //////////////////////////////////////////////////////////////

void P061_data_struct::PCF8574_setReg(uint8_t addr, uint8_t data) {
  I2C_write8(addr, data);
}

uint8_t P061_data_struct::PCF8574_getReg(uint8_t addr) {
  bool success      = false;
  const uint8_t res = I2C_read8(addr, &success);

  return success ? res : 0xff;
}

void P061_data_struct::PCF8574_KeyPadMatrixInit(uint8_t addr) {
  PCF8574_setReg(addr, 0xF0); // low nibble to output 0
}

uint8_t P061_data_struct::PCF8574_KeyPadMatrixScan(uint8_t addr) {
  uint8_t rowMask = 1;
  uint8_t colData;

  colData = PCF8574_getReg(addr) & 0xF0;
  # if P061_DEBUG_LOG

  if (loglevelActiveFor(LOG_LEVEL_INFO) && (millis() % 1000 < 10)) {
    addLog(LOG_LEVEL_INFO, strformat(F("P061 PCF8574 matrix, read data: 0x%x"), colData));
  }
  # endif // if P061_DEBUG_LOG

  if (colData == 0xF0) { // no key pressed?
    return 0;            // no key pressed!
  }

  for (uint8_t row = 0; row <= 4; ++row) {
    if (row == 0) {
      PCF8574_setReg(addr, 0xFF);     // no bit of port A to output
    } else {
      PCF8574_setReg(addr, ~rowMask); // one bit of port A to output 0
      rowMask <<= 1;
    }

    colData = PCF8574_getReg(addr) & 0xF0;

    if (colData != 0xF0) { // any key pressed?
      uint8_t colMask = 0x10;

      for (uint8_t col = 1; col <= 4; ++col) {
        if ((colData & colMask) == 0) { // this key pressed?
          PCF8574_setReg(addr, 0xF0);   // low nibble to output 0
          return (row << 4) | col;
        }
        colMask <<= 1;
      }
    }
  }

  PCF8574_setReg(addr, 0xF0); // low nibble to output 0
  return 0;                   // no key pressed!
}

// PCF8574 Direct //////////////////////////////////////////////////////////////

void P061_data_struct::PCF8574_KeyPadDirectInit(uint8_t addr) {
  PCF8574_setReg(addr, 0xFF); // all to input
}

uint8_t P061_data_struct::PCF8574_KeyPadDirectScan(uint8_t addr) {
  uint8_t colData;

  colData = PCF8574_getReg(addr);
  # if P061_DEBUG_LOG

  if (loglevelActiveFor(LOG_LEVEL_INFO) && (millis() % 1000 < 10)) {
    addLog(LOG_LEVEL_INFO, strformat(F("P061 PCF8574 direct, read data: 0x%x"), colData));
  }
  # endif // if P061_DEBUG_LOG

  if (colData == 0xFF) { // no key pressed?
    return 0;            // no key pressed!
  }
  uint8_t colMask = 0x01;

  for (uint8_t col = 1; col <= 8; ++col) {
    if ((colData & colMask) == 0) { // this key pressed?
      return col;
    }
    colMask <<= 1;
  }

  return 0; // no key pressed!
}

# ifdef P061_ENABLE_PCF8575

// PCF8575 Matrix /////////////////////////////////////////////////////////////

void P061_data_struct::PCF8575_setReg(uint8_t addr, uint16_t data) {
  I2C_write16_LE(addr, data);
}

uint16_t P061_data_struct::PCF8575_getReg(uint8_t addr) {
  bool is_ok{};

  I2C_wakeup(addr);
  const uint16_t data = I2C_read16(addr, &is_ok);

  if (is_ok) {
    return (data << 8) | (data >> 8);
  }
  return 0xFFFF;
}

void P061_data_struct::PCF8575_KeyPadMatrixInit(uint8_t addr) {
  PCF8575_setReg(addr, 0xFF00); // low byte to output 00
}

uint8_t P061_data_struct::PCF8575_KeyPadMatrixScan(uint8_t addr) {
  uint16_t rowMask = 1;
  uint16_t colData;

  PCF8575_setReg(addr, 0xFF00); // P1x all to input
  colData = PCF8575_getReg(addr) & 0xFF00;
  #  if P061_DEBUG_LOG

  if (loglevelActiveFor(LOG_LEVEL_INFO) && (millis() % 1000 < 10)) {
    addLog(LOG_LEVEL_INFO, strformat(F("P061 PCF8575 matrix, read data: 0x%x"), colData));
  }
  #  endif // if P061_DEBUG_LOG

  if (colData == 0xFF00) { // no key pressed?
    return 0;              // no key pressed!
  }

  for (uint8_t row = 0; row <= 8; ++row) {
    if (row == 0) {
      PCF8575_setReg(addr, 0xFFFF);   // no bit of port A to output
    } else {
      PCF8575_setReg(addr, ~rowMask); // one bit of port A to output 0
      rowMask <<= 1;
    }

    colData = PCF8575_getReg(addr) & 0xFF00;

    if (colData != 0xFF00) { // any key pressed?
      uint16_t colMask = 0x0100;

      for (uint8_t col = 1; col <= 8; ++col) {
        if ((colData & colMask) == 0) { // this key pressed?
          PCF8575_setReg(addr, 0xFF00); // low byte to output 00
          return (row << 4) | col;
        }
        colMask <<= 1;
      }
    }
  }

  PCF8575_setReg(addr, 0xFF00); // low byte to output 00
  return 0;                     // no key pressed!
}

// PCF8575 Direct //////////////////////////////////////////////////////////////

void P061_data_struct::PCF8575_KeyPadDirectInit(uint8_t addr) {
  PCF8575_setReg(addr, 0xFFFF); // all to input
}

uint8_t P061_data_struct::PCF8575_KeyPadDirectScan(uint8_t addr) {
  uint16_t colData;

  PCF8575_setReg(addr, 0xFFFF);   // all to input
  colData = PCF8575_getReg(addr); // Read the actual state
  #  if P061_DEBUG_LOG

  if (loglevelActiveFor(LOG_LEVEL_INFO) && (millis() % 1000 < 10)) {
    addLog(LOG_LEVEL_INFO, strformat(F("P061 PCF8575 direct, read data: 0x%x"), colData));
  }
  #  endif // if P061_DEBUG_LOG

  if (colData == 0xFFFF) { // no key pressed?
    return 0;              // no key pressed!
  }
  uint16_t colMask = 0x01;

  for (uint8_t col = 1; col <= 16; ++col) {
    if ((colData & colMask) == 0) { // this key pressed?
      return col;
    }
    colMask <<= 1;
  }

  return 0; // no key pressed!
}

# endif // ifdef P061_ENABLE_PCF8575

#endif // if defined(USES_P061)

#include "../PluginStructs/P016_data_struct.h"

#ifdef USES_P016

# include "../Commands/ExecuteCommand.h"
# include "../Helpers/ESPEasy_Storage.h"
# include <IRutils.h>
# ifdef P016_CHECK_HEAP
#  include "src/Helpers/Memory.h"
# endif // ifdef P016_CHECK_HEAP

# ifdef P16_SETTINGS_V1

// Conversion constructor
tCommandLinesV2::tCommandLinesV2(const tCommandLinesV1& lineV1, uint8_t i)
{
  String log;

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    #  ifndef PLUGIN_016_DEBUG
    log.reserve(20); // less space needed
    #  else // ifndef PLUGIN_016_DEBUG
    log.reserve(80);
    #  endif // ifndef PLUGIN_016_DEBUG
  }

  log  = F("P016: converting "); // Still a little logging
  log += i;
  #  ifdef PLUGIN_016_DEBUG
  log += ':';
  #  endif // ifdef PLUGIN_016_DEBUG

  memcpy(Command, lineV1.Command, P16_Nchars);
  const uint32_t oldCode = lineV1.Code;

  if (oldCode > 0) {
    CodeDecodeType = static_cast<decode_type_t>((oldCode >> 24));                // decode_type
    bitWrite(CodeFlags, P16_FLAGS_REPEAT, oldCode & (0x1 << P16_CMDBIT_REPEAT)); // Repeat flag
    Code = oldCode & 0x7FFFFF;                                                   // Only keep lowest 23 bits
    #  ifdef PLUGIN_016_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      log += F(" type ");
      log += typeToString(CodeDecodeType, (oldCode & (0x1 << P16_CMDBIT_REPEAT)) != 0);
      log += F(" code 0x");
      log += uint64ToString(oldCode, 16);
    }
    #  endif // ifdef PLUGIN_016_DEBUG
  }

  const uint32_t oldAlternativeCode = lineV1.AlternativeCode;

  if (oldAlternativeCode > 0) {
    AlternativeCodeDecodeType = static_cast<decode_type_t>((oldAlternativeCode >> 24));                // decode_type
    bitWrite(AlternativeCodeFlags, P16_FLAGS_REPEAT, oldAlternativeCode & (0x1 << P16_CMDBIT_REPEAT)); // Repeat flag
    AlternativeCode = oldAlternativeCode & 0x7FFFFF;                                                   // Only keep lowest 23 bits
    #  ifdef PLUGIN_016_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      log += F(" alt.type ");
      log += typeToString(AlternativeCodeDecodeType, (oldAlternativeCode & (0x1 << P16_CMDBIT_REPEAT)) != 0);
      log += F(" alt.code 0x");
      log += uint64ToString(oldAlternativeCode, 16);
    }
    #  endif // ifdef PLUGIN_016_DEBUG
  }
  addLogMove(LOG_LEVEL_INFO, log);
}

# endif // ifdef P16_SETTINGS_V1


void P016_data_struct::init(struct EventStruct *event, uint16_t CmdInhibitTime) {
  # if P016_FEATURE_COMMAND_HANDLING
  loadCommandLines(event);
  iCmdInhibitTime = CmdInhibitTime;
  iLastCmd        = 0;
  iLastCmdTime    = 0;
  # endif // if P016_FEATURE_COMMAND_HANDLING
}

# if P016_FEATURE_COMMAND_HANDLING
void P016_data_struct::loadCommandLines(struct EventStruct *event) {
  #  ifdef P16_SETTINGS_V1

  // Convert the settings if both versions are defined and PCONFIG(7) != latest version
  if (PCONFIG(7) != P16_SETTINGS_LATEST) {
    addLog(LOG_LEVEL_ERROR, F("P016 IR: Settings conversion, save task settings to store in new format."));
  }
  #  endif // ifdef P16_SETTINGS_V1
  CommandLines.clear(); // Start fresh

  for (uint8_t i = 0; i < P16_Nlines; i++) {
    CommandLines.push_back(tCommandLinesV2());
    loadCommandLine(event, CommandLines[i], i);
  }
}

void P016_data_struct::saveCommandLines(struct EventStruct *event) {
  for (uint8_t i = 0; i < P16_Nlines; i++) {
    saveCommandLine(event, CommandLines[i], i);
  }
}

void P016_data_struct::loadCommandLine(struct EventStruct *event, tCommandLinesV2& line, uint8_t lineNr)
{
  #  ifdef P16_SETTINGS_V1

  if (PCONFIG(7) != P16_SETTINGS_LATEST) {
    loadCommandLinev1(event, line, lineNr);
    return;
  }
  #  endif // ifdef P16_SETTINGS_V1

  const int loadOffset = lineNr * sizeof(tCommandLinesV2);
  LoadFromFile(SettingsType::Enum::CustomTaskSettings_Type,
               event->TaskIndex,
               reinterpret_cast<uint8_t *>(&line),
               sizeof(tCommandLinesV2),
               loadOffset);
  line.Command[P16_Nchars - 1] = 0; // Terminate in case of uninitalized data
}

# endif // if P016_FEATURE_COMMAND_HANDLING

# ifdef P16_SETTINGS_V1
void P016_data_struct::loadCommandLinev1(struct EventStruct *event, tCommandLinesV2& line, uint8_t lineNr)
{
  tCommandLinesV1 lineV1;

  {
    const int loadOffsetV1 = lineNr * sizeof(tCommandLinesV1);
    LoadFromFile(SettingsType::Enum::CustomTaskSettings_Type,
                 event->TaskIndex,
                 reinterpret_cast<uint8_t *>(&lineV1),
                 sizeof(tCommandLinesV2),
                 loadOffsetV1);
  }

  line                         = tCommandLinesV2(lineV1, lineNr);
  line.Command[P16_Nchars - 1] = 0;
}

# endif // ifdef P16_SETTINGS_V1

# if P016_FEATURE_COMMAND_HANDLING
void P016_data_struct::saveCommandLine(struct EventStruct *event, const tCommandLinesV2& line, uint8_t lineNr)
{
  const int saveOffset = lineNr * sizeof(tCommandLinesV2);

  SaveToFile(SettingsType::Enum::CustomTaskSettings_Type,
             event->TaskIndex,
             reinterpret_cast<const uint8_t *>(&line),
             sizeof(tCommandLinesV2),
             saveOffset);
}

void P016_data_struct::AddCode(uint64_t Code, decode_type_t DecodeType, uint16_t CodeFlags) {
  // add received code
  int _index = P16_Nlines;

  if (Code == 0) {
    return;
  }

  for (int i = 0; i < P16_Nlines; ++i) {
    if (validateCode(i, Code, DecodeType, CodeFlags)) {
      // code already saved
      return;
    }

    if (CommandLines[i].Code == 0) {
      // get first index to add the code
      _index = std::min(i, _index);
    }
  }

  if (_index == P16_Nlines) {
    // no free index
    return;
  }
  CommandLines[_index].Code           = Code;
  CommandLines[_index].CodeDecodeType = DecodeType;
  CommandLines[_index].CodeFlags      = CodeFlags;
  bCodeChanged                        = true;
  #  ifdef PLUGIN_016_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO, strformat(
                 F("[P016] AddCode: %s code: 0x%s to index %d"),
                 typeToString(DecodeType, bitRead(CodeFlags, P16_FLAGS_REPEAT)).c_str(),
                 uint64ToString(Code, 16).c_str(),
                 _index));
  }
  #  endif // PLUGIN_016_DEBUG
}

bool P016_data_struct::ExecuteCode(uint64_t Code, decode_type_t DecodeType, uint16_t CodeFlags) {
  if (Code == 0) {
    return false;
  }

  if ((iLastCmd == Code) && (iLastDecodeType == DecodeType)) {
    // same code as before
    if (iCmdInhibitTime > timePassedSince(iLastCmdTime)) {
      // inhibit time not ellapsed
      return false;
    }
  }

  const unsigned int nr_CommandLines = CommandLines.size();

  for (unsigned int i = 0; i < nr_CommandLines; ++i) {
    if (validateCode(i, Code, DecodeType, CodeFlags)) {
      // code already saved
      iLastCmd        = Code;
      iLastDecodeType = DecodeType;
      iLastCodeFlags  = CodeFlags;
      iLastCmdTime    = millis();

      if (CommandLines[i].Command[0] != 0) {
        #  ifdef P016_CHECK_HEAP
        CheckHeap(F("Before ExecuteCommand_all:"));
        #  endif // ifdef P016_CHECK_HEAP
        ExecuteCommand_all(
          { EventValueSource::Enum::VALUE_SOURCE_SYSTEM, CommandLines[i].Command }, true);
        #  ifdef P016_CHECK_HEAP
        CheckHeap(F("After ExecuteCommand_all:"));
        #  endif // ifdef P016_CHECK_HEAP
        #  ifdef PLUGIN_016_DEBUG

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          addLogMove(LOG_LEVEL_INFO, strformat(
                       F("[P016] Execute added: %s Code: 0x%s with command %d: {%s}"),
                       typeToString(DecodeType, bitRead(CodeFlags, P16_FLAGS_REPEAT)).c_str(),
                       uint64ToString(Code, 16).c_str(),
                       (i + 1),
                       CommandLines[i].Command));
        }
        #  endif // PLUGIN_016_DEBUG
        return true;
      }
    }
  }
  #  ifdef PLUGIN_016_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
    addLogMove(LOG_LEVEL_ERROR, strformat(
                 F("[P016] ValidateCode failed: %s Code: 0x%s"),
                 typeToString(DecodeType, bitRead(CodeFlags, P16_FLAGS_REPEAT)).c_str(),
                 uint64ToString(Code, 16).c_str()));
  }
  #  endif // PLUGIN_016_DEBUG
  return false;
}

bool P016_data_struct::validateCode(int i, uint64_t Code, decode_type_t DecodeType, uint16_t CodeFlags) {
  if ((i >= static_cast<int>(CommandLines.size())) || (i < 0)) { return false; }
  return ((CommandLines[i].Code == Code)
          && (CommandLines[i].CodeDecodeType == DecodeType)
          && (CommandLines[i].CodeFlags == CodeFlags))
         || ((CommandLines[i].AlternativeCode == Code)
             && (CommandLines[i].AlternativeCodeDecodeType == DecodeType)
             && (CommandLines[i].AlternativeCodeFlags == CodeFlags));
}

# endif // if P016_FEATURE_COMMAND_HANDLING
# ifdef P016_CHECK_HEAP
void P016_data_struct::CheckHeap(String dbgtxt) {
  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO, strformat(
                 F("P016: %s FreeMem: %d FreeStack:%d"),
                 dbgtxt.c_str(), FreeMem(), getCurrentFreeStack()));
  }
}

# endif // ifdef P016_CHECK_HEAP

#endif // ifdef USES_P016

#include "../PluginStructs/P113_data_struct.h"

#ifdef USES_P113

P113_data_struct::P113_data_struct(uint8_t i2c_addr, int timing, bool range) : i2cAddress(i2c_addr), timing(timing), range(range) {
  sensor = new (std::nothrow) SFEVL53L1X();
}

P113_data_struct::~P113_data_struct() {
  delete sensor;
}

// **************************************************************************/
// Initialize VL53L1X
// **************************************************************************/
bool P113_data_struct::begin(struct EventStruct *event) {
  initState = nullptr != sensor;

  if (initState) {
    const uint16_t id = sensor->getID();

    // FIXME 2023-08-11 tonhuisman: Disabled, as it seems to mess up the sensor
    // sensor->setI2CAddress(i2cAddress); // Initialize for configured address

    const bool res = sensor->begin();

    if (res) { // 0/false is NO-ERROR
      if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
        addLogMove(LOG_LEVEL_ERROR, strformat(F("VL53L1X: Sensor not found, init failed for 0x%02x, id: 0x%04X status: %d"),
                                              i2cAddress, id, res));
      }
      initState = false;
      return initState;
    }

    sensor->setTimingBudgetInMs(timing);

    if (range) {
      sensor->setDistanceModeLong();
    } else {
      sensor->setDistanceModeShort();
    }

    # if P113_USE_ROI

    if (initState) {
      if ((0 != P113_ROI_X) && (0 != P113_ROI_Y) && (0 != P113_OPT_CENTER)) {
        sensor->setROI(P113_ROI_X, P113_ROI_Y, P113_OPT_CENTER);
      }
    }
    # endif // if P113_USE_ROI

    # ifdef P113_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, strformat(F("VL53L1X: Sensor initialized at address 0x%02x, id: 0x%04x"), i2cAddress, id));
    }
    # endif // ifdef P113_DEBUG
  }

  return initState;
}

bool P113_data_struct::startRead() {
  if (initState && !readActive && (nullptr != sensor)) {
    sensor->startRanging();
    readActive = true;
    distance   = -1;
  }
  return readActive;
}

bool P113_data_struct::readAvailable() {
  bool ready = (nullptr != sensor) && sensor->checkForDataReady();

  if (ready) {
    distance = sensor->getDistance();
    sensor->clearInterrupt();
    sensor->stopRanging();

    // readActive = false;
  }
  return ready;
}

uint16_t P113_data_struct::readDistance() {
  # ifdef P113_DEBUG_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLogMove(LOG_LEVEL_DEBUG, strformat(F("VL53L1X: idx: 0x%02x init: %d"), i2cAddress, initState));
  }
  # endif // P113_DEBUG_DEBUG

  success    = true;
  readActive = false;

  if (distance >= 8190u) {
    # ifdef P113_DEBUG_DEBUG
    addLog(LOG_LEVEL_DEBUG, concat(F("VL53L1X: NO MEASUREMENT"), distance));
    # endif // P113_DEBUG_DEBUG
    success = false;
  }

  # ifdef P113_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO, strformat(F("VL53L1X: Address: 0x%02x / Timing: %d / Long Range: %d / Distance: %d"),
                                         i2cAddress, timing, range, distance));
  }
  # endif // P113_DEBUG

  return distance;
}

uint16_t P113_data_struct::readAmbient() {
  if (nullptr == sensor) {
    return 0u;
  }
  return sensor->getAmbientRate();
}

bool P113_data_struct::isReadSuccessful() {
  return success;
}

# if P113_USE_ROI
uint8_t P113_data_struct::getSPAD(uint8_t y, uint8_t x) {
  const uint8_t offset = ((x & 0x0F) << 3) + (y & 0x07);

  return (y > 7) ? 127 - offset : 128 + offset;
}

void P113_data_struct::opticalIndexToXy(uint8_t oi, uint8_t& y, uint8_t& x) {
  x = 0;
  y = 0;

  if (oi < 128) {
    y += 8;
    oi = 127 - oi;
  } else {
    oi -= 128;
  }
  x  = oi >> 3;
  y += oi & 0x7;
}

void P113_data_struct::drawSelectionArea(uint8_t roix, uint8_t roiy, uint8_t opticalCenter) {
  addHtml(F("<TD rowspan='16' style='padding:0;'>"));
  addHtml(F("<section id='vi'>"
            "<div id='sL'></div>"));
  uint8_t lx  = 0;
  uint8_t ly  = 0;
  uint8_t rx  = 15;
  uint8_t ry  = 15;
  uint8_t ocx = 0;
  uint8_t ocy = 0;

  P113_data_struct::opticalIndexToXy(opticalCenter, ocy, ocx);
  const uint8_t hx = roix / 2;
  const uint8_t hy = roiy / 2;
  const uint8_t dx = roix % 2 != 0 ? 0 : 1;
  const uint8_t dy = roiy % 2 != 0 ? 0 : 1;

  if ((roix < 16) || (roiy < 16)) {
    lx = ocx - hx;
    rx = ocx + hx - dx;
    ly = ocy - hy + dy;
    ry = ocy + hy;

    if ((rx + 1) - (lx + 1) < 4) { // check width >= 4 SPADs
      const uint8_t drx = 3 - ((rx + 1) - (lx + 1));

      if (rx < (15 - drx)) {
        rx += drx;
      } else {
        lx  -= drx;
        ocx -= drx / 2;
      }
    }

    if (rx > 15) { // do we fall off from the right?
      const uint8_t drx = rx - 15;
      rx  -= drx;
      lx  -= drx;
      ocx -= drx;
    }

    if ((ry + 1) - (ly + 1) < 4) { // check height >= 4 SPADs
      const uint8_t dry = 3 - ((ry + 1) - (ly + 1));

      if (ry < ((15 - dry))) {
        ry += dry;
      } else {
        ly  -= dry;
        ocy -= dry / 2;
      }
    }

    if (ry > 15) { // do we fall off from the bottom?
      const uint8_t dry = ry - 15;
      ry  -= dry;
      ly  -= dry;
      ocy -= dry;
    }
  }

  // addLog(LOG_LEVEL_INFO, strformat(F("drawSelectionArea E lx:%d ly:%d rx:%d ry:%d opt.center:%d ocx:%d ocy:%d hx:%d hy:%d dx:%d dy:%d"),
  //                                  lx, ly, rx, ry, opticalCenter, ocx, ocy, hx, hy, dx, dy));

  for (uint8_t y = 0; y < 16; ++y) {
    for (uint8_t x = 0; x < 16; ++x) {
      const bool sel = x >= lx && x <= rx && y >= ly && y <= ry;
      const bool oc  = x == ocx && y == ocy;

      addHtml(F("<div class='cel"));

      if (sel) { addHtml(F(" sel")); }

      if (oc) { addHtml(F(" oc")); }

      addHtml('\'', '>');
      addHtmlInt(getSPAD(y, x));
      addHtml(F("</div>"));
    }
  }

  addHtml(F("</section>"));
}

void P113_data_struct::loadJavascript() {
  serve_JS(JSfiles_e::P113_script); // Source in static/p113_script.js, minified script source in src/src/Static/WebStaticData.h
}

void P113_data_struct::loadCss() {
  addHtml(F("<style type='text/css'>" // Move to WebStaticData.h? (no matching support functions for embedding css in the page-body yet)
            ":root{--bg113b:#dbff0045}[data-theme=dark]{--bg113b:#55304445}"
            "@media (prefers-color-scheme:dark){[data-theme=auto]{--bg113b:#55304445}}"
            "#vi{border:1px solid var(--c3);padding:0;overflow:auto;display:grid;grid-template-columns:repeat(16,2rem);gap:0;position:relative;-webkit-user-select:none;user-select:none}"
            ".cel{width:2rem;height:2rem;border-right:1px solid var(--c3);border-bottom:1px solid var(--c3);text-align:center;line-height:2rem;vertical-align:middle;display:inline-block}"
            ".cel.sel{background-color:var(--bg5)}"
            ".cel.oc{font-style:italic;text-decoration:underline}"
            "#sL{position:absolute;top:0;left:0;border:1px solid var(--bg5);background-color:var(--bg113b);z-index:99999}"

            "</style>"));
}

# endif // if P113_USE_ROI

#endif // ifdef USES_P113

#include "../PluginStructs/P117_data_struct.h"

#ifdef USES_P117

// **************************************************************************/
// Constructor
// **************************************************************************/
P117_data_struct::P117_data_struct(uint16_t altitude,
                                   float    temperatureOffset,
                                   bool     autoCalibration,
                                   uint16_t interval)
  : _altitude(altitude), _temperatureOffset(temperatureOffset), _autoCalibration(autoCalibration), _interval(interval) {}


// **************************************************************************/
// Initialize sensor and read data from SCD30
// **************************************************************************/
uint32_t P117_data_struct::read_sensor(uint16_t *scd30_CO2, uint16_t *scd30_CO2EAvg, float *scd30_Temp, float *scd30_Humid) {
  if (!initialised) {
    initialised = init_sensor(); // Check id device is present
  }

  if (initialised) {
    return scd30.readMeasurement(scd30_CO2, scd30_CO2EAvg, scd30_Temp, scd30_Humid);
  }
  return ERROR_SCD30_NO_DATA;
}

// **************************************************************************/
// Check SCD30 presence and initialize
// **************************************************************************/
bool P117_data_struct::softReset() {
  if (initialised) {
    scd30.softReset();
  }
  return initialised;
}

// **************************************************************************/
// Check SCD30 presence and initialize
// **************************************************************************/
bool P117_data_struct::init_sensor() {
  if (!initialised) {
    scd30.begin();

    scd30.setCalibrationType(_autoCalibration);
    scd30.setMeasurementInterval(_interval);

    scd30.beginMeasuring();
    scd30.setAltitudeCompensation(_altitude);
    scd30.setTemperatureOffset(_temperatureOffset);
    return true;
  }

  return initialised;
}

int P117_data_struct::setCalibrationMode(bool isAuto) {
  if (initialised) {
    _autoCalibration = isAuto;
    return scd30.setCalibrationType(isAuto);
  }
  return ERROR_SCD30_NOT_FOUND_ERROR;
}

int P117_data_struct::setForcedRecalibrationFactor(uint16_t co2_ppm) {
  if (initialised) {
    setCalibrationMode(false); // Force to manual mode
    return scd30.setForcedRecalibrationFactor(co2_ppm);
  }
  return ERROR_SCD30_NOT_FOUND_ERROR;
}

int P117_data_struct::setMeasurementInterval(uint16_t interval) {
  if (initialised) {
    if ((interval >= 2) && (interval <= 1800)) {
      return scd30.setMeasurementInterval(interval);
    } else {
      return ERROR_SCD30_INVALID_VALUE;
    }
  }
  return ERROR_SCD30_NOT_FOUND_ERROR;
}

#endif // ifdef USES_P117

#include "../PluginStructs/P090_data_struct.h"

#ifdef USES_P090

#include "../Globals/I2Cdev.h"

// Register addresses
# define CSS811_STATUS          0x00
# define CSS811_MEAS_MODE       0x01
# define CSS811_ALG_RESULT_DATA 0x02
# define CSS811_RAW_DATA        0x03
# define CSS811_ENV_DATA        0x05
# define CSS811_NTC             0x06
# define CSS811_THRESHOLDS      0x10
# define CSS811_BASELINE        0x11
# define CSS811_HW_ID           0x20
# define CSS811_HW_VERSION      0x21
# define CSS811_FW_BOOT_VERSION 0x23
# define CSS811_FW_APP_VERSION  0x24
# define CSS811_ERROR_ID        0xE0
# define CSS811_APP_START       0xF4
# define CSS811_SW_RESET        0xFF

// ****************************************************************************//
//
//  LIS3DHCore functions
//
//  For I2C, construct LIS3DHCore myIMU(<address>);
//
//  Default <address> is 0x5B.
//
// ****************************************************************************//
CCS811Core::CCS811Core(uint8_t inputArg) : I2CAddress(inputArg)
{}

void CCS811Core::setAddress(uint8_t address)
{
  I2CAddress = address;
}

CCS811Core::status CCS811Core::beginCore(void)
{
  CCS811Core::status returnError = SENSOR_SUCCESS;

  // Wire.begin(); // not necessary

    # ifdef __AVR__
    # else
    # endif

    # ifdef __MK20DX256__
    # else
    # endif

    # ifdef ARDUINO_ARCH_ESP8266
    # else
    # endif

  // Spin for a few ms
  // ESPEASY_VOLATILE(uint8_t) temp = 0;
  // FIXME TD-er: This is a rather odd way to avoid calling "delay"
  // for (uint16_t i = 0; i < 10000; i++)
  // {
  //   temp++;
  // }
  // FIX tonhuisman: No need to 'wait' if no action has been started yet

  while (Wire.available()) // Clear wire as a precaution
  {
    Wire.read();
  }

  // Check the ID register to determine if the operation was a success.
  uint8_t readCheck;
  readCheck   = 0;
  returnError = readRegister(CSS811_HW_ID, &readCheck);

  if (returnError != SENSOR_SUCCESS)
  {
    return returnError;
  }

  if (readCheck != 0x81)
  {
    returnError = SENSOR_ID_ERROR;
  }

  return returnError;
} // CCS811Core::beginCore

// ****************************************************************************//
//
//  ReadRegister
//
//  Parameters:
//    offset -- register to read
//    *outputPointer -- Pass &variable (address of) to save read data to
//
// ****************************************************************************//
CCS811Core::status CCS811Core::readRegister(uint8_t offset, uint8_t *outputPointer)
{
  bool wire_status = false;

  *outputPointer = I2C_read8_reg(I2CAddress, offset, &wire_status);

  if (wire_status) {
    return SENSOR_SUCCESS;
  }
  return SENSOR_I2C_ERROR;
}

// ****************************************************************************//
//
//  writeRegister
//
//  Parameters:
//    offset -- register to write
//    dataToWrite -- 8 bit data to write to register
//
// ****************************************************************************//
CCS811Core::status CCS811Core::writeRegister(uint8_t offset, uint8_t dataToWrite)
{
  if (I2C_write8_reg(I2CAddress, offset, dataToWrite)) {
    return SENSOR_SUCCESS;
  }
  return SENSOR_I2C_ERROR;
}

// ****************************************************************************//
//
//  multiReadRegister
//
//  Parameters:
//    offset -- register to read
//    *inputPointer -- Pass &variable (base address of) to save read data to
//    length -- number of bytes to read
//
//  Note:  Does not know if the target memory space is an array or not, or
//    if there is the array is big enough.  if the variable passed is only
//    two bytes long and 3 bytes are requested, this will over-write some
//    other memory!
//
// ****************************************************************************//
CCS811Core::status CCS811Core::multiWriteRegister(uint8_t offset, uint8_t *inputPointer, uint8_t length)
{
  CCS811Core::status returnError = SENSOR_SUCCESS;

  // define pointer that will point to the external space
  uint8_t i = 0;

  // Set the address
  Wire.beginTransmission(I2CAddress);
  Wire.write(offset);

  while (i < length)           // send data bytes
  {
    Wire.write(*inputPointer); // receive a uint8_t as character
    inputPointer++;
    i++;
  }

  if (Wire.endTransmission() != 0)
  {
    returnError = SENSOR_I2C_ERROR;
  }

  return returnError;
}

// ****************************************************************************//
//
//  Main user class -- wrapper for the core class + maths
//
//  Construct with same rules as the core ( uint8_t busType, uint8_t inputArg )
//
// ****************************************************************************//
CCS811::CCS811(uint8_t inputArg) : CCS811Core(inputArg)
{
  refResistance = 10000;
  resistance    = 0;
  _temperature  = 0;
  tVOC          = 0;
  CO2           = 0;
}

// ****************************************************************************//
//
//  Begin
//
//  This starts the lower level begin, then applies settings
//
// ****************************************************************************//
CCS811Core::status CCS811::begin(void)
{
  uint8_t data[4] = { 0x11, 0xE5, 0x72, 0x8A };    // Reset key

  CCS811Core::status returnError = SENSOR_SUCCESS; // Default error state

  // restart the core
  returnError = beginCore();

  if (returnError != SENSOR_SUCCESS)
  {
    return returnError;
  }

  // Reset the device
  multiWriteRegister(CSS811_SW_RESET, data, 4);
  delay(1);

  if (checkForStatusError() == true)
  {
    return SENSOR_INTERNAL_ERROR;
  }

  if (appValid() == false)
  {
    return SENSOR_INTERNAL_ERROR;
  }

  // Write 0 bytes to this register to start app
  if (I2C_write8(I2CAddress, CSS811_APP_START) != 0)
  {
    return SENSOR_I2C_ERROR;
  }

  delay(200); // FIXME OOPS?

  // returnError = setDriveMode(1); //Read every second
  //    ESPEASY_SERIAL_0.println();

  return returnError;
} // CCS811::begin

// ****************************************************************************//
//
//  Sensor functions
//
// ****************************************************************************//
// Updates the total voltatile organic compounds (TVOC) in parts per billion (PPB)
// and the CO2 value
// Returns nothing
CCS811Core::status CCS811::readAlgorithmResults(void)
{
  I2Cdata_bytes data(4, CSS811_ALG_RESULT_DATA);
  bool allDataRead = I2C_read_bytes(I2CAddress, data);

  if (!allDataRead) {
    return SENSOR_I2C_ERROR;
  }

  // Data ordered:
  // co2MSB, co2LSB, tvocMSB, tvocLSB

  CO2  = ((uint16_t)data[CSS811_ALG_RESULT_DATA + 0] << 8) | data[CSS811_ALG_RESULT_DATA + 1];
  tVOC = ((uint16_t)data[CSS811_ALG_RESULT_DATA + 2] << 8) | data[CSS811_ALG_RESULT_DATA + 3];
  return SENSOR_SUCCESS;
}

// Checks to see if error bit is set
bool CCS811::checkForStatusError(void)
{
  uint8_t value;

  // return the status bit
  readRegister(CSS811_STATUS, &value);
  return value & (1 << 0);
}

// Checks to see if DATA_READ flag is set in the status register
bool CCS811::dataAvailable(void)
{
  uint8_t value;

  CCS811Core::status returnError = readRegister(CSS811_STATUS, &value);

  if (returnError != SENSOR_SUCCESS)
  {
    return false;
  }
  else
  {
    return value & (1 << 3);
  }
}

// Checks to see if APP_VALID flag is set in the status register
bool CCS811::appValid(void)
{
  uint8_t value;

  CCS811Core::status returnError = readRegister(CSS811_STATUS, &value);

  if (returnError != SENSOR_SUCCESS)
  {
    return false;
  }
  else
  {
    return value & (1 << 4);
  }
}

uint8_t CCS811::getErrorRegister(void)
{
  uint8_t value;

  CCS811Core::status returnError = readRegister(CSS811_ERROR_ID, &value);

  if (returnError != SENSOR_SUCCESS)
  {
    return 0xFF;
  }
  else
  {
    return value; // Send all errors in the event of communication error
  }
}

// Returns the baseline value
// Used for telling sensor what 'clean' air is
// You must put the sensor in clean air and record this value
uint16_t CCS811::getBaseline(void)
{
  return I2C_read16_reg(I2CAddress, CSS811_BASELINE);
}

CCS811Core::status CCS811::setBaseline(uint16_t input)
{
  if (I2C_write16_reg(I2CAddress, CSS811_BASELINE, input)) {
    return SENSOR_SUCCESS;
  }
  return SENSOR_I2C_ERROR;
}

// Enable the nINT signal
CCS811Core::status CCS811::enableInterrupts(void)
{
  uint8_t value;
  CCS811Core::status returnError = readRegister(CSS811_MEAS_MODE, &value); // Read what's currently there

  if (returnError != SENSOR_SUCCESS)
  {
    return returnError;
  }

  //    ESPEASY_SERIAL_0.println(value, HEX);
  value |= (1 << 3); // Set INTERRUPT bit
  writeRegister(CSS811_MEAS_MODE, value);

  //    ESPEASY_SERIAL_0.println(value, HEX);
  return returnError;
}

// Disable the nINT signal
CCS811Core::status CCS811::disableInterrupts(void)
{
  uint8_t value;

  CCS811Core::status returnError = readRegister(CSS811_MEAS_MODE, &value); // Read what's currently there

  if (returnError != SENSOR_SUCCESS)
  {
    return returnError;
  }

  value      &= ~(1 << 3); // Clear INTERRUPT bit
  returnError = writeRegister(CSS811_MEAS_MODE, value);
  return returnError;
}

// Mode 0 = Idle
// Mode 1 = read every 1s
// Mode 2 = every 10s
// Mode 3 = every 60s
// Mode 4 = RAW mode
CCS811Core::status CCS811::setDriveMode(uint8_t mode)
{
  if (mode > 4)
  {
    mode = 4; // sanitize input
  }

  uint8_t value;
  CCS811Core::status returnError = readRegister(CSS811_MEAS_MODE, &value); // Read what's currently there

  if (returnError != SENSOR_SUCCESS)
  {
    return returnError;
  }

  value      &= ~(0b00000111 << 4); // Clear DRIVE_MODE bits
  value      |= (mode << 4);        // Mask in mode
  returnError = writeRegister(CSS811_MEAS_MODE, value);
  return returnError;
}

// Given a temp and humidity, write this data to the CSS811 for better compensation
// This function expects the humidity and temp to come in as floats
CCS811Core::status CCS811::setEnvironmentalData(float relativeHumidity, float temperature)
{
  // Check for invalid temperatures
  if ((temperature < -25) || (temperature > 50))
  {
    return SENSOR_GENERIC_ERROR;
  }

  // Check for invalid humidity
  if ((relativeHumidity < 0) || (relativeHumidity > 100))
  {
    return SENSOR_GENERIC_ERROR;
  }

  uint32_t rH   = relativeHumidity * 1000; // 42.348 becomes 42348
  uint32_t temp = temperature * 1000;      // 23.2 becomes 23200

  uint8_t envData[4];

  //Split value into 7-bit integer and 9-bit fractional

  //Incorrect way from datasheet.
  //envData[0] = ((rH % 1000) / 100) > 7 ? (rH / 1000 + 1) << 1 : (rH / 1000) << 1;
  //envData[1] = 0; //CCS811 only supports increments of 0.5 so bits 7-0 will always be zero
  //if (((rH % 1000) / 100) > 2 && (((rH % 1000) / 100) < 8))
  //{
  //	envData[0] |= 1; //Set 9th bit of fractional to indicate 0.5%
  //}

  //Correct rounding. See issue 8: https://github.com/sparkfun/Qwiic_BME280_CCS811_Combo/issues/8
  envData[0] = (rH + 250) / 500;
  envData[1] = 0; //CCS811 only supports increments of 0.5 so bits 7-0 will always be zero

  temp += 25000; //Add the 25C offset
  //Split value into 7-bit integer and 9-bit fractional
  //envData[2] = ((temp % 1000) / 100) > 7 ? (temp / 1000 + 1) << 1 : (temp / 1000) << 1;
  //envData[3] = 0;
  //if (((temp % 1000) / 100) > 2 && (((temp % 1000) / 100) < 8))
  //{
  //	envData[2] |= 1;  //Set 9th bit of fractional to indicate 0.5C
  //}

  //Correct rounding
  envData[2] = (temp + 250) / 500;
  envData[3] = 0;

  CCS811Core::status returnError = multiWriteRegister(CSS811_ENV_DATA, envData, 4);

  return returnError;
} // CCS811::setEnvironmentalData

void CCS811::setRefResistance(float input)
{
  refResistance = input;
}

CCS811Core::status CCS811::readNTC(void)
{
  I2Cdata_bytes data(4, CSS811_NTC);
  bool allDataRead = I2C_read_bytes(I2CAddress, data);

  if (!allDataRead) {
    return SENSOR_I2C_ERROR;
  }

  vrefCounts = (static_cast<uint16_t>(data[CSS811_NTC + 0]) << 8) | data[CSS811_NTC + 1];

  // ESPEASY_SERIAL_0.print("vrefCounts: ");
  // ESPEASY_SERIAL_0.println(vrefCounts);
  ntcCounts = (static_cast<uint16_t>(data[CSS811_NTC + 2]) << 8) | data[CSS811_NTC + 3];

  // ESPEASY_SERIAL_0.print("ntcCounts: ");
  // ESPEASY_SERIAL_0.println(ntcCounts);
  // ESPEASY_SERIAL_0.print("sum: ");
  // ESPEASY_SERIAL_0.println(ntcCounts + vrefCounts);
  resistance = (static_cast<float>(ntcCounts) * refResistance / static_cast<float>(vrefCounts));

  // Code from Milan Malesevic and Zoran Stupic, 2011,
  // Modified by Max Mayfield,
  _temperature = log(static_cast<long>(resistance));
  _temperature = 1  / (0.001129148f + (0.000234125f * _temperature) + (0.0000000876741f * _temperature * _temperature * _temperature));
  _temperature = _temperature - 273.15f; // Convert Kelvin to Celsius

  return SENSOR_SUCCESS;
}

uint16_t CCS811::getTVOC(void)
{
  return tVOC;
}

uint16_t CCS811::getCO2(void)
{
  return CO2;
}

float CCS811::getResistance(void)
{
  return resistance;
}

float CCS811::getTemperature(void)
{
  return _temperature;
}

// getDriverError decodes the CCS811Core::status type and prints the
// type of error to the serial terminal.
//
// Save the return value of any function of type CCS811Core::status, then pass
// to this function to see what the output was.
const __FlashStringHelper * CCS811::getDriverError(CCS811Core::status errorCode)
{
  switch (errorCode)
  {
    case CCS811Core::SENSOR_SUCCESS:
      return F("SUCCESS");

    case CCS811Core::SENSOR_ID_ERROR:
      return F("ID_ERROR");

    case CCS811Core::SENSOR_I2C_ERROR:
      return F("I2C_ERROR");

    case CCS811Core::SENSOR_INTERNAL_ERROR:
      return F("INTERNAL_ERROR");

    case CCS811Core::SENSOR_GENERIC_ERROR:
      return F("GENERIC_ERROR");

    default:
      return F("Unknown");
  }
}

// getSensorError gets, clears, then prints the errors
// saved within the error register.
const __FlashStringHelper * CCS811::getSensorError()
{
  uint8_t error = getErrorRegister();

  if (error == 0xFF)
  {
    return F("Failed to get ERROR_ID register.");
  }
  else
  {
    if (error & 1 << 5)
    {
      return F("HeaterSupply");
    }

    if (error & 1 << 4)
    {
      return F("HeaterFault");
    }

    if (error & 1 << 3)
    {
      return F("MaxResistance");
    }

    if (error & 1 << 2)
    {
      return F("MeasModeInvalid");
    }

    if (error & 1 << 1)
    {
      return F("ReadRegInvalid");
    }

    if (error & 1 << 0)
    {
      return F("MsgInvalid");
    }
  }
  return F("");
}

P090_data_struct::P090_data_struct(uint8_t i2cAddr) :
  myCCS811(0x5B) // start with default, but will update later on with user settings
{
  myCCS811.setAddress(i2cAddr);
}

#endif // ifdef USES_P090

#include "../PluginStructs/P052_data_struct.h"

#ifdef USES_P052

P052_data_struct::~P052_data_struct() {
  modbus.reset();
}

void P052_data_struct::reset() {
  modbus.reset();
}

bool P052_data_struct::init(const ESPEasySerialPort port, const int16_t serial_rx, const int16_t serial_tx) {
  return modbus.init(port, serial_rx, serial_tx, 9600, P052_MODBUS_SLAVE_ADDRESS);
}

const __FlashStringHelper * P052_data_struct::Plugin_052_valuename(uint8_t value_nr, bool displayString) {
  const __FlashStringHelper *strings[] {
    F("Empty"),                  F(""),
    F("Carbon Dioxide"),         F("co2"),
    F("Temperature"),            F("T"),
    F("Humidity"),               F("H"),
    F("Relay Status"),           F("rel"),
    F("Temperature Adjustment"), F("Tadj"),
    F("ABC period"),             F("abc_per"),
    F("Error Status"),           F("err")
  };
  const size_t index         = (2 * value_nr) + (displayString ? 0 : 1);
  constexpr size_t nrStrings = NR_ELEMENTS(strings);

  if (index < nrStrings) {
    return strings[index];
  }
  return F("");
}

# if FEATURE_MQTT_DISCOVER
int Plugin_052_QueryVType(uint8_t value_nr) {
  constexpr int valueVTypes[] = {
    0,                                                     // empty
    static_cast<int>(Sensor_VType::SENSOR_TYPE_CO2_ONLY),  // co2
    static_cast<int>(Sensor_VType::SENSOR_TYPE_TEMP_ONLY), // temperature
    static_cast<int>(Sensor_VType::SENSOR_TYPE_HUM_ONLY),  // humidity
    static_cast<int>(Sensor_VType::SENSOR_TYPE_TEMP_ONLY), // temperature adjustment
    0,                                                     // abc_per
    0,                                                     // err
  };

  if (value_nr < NR_ELEMENTS(valueVTypes)) {
    return valueVTypes[value_nr];
  }
  return 0;
}

# endif // if FEATURE_MQTT_DISCOVER

void P052_data_struct::setABCperiod(int hours)
{
  // Enable: write 1 ... 65534 to HR14 and bit1 set to 0 at HR19
  // Disable: write 0 or 65535 to HR14 and bit1 set to 1 at HR19


  // Read HR19
  uint8_t errorcode = 0;
  int     value     = modbus.readHoldingRegister(P052_HR19_METER_CONTROL, errorcode);

  // Clear bit 1 in register and write back HR19
  if (bitRead(value, 1)) {
    bitClear(value, 1);
    modbus.writeSingleRegister(P052_HR19_METER_CONTROL, value, errorcode);
  }

  // Read HR14 and verify desired ABC period
  value = modbus.readHoldingRegister(P052_HR14_ABC_PERIOD, errorcode);

  // If HR14 (ABC period) is not the desired period,
  // write desired ABC period to HR14
  if (value != hours) {
    modbus.writeSingleRegister(P052_HR14_ABC_PERIOD, hours, errorcode);
  }
}

uint32_t P052_data_struct::getSensorID()
{
  uint8_t errorcode       = 0;
  const uint32_t sensorId = (modbus.readInputRegister(P052_IR30_SENSOR_ID_HIGH, errorcode) << 16) |
                            modbus.readInputRegister(P052_IR31_SENSOR_ID_LOW, errorcode);

  if (errorcode == 0) {
    return sensorId;
  }
  return 0;
}

bool P052_data_struct::readInputRegister(short addr, int& value)
{
  uint8_t errorcode = 0;

  value = modbus.readInputRegister(addr, errorcode);
  return errorcode == 0;
}

bool P052_data_struct::readHoldingRegister(short addr, int& value)
{
  uint8_t errorcode = 0;

  value = modbus.readHoldingRegister(addr, errorcode);
  return errorcode == 0;
}

const char p052_subcommands[] PROGMEM = "writehr|readhr|enableabc|disableabc|setabcperiod|setrelay";
enum class p052_subcommands_e {
  writehr, // Modbus Write Holding Register
  readhr,  // Modbus Read Holding Register
  enableabc,
  disableabc,
  setabcperiod,
  setrelay
};


bool P052_data_struct::plugin_write(struct EventStruct *event, const String& string)
{
  if (!isInitialized()) {
    return false;
  }

  bool success         = true;
  const String command = parseString(string, 1);

  if (equals(command, F("senseair"))) {
    const String subcommand = parseString(string, 2);

    const int command_i = GetCommandCode(subcommand.c_str(), p052_subcommands);

    if (command_i == -1) {
      // No matching subcommand found
      return false;
    }


    switch (static_cast<p052_subcommands_e>(command_i)) {
      case p052_subcommands_e::writehr:
      {
        // FIXME TD-er: Must test, never tested on real hardware yet....

        uint32_t addr = 0;
        uint32_t cmnd = 0;

        if (validUIntFromString(parseString(string, 3), addr) &&
            validUIntFromString(parseString(string, 4), cmnd)) {
          uint8_t errorcode = 0;
          modbus.writeSingleRegister(addr, cmnd, errorcode);
          success = 0 == errorcode;

          if (success && loglevelActiveFor(LOG_LEVEL_INFO)) {
            addLog(LOG_LEVEL_INFO, strformat(
                     F("Senseair command: %s=%d,%d"),
                     subcommand.c_str(),
                     addr,
                     cmnd));
          }
        }

        // Already sent log, thus return here
        return success;
      }
      case p052_subcommands_e::readhr:
      {
        uint32_t addr = 0;

        if (validUIntFromString(parseString(string, 3), addr)) {
          uint8_t   errorcode = 0;
          const int value     = modbus.readHoldingRegister(addr, errorcode);

          if (0 == errorcode) {
            success = true;

            if (Settings.UseRules) {
              eventQueue.add(
                event->TaskIndex,
                subcommand,
                strformat(
                  F("%d,%d"),
                  (addr + 1), // HR1 = addr 0x00
                  value));
            }
          }
        }
        break;
      }
      case p052_subcommands_e::enableabc:
      {
        uint32_t hours = 0;

        if (validUIntFromString(parseString(string, 3), hours)) {
          // FIXME TD-er: Implement


          // Read HR19

          // Clear bit 1 in register and write back HR19

          // Read HR14 and verify desired ABC period

          // If HR14 (ABC period) is not the desired period,
          // write desired ABC period to HR14
        }
        break;
      }
      case p052_subcommands_e::disableabc:
      {
        // FIXME TD-er: Implement

        // Read HR19

        // Set bit 1 in register and write back HR19
        break;
      }
      case p052_subcommands_e::setabcperiod:
      {
        // FIXME TD-er: Must test, never tested on real hardware yet....

        int32_t period = 0;

        if (validIntFromString(parseString(string, 3), period)) {
          if (period >= 0) {
            setABCperiod(period);
            success = true;
          }
        }
        break;
      }
      case p052_subcommands_e::setrelay:
      {
        int32_t state = 0;

        if (validIntFromString(parseString(string, 3), state)) {
          short relaystatus = 1;

          //  Refer to sensor model’s specification for voltage at 100% output.
          if (state == 0) { relaystatus = 0; }
          else if (state == 1) { relaystatus = 0x3FFF; } // 0x3FFF represents 100% output.
          else if (state == -1) { relaystatus = 0x7FFF; }

          if (1 != relaystatus) {
            modbus.writeSingleRegister(0x18, relaystatus);
            success = true;
          }
        }
        break;
      }
    }

    if (success && loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO, strformat(
               F("Senseair command: %s=%d"),
               subcommand.c_str(),
               event->Par2));
    }
  }
  return success;
}

#endif // ifdef USES_P052

#include "../PluginStructs/P168_data_struct.h"

#ifdef USES_P168

# include "../Helpers/CRC_functions.h"

/**************************************************************************
* Constructor
**************************************************************************/
P168_data_struct::P168_data_struct(uint8_t alsGain,
                                   uint8_t alsIntegration,
                                   uint8_t psmMode,
                                   uint8_t readMethod) :
  _als_gain(alsGain), _als_integration(alsIntegration), _psm_mode(psmMode), _readMethod(readMethod), initialized(false)
{
  // We already apply the correct WAIT for a read, so no need or desire to wait any longer.
  // AUTO enforces multiple WAIT options sequentially, so that's inhibited
  if (static_cast<uint8_t>(VEML_LUX_NORMAL) == _readMethod) {
    _readMethod = static_cast<uint8_t>(VEML_LUX_NORMAL_NOWAIT);
  } else if ((static_cast<uint8_t>(VEML_LUX_CORRECTED) == _readMethod) || (static_cast<uint8_t>(VEML_LUX_AUTO) == _readMethod)) {
    _readMethod = static_cast<uint8_t>(VEML_LUX_CORRECTED_NOWAIT);
  }
}

P168_data_struct::~P168_data_struct() {
  delete veml;
}

bool P168_data_struct::init(struct EventStruct *event) {
  veml = new (std::nothrow) Adafruit_VEML7700();

  // - Read sensor serial number
  if ((nullptr != veml) &&
      veml->begin(P168_I2C_ADDRESS)) {
    // Set config & start sensor
    veml->setGain(_als_gain);
    veml->setIntegrationTime(_als_integration);
    veml->setPowerSaveMode(_psm_mode);
    veml->enable(true);

    addLog(LOG_LEVEL_INFO, F("VEML : 6030/7700 Initialized."));

    initialized = true;
  } else {
    addLog(LOG_LEVEL_ERROR, F("VEML : 6030/7700 Init ERROR."));
  }

  return isInitialized();
}

/*****************************************************
* plugin_read
*****************************************************/
bool P168_data_struct::plugin_read(struct EventStruct *event)           {
  bool success = false;

  if (isInitialized() && veml->readReady()) {
    uint16_t amb = veml->readALS();
    float    lux = veml->readLux(static_cast<luxMethod>(_readMethod));
    uint16_t whi = veml->readWhite();

    // if (luxMethod::VEML_LUX_AUTO == static_cast<luxMethod>(_readMethod)) {
    //   addLog(LOG_LEVEL_INFO, strformat(F("VEML : 6030/7700 AutoLux, Lux: %.2f, Gain: %.3f, Integration: %d"),
    //                                    lux, veml->getGainValue(), veml->getIntegrationTimeValue()));
    // }
    UserVar.setFloat(event->TaskIndex, 0, lux);
    UserVar.setFloat(event->TaskIndex, 1, whi);
    UserVar.setFloat(event->TaskIndex, 2, amb);

    success = true;
  }

  return success;
}

/*****************************************************
* plugin_get_config_value
*****************************************************/
bool P168_data_struct::plugin_get_config_value(struct EventStruct *event,
                                               String            & string) {
  bool success = false;

  if (isInitialized()) {
    const String val = parseString(string, 1, '.');

    if (equals(val, F("gain"))) {
      string  = toString(veml->getGainValue(), 3);
      success = true;
    } else
    if (equals(val, F("integration"))) {
      string  = veml->getIntegrationTimeValue();
      success = true;
    }
  }

  return success;
}

#endif // ifdef USES_P168

#include "../PluginStructs/P083_data_struct.h"

#ifdef USES_P083

// Needed also here for PlatformIO's library finder as the .h file 
// is in a directory which is excluded in the src_filter
#include <Adafruit_SGP30.h>

P083_data_struct::P083_data_struct() {
  initialized = sgp.begin();
  init_time   = millis();
}

#endif // ifdef USES_P083

#include "../PluginStructs/P146_data_struct.h"

#ifdef USES_P146

# include "../ControllerQueue/C016_queue_element.h"
# include "../ESPEasyCore/ESPEasyRules.h"
# include "../Globals/C016_ControllerCache.h"
# include "../Globals/MQTT.h"
# include "../Helpers/_CPlugin_init.h"

P146_data_struct::P146_data_struct(struct EventStruct *event)
{
  LoadCustomTaskSettings(event->TaskIndex, _topics, P146_Nlines, 0);
  const char separator = static_cast<char>(P146_SEPARATOR_CHARACTER);

  dumper = new (std::nothrow) ESPEasyControllerCache_CSV_dumper(
    P146_GET_JOIN_TIMESTAMP, P146_GET_ONLY_SET_TASKS, separator,
    ESPEasyControllerCache_CSV_dumper::Target::MQTT);
}

P146_data_struct::~P146_data_struct()
{
  if (dumper != nullptr) {
    delete dumper;
    dumper = nullptr;
  }
}

uint32_t writeToMqtt(const String& str, bool send) {
  if (send) {
    MQTTclient.write((const uint8_t *)str.c_str(), str.length());
  }
  return str.length();
}

uint32_t writeToMqtt(const char& c, bool send) {
  if (send) {
    MQTTclient.write(static_cast<uint8_t>(c));
  }
  return 1;
}

uint32_t createTaskInfoJson(bool send) {
  size_t expected_size = 0;

  expected_size += writeToMqtt('[', send);

  for (taskIndex_t task = 0; validTaskIndex(task); ++task) {
    {
      if (task != 0) {
        expected_size += writeToMqtt(',', send);
      }
      expected_size += writeToMqtt(concat(F("{\"taskName\":\""), getTaskDeviceName(task)), send);
      expected_size += writeToMqtt(concat(F("\",\"taskIndex\":"), task), send);
      expected_size += writeToMqtt(concat(F(",\"pluginId\":"), getPluginID_from_TaskIndex(task).value), send);
      expected_size += writeToMqtt(F(",\"taskValues\":["), send);
    }

    for (taskVarIndex_t rel_index = 0; rel_index < INVALID_TASKVAR_INDEX; ++rel_index) {
      if (rel_index != 0) {
        expected_size += writeToMqtt(',', send);
      }

      // FIXME TD-er: getTaskValueName returns empty string when task is not enabled, therefore use cache.
      //              Should getTaskValueName return empty string when task is disabled?
      expected_size += writeToMqtt(wrap_String(Cache.getTaskDeviceValueName(task, rel_index), '"'), send);
    }
    {
      const String tail = F("]}");
      expected_size += writeToMqtt(tail, send);
    }
  }
  expected_size += writeToMqtt(']', send);
  return expected_size;
}

uint32_t P146_data_struct::sendTaskInfoInBulk(struct EventStruct *event) const
{
  const String topic   = getTopic(P146_TaskInfoTopicIndex, event->TaskIndex);
  size_t expected_size = 0;

  if (P146_GET_SEND_BINARY) { expected_size = createTaskInfoJson(false); }
  else if (dumper != nullptr) { expected_size = dumper->generateCSVHeader(false); }

  if (expected_size > 0) {
    if (MQTTclient.beginPublish(topic.c_str(), expected_size, false)) {
      if (P146_GET_SEND_BINARY) {
        createTaskInfoJson(true);
      } else {
        dumper->generateCSVHeader(true);
      }
      MQTTclient.endPublish();
    }
  }
  return 0;
}

uint32_t P146_data_struct::sendBinaryInBulk(taskIndex_t P146_TaskIndex, uint32_t maxMessageSize) const
{
  const controllerIndex_t enabledMqttController = firstEnabledMQTT_ControllerIndex();

  if (!validControllerIndex(enabledMqttController)) {
    return 0;
  }


  // Keep the current peek position, so we can reset it when we fail to deliver the data to the controller.
  int peekFileNr, peekReadPos, peekFileSize;

  if (!getPeekFilePos(peekFileNr, peekReadPos, peekFileSize)) {
    return 0;
  }

  String message;

  message += peekFileNr;
  message += ';';
  message += peekReadPos;
  message += ';';

  // Need to compute the nr. of chunks.
  // For this we need to 'reserve' some positions for this nr of chunks value.
  // Used an estimate of 5 here.
  const size_t data_left = (maxMessageSize - message.length() - 5);
  const size_t chunkSize = sizeof(C016_binary_element);
  size_t nrChunks        = data_left / ((2 * chunkSize) + 1);

  if (peekReadPos < peekFileSize) {
    // Try to serve the rest of the file in 1 go.
    const size_t chunksLeftInFile = (peekFileSize - peekReadPos) / chunkSize;

    if (chunksLeftInFile < nrChunks) {
      nrChunks = chunksLeftInFile;
    }
  }

  message += nrChunks;
  message += ';';
  const size_t messageLength = message.length();

  const size_t expectedMessageSize = messageLength + (nrChunks * ((2 * chunkSize) + 1));

  if (nrChunks == 0) {
    // Nothing to be sent
    return 0;
  }


  const String topic = getTopic(P146_PublishTopicIndex, P146_TaskIndex);

  if (!MQTTclient.beginPublish(topic.c_str(), expectedMessageSize, false)) {
    // Can't start a message
    return 0;
  }
  writeToMqtt(message, true);

  for (size_t chunk = 0; chunk < nrChunks; ++chunk) {
    C016_binary_element element;

    if (ControllerCache.peek(reinterpret_cast<uint8_t *>(&element), chunkSize))
    {
      // Test data to show layout of binary content:

      /*
            element._timestamp = 0x11223344;
            element.TaskIndex = 0x55;
            element.controller_idx = 0x66;
            element.sensorType = Sensor_VType::SENSOR_TYPE_NONE; // 0x00
            element.valueCount = 0x88;
            element.values[0] = 0x99;
            element.values[1] = 0xAA;
            element.values[2] = 0xBB;
            element.values[3] = 0xCC;
       */

      // Example of MQTT message containing a single CacheController sample:
      // Filenr;Filepos;Sample (24 bytes -> 48 HEX digits);
      // 17;14616;0000194300002a4300003b4300004c434433221155660088;

      // val[0]   val[1]   val[2]   val[3]   timestmp taskidx PluginID SensorType ValueCount
      // 00001943 00002a43 00003b43 00004c43 44332211 55      66       00         88
    }
    writeToMqtt(formatToHex_array(reinterpret_cast<const uint8_t *>(&element), chunkSize), true);
    writeToMqtt(';',                                                                       true);
  }
  MQTTclient.endPublish();

  // Restore peek position
  //  ControllerCache.setPeekFilePos(peekFileNr, peekReadPos);
  return expectedMessageSize;
}

bool P146_data_struct::prepare_BulkMQTT_message(taskIndex_t P146_TaskIndex)
{
  controllerIndex_t enabledMqttController = firstEnabledMQTT_ControllerIndex();

  if (!validControllerIndex(enabledMqttController)) {
    return 0;
  }

  // Just create an 'empty' controller queue message so we will use the controller's mechanism to limit the sending rate.
  String topic;
  String message;
  const bool callbackTask = true;

  return MQTTpublish(enabledMqttController, P146_TaskIndex, std::move(topic), std::move(message), false, callbackTask);
}

bool P146_data_struct::prepareCSVInBulk(taskIndex_t P146_TaskIndex,
                                        bool        joinTimestamp,
                                        bool        onlySetTasks,
                                        char        separator)
{
  if (dumper == nullptr) {
    dumper = new (std::nothrow) ESPEasyControllerCache_CSV_dumper(
      joinTimestamp,
      onlySetTasks,
      separator,
      ESPEasyControllerCache_CSV_dumper::Target::MQTT);
  }

  if (dumper == nullptr) { return false; }
  return prepare_BulkMQTT_message(P146_TaskIndex);
}

uint32_t P146_data_struct::sendCSVInBulk(taskIndex_t P146_TaskIndex, uint32_t maxMessageSize)
{
  if (dumper == nullptr) {
    return 0;
  }
  const controllerIndex_t enabledMqttController = firstEnabledMQTT_ControllerIndex();

  if (!validControllerIndex(enabledMqttController)) {
    return 0;
  }

  int startFileNr = 0;
  int startPos    = 0;
  int endFileNr   = 0;
  int endPos      = 0;

  size_t count    = 0;
  size_t nrChunks = 0;
  bool   done     = false;

  for (auto it = lines.begin(); !done && it != lines.end(); ++it) {
    if (nrChunks == 0) {
      startFileNr = it->startFileNr;
      startPos    = it->startPos;
    }

    const size_t lineLength = it->line.length();

    if ((count + lineLength) < maxMessageSize) {
      ++nrChunks;
      ++count; // CSV line separator
      count    += lineLength;
      endFileNr = it->endFileNr;
      endPos    = it->endPos;
    } else {
      done = true;
    }
  }

  while (!done) {
    if (ESP.getFreeHeap() < 5000) {
      done = true;
    } else {
      if (!dumper->createCSVLine()) {
        done = true;
      } else {
        lines.push_back(dumper->getCSVline());
        const size_t lineLength = lines.back().line.length();

        if (nrChunks == 0) {
          startFileNr = lines.back().startFileNr;
          startPos    = lines.back().startPos;
        }

        if ((count + lineLength) < maxMessageSize) {
          ++nrChunks;
          ++count; // CSV line separator
          count    += lineLength;
          endFileNr = lines.back().endFileNr;
          endPos    = lines.back().endPos;
        } else {
          done = true;
        }
      }
    }
  }

  if (nrChunks == 0) { return 0; }

  String message;

  message += startFileNr;
  message += ';';
  message += startPos;
  message += ';';
  message += endFileNr;
  message += ';';
  message += endPos;
  message += ';';
  message += nrChunks;
  message += ';';
  message += count;

  const size_t messageLength       = message.length();
  const size_t expectedMessageSize = messageLength + count;

  const String topic = getTopic(P146_PublishTopicIndex, P146_TaskIndex);

  if (!MQTTclient.beginPublish(topic.c_str(), expectedMessageSize, false)) {
    // Can't start a message
    return 0;
  }
  writeToMqtt(message, true);

  for (size_t chunk = 0; chunk < nrChunks; ++chunk) {
    dumper->createCSVLine();
    writeToMqtt('\n',               true); // Separator
    writeToMqtt(lines.front().line, true);
    lines.pop_front();
  }

  MQTTclient.endPublish();
  nrChunks = 0;
  count    = 0;

  return expectedMessageSize;
}

bool P146_data_struct::sendViaOriginalTask(
  taskIndex_t P146_TaskIndex, bool sendTimestamp)
{
  // Keep the current peek position, so we can reset it when we fail to deliver the data to the controller.
  int peekFileNr        = 0;
  const int peekReadPos =  ControllerCache.getPeekFilePos(peekFileNr);


  C016_binary_element element{};

  if (!C016_getTaskSample(element)) {
    return false;
  }

  if (!validTaskIndex(element.TaskIndex)) {
    return false;
  }

  if (Settings.UseRules) {
    EventStruct tmpEvent(element.TaskIndex);
    createRuleEvents(&tmpEvent);
  }


  bool success        = false;
  bool unusableSample = false;

  // Send the task values to a controller configured in this Cache Controller Reader task.
  // However we act as if we're sending from the original task to use the taskname and taskvalue names of that task.
  // We can't use the controllers linked in the original task, since this will probably
  // send the values again to the Cache Controller as they did in the first place.

  // Steps to take to flush it via the original tasks:
  // - Temporary copy the existing task values
  // - Copy values from the cache bin files to the task it had
  // - Call CPLUGIN_PROTOCOL_SEND
  // - Restore the values

  TaskValues_Data_t tmp;
  {
    const TaskValues_Data_t*data = UserVar.getRawTaskValues_Data(element.TaskIndex);

    if (data != nullptr) {
      for (uint8_t i = 0; i < element.valueCount; ++i) {
        tmp.copyValue(*data, i, element.sensorType);
      }
    }
  }

  for (controllerIndex_t x = 0; !unusableSample && x < CONTROLLER_MAX; x++)
  {
    // Make sure we are not sending to the cache controller.
    if (Settings.Protocol[x] != 16) {
      EventStruct tmpEvent(element.TaskIndex);
      tmpEvent.ControllerIndex = x;
      tmpEvent.idx             = Settings.TaskDeviceID[x][P146_TaskIndex];

      if (Settings.TaskDeviceSendData[x][P146_TaskIndex] &&
          Settings.ControllerEnabled[x] &&
          Settings.Protocol[x])
      {
        protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(x);

        if (validUserVar(&tmpEvent)) {
          String dummy;

          // FIXME TD-er: Must think about how to include the timestamp
          if (CPluginCall(ProtocolIndex, CPlugin::Function::CPLUGIN_PROTOCOL_SEND, &tmpEvent, dummy)) {
            // FIXME TD-er: What to do when multiple controllers are selected and one fails?
            // Also, what if we only sent 1 value instead of all?
            success = true;
          }
        } else {
          unusableSample = true;
        }
      }
    }
  }

  {
    TaskValues_Data_t*data = UserVar.getRawTaskValues_Data(element.TaskIndex);

    if (data != nullptr) {
      for (uint8_t i = 0; i < element.valueCount; ++i) {
        data->copyValue(tmp, i, element.sensorType);
      }
    }
  }

  if (!success && !unusableSample) {
    // May need to retry later using this sample
    ControllerCache.setPeekFilePos(peekFileNr, peekReadPos);
  }
  return success;
}

bool P146_data_struct::sendViaEvent_AllCache(taskIndex_t P146_TaskIndex,
                                             bool        sendTimestamp)
{
  if (!Settings.UseRules) { return false; }

  // Keep the current peek position, so we can reset it when we fail to deliver the data to the controller.
  int peekFileNr        = 0;
  const int peekReadPos =  ControllerCache.getPeekFilePos(peekFileNr);


  C016_binary_element element{};

  if (!C016_getTaskSample(element)) {
    return false;
  }

  if (!validTaskIndex(element.TaskIndex)) {
    return false;
  }

  const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(element.TaskIndex);

  if (!validDeviceIndex(DeviceIndex)) {
    return false;
  }

  String eventvalues;
  reserve_special(eventvalues, 64); // Enough for most use cases, prevent lots of memory allocations.
  eventvalues = strformat(
    F("%d,%d,%d,%d"),
    element.unixTime,
    element.valueCount,
    peekFileNr,
    peekReadPos);

  for (uint8_t varNr = 0; varNr < element.valueCount; ++varNr) {
    eventvalues += ',';
    uint8_t nrDecimals = 0;

    if (Device[DeviceIndex].configurableDecimals()) {
      nrDecimals = Cache.getTaskDeviceValueDecimals(element.TaskIndex, varNr);
    }

    eventvalues += element.values.getAsString(varNr, element.sensorType, nrDecimals);
  }
  eventQueue.add(element.TaskIndex, F("AllCache"), eventvalues);
  return true;
}

bool P146_data_struct::setPeekFilePos(int peekFileNr, int peekReadPos)
{
  {
    const int modulo_24 = peekReadPos % sizeof(C016_binary_element);

    if (modulo_24 != 0) { peekReadPos -= modulo_24; }
  }

  ControllerCache.setPeekFilePos(peekFileNr, peekReadPos);

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, concat(F("CacheReader : SetReadPos,"), peekFileNr) + ',' + peekReadPos);
  }

  return true;
}

void P146_data_struct::flush() { C016_flush(); }

bool P146_data_struct::getPeekFilePos(int& peekFileNr, int& peekReadPos, int& peekFileSize) const {
  peekFileNr  = 0;
  peekReadPos =  ControllerCache.getPeekFilePos(peekFileNr);

  if (peekFileNr <= 0) {
    return false;
  }

  peekFileSize = ControllerCache.getPeekFileSize(peekFileNr);

  if (peekFileSize < 0) {
    // Peek file is not opened. Setting peek file pos will try to open it.
    ControllerCache.setPeekFilePos(peekFileNr, peekReadPos);
    peekReadPos  = ControllerCache.getPeekFilePos(peekFileNr);
    peekFileSize = ControllerCache.getPeekFileSize(peekFileNr);
  }

  // FIXME TD-er: This is messy, but needed to increment peek file nr.
  if (peekReadPos >= peekFileSize) {
    ControllerCache.setPeekFilePos(peekFileNr, peekReadPos);
    peekReadPos  = ControllerCache.getPeekFilePos(peekFileNr);
    peekFileSize = ControllerCache.getPeekFileSize(peekFileNr);
  }

  if (peekFileSize < 0) {
    // peek file still not open.
    return false;
  }
  return true;
}

String P146_data_struct::getTopic(int index, taskIndex_t P146_TaskIndex) const {
  if ((index != P146_TaskInfoTopicIndex) && (index != P146_PublishTopicIndex)) {
    return EMPTY_STRING;
  }
  String topic = _topics[index];

  topic.replace(F("%tskname%"), getTaskDeviceName(P146_TaskIndex));
  topic = parseTemplate(topic);
  return topic;
}

#endif // ifdef USES_P146

#include "../PluginStructs/P099_data_struct.h"

#ifdef USES_P099
# include "../ESPEasyCore/ESPEasyNetwork.h"
# include "../Helpers/ESPEasy_Storage.h"
# include "../Helpers/Scheduler.h"
# include "../Helpers/StringConverter.h"
# include "../Helpers/SystemVariables.h"


# include <XPT2046_Touchscreen.h>

P099_data_struct::~P099_data_struct() {
  if (touchscreen != nullptr) {
    delete touchscreen;
    touchscreen = nullptr;
  }
}

/**
 * Proper reset and cleanup.
 */
void P099_data_struct::reset() {
  if (touchscreen != nullptr) {
    delete touchscreen;
    touchscreen = nullptr;
  }
  # ifdef PLUGIN_099_DEBUG
  addLog(LOG_LEVEL_INFO, F("P099 DEBUG Touchscreen reset."));
  # endif // PLUGIN_099_DEBUG
}

/**
 * Initialize data and set up the touchscreen.
 */
bool P099_data_struct::init(taskIndex_t taskIndex,
                            uint8_t     cs,
                            uint8_t     rotation,
                            bool        flipped,
                            uint8_t     z_treshold,
                            bool        send_xy,
                            bool        send_z,
                            bool        useCalibration,
                            uint16_t    ts_x_res,
                            uint16_t    ts_y_res) {
  reset();

  _address_ts_cs  = cs;
  _z_treshold     = z_treshold;
  _rotation       = rotation;
  _flipped        = flipped;
  _send_xy        = send_xy;
  _send_z         = send_z;
  _useCalibration = useCalibration;
  _ts_x_res       = ts_x_res;
  _ts_y_res       = ts_y_res;

  touchscreen = new (std::nothrow) XPT2046_Touchscreen(_address_ts_cs);

  if (touchscreen != nullptr) {
    touchscreen->setRotation(_rotation);
    touchscreen->setRotationFlipped(_flipped);
    touchscreen->begin();
    loadTouchObjects(taskIndex);
  # ifdef PLUGIN_099_DEBUG
    addLog(LOG_LEVEL_INFO, F("P099 DEBUG Plugin & touchscreen initialized."));
  } else {
    addLog(LOG_LEVEL_INFO, F("P099 DEBUG Touchscreen initialisation FAILED."));
  # endif // PLUGIN_099_DEBUG
  }
  return isInitialized();
}

/**
 * Properly initialized? then true
 */
bool P099_data_struct::isInitialized() const {
  return touchscreen != nullptr;
}

/**
 * Load the touch objects from the settings, and initialize then properly where needed.
 */
void P099_data_struct::loadTouchObjects(taskIndex_t taskIndex) {
  # ifdef PLUGIN_099_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO, concat(F("P099 DEBUG loadTouchObjects size: "), sizeof(StoredSettings)));
  }
  # endif // PLUGIN_099_DEBUG
  LoadCustomTaskSettings(taskIndex, reinterpret_cast<uint8_t *>(&StoredSettings), sizeof(StoredSettings));

  for (int i = 0; i < P099_MaxObjectCount; i++) {
    StoredSettings.TouchObjects[i].objectname[P099_MaxObjectNameLength - 1] = 0; // Terminate strings in case of uninitialized data
    SurfaceAreas[i]                                                         = 0; // Reset
    TouchTimers[i]                                                          = 0;
    TouchStates[i]                                                          = false;
  }
}

/**
 * Check if the screen is touched.
 */
bool P099_data_struct::touched() {
  if (isInitialized()) {
    return touchscreen->touched();
  }
  return false;
}

/**
 * Read the raw data if the touchscreen is initialized.
 */
void P099_data_struct::readData(uint16_t *x, uint16_t *y, uint8_t *z) {
  if (isInitialized()) {
    touchscreen->readData(x, y, z);
    # ifdef PLUGIN_099_DEBUG
    addLog(LOG_LEVEL_INFO, F("P099 DEBUG readData"));
    # endif // PLUGIN_099_DEBUG
  }
}

/**
 * Only set rotation if the touchscreen is initialized.
 */
void P099_data_struct::setRotation(uint8_t n) {
  if (isInitialized()) {
    touchscreen->setRotation(n);
    # ifdef PLUGIN_099_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, concat(F("P099 DEBUG Rotation set: "), (int)n));
    }
    # endif // PLUGIN_099_DEBUG
  }
}

/**
 * Only set rotationFlipped if the touchscreen is initialized.
 */
void P099_data_struct::setRotationFlipped(bool flipped) {
  if (isInitialized()) {
    touchscreen->setRotationFlipped(flipped);
    # ifdef PLUGIN_099_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      String log = F("P099 DEBUG RotationFlipped set: ");
      log += flipped;
      addLogMove(LOG_LEVEL_INFO, log);
    }
    # endif // PLUGIN_099_DEBUG
  }
}

/**
 * Determine if calibration is enabled and usable.
 */
bool P099_data_struct::isCalibrationActive() {
  return _useCalibration
         && StoredSettings.Calibration.top_left.x > 0
         && StoredSettings.Calibration.top_left.y > 0
         && StoredSettings.Calibration.bottom_right.x > 0
         && StoredSettings.Calibration.bottom_right.y > 0; // Enabled and all values != 0 => Active
}

/**
 * Check within the list of defined objects if we touched one of them.
 * The smallest matching surface is selected if multiple objects overlap.
 * Returns state, and sets selectedObjectName to the best matching object
 */
bool P099_data_struct::isValidAndTouchedTouchObject(uint16_t x,
                                                    uint16_t y,
                                                    String & selectedObjectName,
                                                    int    & selectedObjectIndex,
                                                    uint8_t  checkObjectCount) {
  uint32_t lastObjectArea = 0;
  bool     selected       = false;

  for (uint8_t objectNr = 0; objectNr < checkObjectCount; objectNr++) {
    String objectName = String(StoredSettings.TouchObjects[objectNr].objectname);

    if ((objectName.length() > 0)
        && !equals(objectName.substring(0, 1), '_')                      // Ignore if name starts with an underscore
        && (StoredSettings.TouchObjects[objectNr].bottom_right.x > 0)
        && (StoredSettings.TouchObjects[objectNr].bottom_right.y > 0)) { // Not initial could be valid
      if (SurfaceAreas[objectNr] == 0) {                                 // Need to calculate the surface area
        SurfaceAreas[objectNr] = (StoredSettings.TouchObjects[objectNr].bottom_right.x - StoredSettings.TouchObjects[objectNr].top_left.x) *
                                 (StoredSettings.TouchObjects[objectNr].bottom_right.y - StoredSettings.TouchObjects[objectNr].top_left.y);
      }

      if ((StoredSettings.TouchObjects[objectNr].top_left.x <= x)
          && (StoredSettings.TouchObjects[objectNr].top_left.y <= y)
          && (StoredSettings.TouchObjects[objectNr].bottom_right.x >= x)
          && (StoredSettings.TouchObjects[objectNr].bottom_right.y >= y)
          && ((lastObjectArea == 0)
              || (SurfaceAreas[objectNr] < lastObjectArea))) { // Select smallest area that fits the coordinates
        selectedObjectName  = objectName;
        selectedObjectIndex = objectNr;
        lastObjectArea      = SurfaceAreas[objectNr];
        selected            = true;
      }
      # ifdef PLUGIN_099_DEBUG

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        String log = F("P099 DEBUG Touched: obj: ");
        log += objectName;
        log += ',';
        log += StoredSettings.TouchObjects[objectNr].top_left.x;
        log += ',';
        log += StoredSettings.TouchObjects[objectNr].top_left.y;
        log += ',';
        log += StoredSettings.TouchObjects[objectNr].bottom_right.x;
        log += ',';
        log += StoredSettings.TouchObjects[objectNr].bottom_right.y;
        log += F(" surface:");
        log += SurfaceAreas[objectNr];
        log += F(" x,y:");
        log += x;
        log += ',';
        log += y;
        log += F(" sel:");
        log += selectedObjectName;
        log += '/';
        log += selectedObjectIndex;
        addLogMove(LOG_LEVEL_INFO, log);
      }
      # endif // PLUGIN_099_DEBUG
    }
  }
  return selected;
}

/**
 * Set the enabled/disabled state by inserting or deleting an underscore '_' as the first character of the object name.
 * Checks if the name doesn't exceed the max. length.
 */
bool P099_data_struct::setTouchObjectState(const String& touchObject, bool state, uint8_t checkObjectCount) {
  if (touchObject.isEmpty() || (touchObject[0] == '_')) { return false; }
  const String findObject = concat((state ? F("_") : F("")), touchObject); // When enabling, try to find a disabled object
  bool success            = false;

  for (uint8_t objectNr = 0; objectNr < checkObjectCount; objectNr++) {
    const String thisObject(StoredSettings.TouchObjects[objectNr].objectname);

    if ((thisObject.length() > 0) && findObject.equalsIgnoreCase(thisObject)) {
      if (state) {
        // Keep original character casing
        success = safe_strncpy(StoredSettings.TouchObjects[objectNr].objectname, thisObject.substring(1), P099_MaxObjectNameLength);
      } else {
        if (thisObject.length() < P099_MaxObjectNameLength - 2) {
          // Leave room for the underscore and the terminating 0.
          success = safe_strncpy(
            StoredSettings.TouchObjects[objectNr].objectname,
            concat(F("_"), thisObject), // disabledObject
            P099_MaxObjectNameLength);
        }
      }
      StoredSettings.TouchObjects[objectNr].objectname[P099_MaxObjectNameLength - 1] = 0; // Just to be safe
      # ifdef PLUGIN_099_DEBUG

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        String log = F("P099 setTouchObjectState: obj: ");
        log += thisObject;

        if (success) {
          log += F(", new state: ");
          log += (state ? F("en") : F("dis"));
          log += F("abled.");
        } else {
          log += F("failed!");
        }
        addLogMove(LOG_LEVEL_INFO, log);
      }
      # endif // PLUGIN_099_DEBUG
      // break; // Only first one found is processed
    }
  }
  return success;
}

/**
 * Scale the provided raw coordinates to screen-resolution coordinates if calibration is enabled/configured
 */
void P099_data_struct::scaleRawToCalibrated(uint16_t& x, uint16_t& y) {
  if (isCalibrationActive()) {
    uint16_t lx = x - StoredSettings.Calibration.top_left.x;

    if (lx <= 0) {
      x = 0;
    } else {
      if (lx > StoredSettings.Calibration.bottom_right.x) {
        lx = StoredSettings.Calibration.bottom_right.x;
      }
      float x_fact = static_cast<float>(StoredSettings.Calibration.bottom_right.x - StoredSettings.Calibration.top_left.x) /
                     static_cast<float>(_ts_x_res);
      x = static_cast<uint16_t>(lround(lx / x_fact));
    }
    uint16_t ly = y - StoredSettings.Calibration.top_left.y;

    if (ly <= 0) {
      y = 0;
    } else {
      if (ly > StoredSettings.Calibration.bottom_right.y) {
        ly = StoredSettings.Calibration.bottom_right.y;
      }
      float y_fact = (StoredSettings.Calibration.bottom_right.y - StoredSettings.Calibration.top_left.y) / _ts_y_res;
      y = static_cast<uint16_t>(lround(ly / y_fact));
    }
  }
}

const char p099_subcommands[] PROGMEM = "rot|flip|enable|disable";
enum class p099_subcommands_e {
  rot,
  flip,
  enable,
  disable
};

bool P099_data_struct::plugin_write(struct EventStruct *event, const String& string) {
  String command;
  String subcommand;

  int argIndex = string.indexOf(',');

  if (argIndex) {
    command    = parseString(string, 1);
    subcommand = parseString(string, 2);

    if (equals(command, F("touch"))) {
      int  command_i = GetCommandCode(subcommand.c_str(), p099_subcommands);

      if (command_i == -1) {
        // No matching subcommand found
        return false;
      }

      switch (static_cast<p099_subcommands_e>(command_i)) {
        case p099_subcommands_e::rot:
        {
          // touch,rot,<0..3> : Set rotation to 0, 90, 180, 270 degrees
          setRotation(static_cast<uint8_t>(parseString(string, 3).toInt() % 4));
          return true;
        }
        case p099_subcommands_e::flip:
        {
          // touch,flip,<0|1> : Flip rotation by 0 or 180 degrees
          setRotationFlipped(parseString(string, 3).toInt() > 0);
          return true;
        }
        case p099_subcommands_e::enable:
        {
          // touch,enable,<objectName> : Enables a disabled objectname (with a leading underscore)
          return setTouchObjectState(
            parseString(string, 3),
            true,
            P099_CONFIG_OBJECTCOUNT);
        }
        case p099_subcommands_e::disable:
        {
          // touch,disable,<objectName> : Disables an enabled objectname (without a leading underscore)
          return setTouchObjectState(
            parseString(string, 3),
            false,
            P099_CONFIG_OBJECTCOUNT);
        }
      }
    }
  }
  return false;
}

#endif // ifdef USES_P099


#include "../PluginStructs/P089_ping_service_struct.h"

#ifdef ESP32

P089_ping_service_struct*P089_ping_service = nullptr;

/**
 * Ping execute handler task
 */
void P089_execute_ping_task(void *parameter)
{
  P089_ping_task_data*ping_task_data = static_cast<P089_ping_task_data *>(parameter);

  if ((ping_task_data->status == P089_task_status::Working) && (nullptr != ping_task_data->espPing)) {
    // Blocking operation
    ping_task_data->result = ping_task_data->espPing->ping(ping_task_data->ip, ping_task_data->count);

    // Results are in
    ping_task_data->avgTime = ping_task_data->espPing->averageTime();
    ping_task_data->status  = P089_task_status::Ready;
  }

  ping_task_data->taskHandle = NULL;
  vTaskDelete(ping_task_data->taskHandle);
}

/**
 * P089_ping_service_struct
 */
P089_ping_service_struct::P089_ping_service_struct() {
  espPing = new (std::nothrow) PingClass();
}

P089_ping_service_struct::~P089_ping_service_struct() {
  if (_ping_task_data.taskHandle) {
    vTaskDelete(_ping_task_data.taskHandle);
    _ping_task_data.taskHandle = NULL;
  }
  _requests.clear();
  delete espPing;
  espPing = nullptr;
}

bool P089_ping_service_struct::addPingRequest(taskIndex_t       TaskIndex,
                                              P089_ping_request request) {
  bool result = false; // Something is not ok
  auto it     = _requests.find(TaskIndex);

  if ((request.status == P089_request_status::Request) &&
      ((it == _requests.end()) ||
       ((it->first == TaskIndex) && (it->second.status == P089_request_status::Finished)))) { // No request or Finished for this task
    _requests[TaskIndex]             = request;
    _requests[TaskIndex].status      = P089_request_status::Accepted;
    _requests[TaskIndex].lastStarted = millis();
    result                           = true;
  }

  return result;
}

bool P089_ping_service_struct::getPingResult(taskIndex_t        TaskIndex,
                                             P089_ping_request& request) {
  bool result = false;
  auto it     = _requests.find(TaskIndex);

  if (it != _requests.end()) {
    request = it->second;

    if (it->second.status == P089_request_status::Result) {
      it->second.status = P089_request_status::Finished; // Can be removed
      result            = true;
    }
  }

  // Check if we need to clean outdated requests
  it = _requests.begin();

  while (it != _requests.end()) {
    // Cleanup when counter reached
    if (timePassedSince(it->second.lastStarted) >= P089_FINISHED_REQUEST_TIMEOUT_MS) {
      addLog(LOG_LEVEL_INFO, strformat(F("PING : Clean last request for task %d (%u s)"),
                                       it->first + 1, timePassedSince(it->second.lastStarted) / 1000));
      _requests.erase(it);    // Cleanup
      it = _requests.begin(); // Restart loop
    } else {
      ++it;
    }
  }
  return result;
}

bool P089_ping_service_struct::loop() {
  bool result = false;

  if (timePassedSince(lastLooped) < 100) { // Some call/load throtteling
    return result;
  }
  lastLooped = millis();

  // addLog(LOG_LEVEL_INFO, F("PING : Running P089_ping_service->loop()")); // FIXME

  if (_ping_task_data.status == P089_task_status::Ready) {
    auto it = _requests.find(_ping_task_data.taskIndex);

    if (it != _requests.end()) {
      it->second.result  = _ping_task_data.result;
      it->second.avgTime = _ping_task_data.avgTime;
      it->second.status  = P089_request_status::Result;      // Can be retrieved
      result             = true;
    }
    _ping_task_data.status = P089_task_status::Initial;      // Available for new job
  }

  if (_ping_task_data.status == P089_task_status::Initial) { // Find next job
    auto it = _requests.begin();

    for (; it != _requests.end(); ++it) {
      if (it->second.status == P089_request_status::Accepted) {
        _ping_task_data.taskIndex = it->first;
        _ping_task_data.status    = P089_task_status::Working;
        _ping_task_data.count     = it->second.count;
        _ping_task_data.ip        = it->second.ip;
        _ping_task_data.espPing   = espPing;

        xTaskCreatePinnedToCore(
          P089_execute_ping_task,      // Function that should be called
          "PingClass.ping()",          // Name of the task (for debugging)
          4000,                        // Stack size (bytes)
          &_ping_task_data,            // Parameter to pass
          1,                           // Task priority
          &_ping_task_data.taskHandle, // Task handle
          xPortGetCoreID()             // Core you want to run the task on (0 or 1)
          );
        break;
      }
    }
  }
  return result;
}

#endif // ifdef ESP32

#include "../PluginStructs/P147_data_struct.h"

#ifdef USES_P147

# include "../Helpers/CRC_functions.h"

/**************************************************************************
* Constructor
**************************************************************************/
P147_data_struct::P147_data_struct(struct EventStruct *event)
{
  _sensorType      = static_cast<P147_sensor_e>(P147_SENSOR_TYPE);
  _initialCounter  = P147_LOW_POWER_MEASURE == 0 ? P147_SHORT_COUNTER : P147_LONG_COUNTER;
  _secondsCounter  = _initialCounter;
  _ignoreFirstRead = P147_LOW_POWER_MEASURE == 1;
  _useCompensation = P147_GET_USE_COMPENSATION;
  # if P147_FEATURE_GASINDEXALGORITHM
  _rawOnly = P147_GET_RAW_DATA_ONLY;
  # endif // if P147_FEATURE_GASINDEXALGORITHM

  if (validTaskIndex(P147_TEMPERATURE_TASK) && validTaskVarIndex(P147_TEMPERATURE_VALUE)) {
    _temperatureValueIndex = P147_TEMPERATURE_TASK * VARS_PER_TASK + P147_TEMPERATURE_VALUE;
  }

  if (validTaskIndex(P147_HUMIDITY_TASK) && validTaskVarIndex(P147_HUMIDITY_VALUE)) {
    _humidityValueIndex = P147_HUMIDITY_TASK * VARS_PER_TASK + P147_HUMIDITY_VALUE;
  }
  _initialized = false;
}

/*****************************************************
 * Init sensor and supporting objects
 ****************************************************/
bool P147_data_struct::init(struct EventStruct *event) {
  if (I2C_wakeup(P147_I2C_ADDRESS) == 0) {
    _initialized = true;

    // TODO Add initialization of IndexAlgorithm objects
    # if P147_FEATURE_GASINDEXALGORITHM
    vocGasIndexAlgorithm = new (std::nothrow) VOCGasIndexAlgorithm((P147_LOW_POWER_MEASURE == 0 ? P147_SHORT_COUNTER : P147_LONG_COUNTER) * 1.0f);

    if (nullptr == vocGasIndexAlgorithm) {
      _initialized = false;
    }

    if (_initialized && (_sensorType == P147_sensor_e::SGP41)) {
      noxGasIndexAlgorithm = new (std::nothrow) NOxGasIndexAlgorithm(); // Algorithm doesn't support a sampling interval

      if (nullptr == noxGasIndexAlgorithm) {
        _initialized = false;
      }
    }
    # endif // if P147_FEATURE_GASINDEXALGORITHM

    // Read serial number
    if (_initialized && I2C_write8_reg(P147_I2C_ADDRESS, P147_CMD_READ_SERIALNR_A, P147_CMD_READ_SERIALNR_B)) {
      uint16_t serialNumber[3] = { 0 };
      bool     is_ok           = true;
      delay(1);

      for (uint8_t d = 0; d < 3 && is_ok; d++) {
        serialNumber[d] = readCheckedWord(is_ok);
      }

      if (is_ok) {
        _serial   = static_cast<uint64_t>(serialNumber[0]);
        _serial <<= 16;
        _serial  |= static_cast<uint64_t>(serialNumber[1]);
        _serial <<= 16;
        _serial  |= static_cast<uint64_t>(serialNumber[2]);
      } else {
        _initialized = false;
      }
      addLog(LOG_LEVEL_INFO, concat(F("SGP4x: Serial number: 0x"), ull2String(_serial, 16u)));
      # if P147_FEATURE_GASINDEXALGORITHM

      if (!_rawOnly) {
        addLog(LOG_LEVEL_INFO, F("SGP4x: Attention: First values will be available after initial indexing!"));
      }
      # endif // if P147_FEATURE_GASINDEXALGORITHM
    }

    if (_initialized && I2C_write8_reg(P147_I2C_ADDRESS, P147_CMD_SELF_TEST_A, P147_CMD_SELF_TEST_B)) {
      _state = P147_state_e::MeasureTest;
      Scheduler.setPluginTaskTimer(P147_DELAY_SELFTEST, event->TaskIndex, 0); // Retrieve selftest result after 320 msec
    }

    // addLog(LOG_LEVEL_INFO,
    //        concat(F("P147 : INIT State: "), static_cast<int>(_state)) +
    //        boolToString(_initialized));
  }
  return isInitialized();
}

/*****************************************************
* Destructor
*****************************************************/
P147_data_struct::~P147_data_struct() {
  # if P147_FEATURE_GASINDEXALGORITHM
  delete vocGasIndexAlgorithm;
  delete noxGasIndexAlgorithm;
  # endif // if P147_FEATURE_GASINDEXALGORITHM
}

/*****************************************************
* plugin_tasktimer_in : Handle several delay related tasks
*****************************************************/
bool P147_data_struct::plugin_tasktimer_in(struct EventStruct *event) {
  bool success = false;
  bool is_ok;

  if (isInitialized()) {
    switch (_state)
    {
      case P147_state_e::MeasureTest:
      {
        const uint16_t result = readCheckedWord(is_ok);

        // addLog(LOG_LEVEL_INFO, concat(F("P147 : Selftest result: "), formatToHex(result)) + (is_ok ? F(" ok") : F(" error")));
        bool checkOk = false;

        if (_sensorType == P147_sensor_e::SGP40) {
          checkOk = ((result >> 8) & 0xFF) == 0xD4; // 0xD4xx = OK, 0x4Bxx = Error
        } else {
          checkOk = (result & 0xFF) == 0x00;        // 0xxx00 = OK 01..03 = Error
        }

        if (is_ok && checkOk) {
          success = true;
          _state  = P147_state_e::MeasureStart; // Start sequence
        } else {
          _state = P147_state_e::Uninitialized;
        }
        _initialized = success; // Failing selftest will disable the plugin
        break;
      }

      case P147_state_e::MeasureStart:
      case P147_state_e::MeasureTrigger:
      {
        uint16_t compensationT  = 0x6666; // Default values
        uint16_t compensationRh = 0x8000;
        float    temperature    = 25.0f;  // Default values
        float    humidity       = 50.0f;

        // addLog(LOG_LEVEL_INFO, F("P147 : MeasureStart"));

        if (_useCompensation && ((_temperatureValueIndex > -1) || (_humidityValueIndex > -1))) {
          if (_temperatureValueIndex > -1) {
            temperature = UserVar[_temperatureValueIndex];
          }

          if (_humidityValueIndex > -1) {
            humidity = UserVar[_humidityValueIndex];
          }

          // Sanity checks
          if (definitelyLessThan(temperature, -45.0f)) { temperature = -45.0f; }

          if (definitelyGreaterThan(temperature, 130.0f)) { temperature = 130.0f; }

          if (definitelyLessThan(humidity, 0.0f)) { humidity = 0.0f; }

          if (definitelyGreaterThan(humidity, 100.0f)) { humidity = 100.0f; }

          // Calculate ticks
          compensationT  = static_cast<uint16_t>((temperature + 45) * 65535 / 175);
          compensationRh = static_cast<uint16_t>(humidity * 65535 / 100);
        }

        if (startSensorRead(compensationRh, compensationT)) {
          if ((_readLoop == 0) && (P147_LOW_POWER_MEASURE == 1) && (P147_state_e::MeasureTrigger != _state)) {
            _readLoop = 1;                       // Skip first read after waking up
          }
          _state = P147_state_e::MeasureReading; // Starting a measurement also turns the heater on, just needs extra time

          Scheduler.setPluginTaskTimer((P147_LOW_POWER_MEASURE == 0 || _readLoop == 0)
                                       ? (_sensorType == P147_sensor_e::SGP40 ? P147_DELAY_REGULAR : P147_DELAY_REGULAR_SGP41)
                                       : P147_DELAY_LOW_POWER,
                                       event->TaskIndex,
                                       0);
        }
        break;
      }

      case P147_state_e::MeasureReading: // Get raw data
      {
        _rawVOC = readCheckedWord(is_ok);

        if (is_ok && (_lastCommand == P147_CMD_SGP41_READ_B) && (_sensorType == P147_sensor_e::SGP41)) {
          _rawNOx = readCheckedWord(is_ok);
        }

        // addLog(LOG_LEVEL_INFO,
        //        concat(F("P147 : MeasureReading raw VOC: "), _rawVOC) +
        //        concat(F(", raw NOx: "),                     _rawNOx) +
        //        concat(F(", loop: "),                        _readLoop) +
        //        (is_ok ? F(" ok") : F(" error")));

        if (is_ok && (_readLoop == 0)) {
          success = true;
          _state  = P147_state_e::Ready;

          # if P147_FEATURE_GASINDEXALGORITHM

          // Feed to normalizers
          _vocIndex = vocGasIndexAlgorithm->process(_rawVOC);

          if (_vocIndex == 0) {
            _skipCount++;
          }

          if ((_lastCommand == P147_CMD_SGP41_READ_B) && (_sensorType == P147_sensor_e::SGP41)) {
            _noxIndex = noxGasIndexAlgorithm->process(_rawNOx);
          }
          # endif // if P147_FEATURE_GASINDEXALGORITHM

          // Startup delay check for NOx measurement/normalizer
          if ((_startupNOxCounter == 0) || (_sensorType == P147_sensor_e::SGP40)) {
            _dataAvailable = true;                                               // Data can be read
          }

          if (P147_LOW_POWER_MEASURE == 1) {                                     // Turn  off heater
            I2C_write8_reg(P147_I2C_ADDRESS, P147_CMD_HEATER_OFF_A, P147_CMD_HEATER_OFF_B);
          }
          Scheduler.setPluginTaskTimer(P147_DELAY_MINIMAL, event->TaskIndex, 0); // Next step
        } else {
          _state = P147_state_e::MeasureStart;                                   // Restart from once_a_second

          if (_readLoop > 0) {
            _state = P147_state_e::MeasureTrigger;                               // Trigger only
            Scheduler.setPluginTaskTimer(_sensorType == P147_sensor_e::SGP40 ? P147_DELAY_REGULAR : P147_DELAY_REGULAR_SGP41,
                                         event->TaskIndex, 0);                   // Trigger actual read after heating up
          }
        }

        if (_readLoop > 0) { _readLoop--; }
        break;
      }

      case P147_state_e::Ready:
        _state = P147_state_e::MeasureStart; // When ready, start a new sequence from plugin_once_a_second
        break;

      case P147_state_e::Uninitialized:      // Keep compiler happy
        break;
    }
  }
  return success;
}

/*****************************************************
* plugin_once_a_second
*****************************************************/
bool P147_data_struct::plugin_once_a_second(struct EventStruct *event) {
  bool success = false;

  // addLog(LOG_LEVEL_INFO,
  //        concat(F("P147 : State: "),   static_cast<int>(_state)) +
  //        concat(F(", Last _rawVOC: "), _rawVOC) +
  //        concat(F(", Last _rawNOx: "), _rawNOx) +
  //        concat(F(", count: "),        _secondsCounter));

  if (isInitialized()) {
    _secondsCounter--;

    if (_startupNOxCounter > 0) { _startupNOxCounter--; }

    if (_secondsCounter == 0) {
      // Execute a measurement cycle
      if (_state == P147_state_e::MeasureStart) {
        // Trigger a cycle
        Scheduler.setPluginTaskTimer(P147_DELAY_MINIMAL, event->TaskIndex, 0); // Next step
        success = true;
      }

      // Reset counter
      _secondsCounter = _initialCounter;
    }
  }

  return success;
}

/*****************************************************
* plugin_read
*****************************************************/
bool P147_data_struct::plugin_read(struct EventStruct *event) {
  bool success = false;

  if (isInitialized()) {
    if (_dataAvailable) {
      if (_ignoreFirstRead) {
        _ignoreFirstRead = false;
      } else {
        # if P147_FEATURE_GASINDEXALGORITHM

        if (_rawOnly)
        # endif // if P147_FEATURE_GASINDEXALGORITHM
        {
          UserVar.setFloat(event->TaskIndex, 0, _rawVOC);
        }
        # if P147_FEATURE_GASINDEXALGORITHM
        else {
          UserVar.setFloat(event->TaskIndex, 0, _vocIndex); // Use normalized VOC index
        }
        # endif // if P147_FEATURE_GASINDEXALGORITHM

        if (_sensorType == P147_sensor_e::SGP41) {
          # if P147_FEATURE_GASINDEXALGORITHM

          if (_rawOnly)
          # endif // if P147_FEATURE_GASINDEXALGORITHM
          {
            UserVar.setFloat(event->TaskIndex, 1, _rawNOx);
          }
          # if P147_FEATURE_GASINDEXALGORITHM
          else {
            UserVar.setFloat(event->TaskIndex, 1, _noxIndex); // Use normalized NOx index
          }
          # endif // if P147_FEATURE_GASINDEXALGORITHM
        }
        # if P147_FEATURE_GASINDEXALGORITHM
        success = (_rawOnly || _vocIndex != 0); // Accepted if the VOC index is no longer 0 (NOx index ignored for now)

        if (success && !_rawOnly && (_skipCount > 0)) {
          addLog(LOG_LEVEL_INFO, concat(F("SGP4x: Valid values found, skipped samples: "), _skipCount));
          _skipCount = 0;
        }
        # else // if P147_FEATURE_GASINDEXALGORITHM
        success = true;
        # endif // if P147_FEATURE_GASINDEXALGORITHM
      }
    }
  }
  return success;
}

/*****************************************************
* plugin_write
*****************************************************/
bool P147_data_struct::plugin_write(struct EventStruct *event,
                                    String            & string) {
  bool success = false;

  // const String command = parseString(string, 1);

  // if (equals(command, F("sgp4x"))) {
  //   // const String sub = parseString(string, 2);
  // }
  return success;
}

/*****************************************************
* plugin_get_config_value
*****************************************************/
bool P147_data_struct::plugin_get_config_value(struct EventStruct *event,
                                               String            & string) {
  bool success = false;

  const String var = parseString(string, 1);

  if (equals(var, F("serialnumber"))) { // [<taskname>#serialnumber] = the devices electronic serial number
    string  = ull2String(_serial);
    success = true;
  } else
  if (equals(var, F("rawvoc"))) { // [<taskname>#rawVOC] = the last raw VOC value retrieved from the sensor
    string  = _rawVOC;
    success = true;
  } else
  if (equals(var, F("rawnox")) &&
      (_sensorType == P147_sensor_e::SGP41)) { // [<taskname>#rawNOx] = the last raw NOx value retrieved from the sensor
    string  = _rawNOx;
    success = true;
  }
  return success;
}

// Private

/*****************************************************
 * readCheckedWord : Read 2 data bytes from I2C and validate checksum (3rd byte)
 ****************************************************/
uint16_t P147_data_struct::readCheckedWord(bool& is_ok, long extraDelay) {
  uint16_t result        = 0;
  uint8_t  data[3]       = { 0 };
  const uint32_t timeOut = millis();

  is_ok = false;
  Wire.requestFrom(P147_I2C_ADDRESS, 3);

  while (Wire.available() != 3 && timePassedSince(timeOut) < extraDelay) { // Wait extra 5 msec.
    delay(1);
  }

  if (Wire.available() == 3) {
    for (uint8_t d = 0; d < 3; d++) {
      data[d] = Wire.read();
    }

    if (calc_CRC8(data, 2) == data[2]) { // valid checksum?
      result = (data[0] << 8) | data[1];
      is_ok  = true;
    }
  }

  return result;
}

bool P147_data_struct::startSensorRead(uint16_t compensationRh, uint16_t compensationT) {
  uint8_t data[2] = { 0 };

  Wire.beginTransmission(P147_I2C_ADDRESS);       // Start

  if (_sensorType == P147_sensor_e::SGP40) {
    Wire.write((uint8_t)P147_CMD_SGP40_READ_A);   // SGP40 Command
    Wire.write((uint8_t)P147_CMD_SGP40_READ_B);
    _lastCommand = P147_CMD_SGP40_READ_B;         // Read only VOC
  } else {
    if (_startupNOxCounter == 0) {
      Wire.write((uint8_t)P147_CMD_SGP41_READ_A); // SGP41 regular read Command
      Wire.write((uint8_t)P147_CMD_SGP41_READ_B);
      _lastCommand = P147_CMD_SGP41_READ_B;       // Read VOC and NOx
    } else {
      Wire.write((uint8_t)P147_CMD_SGP41_COND_A); // SGP41 NOx Conditioning Command
      Wire.write((uint8_t)P147_CMD_SGP41_COND_B); // Only raw VOC is returned
      _lastCommand = P147_CMD_SGP41_COND_B;       // Conditioning, read only VOC
    }
  }
  data[0] = (compensationRh >> 8);
  data[1] = (compensationRh & 0xFF);
  Wire.write(data[0]);            // Rel. humidity compensation
  Wire.write(data[1]);
  Wire.write(calc_CRC8(data, 2)); // crc
  data[0] = (compensationT >> 8);
  data[1] = (compensationT & 0xFF);
  Wire.write(data[0]);            // Temperature compensation
  Wire.write(data[1]);
  Wire.write(calc_CRC8(data, 2)); // crc

  return Wire.endTransmission() == 0;
}

#endif // ifdef USES_P147

#include "../PluginStructs/P042_data_struct.h"

#ifdef USES_P042

P042_data_struct::P042_data_struct() {
  //
}

P042_data_struct::~P042_data_struct() {
  if (Candle_pixels != nullptr) {
    SetPixelsBlack(); // Turn off
    Candle_pixels->show();
    delete Candle_pixels;
    Candle_pixels = nullptr;
  }
}

bool P042_data_struct::plugin_init(struct EventStruct *event) {
  if (P042_CONFIG_PIXELCOUNT == 0) { P042_CONFIG_PIXELCOUNT = P042_NUM_PIXEL; }
  Candle_red    = P042_CONFIG_RED;
  Candle_green  = P042_CONFIG_GREEN;
  Candle_blue   = P042_CONFIG_BLUE;
  Candle_bright = P042_CONFIG_BRIGHTNESS;
  Candle_pxlcnt = P042_CONFIG_PIXELCOUNT;
  segment       = Candle_pxlcnt / 4;

  if ((Candle_red == 0) && (Candle_green == 0) && (Candle_blue == 0)) {
    Candle_bright = P042_BRIGHT_START;
  }
  Candle_type  = static_cast<P042_SimType>(P042_CONFIG_CANDLETYPE);
  Candle_color = static_cast<P042_ColorType>(P042_CONFIG_COLORTYPE);

  if ((nullptr == Candle_pixels) || !GPIO_Set) {
    GPIO_Set = validGpio(CONFIG_PIN1);

    if (GPIO_Set) {
      if (nullptr != Candle_pixels) {
        delete Candle_pixels;
      }
      Candle_pixels = new (std::nothrow) NeoPixelBus_wrapper(P042_CONFIG_PIXELCOUNT, CONFIG_PIN1, NEO_GRB + NEO_KHZ800);

      if (nullptr != Candle_pixels) {
        SetPixelsBlack();
        Candle_pixels->setBrightness(Candle_bright);
        Candle_pixels->begin();
      }

          # ifndef BUILD_NO_DEBUG

      if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
        addLogMove(LOG_LEVEL_DEBUG, concat(F("CAND : Init WS2812 Pin : "), static_cast<int>(CONFIG_PIN1)));
      }
          # endif // ifndef BUILD_NO_DEBUG
    }
  }

  return nullptr != Candle_pixels;
}

bool P042_data_struct::plugin_read(struct EventStruct *event) {
  UserVar.setFloat(event->TaskIndex, 0, Candle_red << 16 | Candle_green << 8 | Candle_blue);
  UserVar.setFloat(event->TaskIndex, 1, Candle_bright);
  UserVar.setFloat(event->TaskIndex, 2, static_cast<float>(Candle_type));

  return true;
}

bool P042_data_struct::plugin_once_a_second(struct EventStruct *event) {
  Candle_pixels->setBrightness(Candle_bright);
  Candle_pixels->show(); // This sends the updated pixel color to the hardware.
  return true;
}

bool P042_data_struct::plugin_fifty_per_second(struct EventStruct *event) {
  switch (Candle_type)
  {
    case P042_SimType::TypeOff: // "Update" for OFF
    {
      SetPixelsBlack();
      break;
    }

    case P042_SimType::TypeSimpleCandle: // Update for LIGHT
    {
      type_Static_Light();
      break;
    }

    case P042_SimType::TypeAdvancedCandle: // Random Updates for Simple Candle, Advanced Candle, Fire Simulation
    case P042_SimType::TypeStaticLight:
    {
      if (timeOutReached(Candle_Update)) {
        if (Candle_type == P042_SimType::TypeAdvancedCandle) {
          type_Simple_Candle();
        }

        if (Candle_type == P042_SimType::TypeStaticLight) {
          type_Advanced_Candle();
        }
        Candle_Update = millis() + HwRandom(25, 150);
      }
      break;
    }

    case P042_SimType::TypePolice: // Update for Police
    {
      if (timeOutReached(Candle_Update)) {
        type_Police();
        Candle_Update = millis() + 150;
      }
      break;
    }

    case P042_SimType::TypeBlink: // Update for Blink
    {
      if (timeOutReached(Candle_Update)) {
        type_BlinkStrobe();
        Candle_Update = millis() + 100;
      }
      break;
    }

    case P042_SimType::TypeStrobe: // Update for Strobe
    {
      type_BlinkStrobe();
      break;
    }
    case P042_SimType::TypeColorFader: // Update for ColorFader
    {
      if (timeOutReached(Candle_Update)) {
        type_ColorFader();
        Candle_Update = millis() + 2000;
      }
      break;
    }
  }

  Candle_pixels->show();

  return true;
}

bool P042_data_struct::plugin_write(struct EventStruct *event,
                                    const String      & string) {
  bool   success = false;
  char   cmdSep  = ','; // Try comma first
  String cmd     = parseString(string, 1, cmdSep);

  if (!equals(cmd, F("candle"))) {
    cmdSep = ':'; // Fallback to colon
    cmd    = parseString(string, 1, cmdSep);
  }

  // Test
  // MQTT   : mosquitto_pub -d -t sensors/espeasy/ESP_Candle/cmd  -m "CANDLE_OFF"
  // HTTP   : http://192.168.30.183/tools?cmd=CANDLE%3A5%3AFF0000%3A200
  //          http://192.168.30.183/tools?cmd=CANDLE:4:FF0000:200
  // SERIAL : CANDLE:4:FF0000:200<CR><LF>

  // Commands
  // CANDLE:<FlameType>:<Color>:<Brightness>
  //    <FlameType>  : 1 Static Light, 2 Simple Candle, 3 Advanced Candle, 4 Police, 5 Blink, 6 Strobe, 7 Color Fader
  //    <Color>      : n.def.  Use the default color
  //                   RRGGBB  Use color in RRGGBB style (red, green blue) as HEX
  //    <Brightness> : 0-255
  // Samples:   CANDLE:2::100           Simple Candle with Default color and Brigthness at 100
  //            CANDLE:5:FF0000:200     Blink with RED Color and Brigthness at 200
  //            CANDLE:0::              Candle OFF
  //            CANDLE:1::255           Candle ON - White and full brigthness
  // 2023-01-21: command can also be in lowercase, and separator can either be comma or colon, but all have to be the same

  if (equals(cmd, F("candle"))) {
    const String val_Type   = parseString(string, 2, cmdSep);
    const String val_Color  = parseString(string, 3, cmdSep);
    const String val_Bright = parseString(string, 4, cmdSep);

    if (!val_Type.isEmpty()) {
      if ((val_Type.toInt() > -1) && (val_Type.toInt() < P042_FLAME_OPTIONS)) {
        P042_CONFIG_CANDLETYPE = val_Type.toInt(); // Type
        Candle_type            = static_cast<P042_SimType>(P042_CONFIG_CANDLETYPE);
        # ifndef BUILD_NO_DEBUG

        if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
          addLogMove(LOG_LEVEL_DEBUG, concat(F("CAND : CMD - Type : "), val_Type));
        }
        # endif // ifndef BUILD_NO_DEBUG
      }
    }

    if (!val_Bright.isEmpty()) {
      if ((val_Bright.toInt() > -1) && (val_Bright.toInt() < 256)) {
        P042_CONFIG_BRIGHTNESS = val_Bright.toInt(); // Brightness
        Candle_bright          = P042_CONFIG_BRIGHTNESS;
        # ifndef BUILD_NO_DEBUG

        if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
          addLogMove(LOG_LEVEL_DEBUG, concat(F("CAND : CMD - Bright : "), val_Bright));
        }
        # endif // ifndef BUILD_NO_DEBUG
      }
    }

    uint32_t number = 0;

    if (!val_Color.isEmpty() && validUIntFromString(concat(F("0x"), val_Color), number)) {
      // Split RGB to r, g, b values
      P042_CONFIG_RED       = (number >> 16) & 0xFF;
      P042_CONFIG_GREEN     = (number >> 8) & 0xFF;
      P042_CONFIG_BLUE      = number & 0xFF;
      Candle_red            = P042_CONFIG_RED;
      Candle_green          = P042_CONFIG_GREEN;
      Candle_blue           = P042_CONFIG_BLUE;
      P042_CONFIG_COLORTYPE = 1;
      Candle_color          = static_cast<P042_ColorType>(P042_CONFIG_COLORTYPE);

      # ifndef BUILD_NO_DEBUG

      if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
        String log = F("CAND : CMD - R ");
        log += Candle_red;
        log += F(" G ");
        log += Candle_green;
        log += F(" B ");
        log += Candle_blue;
        addLogMove(LOG_LEVEL_DEBUG, log);
      }
      # endif // ifndef BUILD_NO_DEBUG
    } else {
      P042_CONFIG_COLORTYPE = 0;
      Candle_color          = static_cast<P042_ColorType>(P042_CONFIG_COLORTYPE);
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_DEBUG, F("CAND : CMD - Color : DEFAULT"));
      # endif // ifndef BUILD_NO_DEBUG
    }

    success = true;
  }

  return success;
}

void P042_data_struct::SetPixelsBlack() {
  for (int i = 0; i < Candle_pxlcnt; i++) {
    Candle_pixels->setPixelColor(i, Candle_pixels->Color(0, 0, 0));
  }
}

void P042_data_struct::SetPixelToColor(int PixelIdx) {
  Candle_pixels->setPixelColor(PixelIdx, Candle_pixels->Color(Candle_red, Candle_green, Candle_blue));
}

void P042_data_struct::type_Static_Light() {
  for (int i = 0; i < Candle_pxlcnt; i++) {
    if (Candle_color == P042_ColorType::ColorDefault) {
      Candle_pixels->setPixelColor(i, 255, 255, 255); // Default is white
    } else {
      Candle_pixels->setPixelColor(i, Candle_red, Candle_green, Candle_blue);
    }
  }
}

void P042_data_struct::type_Simple_Candle() {
  int r, g, b;

  if (Candle_color == P042_ColorType::ColorDefault) {
    r = 226, g = 42, b =  35; // Regular (orange) flame
    // r = 158, g =   8, b = 148;   // Purple flame
    // r =  74, g = 150, b =  12;   // Green flame
  } else {
    r = Candle_red, g = Candle_green, b = Candle_blue;
  }

  //  Flicker, based on our initial RGB values
  for (int i = 0; i < Candle_pxlcnt; i++) {
    int flicker = HwRandom(0, P042_RANDOM_PIXEL);
    int r1      = r - flicker;
    int g1      = g - flicker;
    int b1      = b - flicker;

    if (g1 < 0) { g1 = 0; }

    if (r1 < 0) { r1 = 0; }

    if (b1 < 0) { b1 = 0; }
    Candle_pixels->setPixelColor(i, r1, g1, b1);
  }
}

void P042_data_struct::type_Advanced_Candle() {
  Candle_Temp[0] = HwRandom(1, 4);                  // 1..4  LEDs in RED
  Candle_Temp[1] = HwRandom(1, 4) + Candle_Temp[0]; // 1..3  LEDs in Yellow / Orange
  Candle_Temp[2] = HwRandom(0, 2);                  // 0..1  Choose Yellow = 0 / Orange = 1

  int colorbase[3];
  int color1[3];
  int color2[3];
  int color3[3];

  if (Candle_color == P042_ColorType::ColorDefault) {
    colorbase[0] = 255; colorbase[1] = 120; colorbase[2] = 0; // Light Orange #FF7800
    color1[0]    = 115; color1[1] = 50; color1[2] = 0;        // Brown      #733200
    color2[0]    = 180; color2[1] = 80; color2[2] = 0;        // Orange     #B45000
    color3[0]    =  70; color3[1] = 30; color3[2] = 0;        // Dark brown #4A2000
  } else {
    colorbase[0] = Candle_red; colorbase[1] = Candle_green; colorbase[2] = Candle_blue;
    float hsv[3];

    // Calc HSV
    RGB2HSV(Candle_red, Candle_green, Candle_blue, hsv);
    float newH = hsv[0] - 5.0f;

    if (newH < 0) { newH += 359.0f; }
    float newV  = hsv[2] / 2.0f;
    float newV2 = hsv[2] / 4.0f;

    // Calc new RGBs
    HSV2RGB(newH,   hsv[1], hsv[2], color1);
    HSV2RGB(hsv[0], hsv[1], newV,   color2);
    HSV2RGB(newH,   hsv[1], newV2,  color3);
  }

  for (int j = 0; j < 4; j++) {
    for (int i = 1; i <= segment; i++) {
      if (i <= Candle_Temp[0]) {
        Candle_pixels->setPixelColor(j * segment + i - 1, colorbase[0], colorbase[1], colorbase[2]);
      }

      if ((i > Candle_Temp[0]) && (i <= Candle_Temp[1])) {
        if (Candle_Temp[2] == 0) {
          Candle_pixels->setPixelColor(j * segment + i - 1, color1[0], color1[1], color1[2]);
        } else {
          Candle_pixels->setPixelColor(j * segment + i - 1, color2[0], color2[1], color2[2]);
        }
      }

      if (i > Candle_Temp[1]) {
        Candle_pixels->setPixelColor(j * segment + i - 1, color3[0], color3[1], color3[2]);
      }
    }
  }
}

void P042_data_struct::type_Police() {
  Candle_Temp[0]++;

  if (Candle_Temp[0] > 3) {
    Candle_Temp[0] = 0;
  }

  for (unsigned int i = 0; i < 4; i++) {
    if (i == Candle_Temp[0])
    {
      for (int j = 0; j < segment; j++) {
        if (Candle_color == P042_ColorType::ColorDefault) {
          Candle_pixels->setPixelColor(i * segment + j, 0, 0, 255);
        } else {
          Candle_pixels->setPixelColor(i * segment + j, Candle_red, Candle_green, Candle_blue);
        }
      }
    } else {
      for (int j = 0; j < segment; j++) {
        Candle_pixels->setPixelColor(i * segment + j, 0, 0, 0);
      }
    }
  }
}

void P042_data_struct::type_BlinkStrobe() {
  Candle_Temp[0]++;

  if (Candle_Temp[0] > 1) {
    Candle_Temp[0] = 0;
  }

  for (int i = 0; i < Candle_pxlcnt; i++) {
    if (Candle_Temp[0] == 0) {
      Candle_pixels->setPixelColor(i, 0, 0, 0);
    } else {
      if (Candle_color == P042_ColorType::ColorDefault) {
        Candle_pixels->setPixelColor(i, 255, 255, 255); // Default is white
      } else {
        Candle_pixels->setPixelColor(i, Candle_red, Candle_green, Candle_blue);
      }
    }
  }
}

void P042_data_struct::type_ColorFader() {
  int   colors[3];
  float hsv[3];

  if (Candle_color != P042_ColorType::ColorDefault) {
    if ((Candle_Temp[0] > 254) && (Candle_Temp[1] == 1)) {
      Candle_Temp[1] = 0;
    }

    if ((Candle_Temp[0] < 55) && (Candle_Temp[1] == 0)) {
      Candle_Temp[1] = 1;
    }

    if (Candle_Temp[1] > 0) {
      Candle_Temp[0]++;
    } else {
      Candle_Temp[0]--;
    }

    // Calc HSV
    // void RGB2HSV(uint8_t r, uint8_t g, uint8_t b, float hsv[3])
    RGB2HSV(Candle_red, Candle_green, Candle_blue, hsv);

    // Calc RGB with new V
    // HSV2RGB(float hue, float sat, float val, int colors[3])
    // hue: 0-359, sat: 0-255, val (lightness): 0-255
    HSV2RGB(hsv[0], hsv[1], Candle_Temp[0], colors);

    for (int i = 0; i < Candle_pxlcnt; i++) {
      Candle_pixels->setPixelColor(i, colors[0], colors[1], colors[2]);
    }
  } else {
    Candle_Temp[0]++;

    if (Candle_Temp[0] > 359) {
      Candle_Temp[0] = 0;
    }

    // hue: 0-359, sat: 0-255, val (lightness): 0-255
    HSV2RGB(Candle_Temp[0], 255, 255, colors);

    for (int i = 0; i < Candle_pxlcnt; i++) {
      Candle_pixels->setPixelColor(i, colors[0], colors[1], colors[2]);
    }
  }
}

#endif // ifdef USES_P042

#include "../PluginStructs/P031_data_struct.h"

#ifdef USES_P031

uint8_t P031_data_struct::init(uint8_t data_pin, uint8_t clock_pin, bool pullUp, uint8_t clockdelay) {
  _dataPin    = data_pin;
  _clockPin   = clock_pin;
  _clockdelay = clockdelay;

  if (_clockdelay > P031_MAX_CLOCK_DELAY) {
    _clockdelay = P031_MAX_CLOCK_DELAY;
  }
  input_mode = pullUp ? INPUT_PULLUP : INPUT;
  state      = P031_IDLE;

  pinMode(_dataPin,  input_mode); /* Keep Hi-Z except when sending data */
  pinMode(_clockPin, OUTPUT);
  resetSensor();
  return readStatus();
}

bool P031_data_struct::process() {
  switch (state) {
    case P031_IDLE: return false; // Nothing changed, nothing to do
    case P031_WAIT_TEMP: {
      if (digitalRead(_dataPin) == LOW) {
        float tempRaw = readData(16);

        // Temperature conversion coefficients from SHT1X datasheet for version 4
        const float d1 = -39.7f; // 3.5V
        const float d2 = 0.01f;  // 14-bit
        tempC = d1 + (tempRaw * d2);
        state = P031_WAIT_HUM;   // Wait for humidity
        sendCommand(SHT1X_CMD_MEASURE_RH);
      }
      break;
    }
    case P031_WAIT_HUM:
    {
      if (digitalRead(_dataPin) == LOW) {
        float raw = readData(16);

        // Temperature conversion coefficients from SHT1X datasheet for version 4
        const float c1 = -2.0468f;
        const float c2 = 0.0367f;
        const float c3 = -1.5955E-6f;
        const float t1 = 0.01f;
        const float t2 = 0.00008f;

        const float rhLinear = c1 + c2 * raw + c3 * raw * raw;
        rhTrue = (tempC - 25) * (t1 + t2 * raw) + rhLinear;

        /*
            String log = F("SHT1X : Read humidity (raw): ");
            log += String(raw);
            log += F(" (Linear): ");
            log += String(rhLinear);
            log += F(" (True): ");
            log += String(rhTrue);
            addLog(LOG_LEVEL_DEBUG, log);
         */
        state = P031_MEAS_READY; // Measurement ready
        return true;
      }
      break;
    }
    case P031_MEAS_READY: return true;
    default:
      // It is already an error state, just return.
      return false;
  }

  // Compute timeout
  if (timePassedSince(sendCommandTime) > 320) {
    state = P031_NO_DATA; // No data after 320 msec
  }
  return false;
}

void P031_data_struct::startMeasurement() {
  state = P031_WAIT_TEMP; // Wait for temperature
  sendCommand(SHT1X_CMD_MEASURE_TEMP);
}

void P031_data_struct::resetSensor()
{
  state = P031_IDLE;
  delay(11);

  for (int i = 0; i < 9; i++) {
    digitalWrite(_clockPin, HIGH);
    digitalWrite(_clockPin, LOW);
  }
  sendCommand(SHT1X_CMD_SOFT_RESET);
  delay(11);
}

uint8_t P031_data_struct::readStatus()
{
  sendCommand(SHT1X_CMD_READ_STATUS);
  return readData(8);
}

void P031_data_struct::sendCommand(const uint8_t cmd)
{
  sendCommandTime = millis();

  // Transmission Start sequence
  digitalWrite(_dataPin, HIGH);
  pinMode(_dataPin, OUTPUT);
  digitalWrite(_clockPin, HIGH);
  P031_DELAY_LONGER_CABLES
    digitalWrite(_dataPin, LOW);

  digitalWrite(_clockPin, LOW);
  P031_DELAY_LONGER_CABLES
    digitalWrite(_clockPin, HIGH);
  P031_DELAY_LONGER_CABLES
    digitalWrite(_dataPin, HIGH);

  digitalWrite(_clockPin, LOW);
  P031_DELAY_LONGER_CABLES

  // Send the command (address must be 000b)
    shiftOut(_dataPin, _clockPin, MSBFIRST, cmd);

  // Wait for ACK
  pinMode(_dataPin, input_mode);
  bool ackerror = false;

  digitalWrite(_clockPin, HIGH);
  P031_DELAY_LONGER_CABLES

  if (digitalRead(_dataPin) != LOW) { ackerror = true; }
  digitalWrite(_clockPin, LOW);
  P031_DELAY_LONGER_CABLES

  if ((cmd == SHT1X_CMD_MEASURE_TEMP) || (cmd == SHT1X_CMD_MEASURE_RH)) {
    delayMicroseconds(1); /* Give the sensor time to release the data line */

    if (digitalRead(_dataPin) != HIGH) { ackerror = true; }
  }

  if (ackerror) {
    state = P031_COMMAND_NO_ACK;
  }
}

int P031_data_struct::readData(const int bits) const
{
  int val = 0;

  if (bits == 16) {
    // Read most significant uint8_t
    val   = shiftIn(_dataPin, _clockPin, MSBFIRST);
    val <<= 8;

    // Send ACK
    pinMode(_dataPin, OUTPUT);
    digitalWrite(_dataPin,  LOW);
    digitalWrite(_clockPin, HIGH);
    P031_DELAY_LONGER_CABLES
      digitalWrite(_clockPin, LOW);
    P031_DELAY_LONGER_CABLES
      pinMode(_dataPin, input_mode);
  }

  // Read least significant uint8_t
  val |= shiftIn(_dataPin, _clockPin, MSBFIRST);

  // Keep DATA pin high to skip CRC
  digitalWrite(_clockPin, HIGH);
  P031_DELAY_LONGER_CABLES
    digitalWrite(_clockPin, LOW);

  P031_DELAY_LONGER_CABLES

  return val;
}

uint8_t P031_data_struct::shiftIn(uint8_t dataPin, uint8_t clockPin, uint8_t bitOrder) const
{
  uint8_t value = 0;
  uint8_t i;

  for (i = 0; i < 8; ++i) {
    digitalWrite(clockPin, HIGH);
    P031_DELAY_LONGER_CABLES

    if (bitOrder == LSBFIRST) {
      value |= digitalRead(dataPin) << i;
    }
    else {
      value |= digitalRead(dataPin) << (7 - i);
    }
    digitalWrite(clockPin, LOW);
    P031_DELAY_LONGER_CABLES
  }
  return value;
}

void P031_data_struct::shiftOut(uint8_t dataPin, uint8_t clockPin, uint8_t bitOrder, uint8_t val) const
{
  uint8_t i;

  for (i = 0; i < 8; i++)  {
    if (bitOrder == LSBFIRST) {
      digitalWrite(dataPin, !!(val & (1 << i)));
    }
    else {
      digitalWrite(dataPin, !!(val & (1 << (7 - i))));
    }

    digitalWrite(clockPin, HIGH);
    P031_DELAY_LONGER_CABLES
      digitalWrite(clockPin, LOW);
    P031_DELAY_LONGER_CABLES
  }
}

#endif // ifdef USES_P031

#include "../PluginStructs/P006_data_struct.h"

#ifdef USES_P006


# define BMP085_I2CADDR           0x77
# define BMP085_CAL_AC1           0xAA // R   Calibration data (16 bits)
# define BMP085_CAL_AC2           0xAC // R   Calibration data (16 bits)
# define BMP085_CAL_AC3           0xAE // R   Calibration data (16 bits)
# define BMP085_CAL_AC4           0xB0 // R   Calibration data (16 bits)
# define BMP085_CAL_AC5           0xB2 // R   Calibration data (16 bits)
# define BMP085_CAL_AC6           0xB4 // R   Calibration data (16 bits)
# define BMP085_CAL_B1            0xB6 // R   Calibration data (16 bits)
# define BMP085_CAL_B2            0xB8 // R   Calibration data (16 bits)
# define BMP085_CAL_MB            0xBA // R   Calibration data (16 bits)
# define BMP085_CAL_MC            0xBC // R   Calibration data (16 bits)
# define BMP085_CAL_MD            0xBE // R   Calibration data (16 bits)
# define BMP085_CONTROL           0xF4
# define BMP085_TEMPDATA          0xF6
# define BMP085_PRESSUREDATA      0xF6
# define BMP085_READTEMPCMD       0x2E
# define BMP085_READPRESSURECMD   0x34


bool P006_data_struct::begin()
{
  if (!initialized) {
    if (I2C_read8_reg(BMP085_I2CADDR, 0xD0) != 0x55) { return false; }

    /* read calibration data */
    ac1 = I2C_read16_reg(BMP085_I2CADDR, BMP085_CAL_AC1);
    ac2 = I2C_read16_reg(BMP085_I2CADDR, BMP085_CAL_AC2);
    ac3 = I2C_read16_reg(BMP085_I2CADDR, BMP085_CAL_AC3);
    ac4 = I2C_read16_reg(BMP085_I2CADDR, BMP085_CAL_AC4);
    ac5 = I2C_read16_reg(BMP085_I2CADDR, BMP085_CAL_AC5);
    ac6 = I2C_read16_reg(BMP085_I2CADDR, BMP085_CAL_AC6);

    b1 = I2C_read16_reg(BMP085_I2CADDR, BMP085_CAL_B1);
    b2 = I2C_read16_reg(BMP085_I2CADDR, BMP085_CAL_B2);

    mb = I2C_read16_reg(BMP085_I2CADDR, BMP085_CAL_MB);
    mc = I2C_read16_reg(BMP085_I2CADDR, BMP085_CAL_MC);
    md = I2C_read16_reg(BMP085_I2CADDR, BMP085_CAL_MD);

    initialized = true;
  }

  return true;
}

uint16_t P006_data_struct::readRawTemperature()
{
  I2C_write8_reg(BMP085_I2CADDR, BMP085_CONTROL, BMP085_READTEMPCMD);
  delay(5);
  return I2C_read16_reg(BMP085_I2CADDR, BMP085_TEMPDATA);
}

uint32_t P006_data_struct::readRawPressure()
{
  uint32_t raw;

  I2C_write8_reg(BMP085_I2CADDR, BMP085_CONTROL, BMP085_READPRESSURECMD + (oversampling << 6));

  delay(26);

  raw   = I2C_read16_reg(BMP085_I2CADDR, BMP085_PRESSUREDATA);
  raw <<= 8;
  raw  |= I2C_read8_reg(BMP085_I2CADDR, BMP085_PRESSUREDATA + 2);
  raw >>= (8 - oversampling);

  return raw;
}

int32_t P006_data_struct::readPressure()
{
  int32_t  UT, UP, B3, B5, B6, X1, X2, X3, p;
  uint32_t B4, B7;

  UT = readRawTemperature();
  UP = readRawPressure();

  // do temperature calculations
  X1 = (UT - (int32_t)(ac6)) * ((int32_t)(ac5)) / 32768.0f /*pow(2, 15)*/;
  X2 = ((int32_t)mc * 2048.0f /*pow(2, 11)*/) / (X1 + (int32_t)md);
  B5 = X1 + X2;

  // do pressure calcs
  B6 = B5 - 4000;
  X1 = ((int32_t)b2 * ((B6 * B6) >> 12)) >> 11;
  X2 = ((int32_t)ac2 * B6) >> 11;
  X3 = X1 + X2;
  B3 = ((((int32_t)ac1 * 4 + X3) << oversampling) + 2) / 4;

  X1 = ((int32_t)ac3 * B6) >> 13;
  X2 = ((int32_t)b1 * ((B6 * B6) >> 12)) >> 16;
  X3 = ((X1 + X2) + 2) >> 2;
  B4 = ((uint32_t)ac4 * (uint32_t)(X3 + 32768)) >> 15;
  B7 = ((uint32_t)UP - B3) * (uint32_t)(50000UL >> oversampling);

  if (B7 < 0x80000000)
  {
    p = (B7 * 2) / B4;
  }
  else
  {
    p = (B7 / B4) * 2;
  }
  X1 = (p >> 8) * (p >> 8);
  X1 = (X1 * 3038) >> 16;
  X2 = (-7357 * p) >> 16;

  p = p + ((X1 + X2 + (int32_t)3791) >> 4);
  return p;
}

float P006_data_struct::readTemperature()
{
  int32_t UT, X1, X2, B5; // following ds convention
  float   temp;

  UT = readRawTemperature();

  // step 1
  X1    = (UT - (int32_t)ac6) * ((int32_t)ac5) / 32768.0f /*pow(2, 15)*/;
  X2    = ((int32_t)mc * 2048.0f /*pow(2, 11)*/) / (X1 + (int32_t)md);
  B5    = X1 + X2;
  temp  = (B5 + 8) / 16.0f /*pow(2, 4)*/;
  temp /= 10;

  return temp;
}

#endif // ifdef USES_P006

///////////////////////////////////////////////////////////////////////////////////////////////////
// Plugin data structure for P164 "GASES - ENS16x (TVOC, eCO2)"
// Plugin for ENS160 & ENS161 TVOC and eCO2 sensor with I2C interface from ScioSense
// Based upon: https://github.com/sciosense/ENS160_driver
// For documentation see 
// https://www.sciosense.com/wp-content/uploads/documents/SC-001224-DS-9-ENS160-Datasheet.pdf
//
// Based upon: https://github.com/sciosense/ENS160_driver
// MIT License for the original code referenced above
// Copyright (c) 2020 Sciosense
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 
// and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
// 2023 By flashmark
///////////////////////////////////////////////////////////////////////////////////////////////////

#include "../PluginStructs/P164_data_struct.h"

#ifdef USES_P164

// A curious delay inserted in the original code [ms] 
#define ENS160_BOOTING          10
// Max time for device to react on a reset [ms]
#define ENS160_MAXBOOTING       2000

// ENS160 registers for version V0
#define ENS160_REG_PART_ID      0x00    // 2 byte register for part identification
#define ENS160_REG_OPMODE       0x10    // Operation mode register
#define ENS160_REG_CONFIG       0x11    // Pin configuration register
#define ENS160_REG_COMMAND      0x12    // Additional system commands
#define ENS160_REG_TEMP_IN      0x13    // Host ambient temperature information
#define ENS160_REG_RH_IN        0x15    // Host relative humidity information
#define ENS160_REG_DATA_STATUS  0x20    // Operating mode status readback
#define ENS160_REG_DATA_AQI     0x21    // Air quality according to index according to UBA
#define ENS160_REG_DATA_TVOC    0x22    // Equivalent TVOC concentartion (ppb)
#define ENS160_REG_DATA_ECO2    0x24		// Equivalent CO2 concentration (ppm)
#define ENS160_REG_DATA_AQI_S   0x26    // Relative Air Quality Index according to ScioSense [ENS161 only]
#define ENS160_REG_DATA_BL      0x28    // Undocumented
#define ENS160_REG_DATA_T       0x30    // Temperature used in calculations
#define ENS160_REG_DATA_RH      0x32    // Relative humidity used in calculations
#define ENS160_REG_DATA_MISR    0x38    //Data integrity field (optional)
#define ENS160_REG_GPR_WRITE_0  0x40    // General purpose write registers [8 bytes]
#define ENS160_REG_GPR_WRITE_1  (ENS160_REG_GPR_WRITE_0 + 1)
#define ENS160_REG_GPR_WRITE_2  (ENS160_REG_GPR_WRITE_0 + 2)
#define ENS160_REG_GPR_WRITE_3  (ENS160_REG_GPR_WRITE_0 + 3)
#define ENS160_REG_GPR_WRITE_4  (ENS160_REG_GPR_WRITE_0 + 4)
#define ENS160_REG_GPR_WRITE_5  (ENS160_REG_GPR_WRITE_0 + 5)
#define ENS160_REG_GPR_WRITE_6  (ENS160_REG_GPR_WRITE_0 + 6)
#define ENS160_REG_GPR_WRITE_7  (ENS160_REG_GPR_WRITE_0 + 7)
#define ENS160_REG_GPR_READ_0   0x48    // General purpose read registers [8 bytes]
#define ENS160_REG_GPR_READ_1   (ENS160_REG_GPR_READ_0 + 1)
#define ENS160_REG_GPR_READ_2   (ENS160_REG_GPR_READ_0 + 2)
#define ENS160_REG_GPR_READ_3   (ENS160_REG_GPR_READ_0 + 3)
#define ENS160_REG_GPR_READ_4   (ENS160_REG_GPR_READ_0 + 4)
#define ENS160_REG_GPR_READ_5   (ENS160_REG_GPR_READ_0 + 5)
#define ENS160_REG_GPR_READ_6   (ENS160_REG_GPR_READ_0 + 6)
#define ENS160_REG_GPR_READ_7   (ENS160_REG_GPR_READ_0 + 7)

// ENS160_REG_PART_ID values for Chip ID 
#define ENS160_PARTID           0x0160  // ENS160
#define ENS161_PARTID           0x0161  // ENS161

//ENS160 COMMAND register values
#define ENS160_COMMAND_NOP          0x00    // NOP, No operation
#define ENS160_COMMAND_CLRGPR       0xCC    // CLRGRP, Clears GPR Read Registers
#define ENS160_COMMAND_GET_APPVER   0x0E    // GET_APPVER, Get firmware version
#define ENS160_COMMAND_SETTH        0x02    // Not specified in datasheet
#define ENS160_COMMAND_SETSEQ       0xC2    // Not specified in datasheet

// ENS160 OPMODE register values
#define ENS160_OPMODE_RESET         0xF0    // RESET 
#define ENS160_OPMODE_DEEP_SLEEP    0x00    // DEEPSLEEP
#define ENS160_OPMODE_IDLE          0x01    // IDLE
#define ENS160_OPMODE_STD           0x02    // STANDARD
#define ENS160_OPMODE_LP            0x03    // LOW POWER (ENS161 only)
#define ENS160_OPMODE_ULP           0x04    // ULTRA LOW POWER (ENS161 only)
#define ENS160_OPMODE_CUSTOM        0xC0    // Not specified in datasheet

// ENS160 unspecified bitfields?
#define ENS160_BL_CMD_START         0x02
#define ENS160_BL_CMD_ERASE_APP     0x04
#define ENS160_BL_CMD_ERASE_BLINE   0x06
#define ENS160_BL_CMD_WRITE         0x08
#define ENS160_BL_CMD_VERIFY        0x0A
#define ENS160_BL_CMD_GET_BLVER     0x0C
#define ENS160_BL_CMD_GET_APPVER    0x0E
#define ENS160_BL_CMD_EXITBL        0x12

// ENS160 unspecified bitfields?
#define ENS160_SEQ_ACK_NOTCOMPLETE  0x80
#define ENS160_SEQ_ACK_COMPLETE     0xC0

#define IS_ENS160_SEQ_ACK_NOT_COMPLETE(x)   (ENS160_SEQ_ACK_NOTCOMPLETE == (ENS160_SEQ_ACK_NOTCOMPLETE & (x)))
#define IS_ENS160_SEQ_ACK_COMPLETE(x)       (ENS160_SEQ_ACK_COMPLETE == (ENS160_SEQ_ACK_COMPLETE & (x)))

// ENS160 STATUS bitfields
#define ENS160_STATUS_STATAS        0x80    // STATAS: Indicates that an OPMODE is running
#define ENS160_STATUS_STATER        0x40    // STATER: High indicated that an error is detected
#define ENS160_STATUS_VALIDITY      0x0C    // VALIDITY FLAG
#define ENS160_STATUS_VAL_NORM      0x00    //   0: Normal operation
#define ENS160_STATUS_VAL_WARM      0x01    //   1: Warm-Up phase
#define ENS160_STATUS_VAL_NOUSE     0x02    //   2: Not used
#define ENS160_STATUS_VAL_INVAL     0x03    //   3: Invalid output
#define ENS160_STATUS_NEWDAT        0x02    // NEWDAT: 1= New data in data registers available
#define ENS160_STATUS_NEWGPR        0x01    // NEWGRP: 1= New data in GRP_READ registers available 

// Checkers for bitfields in STATUS register
#define IS_NEWDAT(x)                (ENS160_STATUS_NEWDAT == (ENS160_STATUS_NEWDAT & (x)))
#define IS_NEWGPR(x)                (ENS160_STATUS_NEWGPR == (ENS160_STATUS_NEWGPR & (x)))
#define IS_NEW_DATA_AVAILABLE(x)    (0 != ((ENS160_STATUS_NEWDAT | ENS160_STATUS_NEWGPR ) & (x)))
#define GET_STATUS_VALIDITY(x)      (((x) & ENS160_STATUS_VALIDITY) >> 2)

// TODO: add comment on this
#define CONVERT_RS_RAW2OHMS_I(x)    (1 << ((x) >> 11))
#define CONVERT_RS_RAW2OHMS_F(x)    (pow (2, (float)(x) / 2048))

// Form IDs used on the device setup page. Should be a short unique string.
#define P164_GUID_TEMP_T     "f01"
#define P164_GUID_TEMP_V     "f02"
#define P164_GUID_HUM_T      "f03"
#define P164_GUID_HUM_V      "f04"


///////////////////////////////////////////////////////////////////////////////////////////////////
// Constructor                                                                                   //
///////////////////////////////////////////////////////////////////////////////////////////////////
P164_data_struct::P164_data_struct(struct EventStruct *event) :
  i2cAddress(P164_PCONFIG_I2C_ADDR)
{
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// Initialization of the connected device                                                        //
///////////////////////////////////////////////////////////////////////////////////////////////////
bool P164_data_struct::begin()
{
  // Start connecting the device over I2C
  if (!start(i2cAddress)) {
    addLogMove(LOG_LEVEL_ERROR, F("P164: device initialization FAILED"));
    return false;
  }
  setMode(ENS160_OPMODE_STD); // For now we only support the standard acquisition mode

  #ifdef P164_ENS160_DEBUG
    addLogMove(LOG_LEVEL_DEBUG, F("P164: begin(): success"));
  #endif
  return true;
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// Fetch the processed device values as stored in the software object                            //
///////////////////////////////////////////////////////////////////////////////////////////////////
bool P164_data_struct::read(float& tvoc, float& eco2, float& aqi)
{
  bool success = measure();       // Read measurement values from device
  tvoc = (float)_data_tvoc;       // Latest acquired TVOC value
  eco2 = (float)_data_eco2;       // Latest aquired eCO2 value
  aqi = (float)_data_aqi;         // Latest aquired AQI value
  return success;
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// Fetch the processed device values as stored in the software object using compensation         //
///////////////////////////////////////////////////////////////////////////////////////////////////
bool P164_data_struct::read(float& tvoc, float& eco2, float& aqi, float temp, float hum)
{
  this->set_envdata(temp, hum);   // Write new compensation temp & hum to device
  bool success = measure();       // Read measurement values from device
  tvoc = (float)_data_tvoc;       // Latest acquired TVOC value
  eco2 = (float)_data_eco2;       // Latest aquired eCO2 value
  aqi = (float)_data_aqi;         // Latest aquired AQI value
  return success;
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// Implementation of plugin PLUGIN_WEBFORM_LOAD call                                             //
// Note: this is not a class function, only data from event is available                         //
///////////////////////////////////////////////////////////////////////////////////////////////////
bool P164_data_struct::webformLoad(struct EventStruct *event)
{
  bool  found = false; // A chip has responded at the I2C address
  uint16_t chipID = 0;

  addRowLabel(F("Detected Sensor Type"));
  chipID = I2C_read16_LE_reg(P164_PCONFIG_I2C_ADDR, ENS160_REG_PART_ID, &found);
  if (!found) {
    addHtml(F("No device found"));
  } else if (chipID == ENS160_PARTID) {
    addHtml(F("ENS160"));
  } else if (chipID == ENS161_PARTID) {
    addHtml(F("ENS161"));
  } else {
    addHtmlInt(chipID);
  }

  P164_data_struct *P164_data = static_cast<P164_data_struct *>(getPluginTaskData(event->TaskIndex));
  if (P164_data != nullptr) {
    addHtml(F(" Firmware: "));
    addHtmlInt(P164_data->getMajorRev());
    addHtml(F("."));
    addHtmlInt(P164_data->getMinorRev());    
    addHtml(F("."));
    addHtmlInt(P164_data->getBuild());
  }

  addFormNote(F("Both Temperature and Humidity task & values are needed to enable compensation"));
  // temperature
  addRowLabel(F("Temperature Task"));
  addTaskSelect(F(P164_GUID_TEMP_T), P164_PCONFIG_TEMP_TASK);
  if (validTaskIndex(P164_PCONFIG_TEMP_TASK))
  {
    LoadTaskSettings(P164_PCONFIG_TEMP_TASK); // we need to load the values from another task for selection!
    addRowLabel(F("Temperature Value"));
    addTaskValueSelect(F(P164_GUID_TEMP_V), P164_PCONFIG_TEMP_VAL, P164_PCONFIG_TEMP_TASK);
  }
  // humidity
  addRowLabel(F("Humidity Task"));
  addTaskSelect(F(P164_GUID_HUM_T), P164_PCONFIG_HUM_TASK);
  if (validTaskIndex(P164_PCONFIG_HUM_TASK))
  {
    LoadTaskSettings(P164_PCONFIG_HUM_TASK); // we need to load the values from another task for selection!
    addRowLabel(F("Humidity Value"));
    addTaskValueSelect(F(P164_GUID_HUM_V), P164_PCONFIG_HUM_VAL, P164_PCONFIG_HUM_TASK);
  }
  LoadTaskSettings(event->TaskIndex); // we need to restore our original taskvalues!

  return true;
}

///////////////////////////////////////////////////////////////////////////////////////////////////
/// Implementation of plugin PLUGIN_WEBFORM_SAVE call                                            //
// Note: this is not a class function, only data from event is available                         //
///////////////////////////////////////////////////////////////////////////////////////////////////
bool P164_data_struct::webformSave(struct EventStruct *event)
{
  P164_PCONFIG_I2C_ADDR  = getFormItemInt(F("i2c_addr"));
  P164_PCONFIG_TEMP_TASK = getFormItemInt(F(P164_GUID_TEMP_T));
  P164_PCONFIG_TEMP_VAL  = getFormItemInt(F(P164_GUID_TEMP_V));
  P164_PCONFIG_HUM_TASK  = getFormItemInt(F(P164_GUID_HUM_T));
  P164_PCONFIG_HUM_VAL   = getFormItemInt(F(P164_GUID_HUM_V));
  return true;
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// Implementation of plugin PLUGIN_TEN_PER_SECOND call                                           //
///////////////////////////////////////////////////////////////////////////////////////////////////
bool P164_data_struct::tenPerSecond(struct EventStruct *event)
{
  return evaluateState();
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// Set operation mode of sensor                                                                  //
// Note: This function is to set the operation mode to the plugin software structure only        //
//       The statemachine shall handle the actual programming of the OPMODE register             //
///////////////////////////////////////////////////////////////////////////////////////////////////
bool P164_data_struct::setMode(uint8_t mode) {
  bool result = false;

  // LP only valid for rev>0
  if (!(mode == ENS160_OPMODE_LP) and (_revENS16x == 0)) {
    this->_opmode = mode;
    result        = true;
  }

  #ifdef P164_ENS160_DEBUG
    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      String log;
      log += F("P164: setMode(");
      log += mode;
      log += F(")");
      addLogMove(LOG_LEVEL_DEBUG, log);
    }
  #endif // ifdef P164_ENS160_DEBUG

  return result;
}

///////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
//                     **** The sensor handling code ****                                        //
// This is based upon Sciosense code on github                                                   //
// The code is adapted to fit the ESPEasy structures and statemachine behavior is added          //
///////////////////////////////////////////////////////////////////////////////////////////////////

///////////////////////////////////////////////////////////////////////////////////////////////////
// Helper function to display the current state in readable format                               //
///////////////////////////////////////////////////////////////////////////////////////////////////
#ifdef P164_ENS160_DEBUG
String printState(int state) {
  switch (state) {
    case P164_STATE_INITIAL:      return F("initial"); break;
    case P164_STATE_ERROR:        return F("error"); break;
    case P164_STATE_RESETTING:    return F("resetting"); break;
    case P164_STATE_STARTING1:    return F("starting1"); break;
    case P164_STATE_STARTING2:    return F("starting2"); break;
    case P164_STATE_IDLE:         return F("idle"); break;
    case P164_STATE_DEEPSLEEP:    return F("deepsleep"); break;
    case P164_STATE_OPERATIONAL:  return F("operational"); break;
    default:                      return F("***ERROR***"); break;
  }
}
#endif

///////////////////////////////////////////////////////////////////////////////////////////////////
// Evaluate the plugin statemachine and determine next step                                      //
// To prevent using delay() to wait for the device responses a statemachine is introduced        //
// Main states follow the device states according datasheet:                                     //
//   + IDLE         Enabled, waiting for commands                                                //
//   + DEEP SLEEP   Low power standby                                                            //
//   + OPERATIONAL  Active gas sensing                                                           //
// Added states to administrate plugin software status:                                          //
//   + INITIAL      Class is constructed, waiting for begin()                                    //
//   + ERROR        Communication with the device failed or other fatal error conditions         //
//   + RESETTING    Waiting for device reset to be finished                                      //
//   + STARTING1    Startup sequence waiting for previous command to be acknowledged             //
//   + STARTING2    Startup sequence waiting for previous command to be acknowledged             //
// Note that the ENS161 device has various gas sensing operation modes which all map to the      //
// same OPERATIONAL state. These are combined in the same OPMODE register in the device          //
//   - STANDARD                                                                                  //
//   - LOW POWER                                                                                 //
//   - ULTRA LOW POWER                                                                           //
// The same OPMODE register is also used to reset the device.                                    //
// Thus OPMODE register shall not be confused with this software state                           //
///////////////////////////////////////////////////////////////////////////////////////////////////
bool P164_data_struct::evaluateState()
{
  bool success  = true;                // State transition went without problems with device
  P164_state  newState = this->_state; // Determine next state, start with current state

  switch (this->_state) {
    case P164_STATE_INITIAL:
      // Waiting for external call to begin()
      this->_available = false;
      break;
    case P164_STATE_ERROR:
      // Stay here until correct device ID is detected and status register can be read
      // If there is a proper device connected then reset it and initialize it
      this->_available = false;
      success = this->checkPartID() && this->getStatus();
      if (success){
        success = this->writeMode(ENS160_OPMODE_RESET);  // Reset the device, takes some time
        newState = P164_STATE_RESETTING;
      }
      break;
    case P164_STATE_RESETTING:
      // Device has been reset, give it some time to get accessible again
      this->_available = false;

      // Once device has rebooted check if it is a supported device at the given I2C address
      if (timePassedSince(this->_lastChange) > ENS160_BOOTING) { 
        if (this->checkPartID())
        {
          newState = P164_STATE_STARTING1;
        }
        else if (timePassedSince(this->_lastChange) > ENS160_MAXBOOTING) {
          newState = P164_STATE_ERROR;
        }
      }
      break;
    case P164_STATE_STARTING1:
      // A valid device is found, check if its status is ready to continue
      this->_available = false;
      this->getStatus();
      if (GET_STATUS_VALIDITY(this->_statusReg) == ENS160_STATUS_VAL_NORM)
      {
        this->writeMode(ENS160_OPMODE_IDLE);
        this->clearCommand();
        newState = P164_STATE_STARTING2;
      }
      break;
    case P164_STATE_STARTING2:
      this->_available = false;
      this->getStatus();
      if (GET_STATUS_VALIDITY(this->_statusReg) == ENS160_STATUS_VAL_NORM) {
        this->getFirmware();
        newState = P164_STATE_IDLE;
      }
      break;
    case P164_STATE_IDLE:
      // Set device into desired operation mode as requested through _opmode
      this->_available = true;

      switch (this->_opmode) {
        case ENS160_OPMODE_STD:
          this->writeMode(ENS160_OPMODE_STD);
          newState = P164_STATE_OPERATIONAL;
          break;
        case ENS160_OPMODE_LP:
          this->writeMode(ENS160_OPMODE_LP);
          newState = P164_STATE_OPERATIONAL;
          break;
        case ENS160_OPMODE_ULP:
          this->writeMode(ENS160_OPMODE_ULP);
          newState = P164_STATE_OPERATIONAL;
          break;
        case ENS160_OPMODE_RESET:
          this->writeMode(ENS160_OPMODE_RESET);
          this->_opmode = ENS160_OPMODE_IDLE; // Prevent reset loop
          newState      = P164_STATE_RESETTING;
          break;
      }
      break;
    case P164_STATE_DEEPSLEEP:
      // Device is put to DEEPSLEEP mode. If requested move to another mode. But alsways through IDLE
      this->_available = true;

      if (this->_opmode != ENS160_OPMODE_DEEP_SLEEP) {
        this->writeMode(ENS160_OPMODE_IDLE); // Move through Idle state
        newState = P164_STATE_IDLE;
      }
      break;
    case P164_STATE_OPERATIONAL:
      // Device is in one of the operational modes
      this->_available = true;

      switch (this->_opmode) {
        case ENS160_OPMODE_DEEP_SLEEP:
          this->writeMode(ENS160_OPMODE_DEEP_SLEEP); // Put device into DEEPSLEEP mode
          newState = P164_STATE_DEEPSLEEP;
          break;
        case P164_STATE_IDLE:
        case ENS160_OPMODE_RESET:
          this->writeMode(ENS160_OPMODE_IDLE); // Move through Idle state
          newState = P164_STATE_IDLE;
          break;
      }
      break;
    default:
      // Unplanned state, force into error state
      newState = P164_STATE_ERROR;
      break;
  }

  if (newState != this->_state) {
    #ifdef P164_ENS160_DEBUG
      if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
        String log = F("P164: State transition ");
        log += printState(this->_state);
        log += F(" -> ");
        log += printState(newState);
        log += F("; opmode= ");
        log += this->_opmode;
        addLogMove(LOG_LEVEL_DEBUG, log);
      }
    #endif // ifdef P164_ENS160_DEBUG
    this->_state      = newState;
    this->_lastChange = millis();
  }

  return success;
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// Helper function to enter a new state                                                          //
// Function must be called outside evaluateState() when an action causes a state transition      //
///////////////////////////////////////////////////////////////////////////////////////////////////
void P164_data_struct::moveToState(P164_state newState)
{
  #ifdef P164_ENS160_DEBUG
    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      String log;
      log += F("P164: Move to state: ");
      log += printState(this->_state);
      log += F(" --> ");
      log += printState(newState);
      addLogMove(LOG_LEVEL_DEBUG, log);
    }
  #endif // ifdef P164_ENS160_DEBUG

  this->_state      = newState; // Enter the new state
  this->_lastChange = millis(); // Mark time of transition
  this->evaluateState();        // Check if we can already move on
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// Init the device:                                                                              //
// Reset ENS16x                                                                                  //
// Returns false on encountered errors                                                           //
///////////////////////////////////////////////////////////////////////////////////////////////////
bool P164_data_struct::start(uint8_t slaveaddr)
{
  uint8_t result = 0;

  // Initialize internal bookkeeping;
  this->_state      = P164_STATE_INITIAL;   // Assume nothing, start clean
  this->_lastChange = millis();             // Bookmark last state change as now
  this->_available  = false;
  this->_opmode     = ENS160_OPMODE_STD;
  this->i2cAddress  = slaveaddr;

  result = this->writeMode(ENS160_OPMODE_RESET);  // Reset the device, takes some time
  this->moveToState(P164_STATE_RESETTING);        // Go to next state RESETTING

  #ifdef P164_ENS160_DEBUG
    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      String log;
      log += F("P164: start() result= ");
      log += result ? F("ok") : F("nok");
      addLogMove(LOG_LEVEL_DEBUG, log);
    }
  #endif
  return result;
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// Perform prediction measurement and store result in internal variables                         //
// Return: true if data is fresh (first reading of new data)                                     //
///////////////////////////////////////////////////////////////////////////////////////////////////
bool P164_data_struct::measure() {
  bool    newData = false;      // True when new data is availabl at device
  bool    is_ok;                // Dump I2C transaction status

  if (this->_state == P164_STATE_OPERATIONAL)  {
    if (this->getStatus()) {              // Check status register if new data is aquired
      if (IS_NEWDAT(this->_statusReg)) {
        newData = true;
        _data_aqi  = I2C_read8_reg(i2cAddress, ENS160_REG_DATA_AQI, &is_ok);
        _data_tvoc = I2C_read16_LE_reg(i2cAddress, ENS160_REG_DATA_TVOC, &is_ok);
        _data_eco2 = I2C_read16_LE_reg(i2cAddress, ENS160_REG_DATA_ECO2, &is_ok);
        if (_revENS16x > 0) {             // AQI500 only available for ENS161
          _data_aqi500 = I2C_read16_LE_reg(i2cAddress, ENS160_REG_DATA_AQI_S, &is_ok);
        }
        else {
          _data_aqi500 = 0;
        }
      }
    }
    else {
      // Some issues with the device connectivity, move to error state
      this->moveToState(P164_STATE_ERROR);
    }
  }

  #ifdef P164_ENS160_DEBUG
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      String log;
      log += F("P164: measure() state= ");
      log += printState(this->_state);
      log += F(", aqi= ");
      log += _data_aqi;
      log += F(", tvoc= ");
      log += _data_tvoc;
      log += F(", eco2= ");
      log += _data_eco2;
      log += F(", AQI500= ");
      log += _data_aqi500;
      log += F(", newdata = ");
      log += newData;
      addLogMove(LOG_LEVEL_INFO, log);
    }
  #endif // ifdef P164_ENS160_DEBUG

  return newData;
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// Following code is used to handle custom aquisition modes as defined by Sciosense Github code  //
// Note that custom modes are not documented in the official datasheet                           //
///////////////////////////////////////////////////////////////////////////////////////////////////
#ifdef P164_USE_CUSTOMMODE

///////////////////////////////////////////////////////////////////////////////////////////////////
// Initialize definition of custom mode with <n> steps                                           //
// This feature is not documented in the datasheet, but code is provided by Sciosense            //
///////////////////////////////////////////////////////////////////////////////////////////////////
bool P164_data_struct::initCustomMode(uint16_t stepNum) {
  bool result = false;

  if (stepNum > 0) {
    this->_stepCount = stepNum;
    result           = this->writeMode(ENS160_OPMODE_IDLE);
    result           = this->clearCommand();
    result           = I2C_write8_reg(i2cAddress, ENS160_REG_COMMAND, ENS160_COMMAND_SETSEQ);
  } 
  else {
    result = false;
  }

  return result;
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// Add a step to custom measurement profile with definition of duration                          //
// enabled data acquisition and temperature for each hotplate                                    //
// This feature is not documented in the datasheet, but code is provided by Sciosense            //
///////////////////////////////////////////////////////////////////////////////////////////////////
bool P164_data_struct::addCustomStep(uint16_t time, bool measureHP0, bool measureHP1,
                                     bool measureHP2, bool measureHP3, uint16_t tempHP0,
                                     uint16_t tempHP1, uint16_t tempHP2, uint16_t tempHP3) {
  uint8_t seq_ack;
  uint8_t temp;
  bool    is_ok;

  #ifdef P164_ENS160_DEBUG
    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      addLogMove(LOG_LEVEL_DEBUG, concat(F("setCustomMode() write step "), this->_stepCount));
    }
  #endif // ifdef P164_ENS160_DEBUG

  // TODO check if delay is needed
  // delay(ENS160_BOOTING);                   // Wait to boot after reset

  temp = (uint8_t)(((time / 24) - 1) << 6);

  if (measureHP0) { temp = temp | 0x20; }
  if (measureHP1) { temp = temp | 0x10; }
  if (measureHP2) { temp = temp | 0x8; }
  if (measureHP3) { temp = temp | 0x4; }
  I2C_write8_reg(i2cAddress, ENS160_REG_GPR_WRITE_0, temp);

  temp = (uint8_t)(((time / 24) - 1) >> 2);
  I2C_write8_reg(i2cAddress, ENS160_REG_GPR_WRITE_1, temp);

  I2C_write8_reg(i2cAddress, ENS160_REG_GPR_WRITE_2, (uint8_t)(tempHP0 / 2));
  I2C_write8_reg(i2cAddress, ENS160_REG_GPR_WRITE_3, (uint8_t)(tempHP1 / 2));
  I2C_write8_reg(i2cAddress, ENS160_REG_GPR_WRITE_4, (uint8_t)(tempHP2 / 2));
  I2C_write8_reg(i2cAddress, ENS160_REG_GPR_WRITE_5, (uint8_t)(tempHP3 / 2));

  I2C_write8_reg(i2cAddress, ENS160_REG_GPR_WRITE_6, (uint8_t)(this->_stepCount - 1));

  if (this->_stepCount == 1) {
    I2C_write8_reg(i2cAddress, ENS160_REG_GPR_WRITE_7, 128);
  } else {
    I2C_write8_reg(i2cAddress, ENS160_REG_GPR_WRITE_7, 0);
  }

  // TODO check if delay is needed
  // delay(ENS160_BOOTING);
  seq_ack = I2C_read8_reg(i2cAddress, ENS160_REG_GPR_READ_7, &is_ok);


  // TODO check if delay is needed
  // delay(ENS160_BOOTING);                   // Wait to boot after reset

  if ((ENS160_SEQ_ACK_COMPLETE | this->_stepCount) != seq_ack) {
    this->_stepCount = this->_stepCount - 1;
    return false;
  } else {
    return true;
  }
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// Perfrom raw measurement and store result in internal variables                                //
///////////////////////////////////////////////////////////////////////////////////////////////////
bool P164_data_struct::measureRaw() {
  bool    newData = false;
  bool    is_ok = true;

  if (this->_state == P164_STATE_OPERATIONAL)  {
    this->getStatus();
    if (IS_NEWGPR(this->_statusReg)) {
      newData = true;

      // Read raw resistance values
      _hp0_rs = CONVERT_RS_RAW2OHMS_F(I2C_read16_LE_reg(i2cAddress, ENS160_REG_GPR_READ_0, &is_ok));
      _hp1_rs = CONVERT_RS_RAW2OHMS_F(I2C_read16_LE_reg(i2cAddress, ENS160_REG_GPR_READ_2, &is_ok));
      _hp2_rs = CONVERT_RS_RAW2OHMS_F(I2C_read16_LE_reg(i2cAddress, ENS160_REG_GPR_READ_4, &is_ok));
      _hp3_rs = CONVERT_RS_RAW2OHMS_F(I2C_read16_LE_reg(i2cAddress, ENS160_REG_GPR_READ_6, &is_ok));

      // Read baselines
      _hp0_bl = CONVERT_RS_RAW2OHMS_F(I2C_read16_LE_reg(i2cAddress, ENS160_REG_DATA_BL+0, &is_ok));
      _hp1_bl = CONVERT_RS_RAW2OHMS_F(I2C_read16_LE_reg(i2cAddress, ENS160_REG_DATA_BL+2, &is_ok));
      _hp2_bl = CONVERT_RS_RAW2OHMS_F(I2C_read16_LE_reg(i2cAddress, ENS160_REG_DATA_BL+4, &is_ok));
      _hp3_bl = CONVERT_RS_RAW2OHMS_F(I2C_read16_LE_reg(i2cAddress, ENS160_REG_DATA_BL+6, &is_ok));

      _misr = I2C_read8_reg(i2cAddress, ENS160_REG_DATA_MISR, &is_ok);
    }
  }

  return newData;
}
#endif // P164_USE_CUSTOMMODE

///////////////////////////////////////////////////////////////////////////////////////////////////
// Writes t (degC) and h (%rh) to ENV_DATA. Returns false on I2C problems.                       //
///////////////////////////////////////////////////////////////////////////////////////////////////
bool P164_data_struct::set_envdata(float t, float h) {
  uint16_t t_data  = (uint16_t)((t + 273.15f) * 64.0f);
  uint16_t rh_data = (uint16_t)(h * 512.0f);

  return this->set_envdata210(t_data, rh_data);
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// Writes t and h (in ENS210 format) to ENV_DATA. Returns false on I2C problems.                 //
///////////////////////////////////////////////////////////////////////////////////////////////////
bool P164_data_struct::set_envdata210(uint16_t t, uint16_t h) {
  uint8_t trh_in[4];           // Buffer for I2C registers TEMP_IN + RH_IN

  // temp = (uint16_t)((t + 273.15f) * 64.0f);
  trh_in[0] = t & 0xff;        // TEMP_IN LSB
  trh_in[1] = (t >> 8) & 0xff; // TEMP_IN MSB

  // temp = (uint16_t)(h * 512.0f);
  trh_in[2] = h & 0xff;        // RH_IN LSB
  trh_in[3] = (h >> 8) & 0xff; // RH_IN MSB

  uint8_t result = I2C_writeBytes_reg(i2cAddress, ENS160_REG_TEMP_IN, trh_in, 4); 

  return result;
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// Read firmware revision                                                                        //
// Precondition: Device MODE is IDLE                                                             //
// Note: This is according to original Sciosense software and matches ENS161 datasheet           //
//       The ENS160 datasheet is unclear, suggesting GRP_READ0 and GRP_READ1 registers           //
///////////////////////////////////////////////////////////////////////////////////////////////////
bool P164_data_struct::getFirmware() {
  bool result = false;  // Build return value for function
  bool is_ok;           // Temporary flag to track status
  unsigned long ts;     // Timestamp to limit polling time

  result = I2C_write8_reg(i2cAddress, ENS160_REG_COMMAND, ENS160_COMMAND_GET_APPVER);
  
  // Wait till GRP_READ registers are available.
  is_ok = result;
  ts = millis();
  while (is_ok) {                                     // Abuse is_ok to keep polling until:
    is_ok &= this->getStatus();                       // - Device is inaccessible
    is_ok &= (! IS_NEWGPR(this->_statusReg));         // - Firmware data is available
    is_ok &= (timePassedSince(ts) < ENS160_BOOTING);  // - Timeout occured
  }
  if (! IS_NEWGPR(this->_statusReg)) {                // Check if firmware could be read
    result = false;
    addLogMove(LOG_LEVEL_ERROR, F("P164: Could not read firmware version"));
  }

  if (result)  {
    this->_fw_ver_major = I2C_read8_reg(i2cAddress, ENS160_REG_GPR_READ_4, &is_ok);
    this->_fw_ver_minor = I2C_read8_reg(i2cAddress, ENS160_REG_GPR_READ_5, &is_ok);
    this->_fw_ver_build = I2C_read8_reg(i2cAddress, ENS160_REG_GPR_READ_6, &is_ok);
  }
  else {
    this->_fw_ver_major = 0;
    this->_fw_ver_minor = 0;
    this->_fw_ver_build = 0;
  }

  #ifdef P164_ENS160_DEBUG
    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      String log;
      log += F("P164: getFirmware() result: ");
      log += result ? F("ok") : F("nok");
      log += F(", major=");
      log += this->_fw_ver_major;
      log += F(", minor=");
      log += this->_fw_ver_minor;
      log += F(", build=");
      log += this->_fw_ver_build;
      log += F(", registers=");
      for (int i=0; i<8; i++)
      {
        log += formatToHex_decimal(I2C_read8_reg(i2cAddress, ENS160_REG_GPR_READ_0+i, &is_ok));
        log += ", ";
      }
      addLogMove(LOG_LEVEL_DEBUG, log);
    }
  #endif // ifdef P164_ENS160_DEBUG

  return result;
}


///////////////////////////////////////////////////////////////////////////////////////////////////
// Write to opmode register of the device                                                        //
///////////////////////////////////////////////////////////////////////////////////////////////////
bool P164_data_struct::writeMode(uint8_t mode) {
  bool result;

  result = I2C_write8_reg(i2cAddress, ENS160_REG_OPMODE, mode);

    #ifdef P164_ENS160_DEBUG
      if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
        String log;
        log += F("P164: writeMode(");
        log += mode;
        log += F(") result: ");
        log += result ? F("ok") : F("nok");
        addLogMove(LOG_LEVEL_DEBUG, log);
      }
    #endif // ifdef P164_ENS160_DEBUG

  return result;
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// Read the part ID from ENS160 device and check for validity                                    //
///////////////////////////////////////////////////////////////////////////////////////////////////
bool P164_data_struct::checkPartID(void) {
  uint16_t part_id;   // Resulting PartID
  bool     result = false;

  part_id = I2C_read16_LE_reg(i2cAddress, ENS160_REG_PART_ID, &result);

  if (!result) {
    this->_revENS16x = 0xFF;
    result           = false;
  }
  else if (part_id == ENS160_PARTID) {
    this->_revENS16x = 0;
    result           = true;
  }
  else if (part_id == ENS161_PARTID) {
    this->_revENS16x = 1;
    result           = true;
  }
  else {
    this->_revENS16x = 0xFF;
    result           = false;
  }

  #ifdef P164_ENS160_DEBUG
    String log;
    log += F("P164: checkPartID() result: ");
    switch (part_id) {
      case ENS160_PARTID: log += F("ENS160"); break;
      case ENS161_PARTID: log += F("ENS161"); break;
      default:            log += F("no valid part ID read"); break;
    }
    addLogMove(LOG_LEVEL_DEBUG, log);
  #endif // ifdef P164_ENS160_DEBUG

  return result;
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// Clear any pending command in device                                                           //
///////////////////////////////////////////////////////////////////////////////////////////////////
bool P164_data_struct::clearCommand(void) {
  bool result = false;

  result = I2C_write8_reg(i2cAddress, ENS160_REG_COMMAND, ENS160_COMMAND_NOP);
  result = I2C_write8_reg(i2cAddress, ENS160_REG_COMMAND, ENS160_COMMAND_CLRGPR);
  #ifdef P164_ENS160_DEBUG
    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      addLogMove(LOG_LEVEL_DEBUG, concat(F("P164: clearCommand() result: "), result ? "ok" : "nok"));
    }
  #endif // ifdef P164_ENS160_DEBUG

  return result;
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// Read status register from device                                                              //
///////////////////////////////////////////////////////////////////////////////////////////////////
bool P164_data_struct::getStatus()
{
  bool ret = false;

  this->_statusReg = I2C_read8_reg(i2cAddress, ENS160_REG_DATA_STATUS, &ret);
  #ifdef P164_ENS160_DEBUG
    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      String log;
      log += F("P164: Status register: ");
      log += formatToHex_decimal(this->_statusReg, HEX);
      log += F(", VALIDITY: ");
      log += GET_STATUS_VALIDITY(this->_statusReg);
      log += F(", STATAS: ");
      log += (this->_statusReg & ENS160_STATUS_STATAS) == ENS160_STATUS_STATAS;
      log += F(", STATER: ");
      log += (this->_statusReg & ENS160_STATUS_STATER) == ENS160_STATUS_STATER;
      log += F(", NEWDAT: ");
      log += (this->_statusReg & ENS160_STATUS_NEWDAT) == ENS160_STATUS_NEWDAT;
      log += F(", NEWGRP: ");
      log += (this->_statusReg & ENS160_STATUS_NEWGPR) == ENS160_STATUS_NEWGPR;
      log += F(", return: ");
      log += ret;
      addLogMove(LOG_LEVEL_DEBUG, log);
    }
  #endif // ifdef P164_ENS160_DEBUG
  return ret;
}

#ifdef P164_LEGACY_CODE
///////////////////////////////////////////////////////////////////////////////////////////////////
// I2C functionality copied from Sciosense.                                                      //
///////////////////////////////////////////////////////////////////////////////////////////////////

///////////////////////////////////////////////////////////////////////////////////////////////////
// Read a consecutive range of registers from device                                             //
// Return: boolean == true when I2C transaction was succesful                                    //
// Note: This functionality is not found in ESPEasy I2C library                                  //
///////////////////////////////////////////////////////////////////////////////////////////////////
bool P164_data_struct::readI2C(uint8_t addr, uint8_t reg, uint8_t *buf, uint8_t num) {
  uint8_t pos    = 0;
  uint8_t result = 0;

  #ifdef P164_ENS160_DEBUG
    String log;   // Debug String concatenated in multiple sections. Keep outside local if statement
    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      log += F("P164: I2C read address: ");
      log += formatToHex_decimal(addr);
      log += F(", register: ");
      log += formatToHex_decimal(reg);
      log += F(" data:");
    }
  #endif // ifdef P164_ENS160_DEBUG

  // on arduino we need to read in 32 byte chunks
  while (pos < num) {
    uint8_t read_now = min((uint8_t)32, (uint8_t)(num - pos));
    Wire.beginTransmission((uint8_t)addr);

    Wire.write((uint8_t)reg + pos);
    result = Wire.endTransmission();
    Wire.requestFrom((uint8_t)addr, read_now);

    for (int i = 0; i < read_now; i++) {
      buf[pos] = Wire.read();
      #ifdef P164_ENS160_DEBUG
        if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
          log += F(" ");
          log += formatToHex_decimal(buf[pos]);
        }
      #endif // ifdef P164_ENS160_DEBUG
      pos++;
    }
  }
  #ifdef P164_ENS160_DEBUG
    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      log += F(".");
      addLogMove(LOG_LEVEL_DEBUG, log);
    }
  #endif // ifdef P164_ENS160_DEBUG

  return result == 0;
}
#endif // P164_LEGACY_CODE

#endif // ifdef USES_P164

#include "../PluginStructs/P024_data_struct.h"

#ifdef USES_P024

P024_data_struct::P024_data_struct(uint8_t i2c_addr) : i2cAddress(i2c_addr) {}

float P024_data_struct::readTemperature(uint8_t reg)
{
  float temp = readRegister024(reg);

  temp *= .02f;
  temp -= 273.15f;
  return temp;
}

uint16_t P024_data_struct::readRegister024(uint8_t reg) {
  uint16_t ret;

  Wire.beginTransmission(i2cAddress);
  Wire.write(reg);
  Wire.endTransmission(false);
  Wire.requestFrom(i2cAddress, (uint8_t)3);
  ret  = Wire.read();      // receive DATA
  ret |= Wire.read() << 8; // receive DATA
  Wire.read();
  return ret;
}

#endif // ifdef USES_P024

#include "../PluginStructs/P069_data_struct.h"

#ifdef USES_P069

#include "../Globals/I2Cdev.h"


# define LM75A_BASE_ADDRESS        0x48
# define LM75A_DEGREES_RESOLUTION  0.125f
# define LM75A_REG_ADDR_TEMP     0


P069_data_struct::P069_data_struct(uint8_t addr) :
  _i2c_device_address(addr) {}

float P069_data_struct::getTemperatureInDegrees() const
{
  // Go to temperature data register
  bool success = false;
  int16_t value = I2C_readS16_reg(_i2c_device_address, LM75A_REG_ADDR_TEMP, &success);
  if (!success)
  {
    // Transmission error
    return NAN;
  }

  // Shift data (left-aligned)
  value >>= 5;

  // Relocate negative bit (11th bit to 16th bit)
  if (value & 0x0400) // negative?
  {
    value |= 0xFC00;  // expand to 16 bit
  }

  // Real value can be calculated with sensor resolution
  const float real_result = static_cast<float>(value) * LM75A_DEGREES_RESOLUTION;

  return real_result;
}

#endif // ifdef USES_P069

#include "../PluginStructs/P106_data_struct.h"

#ifdef USES_P106


// Needed also here for PlatformIO's library finder as the .h file
// is in a directory which is excluded in the src_filter
# include <Adafruit_Sensor.h>
# include <Adafruit_BME680.h>

P106_data_struct::~P106_data_struct()
{
  if (bme != nullptr) {
    delete bme;
    bme = nullptr;
  }

}

bool P106_data_struct::begin(uint8_t addr, bool initSettings)
{
  if (!initialized) {
    if (bme == nullptr) {
      bme = new (std::nothrow) Adafruit_BME680();
    }
    if (bme == nullptr) {
      return false;
    }
    initialized = bme->begin(addr, initSettings);

    if (initialized) {
      // Set up oversampling and filter initialization
      bme->setTemperatureOversampling(BME68X_OS_8X);
      bme->setHumidityOversampling(BME68X_OS_2X);
      bme->setPressureOversampling(BME68X_OS_4X);
      bme->setIIRFilterSize(BME68X_FILTER_SIZE_3);
      bme->setGasHeater(320, 150); // 320*C for 150 ms
    }
  }

  return initialized;
}

  bool P106_data_struct::performReading()
  {
    if (bme == nullptr) return false;
    return bme->performReading();
  }

  float P106_data_struct::getTemperature() const
  {
    if (bme == nullptr) return 0.0f;
    return bme->temperature;
  }

  float P106_data_struct::getHumidity() const
  {
    if (bme == nullptr) return 0.0f;
    return bme->humidity;
  }


  float P106_data_struct::getPressure() const
  {
    if (bme == nullptr) return 0.0f;
    return bme->pressure / 100.0f;
  }


  float P106_data_struct::getGasResistance() const
  {
    if (bme == nullptr) return 0.0f;
    return bme->gas_resistance;
  }



#endif // ifdef USES_P106

#include "../PluginStructs/P094_data_struct.h"

#ifdef USES_P094

// Needed also here for PlatformIO's library finder as the .h file
// is in a directory which is excluded in the src_filter
# include <ESPeasySerial.h>

# include <Regexp.h>

#include <MD5Builder.h>

# include "../DataStructs/mBusPacket.h"
# include "../Globals/MQTT.h"

// # include "../Globals/ESPEasy_time.h"
// # include "../Globals/TimeZone.h"
// # include "../Helpers/ESPEasy_Storage.h"
// # include "../Helpers/StringConverter.h"


P094_data_struct::P094_data_struct() :  easySerial(nullptr) {}

P094_data_struct::~P094_data_struct() {
  if (easySerial != nullptr) {
    delete easySerial;
    easySerial = nullptr;
  }
}

void P094_data_struct::reset() {
  if (easySerial != nullptr) {
    delete easySerial;
    easySerial = nullptr;
  }
}

bool P094_data_struct::init(ESPEasySerialPort port,
                            const int16_t     serial_rx,
                            const int16_t     serial_tx,
                            unsigned long     baudrate) {
  if ((serial_rx < 0) && (serial_tx < 0)) {
    return false;
  }
  reset();
  easySerial = new (std::nothrow) ESPeasySerial(port, serial_rx, serial_tx);

  if (easySerial == nullptr) {
    return false;
  }
  easySerial->begin(baudrate);
  return true;
}

void  P094_data_struct::setFlags(unsigned long filterOffWindowTime_ms,
                bool          intervalFilterEnabled,
                bool          mute,
                bool          collectStats)
{
  filterOffWindowTime     = filterOffWindowTime_ms;
  interval_filter.enabled = intervalFilterEnabled;
  collect_stats           = collectStats;
  mute_messages           = mute;
}


void P094_data_struct::loadFilters(struct EventStruct *event, uint8_t nrFilters)
{
  int offset_in_block = 0;


  const size_t chunkSize    = P094_filter::getBinarySize();
  const size_t maxNrFilters = 1024u / chunkSize;

  if (nrFilters > maxNrFilters) { nrFilters = maxNrFilters; }

  _filters.clear();

  size_t nrChunks = 8;

  if (nrFilters < nrChunks) {
    nrChunks = nrFilters;
  }

  const size_t bufferSize = nrChunks * chunkSize;

  while (nrFilters > 0) {
    uint8_t buffer[bufferSize];
    ZERO_FILL(buffer);

    LoadCustomTaskSettings(event->TaskIndex, buffer, bufferSize, offset_in_block);
    offset_in_block += bufferSize;

    uint8_t *readPos = buffer;

    for (size_t i = 0; i < nrChunks && nrFilters > 0; ++i) {
      P094_filter filter;
      filter.fromBinary(readPos);

      if (filter.isValid()) {
        _filters.push_back(filter);
      }

      --nrFilters;
      readPos += chunkSize;
    }
  }
}

String P094_data_struct::saveFilters(struct EventStruct *event) const
{
  int offset_in_block = 0;

  String res;
  const size_t nrFilters = _filters.size();
  size_t currentFilter = 0;
  size_t actualNrFilters = 0;
  const size_t chunkSize = P094_filter::getBinarySize();
  const size_t nrChunks  = 8;
  #ifdef ESP32
  const size_t bufferSize = 1024;
  #else
  const size_t bufferSize = 256;
  #endif

  std::vector<uint8_t> buffer;
  buffer.resize(bufferSize);
  

  while ((offset_in_block + bufferSize) <= 1024 && res.isEmpty()) {
    for (auto it = buffer.begin(); it != buffer.end(); ++it) {
      *it = 0;
    }

    uint8_t *writePos  = &buffer[0];
    size_t   writeSize = 0;

    while (writeSize < bufferSize && currentFilter < nrFilters) {
      if (_filters[currentFilter].isValid()) {
        size_t size{};
        const uint8_t *binaryData = _filters[currentFilter].toBinary(size);
        memcpy(writePos, binaryData, size);
        writePos  += size;
        writeSize += size;
        ++actualNrFilters;
      }
      ++currentFilter;
    }
    res              = SaveCustomTaskSettings(event->TaskIndex, &buffer[0], bufferSize, offset_in_block);
    offset_in_block += bufferSize;
  }
  if (P094_NR_FILTERS < actualNrFilters) {
    P094_NR_FILTERS = actualNrFilters;
  }
  return res;
}

void P094_data_struct::clearFilters()
{
  _filters.clear();
}

bool P094_data_struct::addFilter(struct EventStruct *event, const String& filter)
{
  P094_filter f;

  f.fromString(filter);

  if (!f.isValid()) {
    return false;
  }

  if (isDuplicate(f)) {
    if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
      addLogMove(LOG_LEVEL_ERROR, concat(F("CUL Reader : Duplicate filter found: "), f.toString()));
    }

    return false;
  }

  _filters.push_back(f);

  // No sorting as this may have unexpected side-effects
  // std::sort(_filters.begin(), _filters.end());

  if (P094_NR_FILTERS < _filters.size()) {
    P094_NR_FILTERS = _filters.size();
  }
  return true;
}

String P094_data_struct::getFiltersMD5() const
{
  if (mute_messages) {
    return F("blockall");
  }

  MD5Builder md5;
  uint8_t  checksum[16]{};
  md5.begin();

  uint8_t nrFiltersAdded = 0;
  const char separator[] = {'|', 0};
  for (auto it = _filters.begin(); it != _filters.end(); ++it) {
    if (it->isValid()) {
      if (nrFiltersAdded != 0) {
        md5.add(separator);
      }
      md5.add(it->toString().c_str());
      ++nrFiltersAdded;
    }
  }

  if (nrFiltersAdded == 0) {
    // No filters, thus all messages will just pass
    return F("pass");
  }

  md5.calculate();
  md5.getBytes(checksum);

  return formatToHex_array(checksum, sizeof(checksum));
}

void P094_data_struct::WebformLoadFilters(uint8_t nrFilters) const
{
  if (nrFilters > 0) {
    addFormNote(F("Filter Fields: Manufacturer, Meter Type, Serial, Filter Window"));
  }

  for (uint8_t filterLine = 0; filterLine < nrFilters; ++filterLine)
  {
    if (filterLine < _filters.size()) {
      _filters[filterLine].WebformLoad(filterLine);
    } else {
      P094_filter dummy;
      dummy.WebformLoad(filterLine);
    }
  }
}

void P094_data_struct::WebformSaveFilters(struct EventStruct *event, uint8_t nrFilters)
{
  _filters.clear();

  for (uint8_t filterLine = 0; filterLine < nrFilters; ++filterLine)
  {
    P094_filter dummy;

    if (dummy.WebformSave(filterLine)) {
      // Filter with filled in values, worth storing
      if (!isDuplicate(dummy)) {
        _filters.push_back(dummy);
      }
    }
  }
  addHtmlError(saveFilters(event));
}

bool P094_data_struct::isInitialized() const {
  return easySerial != nullptr;
}

void P094_data_struct::sendString(const String& data) {
  if (isInitialized()) {
    if (data.length() > 0) {
      setDisableFilterWindowTimer();
      easySerial->write(data.c_str());

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLogMove(LOG_LEVEL_INFO, concat(F("Proxy: Sending: "), data));
      }
    }
  }
}

# if P094_DEBUG_OPTIONS

const __FlashStringHelper* getDebugSentences(int& count) {
  // *INDENT-OFF*
  switch (count) {
    case 1: return F("b3C449344369291352337D55472593107009344230A920000200C0538ECE32625004C0527262500426CBF2CCC0805BDF032262500C2086CDF21326CFFFF046D26BB1103DA22B4E093E2"); break; //QDS.0A.00073159"); break; //QDS.37.35919236
    case 2: return F("b9644A732260729700A0AB8487A4E10002002747D00046D030AC1270CB02E0600000000446D3B17BF2C4C0600000083410084016D3B17DE268C010600000000CC190B0106000000008C020600000000CC0206B190000000008C030600000000CC030600008B9400008C040600000000CC04060000000099348C050600000000CC0506000000008C0673DA0600000000CC0606000000008C070600E0F80000003C22030402000F841001245C84E7"); break; //LUG.0A.70290726
    case 3: return F("bCE44A8153132000801022ADC7F6900C005098D2F2D70E24F3E43458739FD572B2DB7CB22EA563C57F3017308E093A4CBC662DF70F000A2E2B18215FC7098DBC7DC8A2ABAD8202F700C5A7D8B0FC89094823FC6B54565730369E73039146898536381B5DE8B8F3A5377A807EB30383ACC0176176C6C18265932082844F0B5A3F69B0A66FD0E35FAED9A53B825E073FC1E67193A727C97BC1025229C87421FA0381443A5F2F2897AE44D383FE125614F08BABEC6B46DF0FFCB910DAD1B3CD53B44AA83726492D845F840A2D20B738E9FB212D5C74FF91FD2796A22D669CBF0B0FEC1BAFA171A65FFB165B2E9"); break; //EMH.02.08003231
    case 4: return F("b5344E2306291001500030F388C30A7900F002C2583AE010032E1E493C32BEF51CDA37A430030071027A19EE14B0BBCAD656D0783516CCB7CFBC6AAFAECDCAD70020FE3DA54FCBC8EC2AED88DFD0972C55CF9336E1683574ABADBD046BB53623F8013"); break; //LGB.03.15009162
    case 5: return F("b5344A732806139690404B70A8C2063900F002C25923338000C8BC361CE2EE050FD3B7A6340300710DEC49523134391877289A80A53A505655A833F754F221E619D08FB4DB5AD773EAB16B545B306C69D1493CD851012BBF4624A5DDA556AF07E83E5"); break; //LUG.04.69396180
    case 6: return F("b9644A732460335700A043C1F7ACD0000200274DE02046D030D94250C804C0623000000446D3B177F2C4C060000005D610084016D3B179E248C010600000000CCB81D0106000000008C020600000000CC0206B190000000008C030600000000CC030600008B9400008C040600000000CC04060000000099348C050600000000CC0506000000008C0673DA0600000000CC0606000000008C070600E0F80000003C22000000000F0010004FF980F9"); break; //LUG.04.70350346
    case 7: return F("b13440000000000DA00DAA8CF7101FD0C3A02FD171101CB938032"); break; //@@@.DA.DA000000
    case 8: return F("b63445A146699750001026CE68C20D7900F002C25D7CE0C000B7C13179B38522166CE7AD700400710CDEF8D2A82F77DD15E367871F1E04261AAFAC430C2B55C1DED4A3148306D4C296CF10D72C9E79310A47DD73FDBFDF2CEA6490B6CA12A30EE5D64621A90B5E71F75D50D24C87B10E2ADDF802E"); break; //EBZ.02.00759966
    case 9: return F("b5E44496A3680003888049D2D7A1D0050053FBA7B54810C548AC112ECFC76CE753AF07A625248C05827C843371AB5DC6C6C8D5D457E845B4B67FB4CEFF06720EA7A9112BFD0A96BC7E97D49FB9BBD59155D109433F0C4823DEA7A13E5281C00E4945F5B05D7518CE085EC8BFE738122"); break; //ZRI.04.38008036
    case 10 : return F("b1B44A5110301808238379BA77241022436931581038A88000002A7184A0AD900E327803A"); break; //ELS.03.36240241"); break; //DME.37.82800103
    case 11 : return F("b3E44B405399098502E0439DC7A0F70300530C7144B5962760A55DE8BA4E49C37676B0CD702698B5FBCE59E35E33D33F736AE13FB1C31DD43ADDC3FE7FF8E1EDC01D749974884BBF96580FE"); break; //AMT.04.50989039
    case 12 : return F("b5B4479169014216130377E5B8C2034900F002C256448000029E1A134984E32773DF97289000047791611023400302CBD0710FF4380B4AE49A140E94F319BE97049FDA8DDC96A8DC437F3BFB02ADC86082E9507934C7ED4FC6F4F678D613F25C09A1DDE927D817F5A824A8012"); break; //ESY.02.47000089"); break; //ESY.37.61211490
    case 13 : return F("b7B445A1415200000023724958C20A0900F002C25B4F60800C0E1D417100AD1BB6EDC72324371005A140102A00050F9B10710112A08DB9869998DE2C0D8614F76213F8682D10A8EF2951413C839461E8E3139EA62193E02B1584E6EC8EDB082AB70C6504F1ADF9E6ABD270E96FE8745AEB93C454FC3C9EAA2D5FCD6679E8A38A3E0818D4B6652993CEE5F8E514867801D"); break; //EBZ.02.00714332"); break; //EBZ.37.00002015
    case 14 : return F("b49449344100549253508227F780DFF5F3500824600007E0007B06EFF2BE9FF000000007F2C000000009E24000000DAC000008000800080008000800080000000E9F10000000000000000002F046D010E8625249880E6"); break; //QDS.08.25490510
    case 15 : return F("b9644A732370335700A045D187A440000200274DD02046D071189260C33FD0638010000446D3B177F2C4C06000000DFDD0084016D3B179F258C010641000000CC0DBA0106000000008C020600000000CC0206B190000000008C030600000000CC030600008B9400008C040600000000CC04060000000099348C050600000000CC0506000000008C0673DA0600000000CC0606000000008C070600E0F80000003C22000000000F0010004FF980E0"); break; //LUG.04.70350337
    case 16 : return F("b4806AC1956030015020363917256030015AC190203D8000000466D00AC9200118625000D78113131363533303030619C3531343530303030308940FD1A014C933B263A494700004C130546000001FD67030D9A8057"); break; //FML.03.15000356"); break; //FML.03.15000356
    case 17 : return F("b9E44A815242292070102881F7F4B0098050B0E989C939C3479F8904137236FE582B853DCACDD8DB48A717A6B42935CB977102079E6397B07AAD2A648E5B65E44D97F9A020B2BFAE433FA37FCB2A2C32711A5986B301D2F6E4A424C1144D808CE9D592C316B117C572689AC6C1322CC05E81F590CAAF457390F6B39DACC946FA314F8E8A34268157AC4338781C3EF5807F9221394DD1FAB5165E1261614B8B85758851295334DF52D9A4DCE2E1E17A555A21007D2DA802B"); break; //EMH.02.07922224
    case 18 : return F("b2E44B05C99010100021B65BE7AC30000002F2F0A6605020AFB1A33041AA002FD971D00002F2F2F2F2F2F2F2F2F2FDF772F2F2F2F2F25EE8008"); break; //WEP.1B.00010199
    case 19 : return F("b2E44B05C75000000041B8EF87A510000002F2F0A6661010AFB1A8905449802FD971D00002F2F2F2F2F2F2F2F2F2FDF772F2F2F2F2F25EE80F8"); break; //WEP.1B.00000075
    case 20 : return F("b1E44C418EA76010001034ECB7A820010A5597AB1CCF2014088590E64BCAB21DC1CC068DAA08035"); break; //FFD.03.000176EA
    case 21 : return F("b4E442423245450514A07545F7AC6004005BDB5EDF3750DF41725EE867C3E39750E20B9F5FB092089B6A5DC7AA586101778BCD5EAD4995B102AC639F0FB4D12403EFE3554F72CAE4F5D348CC374F571CCE4A98634318027AAF3E7DD8600"); break; //HYD.07.51505424
    case 22 : return F("b4344A511031008667607CFF48C0031900F002C2531D311006D980E9CC8D28524C6417AE570210710CFF2DB65BFE77C51602690DAEB954A5455A2ABED621B74AC56A79D81390EBFCADC9D3D34F5928DDC"); break; //DME.07.66081003
    case 23 : return F("b4344A511031008667607CFF48C0031900F002C2532D31100BC55A548A0082663A6FC7AE670210710C7B178D748F610E2CB16DE82C823EF83334EF1A0C383FB42DF7BED1846323211FC25DCC1EBF085DE"); break; //DME.07.66081003
    case 24 : return F("b4344A511031008667607CFF48C0031900F002C2533D311004E0C2E86D68C08F425747AE77021071013F9284E0CB5896FFB27D33882716D09F7EC8175FD04516AAE9122E584095D6441E14E5B4ED189DD"); break; //DME.07.66081003
    case 25 : return F("b6E44A511825169584004B8737AB500600554B11A7F9F7DDCAC35695A3191EA83FD79D1876F378419D5AB37CA1BD857243492B6379B258A4831015F9F0D4B7098218D7E7C44421422FCABC0770F0E67C16EFA13ABEE798D58062CDB0F06AA312592C085F046D29B64F7031FE17CC7EC8B44D61FB5E2F37301ACA7AAC025666C802C"); break; //DME.04.58695182
    case 26 : return F("b4944C51402203571000451A77A090001202F2F046D2E299926040687ED7214000001FD17000413F6670400043B009378000000042B00000000025B1900025F1986B90002610A0003FD0C05000002FD0B3011F52BA393"); break; //EFE.04.71352002
    case 27 : return F("b2844C5146427807103073D877234626016C5140007D72000202F2F04DD1C6D2F3498260413B96E010001FD1700066080E7"); break; //EFE.07.16606234"); break; //EFE.07.71802764
    case 28 : return F("bA644C514960080900307DABF7296008090C5140007ED0000202F2F426E8D6C7E2944133C08000001FD1700840113039188130000C40113D611000084021324108D5A0000C40213A70F00008403132C0F00003550C403137B0E0000840413C0080000C404749913650800008405133C080000C40513AD831B07000084061380050000C406138401006999008407133A000000C407133A00000084286B081300000000046D332A8F260413A7137132000003FD0C02032102FD0B01114D8480FA"); break; //EFE.07.90800096"); break; //EFE.07.90800096
    case 29 : return F("b5E442515695800000C1A452E7A590050252502B45C2E823EF856FABFC4775E28D2904492FB74BA65A843BA7E63BC698D83279AE2BE04B957699517818F7B33F8E58001A7C9CE0E73C5769487247954D0A000E811754BAB1CED0E61567EB1FE504B091E0C7DCF4632E3FAC31618804D"); break; //EIE.1A.00005869
    case 30 : return F("b4806AC1956030015020363917256030015AC19020323000000466D0009780004872B000D7811313136353330303028633531343530303030308940FD1A014C933B263A494700004C130546000001FD67030D9A8030"); break; //FML.03.15000356"); break; //FML.03.15000356
    case 31 : return F("bY5044972608062002001A7AC90340A517F5A8F83D78281AEFF06FDBDEDEF842336FA663F292D6EEACB0F54CA02FB47C8F587862B352ED30166FEE61753998230C38444F845C9FCC364D3C614095F92D14A28010"); break; //ITW.1A.02200608
    case 32 : return F("bY5E449726900634160004728499181897A60030BA0040A53FB81E378B33F9E23FEDF6A0B1B381F4972C2842041CC7EC8D74D675CE56222039CE82385073750F2B695CBEC9B0E8A48EC3F09B74C26E4194A06B7974203DDD0C7976874216E0D282DE"); break; //ITW.04.16340690"); break; //ITW.30.18189984
    case 33 : return F("bY50449726141331160007728499181897A600307E0030A5500A4109842EF76DD4A2DEDF6722CCB4D0746C8505086D91ED34B41AFD24FED0111715A21E549191B1529EE2AE8229E50E7900000000000070208AD8"); break; //ITW.07.16311314"); break; //ITW.30.18189984
    case 34 : return F("bY5044972625353893071A7A0B0040A5B12EC2E009C467CAD2AF0A38712684FE764C6181D2969A7F0F7B076CB9719FEB21C32E48161EEA5A1E6E92F354FED894994C368F17E6F68E2E6AA1D7C289E3FD7A908015"); break; //ITW.1A.93383525
    case 35 : return F("b2E449726606670194107A6AB8CB0D47ABF0000A00413E84B070004FD3442178001C00004933C00000000333B00000A2D00325A0000CD2E801C"); break; //ITW.07.19706660
    case 36 : return F("bY50449726141331160007728499181897A600307E0030A5500A4109842EF76DD4A2DEDF6722CCB4D0746C8505086D91ED34B41AFD24FED0111715A21E549191B1529EE2AE8229E50E7900000000000070208AD8"); break; //ITW.07.16311314"); break; //ITW.30.18189984
    case 37 : return F("b314493447236379635085D337A040000200B6E3103004B6E070300420D0E6C7F2CCB086E310300C2086C9E24326C1422FFFF046D240E86250E4E80E2"); break; //QDS.08.96373672
    case 38 : return F("b494493447236379635083FD9780DFF5F350082030000F00007B06EFFC6F5FF310300007F2C070300009E24310300E13000008000800000000000001F007300A145C9006BFF64003D000C002F046D200E86255AA081DF"); break; //QDS.08.96373672
    case 39 : return F("b494493447153871216062D53780DFF5F350082DA00007F0007C113FF8331FF969799997F2C279899999F2596979988C999000000000000FFFF000000000000008B35000000FFFFFEFF00002F046D25068C2608D2803F"); break; //QDS.06.12875371
    case 40 : return F("b344493447153871216069A667A8000082004ED3926096C2701FD0C110157046D17138F2602FD3CC2010DFF5F0C005FC50861FF000006130701FFFC138B803F"); break; //QDS.06.12875371
    case 41 : return F("b3D44934417196746221AD6FF7AA210000081027C034955230182026CAC3BB62181037C034C41230082036CFFFF0238E2FD171000326CFFFF046D0311B62102FDFE8FAC7E19004F33802D"); break; //QDS.1A.46671917
    case 42 : return F("b3744934417196746221A40247AA310002081027C034955230182026C5AE5B62181037C034C41230082036CFFFF0238E2FD171000326CFFFF046D0311B621AD04802B"); break; //QDS.1A.46671917
    case 43 : return F("b39449344775149131706E1D57A680000200C13750000004C1300000010FC00426CFFFFCC081358000000C2086C9F61212502BB560000326CFFFF046D070B8A268D5880FA"); break; //QDS.06.13495177
    case 44 : return F("b4944934477514913170662C2780DFF5F3500827B0000810007C113FF5A69FF75000000FFFF000000009F255800004659000080008000800080008000800080009B248001000000000004002F046D1F0D8A266CB280FA"); break; //QDS.06.13495177
    case 45 : return F("b3744934484145047231AAE907A9600002081027C034955230082026CD1DAFFFF81037C034C41230082036CFFFF02DDAAFD170000326CFFFF046D0503AD21AB3580D9"); break; //QDS.1A.47501484
    case 46 : return F("b39449344843350131707BA4A7AF00000200C13000100004C13000000C11300426CFFFFCC081361000000C2086C9F6DF62502BB560000326CFFFF046D370A8A26486681E6"); break; //QDS.07.13503384
    case 47 : return F("b49449344843350131707395D780DFF5F350082000000EE0007C113FF4BC0FF00010000FFFF000000009F256100009FB5000080008000800080008000800080009B248001000000000005002F046D020D8A263BD880E7"); break; //QDS.07.13503384
    case 48 : return F("b3C4493440989323533372A78728903252493443307C50000200C133698392007004C1331430400426C7F2CCC081319DD26200700C2086C9F25326CFFFF046D058B67079026F78582DC"); break; //QDS.07.24250389"); break; //QDS.37.35328909
    case 49 : return F("b2C449344098932353337D4E7728903252493443307C000082004ED39ACEB2C06702501FD0C10046D1C06902602FD705D3CC20189FB81DA"); break; //QDS.07.24250389"); break; //QDS.37.35328909
    case 50 : return F("b5344934409893235333714F478077989032524934433070DFF5F3500D11E82CE0000100007C113FFFF362007007FCFC92C314304009F252620070020029102810C8E02730241023902940287023F0254029062770227012F046D14108D26710885D5"); break; //QDS.37.35328909
    case 51 : return F("b1F44685034025063591B28F57AE100000001FD17000265EF070F6A18DFC253E0F351D0900E8180F9"); break; //TCH.1B.63500234
    case 52 : return F("b294468501904601473F0AE14A0009F27EDA5B130CA280080770001045667006BA1007CB2008DC3009ED4000FE50096BA80EB"); break; //TCH.F0.14600419
    case 53 : return F("b294468508220285376F0F19BA0009F27A1280028A128000033000006BC4C006BA1007CB2008DC3009ED4000FE50096BA80DC"); break; //TCH.F0.53282082
    case 54 : return F("b50446850920420745937163972612600006850FE036B0000200415CC4CB0551F0084041552FC1C0082046C7C228D92D804951F1E72FE000000006F1BF114A212D1B377124C1EA62E83430E5006511443943F9B47F52901FD17002F1F2F800E"); break; //TCH.03.00002661"); break; //TCH.37.74200492
    case 55 : return F("b3644685005012040714351C0A004372900000060DA4E029B5FBAC4123A84170DBBFBF13D06000000000000000000AE3800000000000000000000000000FFFF94A6"); break; //TCH.43.40200105
    case 56 : return F("b3644685005012040714351C0A0009F298F560290E10101A40000FFF719F04E99E7B2F9E2E512000000000000000042B000000000000000000000000000FFFF95A6"); break; //TCH.43.40200105
    case 57 : return F("b8E44A81524229207010276807F680080056E68ED762185BEDAB68F79E2A8BF6562ED1C21105B74E82AA9D1B5E79311AB713DC2F86C785D18F6E470067B86284E8A9D1AF9F36330FE18968F826F8D1B21E99F1C08D45FB9CDBF04B37136E2FC1CED74FF3186482FA3058F6F1BD712BBE20098CCB0C72D51F6013CBB974678706CF2F4D6D0D07A793FE1EC8EAF701AD2BEE5922E69F72C356AC1B009A0E0646B0413DD45802B"); break; //EMH.02.07922224
    case 58 : return F("b374465B2118222001604DDD67A26140000046D1107152A01FD0C06326E796CFFFF0DFF5F0C00083D300001061308131C0BFFFC02FD1700140C7896145559D9FF8032"); break; //LSE.04.00228211
    case 59 : return F("b4BC465B251A16000F193DC2CA083110049244E01394465324604401311FC17067A4F0000000C13714800004C1310BC7A000000426C7F2C02BB560000326CFFFF420A046D2009912682046C9F258C04136643DF650000FFFF80DE"); break; //LSE.00.00000000
    case 60 : return F("b4BC465B251A18000F1950519A08311009E2423003944653252044013781E17067A500000000C13063401004C1310623C000000426C7F2C02BB560000326CFFFF420A046D330E912682046C9F258C04138720452E01000B2780DD"); break; //LSE.00.00000000
    case 61 : return F("b43C465B251A12001F0DDDD95A08310008A2460013144653221587296186735087A4F0000000B6E9102004B6E000041BB00426CFFFF326CFFFF046D2108912682BC3E046C9F258B046E9102000B2A80DD"); break; //LSE.00.00000000
    case 62 : return F("b25C465B251A0000AF1033454A2653205834710290E862400005F3573BB1B9A276422A2271D001D0004C9253580E4"); break; //LSE.00.00000000
    case 63 : return F("b4344A511031008667607CFF48C0031900F002C2531D311006D980E9CC8D28524C6417AE570210710CFF2DB65BFE77C51602690DAEB954A5455A2ABED621B74AC56A79D81390EBFCADC9D3D34F5928DDC"); break; //DME.07.66081003
    case 64 : return F("b4344A511031008667607CFF48C0031900F002C2532D31100BC55A548A0082663A6FC7AE670210710C7B178D748F610E2CB16DE82C823EF83334EF1A0C383FB42DF7BED1846323211FC25DCC1EBF085DE"); break; //DME.07.66081003
    case 65 : return F("b4344A511031008667607CFF48C0031900F002C2533D311004E0C2E86D68C08F425747AE77021071013F9284E0CB5896FFB27D33882716D09F7EC8175FD04516AAE9122E584095D6441E14E5B4ED189DD"); break; //DME.07.66081003
    case 66 : return F("b6E44A511825169584004B8737AB500600554B11A7F9F7DDCAC35695A3191EA83FD79D1876F378419D5AB37CA1BD857243492B6379B258A4831015F9F0D4B7098218D7E7C44421422FCABC0770F0E67C16EFA13ABEE798D58062CDB0F06AA312592C085F046D29B64F7031FE17CC7EC8B44D61FB5E2F37301ACA7AAC025666C802C"); break; //DME.04.58695182
    case 67 : return F("bYB04424341931031950067A800000002F2F0413F1570000046D092EC52504FD17004000000E780000000000004413184B0000426CBF2C840113F057000082016CDE24D3013B4E0400C4016D3128692C8104FD280182046CDE24840413F0570000C40413FD560000840513D1530000C40513CC4F0000840613184B0000C40678D313EA47000084071323430000C40713153F0000840813FD3B0000C4081393380000840913A3340000C4091311310000271081D7"); break; //MAD.06.19033119
    case 68 : return F("b4E44A5113748906370076F917A570040053A66831E6B6C8F06FB2C7CC1F60A673B63ACBF2F6A8D3347D34DE4BBDD4677E5DEC850D5CBBDF5B0AA7095B415EFCD93A124AC3B884FAF226845C439DBF3A2EB486CBE7D8D845383E3E480ED"); break; //DME.07.63904837
    case 69 : return F("b5344A511292535727507858F8C000E900F002C25823E090045AF7EFEDB43FBE691577A82003107105692A21A8C0C7531EEEAC3AA631D6BD790CA1B271C7E3C8860A189DA37AE30E89AB63AD316663EB4AD472596156E592602F8A016E51A5E0D8753"); break; //DME.07.72352529
    case 70 : return F("b5344A5117157367276075A048C00B0900F002C25218501000C373A89C912675F09407A2100310710DEF25F58D772CBD51AD6D2B487ED868B6B086976F51AE52E65D877F0310E9DCA942F4E7F94196DD2821329E662FD4E4C9F20A1A8829C54328EEC"); break; //DME.07.72365771
    case 71 : return F("b2E44A511880110827B0780AF7A4573200592F4DF44D5F258DF39AB376DD4A5BC338BBB2493257A98DC52C5E6299029DC79C01ED377C7F48028"); break; //DME.07.82100188
    case 72 : return F("b3644E61E95413900010E89957212517615E61E3C074230206593240B570397DF6230FBC015B276D63CD694FA79B17D1436E0E9FA1003C0763761BD8FBD67B73A8D"); break; //GWF.07.15765112"); break; //GWF.0E.00394195
    case 73 : return F("b9644FA12237704190007308E7AA8000020046D1E2B862B0413D1EA01F9CA0002FD17000001FD481D426C7F2C4413455C9E8A000084011370E50100C40113A0C200F70100840213EA9E0100C4021349870100735F8403130A700100C40313595301008404C386139F2B0100C4041345FA0000840513BABE2BCD0000C40513DEAA00008406139E8A00A1F700C40613D966000084071390400000C4F6040713E11C00008408136200000087F382DE"); break; //DWZ.07.19047723
    case 74 : return F("bA944FA1283211320020766737A6B009025ADD27B2058B2ADAAE5B5D2C8B11BD295CE460BD1ECA50B533A58FF19F7F9DEC291893DA502B763E7C80CEF440B899C92DE87D5548838FC2C9FF3873927441B0B17222FB14545D2E5A0CF81216074B3DF0911CB99BD20A558F2D873A9A649092F3C7B01BA2BEBCC26A58C0FD99ABC2E5E0E5AFC241A27B8C0863C571C3D586B7E2A06044B792CFE021C21BB1B71BCEE4D5D84E75C3FC0624DF07C8C8832C82140086903FD0C08000002FD0B011116A380EF"); break; //DWZ.07.20132183
    case 75 : return F("b3944934419504913170628617A660000200C13480900004C130000007BBF00426CFFFFCC081315020000C2086C9FC6D42A02BB560000326CFFFF046D1316952B82B084D3"); break; //QDS.06.13495019
    case 76 : return F("b2E446850915944496262C568A0009F276D03B017BC0000060A0A0809A71C0908090706070A0A09090A0B0B080708CB640A060A07071EA586E2"); break; //TCH.62.49445991
    case 77 : return F("b2E446850573060566562ADA6A0007E293200C0191400001400000000CC37000000000000000001010101010101023FB20101010001328686E1"); break; //TCH.62.56603057
    case 78 : return F("b2E4468505162250070628CD3A0009F277D00600A0B00000001010101002A010302010101020102010101010101018F6C0001010102AF7786F8"); break; //TCH.62.00256251
    case 79 : return F("b2F446850302976627462A01BA2064D280000600A240004000307070747EA0706060000000000000000000000000077A2000000000000FFFF80F6"); break; //TCH.62.62762930
    case 80 : return F("b2F446850370321809562C217A2069F270C00A00E0700000001000001B0E800000100010100000101000100000100B1AD000101010001328680F9"); break; //TCH.62.80210337
    case 81 : return F("b2D44653277993613170784017ABA0000000C13196100004C131100003AE500426C7F2C02BB560000326CFFFF046D2ACD2E068527CCD580DF"); break; //LSE.07.13369977
    case 82 : return F("bY24442D2C394864681D168D20AE91BF1622CC255857FE7B524DD67C4944CD428FBE5E0DFAB98021"); break; //KAM.16.68644839
    case 83 : return F("b2D446532490340131706417B7AB20000000C13814000004C13100000A12600426C7F2C02BB560000326CFFFF046D2ACD3B168327AC23800F"); break; //LSE.06.13400349
    case 84 : return F("b3644E61E08106100020E7F627236090021AE4C01071B0020A56EE84D172DF9648D9C4409131B88D3FD85BFD022BCB54971CACB714D617E5563719B95E9B2059A59"); break; //SEN.07.21000936"); break; //GWF.0E.00611008
    case 85 : return F("b3E44FA1287530019011691D07AC6002025D541F7DE1D910813127884F7DF4D80BF600A4323BD730B4639E4E0EA8B86129BDE9DD71D0F800C000109002487721866530201310101B10187CE"); break; //DWZ.16.19005387
    case 86 : return F("b9644FA1261281221000689447AB9000020046D302BAF2704136300009A0E0002FD17000001FD481C426C000044130E9B0000000084011300000000C4011300002A2D000084021300000000C402130000000098F984031300000000C40313000000008404B0D51300000000C404130000000084051300A00E000000C4051300000000840613000000B5CF00C406130000000084071300000000C4E74A071300000000840813000000009995812A"); break; //DWZ.06.21122861
    case 87 : return F("b3E44FA12336300190106617D7A81002025F1114AE0F19A04778065DD02E84809E9F7163157F05FB2506D26A3835904CED370FBCA9E0F800C00010900E10E03000A70207F3101013FFE80E3"); break; //DWZ.06.19006333
    case 88 : return F("b4C44B40968440303170787F77A3F0000000C1305000000046D1A2EAAA19F250F8F00010000000000000000000000D5BD00000000000000000000000000000000FFFF00000000000000000000000000000000FFFF000000FFFF80EE"); break; //BMT.07.03034468
    case 89 : return F("bY29442D2C394864681D168D20B3B0BF162236090AB83DFCC84216131495B5A2DF59242760EDECA043AF20801F"); break; //KAM.16.68644839
    case 90 : return F("b2844C51473278071030605C97237253816C5140006612000202F2F0464E66D243498260413070B010001FD1700146B80DD"); break; //EFE.06.16382537"); break; //EFE.06.71802773
    case 91 : return F("b39449344715387121606AE447AD40000200C13969799994C132798998CC999426C7F2CCC081396979999C2086C9F18DC2502BB560000326CFFFF046D2E0D8D261542803D"); break; //QDS.06.12875371
    case 92 : return F("b39449344724733131706336E7A270000200C13330600004C13100000218B00426C7F2CCC081382010000C2086C9FF2142A02BB560000326CFFFF046D1116952BF4D081DC"); break; //QDS.06.13334772
    case 93 : return F("b3944934451926513180637967A7D0000200C13110000004C13000000B0B100426CFFFFCC081311000000C2086C9F52D52A02BB560000326CFFFF046D3A0B862B28D08027"); break; //QDS.06.13659251
    case 94 : return F("bY5444A85C3281262703077AA90040254A1AED9189683FF741015BCF9C9FD17914758544A14121969793DAA718C7C091F9E26BF16197828BD514A4E66C5460849605A64ACFBD3D3167332F6AF040711E426CBF237DD6802A"); break; //WEH.07.27268132
    case 95 : return F("b394493444993671216075B197AD00000200C13850441004C13286138A3F900426CBF2CCC081345574000C2086CDF284D2302BB560000326CFFFF046D330BD62497B485F8"); break; //QDS.07.12679349
    case 96 : return F("b39449344782049131707F5A37A480000200C13121902004C13000000D42000426CFFFFCC081353750100C2086C9F077F2A02BB560000326CFFFF046D0716952B9ADF80EA"); break; //QDS.07.13492078
    case 97 : return F("b3944934462526513180768437A880000200C13060400004C13000000950300426CFFFFCC081398000000C2086C9F0D7D2A02BB560000326CFFFF046D320B862BCC358012"); break; //QDS.07.13655262
    case 98 : return F("b3C4493440989323533372A78728903252493443307C50000200C133698392007004C1331430400426C7F2CCC081319DD26200700C2086C9F25326CFFFF046D058B67079026F78582DC"); break; //QDS.07.24250389"); break; //QDS.37.35328909
    case 99 : return F("b6644496A721102551437F5C77224831715496A000726005005827161FD1AB240AE86C2A6D7F691E4C8531E4530AE4FC8A294BE87862FDDDCE843D7679005C55E082A744BC18EA87FF12298AE4258EE9D89B1F9511318B8D7152464FB11007F5CEB827893513C954A6E9735185FE268124771D567A080E8"); break; //ZRI.07.15178324"); break; //ZRI.37.55021172
    case 100 : return F("bY50449726041331160007728499181897A60030E10030A59E74DF73596296EE08CF3F1BA88B47A3A264C30177EC921B750B03B99F992A0EEB064560EFAD6C5EC7CCA1009D72C63B0E79000000000000697E801E"); break; //ITW.07.16311304"); break; //ITW.30.18189984
    case 101 : return F("b2F44090773754205100794947ADD100000046D070DBB2404130F000029E0000259F0D834FD17010000000424132B5729820001FD7462313A802A"); break; //AXI.07.05427573
    case 102 : return F("bAE44EE4D449858203C07C8B87A0100A0257AB76440C2E16196857D0FAC3205AFB9D036CD7885C1F60C61095F2300D08DF56026E74FFB2F876BD89D27F65B3EA3729F53B26A48F2A5684EDD67A16177F8DD127C2CD8FE1C42CD5035E7EE110515C7369DB07AA59D5954E077C30AA29423D12429C77F6A7DEF679B3A90D7FD075AB7ED262466ECB4FDB66C609FC5DCBEA89FBCEB7BAA4EB8C312CF1A242B39A696A25E81CF0B5994B25CBDC06749D29F5B0F9E9F98476FFF9ED428840C0082459878C8FCEBA90EC380F7"); break; //SON.07.20589844
    case 103 : return F("b4344010648020780011656A78C001D900F002C2561675D61AD80F90FDEC404B8CAB47A80032007100A387268EF9F9CC8106E5D7FEE6E9D41D7E8780E2B18C2F71743F96AC442C6191D9B8E8E2AEE85F1"); break; //APA.16.80070248
    case 104 : return F("b2E44685017467703627254FFA0009F299A23A02C3A000008060605050B45060504020407060412140C0F0F100E111BFE070B0A0908C0EC81DC"); break; //TCH.72.03774617
    case 105 : return F("b2E446850812896626572C47AA0009F290D1620272200060001018DE4606A430F1101000701010302010403020403DC700002050302E7F581EE"); break; //TCH.72.62962881
    case 106 : return F("b2E44685007457014707281B0A0009F270400600A0100000000000001A1A60000000000010000000000000000010086A10000000000FFFF8007"); break; //TCH.72.14704507
    case 107 : return F("b2F4468509851603374725693A2069F27CE00600A600000000409090A8C140B090C0C0F0E070F0C0D0F0E0E0B0F0A640F09090D0E1009932C80FB"); break; //TCH.72.33605198
    case 108 : return F("b2F446850396450729572A1CAA2069F27FC09800D3E0200000238383EF87E353F3A3B3A2C2E1E2B3830111E2A181FA0AB2432273805208FCC86D2"); break; //TCH.72.72506439
    case 109 : return F("b76447916987435614037E4C2729646866079161102FC0060058CA7D7567090FFC84CBDE4FE2996BF8080E6A2C41D6DF46D8921894C814CC0BB36C550312F03A2734512ED7A69349DE3C349CFC079B62A3AD93BAA84A087C739785EFC9A415F6AF0662B05401FD0C4C9D4A676DA7FC1F6C592823EA6B6DFCAA3DDB2E4D5B6E87C5EA1B92E903D62"); break; //ESY.02.60864696"); break; //ESY.37.61357498
    case 110 : return F("bCE44A8154457060801022C397FDE00C005F4FEE3A225126DE3E8344CEACB8502C615B4F7EE9D9CA99F2E3AB5AEA9B3417ABD2B8DF835C0A8C31510DC184ACC8E261E24B717F51C01887D9B39D57965A4004CB68B65206E173F7374489CCBBAB63D4F4B33B488DE06DD33C93EB719BDD805E331238755D22C7E94ECD3636C1CCD965CF4E6DC37123EA8D95771BBCD12431AD12B646EBD21FCB77BD2D100C9CCCE7546268B3AF080DEC8F6B61BCC209013295A74A2730D9CB11E52056D0C0879EA29A2CE24210EAF58587855A790FF343CF82914E0164ED08C70FD54DB800BD9B1C0F2431883324E43A1D293"); break; //EMH.02.08065744
    case 111 : return F("bCE44A8153132000801022ADC7F9000C00546CF1AA7C3268079B9F3A0D8D2FA9AE941DDCDA1D0D8CFA6358FB9FDAD0AF38BB344D954ADD742E77428CA48DD918F361E23D5805BA6BB4CD2470016F66B92CF5D2AF69C7751F1FD2D887A10F26865E6AD95F58754980416935DA7A6C669823CDA9A3CF4D0EBE233D7F706DA6ECFD8209C8F140A7666B9AB2D9ED8C0D5AE04C3600B0899207B54AA98FABF9D14F773F41A53378E9A5678516B95EDEDED334821972EC196927F794E72725C78300FE65155E4E49B43A329EE44AD6FD90070548499CCFFD9FB71ACB4183B4B14DEF7D8B99BF02B754E651850960D80F6"); break; //EMH.02.08003231
    case 112 : return F("b7B445A1415200000023724958C2049900F002C259D7515009EC6163C4A7CB4E8935772324371005A140102490050646D0710FDE80DD548AA00BBCFEB3152BF11CDD91D4D31F1C73144BBB17696622104EB91D19A32FF7CA939126767495134EE598AF96AC9F0081A83B4D7EAF7A497981C15604C85E4E3B9FBA393BDAD8E0DED15EED4BA887D9970D17797849FE68045"); break; //EBZ.02.00714332"); break; //EBZ.37.00002015
    case 113 : return F("b5B445A14152000000237E4CE8C2048900F002C259C751500003AD02FC41ACF50E92F72324371005A140102480030E8FC8710B2BC8D30DC227AD803ED24F3B6FCC6E5131442E65A7F5CC4589D51AE2FB2690E9457810BDAAAC81988A3E5E81E6CDA24BE256218D907511C8044"); break; //EBZ.02.00714332"); break; //EBZ.37.00002015
    case 114 : return F("b1E44C418557901000102E4437ADB00108561D52810CD237CA87FB7C5A2F62A65C25C457BF78028"); break; //FFD.02.00017955
    case 115 : return F("bY4344471325798761013672193170604713010210000000000004788F419E030DFD110E39313133303730363030475A4431060200000000000006823C0000000000004E88"); break; //DZG.02.60703119"); break; //DZG.36.61877925
    case 116 : return F("b7B447916419806613037A83A8C204C900F002C25B4AA0000CC2128E94C8F3D946F737210552961791611024C00500ACF0710FCFB475BDA1FD49D52FF6FB45E833D5D97609373AB3EB562E01CE23D1389FFEC7EE41D4E7B20D35D8B80581C289834F4E3C542C09B0C37331332A321B79DAF7F60CDB028452AE3A26291EFD25C87DDC9EACF462C7B441510340F755C800B"); break; //ESY.02.61295510"); break; //ESY.37.61069841
    case 117 : return F("b6B44791641980661303756A58C204B900F002C25B3AA0000ABEB03EC47E933C072B97210552961791611024B00400536871036D216BF2B74E11F1952539F406B7D4DB7721B1154E21C36F0F55BD4B5B5FF9977572BCD27D93A909347A0D84AE9E804C361E9E6D98F9BE296F76E3A5100357CDD3FA09AABE44103EB508044"); break; //ESY.02.61295510"); break; //ESY.37.61069841
    case 118 : return F("b73447916856565601002F8DC8C0037900F012C25809A62002B5C02299AC829AD82267A370050471010A9AE87B893A51E226DD3203AD775D84E2B2E04D96F85DA0CD258EBC10912241ACAEB1406433A637F6C70811B7FC99565C388A0365A9B57F94E0380317F5CA1052D6FE55C7BE3118A4D1CD05AABC646F5D288B129D7934E68CF030A"); break; //ESY.02.60656585
    case 119 : return F("b7B447916419806613037A83A8C20EC900F002C259EB10000F76F8B1B2D9E402B9498721055296179161102EC00508B8E0710072DE1601CB75C3C9C30E9E037024B1D516151D3724CCF1654B2AC7707B5B41F2BBE3F2C19E9467A0740AB6A4F7F03F5E764FCCECC689EEA66BB4A9A6F25FDDCE8A60E9D998A43936B8A3B2EB57A8F667B476D1ED1CD2381D0ABF6118033"); break; //ESY.02.61295510"); break; //ESY.37.61069841
    case 120 : return F("b6344FA30883161560002C10A8C2042900F002C2549030000BC2C8A72651D570AEB967AA7004007103CB056B91B8007B8882351411FD19256641A220725C724891287445B85FDFDA2DB638B542BB7FC7209833DE16EDCBB90FA686C10076EED63BD40AB0EE3EBCDFDF40032F5C0DCD66322BE804A"); break; //LGZ.02.56613188
    case 121 : return F("b6B445A14043300000237C1FB8C200E900F002C250E000000F96A649A04D7DC457E8972807759005A1401020E0040A6D20710A07B663F2ED91A0DEA4515C0C1D4A296569A7C4DC27AE724F38CF0E518949AFAA9C3AA8703638F5305E2A333941756C4F5BF4703A411B7FE92F58D7F2724E31E10DCDCBF2C7EE18CD4738043"); break; //EBZ.02.00597780"); break; //EBZ.37.00003304
    case 122 : return F("b5B445A14043300000237FF3F8C2001900F002C2501000000574262230ACE2FB2FB5072807759005A140102010030303C8710DD05806D5E2EFB0ACD9DA1964A191D7BADA8E1A508EE7CFA86336B14B1C756AA1E20FA5CE029D2CE99CC84BFA0CAFD723A5D0BC20E6FECC585FF"); break; //EBZ.02.00597780"); break; //EBZ.37.00003304
    case 123 : return F("b4E447916110000600702D8607A99004005860F4BADF0716289AEE56290239E4549E908B33CF4C280DEDD3382DF293865824ADFA25EBDDDF28DB046A59DA2A4C2DB62CF177F2E77EF3E62D6A67DEA6BBD01BEA1DFB9ED3DC8743E2E"); break; //ESY.02.60000011
    case 124 : return F("bD344A815394643100002D3DD8C2003900F002C25741E0000A17997475A31870EDCF27A0300B007101E6BCA68B34F4603824A04FC0B87A398438300B31366E3B94F45C0EEBFF210719F87A8382509BC329B59A4031ECAC4CA734374B06825329A3665EAA2C626DA096C8E50002E95443D540F92C90EE305931E8E066BB8A76AA2DDE94042B90B94E9D3174612DBF8755145BBA760A85B7C84DDF075950FFDE6EEB4EF76A664F4A474CE050E12021434806BB9C4CDDA5CA8F13DDE8010C5DB0EAD57FD5E6DF042A1C0AE83DA23EB2B8C3444A11D8A6F6274072E516F42DA449DC460D14C1D9E12F95CAE43112976493AD493F0"); break; //EMH.02.10434639
    case 125 : return F("b8E44A815262292070102C8F57FED00800592BC9808524F745DE09733D6B862F1FEF9AC816CCD59B730C0BC8D173E7B1B187735C505768A284780C3B7E53CCF252EC84F44DF2A8949DB12139FD80D000930321F57E73C8F22B44AB57C03F93C35B02B853E45A0960F0FCD326F4760CA76945EADF6F294356AC8308DE284EEBCECB57EDF9BB63BCC5D424A764348B2D40E86C812AEA58CA14B652DD3853BEF5BDF56A27F8031"); break; //EMH.02.07922226
    case 126 : return F("b5344A81594176710020267788C20D7900F002C256A000000E4D7497D043A0EE5B2A47AD7003007102F390C301AD7CB52A6B8633CA25AA26BDE4FDF94F3230E43CCBF66EEC0D4C0C7A6E66310DC6376768FE891C0CA84DD365D3FBF23690BB55E812B"); break; //EMH.02.10671794
    case 127 : return F("b2644AC482711000050378A347201271100AC4850021B0000002F2F0C01C103895936002F2F2F2F2F2F2F2F84F48029"); break; //REL.02.00112701"); break; //REL.37.00001127
    case 128 : return F("b7C44361C120001000002CA9C8C203F7A3F00000004050000000004FBCBA782750000000004FB82F53C000000000412B22A0000000004FB140000000004FB943C59F00000000004FDD9FC010000000004FDD954D8FC020000000004FDD9FC030000000004CF6EFDC8FC012909000004FDC8FC02F10400CDDC0004FDC8FC03F104000002FB2EF40101DC37FD1700DE8C80FE"); break; //GAV.02.00010012
    case 129 : return F("b3244361C373601000102603A8C20457A4500000004050900000004FBDA3482750800000004FB82F53C0000000004C3AE2A2415000001FD17001EB2801B"); break; //GAV.02.00013637
    case 130 : return F("bY394447135523636001027A830000000478232D9D030DFD110E35353332333630363030475A44310602ED8B0000000006823C00000000000004E4802B"); break; //DZG.02.60632355
    case 131 : return F("b314493447236379635085D337A040000200B6E3103004B6E070300420D0E6C7F2CCB086E310300C2086C9E24326C1422FFFF046D240E86250E4E80E2"); break; //QDS.08.96373672
    case 132 : return F("b494493447236379635083FD9780DFF5F350082030000F00007B06EFFC6F5FF310300007F2C070300009E24310300E13000008000800000000000001F007300A145C9006BFF64003D000C002F046D200E86255AA081DF"); break; //QDS.08.96373672
    case 133 : return F("b494493447153871216062D53780DFF5F350082DA00007F0007C113FF8331FF969799997F2C279899999F2596979988C999000000000000FFFF000000000000008B35000000FFFFFEFF00002F046D25068C2608D2803F"); break; //QDS.06.12875371
    case 134 : return F("b344493447153871216069A667A8000082004ED3926096C2701FD0C110157046D17138F2602FD3CC2010DFF5F0C005FC50861FF000006130701FFFC138B803F"); break; //QDS.06.12875371
    case 135 : return F("b3D44934417196746221AD6FF7AA210000081027C034955230182026CAC3BB62181037C034C41230082036CFFFF0238E2FD171000326CFFFF046D0311B62102FDFE8FAC7E19004F33802D"); break; //QDS.1A.46671917
    case 136 : return F("b3744934417196746221A40247AA310002081027C034955230182026C5AE5B62181037C034C41230082036CFFFF0238E2FD171000326CFFFF046D0311B621AD04802B"); break; //QDS.1A.46671917
    case 137 : return F("b39449344775149131706E1D57A680000200C13750000004C1300000010FC00426CFFFFCC081358000000C2086C9F61212502BB560000326CFFFF046D070B8A268D5880FA"); break; //QDS.06.13495177
    case 138 : return F("b4944934477514913170662C2780DFF5F3500827B0000810007C113FF5A69FF75000000FFFF000000009F255800004659000080008000800080008000800080009B248001000000000004002F046D1F0D8A266CB280FA"); break; //QDS.06.13495177
    case 139 : return F("b3744934484145047231AAE907A9600002081027C034955230082026CD1DAFFFF81037C034C41230082036CFFFF02DDAAFD170000326CFFFF046D0503AD21AB3580D9"); break; //QDS.1A.47501484
    case 140 : return F("b39449344843350131707BA4A7AF00000200C13000100004C13000000C11300426CFFFFCC081361000000C2086C9F6DF62502BB560000326CFFFF046D370A8A26486681E6"); break; //QDS.07.13503384
    case 141 : return F("b49449344843350131707395D780DFF5F350082000000EE0007C113FF4BC0FF00010000FFFF000000009F256100009FB5000080008000800080008000800080009B248001000000000005002F046D020D8A263BD880E7"); break; //QDS.07.13503384
    case 142 : return F("b3C4493440989323533372A78728903252493443307C50000200C133698392007004C1331430400426C7F2CCC081319DD26200700C2086C9F25326CFFFF046D058B67079026F78582DC"); break; //QDS.07.24250389"); break; //QDS.37.35328909
    case 143 : return F("b2C449344098932353337D4E7728903252493443307C000082004ED39ACEB2C06702501FD0C10046D1C06902602FD705D3CC20189FB81DA"); break; //QDS.07.24250389"); break; //QDS.37.35328909
    case 144 : return F("b5344934409893235333714F478077989032524934433070DFF5F3500D11E82CE0000100007C113FFFF362007007FCFC92C314304009F252620070020029102810C8E02730241023902940287023F0254029062770227012F046D14108D26710885D5"); break; //QDS.37.35328909
    case 145 : return F("b624468501509651494085A758C002A900F002C25DECE080028421E778E39B0665A707ADE0030071080A1255F4529D6628D1BD017B641EF0A8046B680C77DAB285B3C2A943522663000821E48556555C93D048F5DF327EFA4BECCA1914A1C0F9EFCE6532756F5E9BF68B0E01E8802D91270"); break; //TCH.08.14650915
    case 146 : return F("b31449344401892903408DFBB7A9B0000200B6E4800004B6E14010042A7D46CBF2CCB086E480000C2086CDF23326CB9BCFFFF046D0E0BD624CF16800B"); break; //QDS.08.90921840
    case 147 : return F("b314493447236379635085D337A040000200B6E3103004B6E070300420D0E6C7F2CCB086E310300C2086C9E24326C1422FFFF046D240E86250E4E80E2"); break; //QDS.08.96373672
    case 148 : return F("b3444EE4D774933221608D1BB7AF7000000046D280DBB24036EEB000059A9426CE1F7436E00000002FF2C000002598620C80B0265F10802FD66A000779E80EA"); break; //SON.08.22334977
    case 149 : return F("b2E44B00971280018540843227A0E0000202F2F036E0000000F1000016F1B8563822A0000E70A812A000000000000AF010000000000FFFF81DD"); break; //BMP.08.18002871
    case 150 : return F("b2E44B00971280018540843227A0E0000002F2F0F0000000000000000718D00000000000000000000000000000000FFFF0B2A00002FF97381DE"); break; //BMP.08.18002871
    case 151 : return F("b26446532392238253508D6A07AAF0000000B6E0000004B6E000000422F866C7F2C326CFFFF046D2D0A83279B6780E8"); break; //LSE.08.25382239
    case 152 : return F("b62446850189272149408C4E28C00CF900F002C2564B20800A81E8BCCCA14532696097A6400300710F61F531C03426B9317EAD69912E862AC2017148D8C179D50E133470B7A04CD063D00E6FB1C522DDDC80A3300335E26FE16F9D339A61C0F41BC1ACF4A0A8A29310674536C344CA64D7D"); break; //TCH.08.14729218
    case 153 : return F("b434468501892721494083F2A8C20CD900F002C255FB20800903D2653461ABE7080E57A5F002007103032214C1915B7A16175B00F90B8EB889A4C280207D9C74F5A4088C584D951BDB20851C3DF9E"); break; //TCH.08.14729218
    case 154 : return F("bY2F44C5145935816213087A080000202F2F046D0F2EBB24036E000000426C9F25436E000000317F00346D00200000F1458737"); break; //EFE.08.62813559
    case 155 : return F("b3E44F536861202000108F54A7A9B0020250A86DF3204D8B7DAFBD62C57159092FFCBB77F612E656C59AD065C96CC24B353B024A6930F800C00010900E10E03005DC1207F1B05074A5380F2"); break; //MWU.08.00021286
    case 156 : return F("b2E446850844616516180E459A0015C284604600A7200003900341D9A5C477E9ABC873D05000000000000000000019BB4000406456E897887DD"); break; //TCH.80.51164684
    case 157 : return F("b2E4468506974806164805122A001DE26EB02900D570200000000040D21820D4E301126353030334B44170B0A01004EE40000000000FFFF83D9"); break; //TCH.80.61807469
    case 158 : return F("b324468500441169269802F7CA0119F272F01600A3B00C8082F09000007730000000500000220140B1807030000007111000000000000000001C29A85D5"); break; //TCH.80.92164104
    case 159 : return F("b33446850710351129480ABE9A20F9F270000D00E03000128090B0900DA3600000000000000000000000100020000274800000000000000000000FFFF80E3"); break; //TCH.80.12510371
    case 160 : return F("b4944C51402203571000451A77A090001202F2F046D2E299926040687ED7214000001FD17000413F6670400043B009378000000042B00000000025B1900025F1986B90002610A0003FD0C05000002FD0B3011F52BA393"); break; //EFE.04.71352002
    case 161 : return F("b2844C5146427807103073D877234626016C5140007D72000202F2F04DD1C6D2F3498260413B96E010001FD1700066080E7"); break; //EFE.07.16606234"); break; //EFE.07.71802764
    case 162 : return F("bA644C514960080900307DABF7296008090C5140007ED0000202F2F426E8D6C7E2944133C08000001FD1700840113039188130000C40113D611000084021324108D5A0000C40213A70F00008403132C0F00003550C403137B0E0000840413C0080000C404749913650800008405133C080000C40513AD831B07000084061380050000C406138401006999008407133A000000C407133A00000084286B081300000000046D332A8F260413A7137132000003FD0C02032102FD0B01114D8480FA"); break; //EFE.07.90800096"); break; //EFE.07.90800096
    case 163 : return F("b4806AC1956030015020363917256030015AC19020323000000466D0009780004872B000D7811313136353330303028633531343530303030308940FD1A014C933B263A494700004C130546000001FD67030D9A8030"); break; //FML.03.15000356"); break; //FML.03.15000356
    case 164 : return F("b3644A511621280223837CE5E7241022436931581038C002005A8F458578136DF07A14CE37F82BA702DF936647F231F18C961CF7A6CE31CFAACE57153A8888B"); break; //ELS.03.36240241"); break; //DME.37.22801262
    case 165 : return F("b1F44685034025063591B28F57AE100000001FD17000265EF070F6A18DFC253E0F351D0900E8180F9"); break; //TCH.1B.63500234
    case 166 : return F("b294468501904601473F0AE14A0009F27EDA5B130CA280080770001045667006BA1007CB2008DC3009ED4000FE50096BA80EB"); break; //TCH.F0.14600419
    case 167 : return F("b294468508220285376F0F19BA0009F27A1280028A128000033000006BC4C006BA1007CB2008DC3009ED4000FE50096BA80DC"); break; //TCH.F0.53282082
    case 168 : return F("b50446850920420745937163972612600006850FE036B0000200415CC4CB0551F0084041552FC1C0082046C7C228D92D804951F1E72FE000000006F1BF114A212D1B377124C1EA62E83430E5006511443943F9B47F52901FD17002F1F2F800E"); break; //TCH.03.00002661"); break; //TCH.37.74200492
    case 169 : return F("b3644685005012040714351C0A004372900000060DA4E029B5FBAC4123A84170DBBFBF13D06000000000000000000AE3800000000000000000000000000FFFF94A6"); break; //TCH.43.40200105
    case 170 : return F("b3644685005012040714351C0A0009F298F560290E10101A40000FFF719F04E99E7B2F9E2E512000000000000000042B000000000000000000000000000FFFF95A6"); break; //TCH.43.40200105
    case 171 : return F("b5E44496A973430000D1AFCF97AF7CB50057AB6BD92ED8A65D4DB8A19DDA1B6D3CD164A0F600C93485BCFC48263B255C4FC57033B6114A4DD1590F6E3B22855F7161BBFB100973B49CDB3593DEC5164ECF04F1CAB0311E89B873ECECCD7FBF0D60D125734B551865D12F7D012748025"); break; //ZRI.1A.00303497
    case 172 : return F("b4E449344492512971437674F72090328039344141A7510002081027CB4ED034955230F82026C8927C1027C0354466C2C230FC4026D2C0E892781037C034C412307BC3282036C892702FD171000326CFFFF048C536D2809BA22ECC883DC"); break; //QDS.1A.03280309"); break; //QDS.37.97122549
    case 173 : return F("b4E449344505427971537894372380266079344151A6203002081027C8B4A034955230282026CFFFFC1027C035446B3DE2302C4026D1107632681037C034C41234AE90082036CFFFF02FD170008326CFFFF0469C16D000BB822FEF28024"); break; //QDS.1A.07660238"); break; //QDS.37.97275450
    case 174 : return F("b3E44934435188745211A19F67801FD080D81027C034955230182026CF8CB932381037C034C41230082036CFFFF0359F5FD17300010326CFFFF046D060A8428027576FDAC7E5F0192628012"); break; //QDS.1A.45871835
    case 175 : return F("b3E44934455319745221A29607801FD088A81027C034955230F82026CB7439C2381037C034C41230082036CFFFF037174FD17500010326CFFFF046D260B862B02842AFDAC7E8200271080FB"); break; //QDS.1A.45973155
    case 176 : return F("b3E44934404155047231ABDD57801FD084C81027C034955230082026CCC02FFFF81037C034C41230082036CFFFF03E0CFFD17000000326CFFFF046D080DAC2102EA18FDAC7E11005C3E80E0"); break; //QDS.1A.47501504
    case 177 : return F("bY60442515485001000C1A7A23005025568AED71E43AF834900BEC738E08C4FA2637B8915FB401FD6296F19C3AEECEEBC3164B967CD5445E6AAFE90F416314191CB1839210B7CD2EFE168911FD465DAB56CCDA9C82862B90F29353AB57532B49E67E"); break; //EIE.1A.00015048
    case 178 : return F("bY75442515639716000C1A8C208A900F002C25A7000000DDCE55D203E089D07AA800500710760097C0A7F24E9681882D62EFF802EC33146C9B3828FD5B2026432F40E7098DA78C4538579DDE260B2DCCE933093DA312A2C499D4473F150422121279632724B2FCB44A2110D2A2DA87B8C084512CDD698F"); break; //EIE.1A.00169763
    case 179 : return F("b5E442515954400000C1AA17A7A74005025F6B841CED5F1892796E8217F31F08E864EF5C0BBBDFE640AE3711C34C4B5B8AE3B821F1F0F3FF81C8259CBDABD6D05A0A751305C3399E9450DE8E86BE0BDB2D7AFA79BB10179B7EB1F37983CB8C7F10746888DFA54B4CD95AB8C78018025"); break; //EIE.1A.00004495
    case 180 : return F("b5E442515695800000C1A452E7AF200502547F361FEDBED8040998A4AF106CE368D0D469CFE69DD1983E5D5A84078AB4F4F2AFD97FD57668A660038C14F4B79CC3CAB703C3C4ADB6B19AE027E25C4E157B23E9A4D5A7CF24F4962BD29591A73F3E3BD13C9F26D15826364C2DFB28051"); break; //EIE.1A.00005869
    case 181 : return F("b37449344981002451F1AF6837AC418002081027C034955230082026C7CB5FFFF81037C034C41230282036C6D2A027047FD178400326C962A046D080FA623CD2B8AD1"); break; //QDS.1A.45021098
    case 182 : return F("bY5844972642631092001A727299291897A60030B21340A5C84E203CFF62E039C2A1F61CF679A05B7DA7F31E4F0AAA0F30EFFECF4AC176AC9E173C426799C618D50A4B285CBB9074CDF78FA733C7AEC4F66C45D760FDFEE2327E8016"); break; //ITW.1A.92106342"); break; //ITW.30.18299972
    case 183 : return F("b3E449344981002451F1A2CED7801FD08FB81027C034955230082026C61FAFFFF81037C034C41230282036C6D2A034D22FD17840018326C962A046D0A0EA523028B4BFDAC7EF0002E078AD5"); break; //QDS.1A.45021098
    case 184 : return F("bY5044972694376092001A7A3E1340A553D753D0583A78B2BC306DF80BA1DBF6FC88DAFCD83AF7D955EC6196B643A571494B99AE831F8894A4726042E23EA5C474411A585EEEEE8E84A15F54562C31168159804D"); break; //ITW.1A.92603794
    case 185 : return F("bY5044972625353893071A7AE40040A517031B1A58F312C641E1E45D19F6DD3EA9B61FC929D70F39747F6A8A3BA53D7ED25B2A86C741728467DADC3652D54F40660B397E72F60CE3434006A1D843B3248D9C8016"); break; //ITW.1A.93383525
    case 186 : return F("b1744242349075722754982E471F202000001FD0C6202FD173000CFB38EF8"); break; //HYD.49.22570749
    case 187 : return F("b1744242349075722754982E471F602000001FD0C6202FD17300044308CEF"); break; //HYD.49.22570749
    case 188 : return F("b1744242349075722754982E471FC02000001FD0C6202FD173000B52092F0"); break; //HYD.49.22570749
    case 189 : return F("b1744242349075722754982E4711402000001FD0C6202FD17300084489CF0"); break; //HYD.49.22570749
    case 190 : return F("b63442D2C272951803504DCB48C2064900F002C251664000038AA5E8D66325C253B6B7A6400400710EDD4746D6402CF31496EE7AE09E634270E5701ED9E5D16E7A5A22EBC15B0CB6AC0EF980F73E5AD3BF6E658AFC24F614AFBF844AD1EC8CA21C0FCF8FC2E9E64C0B28542EE8C7EA37B444A8036"); break; //KAM.04.80512927
    case 191 : return F("b5E44A7329022226704041EDE7A7500502599F38B5BB9B53F705A6B158D76D3C33F390AE5F6E48A051680C3B866F317F4DBC781E920051DB2619F768DA54EB632DA8746E483A5569A9D8C0E16905ED61857D1B9A07C6EB6AE501B22E0D55A7DD4AF67DB88D6DDCCA5B78E5B88528014"); break; //LUG.04.67222290
    case 192 : return F("b60446850090510825937364F8C0039900F002C2584340D0063667A2334810387B25C72989280612423FE048500307DA907106591F67C43C0B36FCA410346B6A06E7CAADE06D06CF2911ED2775E3297F20105876C245C9309EC93EA0F68B3F9FA466371C73B4A39B80FEBAD1F9C40758028"); break; //HYD.04.61809298"); break; //TCH.37.82100509
    case 193 : return F("b62446850189272149408C4E28C00CF900F002C2564B20800A81E8BCCCA14532696097A6400300710F61F531C03426B9317EAD69912E862AC2017148D8C179D50E133470B7A04CD063D00E6FB1C522DDDC80A3300335E26FE16F9D339A61C0F41BC1ACF4A0A8A29310674536C344CA64D7D"); break; //TCH.08.14729218
    case 194 : return F("b434468501892721494083F2A8C20CD900F002C255FB20800903D2653461ABE7080E57A5F002007103032214C1915B7A16175B00F90B8EB889A4C280207D9C74F5A4088C584D951BDB20851C3DF9E"); break; //TCH.08.14729218
    case 195 : return F("b48442423240756341200933E7AA0303A31A074C27AB1B79A4BF71E2818DAA788D064B94612F5AED2C06E10F1A022615C7EA6E4E97D999D450C33C358340DCB3D948F7F9B58E2B922ED1A580BA905B4E34388A184D5"); break; //HYD.00.34560724
    case 196 : return F("bY5044972670314082001A7A120040A5AE9E0AFE53E0E5216516C41E94CCEA4220BC25A8A5CCF38635E315900BA6BEB0CB5E343BF419524FFED59CD28D106314AC875F9812782700A8267FFCDB4A24251099804A"); break; //ITW.1A.82403170
    case 197 : return F("bY5044972670314082000A7A7A0040A5CE2F908D1B6C7FAB19CF06F0EE140BB44A84C7FF54BDA05D33D6EE45686D054984A4467283EDA6514E8094E361082D9555BD043A64D3F593BAE29C577984921E3CBC92D7"); break; //ITW.0A.82403170
    case 198 : return F("b3B44931536000000013721968C30AA900F002C25BBD101005A558A6E08987D124F027212334938931581036C0010657B0710B23E83B33BD1C8F5A3AB0DAFCEFB35C1D44718C68011"); break; //ELS.03.38493312"); break; //ELS.37.00000036
    case 199 : return F("b2E44B05C95720000021B556C7A500020059C8692AAFADBDBCAA36875B54901F2E32655B735A59AB899223306CD8402A02D189C816E86BB8061"); break; //WEP.1B.00007295
    case 200 : return F("b3B449526564412004237BD198C20E7900F002C25641C00002B7452AB08B6F3843AA6725644120095264203E700108D660710B78BE5BE2867BD38DCE24619A59D0A6BB9384E3A80E2"); break; //ITU.03.00124456"); break; //ITU.37.00124456
    case 201 : return F("b2E44A5111863054230037FB57A30002105DE7DCE381F74E06136FEB49A5B3D45B688341DDCF387CC0D6344DF5BF60078C7A596B1A0D3BE8020"); break; //DME.03.42056318
    case 202 : return F("b1B44A5110301808238379BA77241022436931581038A88000002A7184A0AD900E327803A"); break; //ELS.03.36240241"); break; //DME.37.82800103
    case 203 : return F("b2644AC488437000050379BF37201843700AC485003210000002F2F0CFB6E16038161002F2F2F2F2F2F2F2F2C138057"); break; //REL.03.00378401"); break; //REL.37.00003784
    case 204 : return F("b3644496A855900500537290F7285590000496A0103CC002025CF5138AEB21021CE8339724700D0AF89B3CDDAAE2BAD28479AC27ADBE1E3C3B849269E632772E54C"); break; //ZRI.03.00005985"); break; //ZRI.37.50005985
    case 205 : return F("b1B44A5110301808238379BA77241022436931581038F88000002A7183766D900E3278003"); break; //ELS.03.36240241"); break; //DME.37.82800103
    case 206 : return F("b3B449526404107004237CE7B8C2076900F002C25832C020029B8AE8C5A07F337A5EF72046318043041000375001016F10710C9B050BA32F8730DFCC9465A1C3385BA84D8190D801B"); break; //PIP.03.04186304"); break; //ITU.37.00074140
    case 207 : return F("b3B4493154809000001379A2E8C2028900F002C252907000090080FACAA513D7A7B7B729549120430413A03CB001054A107101672D7C39DD223F7A8D3C4FFF7E9CA294237B1AD83DE"); break; //PIP.03.04124995"); break; //ELS.37.00000948
    case 208 : return F("b1944C418637901000103C9CE7A690000A00414EE75620202FD08EAF2A1B38039"); break; //FFD.03.00017963
    case 209 : return F("b3B449526544412004237036C8C2065900F002C25943B00000CC9351967A937062362723991270492262203650810BBE30710483A983059AFE3C21836C683F23BA2B5A04EBAE38021"); break; //ITR.03.04279139"); break; //ITU.37.00124454
    case 210 : return F("b3B4493154909000001375BA68C20BD900F002C25BE0200004F4E34B29075C9986ACB722009210493150003B10010E2D407103C75563866FAC12EBCE6FC5829323547A3CC9A488057"); break; //ELS.03.04210920"); break; //ELS.37.00000949
    case 211 : return F("b3B44931558130000013727848C20DD900F002C25DE0C0000ADFB2D6B2E02C73267AF72011713419315250339001037AB0710A68555FAD7A29A1A875EE33FD26D13EAE33A7AF08035"); break; //ELS.03.41131701"); break; //ELS.37.00001358
    case 212 : return F("b3644A511870350133837198E7280409628931580038F002105F09DD43A485B949F2AB07122E6A6CB3E4BDF9055A351316D145C3EF8522BDF56D9D12495B281D5B0"); break; //ELS.03.28964080"); break; //DME.37.13500387
    case 213 : return F("b3644A511621280223837CE5E724102243693158103850020050075969BAC2F21DD6DEDFFDBDDCF9F0E9A7E066046DF920DF3BFE4DA1174DE1943F173A93C0E2A5B"); break; //ELS.03.36240241"); break; //DME.37.22801262
    case 214 : return F("b1B44A5110301808238379BA77241022436931581038F88000002A7183766D900E3278003"); break; //ELS.03.36240241"); break; //DME.37.82800103
    case 215 : return F("b2E44B05C10130000021B34137A490000002F2F0A6653020AFB1A5105E7D102FD971D00002F2F2F2F2F2F2F2F2F2FDF772F2F2F2F2F25EE8016"); break; //WEP.1B.00001310
    case 216 : return F("b2E44B05C77010000041B1E0B7AE60000002F2F0A6641020AFB1A8103251802FD971D00002F2F2F2F2F2F2F2F2F2FDF772F2F2F2F2F25EE80F9"); break; //WEP.1B.00000177
    case 217 : return F("b4E44333081400004032AF18B7A03004005E5AEF57BA739B9789BD9AAFAE16068AFF9FB85A70613B3800B85D28B409B5BDD9607BE2A1450EEE20137C663FB522F4E6B9E914E8E3795C577BFB32D6E0152C21EB0358C41DD6D9F76498020"); break; //LAS.2A.04004081
    case 218 : return F("b1E443330886102001E1B8E9A7A180000202F2F026584030778281E18ADDC240B0000AB01768017"); break; //LAS.1B.00026188
    case 219 : return F("b2E443330603903003C1B18AF7A77002025BB29E575E63C4AA6174A82CED44D5497FA0207F44D4518D8CFA53CD7024276A675D4E0621C0F80E5"); break; //LAS.1B.00033960
    case 220 : return F("b7044B40908794920101B6F8B7A320000000265490A42651F0A8201650BCEB9072265D70912655F0A62659905526576FB4F0A02FB1ABD0142FB1ABB018201FB1A36B6C60122FB1AB90112FB1ABC0162FB1AC24B1C0152FB1ACF01066D3B1D2EAA25000FFFBE95FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA00FFFFFFFFFFFFFF7B5382DE"); break; //BMT.1B.20497908
    case 221 : return F("b60449615347810630C1BE5957A8C0000202F2F026505094265F908822DF20165880822658C081265680A62655808B1785265680A02FB1A880142FB1A95018201130DFB1AE00122FB1A660112FB1A100262FB491D1A660152FB1A1F0202FD1B60030DFD0F817605302E302E340F6C04"); break; //ELV.1B.63107834
    case 222 : return F("bY2944961565181468201B7A140000202F2F0265E20842659A0802FD1B30030DFD0F05302E302E340F5E46"); break; //ELV.1B.68141865
    case 223 : return F("b1644AF4C40020041011B788C7A0A0000000266F3000266F10026FE800F"); break; //SEO.1B.41000240
    case 224 : return F("b5E442515695800000C1A452E7A590050252502B45C2E823EF856FABFC4775E28D2904492FB74BA65A843BA7E63BC698D83279AE2BE04B957699517818F7B33F8E58001A7C9CE0E73C5769487247954D0A000E811754BAB1CED0E61567EB1FE504B091E0C7DCF4632E3FAC31618804D"); break; //EIE.1A.00005869
    case 225 : return F("b5344E2306291001500030F388C30A7900F002C2583AE010032E1E493C32BEF51CDA37A430030071027A19EE14B0BBCAD656D0783516CCB7CFBC6AAFAECDCAD70020FE3DA54FCBC8EC2AED88DFD0972C55CF9336E1683574ABADBD046BB53623F8013"); break; //LGB.03.15009162
    case 226 : return F("b5344A732806139690404B70A8C2063900F002C25923338000C8BC361CE2EE050FD3B7A6340300710DEC49523134391877289A80A53A505655A833F754F221E619D08FB4DB5AD773EAB16B545B306C69D1493CD851012BBF4624A5DDA556AF07E83E5"); break; //LUG.04.69396180
    case 227 : return F("b9644A732460335700A043C1F7ACD0000200274DE02046D030D94250C804C0623000000446D3B177F2C4C060000005D610084016D3B179E248C010600000000CCB81D0106000000008C020600000000CC0206B190000000008C030600000000CC030600008B9400008C040600000000CC04060000000099348C050600000000CC0506000000008C0673DA0600000000CC0606000000008C070600E0F80000003C22000000000F0010004FF980F9"); break; //LUG.04.70350346
    case 228 : return F("b13440000000000DA00DAA8CF7101FD0C3A02FD171101CB938032"); break; //@@@.DA.DA000000
    case 229 : return F("b63445A146699750001026CE68C20D7900F002C25D7CE0C000B7C13179B38522166CE7AD700400710CDEF8D2A82F77DD15E367871F1E04261AAFAC430C2B55C1DED4A3148306D4C296CF10D72C9E79310A47DD73FDBFDF2CEA6490B6CA12A30EE5D64621A90B5E71F75D50D24C87B10E2ADDF802E"); break; //EBZ.02.00759966
    case 230 : return F("b5E44496A3680003888049D2D7A1D0050053FBA7B54810C548AC112ECFC76CE753AF07A625248C05827C843371AB5DC6C6C8D5D457E845B4B67FB4CEFF06720EA7A9112BFD0A96BC7E97D49FB9BBD59155D109433F0C4823DEA7A13E5281C00E4945F5B05D7518CE085EC8BFE738122"); break; //ZRI.04.38008036
    case 231 : return F("b1B44A5110301808238379BA77241022436931581038A88000002A7184A0AD900E327803A"); break; //ELS.03.36240241"); break; //DME.37.82800103
    case 232 : return F("b3E44B405399098502E0439DC7A0F70300530C7144B5962760A55DE8BA4E49C37676B0CD702698B5FBCE59E35E33D33F736AE13FB1C31DD43ADDC3FE7FF8E1EDC01D749974884BBF96580FE"); break; //AMT.04.50989039
    case 233 : return F("b5B4479169014216130377E5B8C2034900F002C256448000029E1A134984E32773DF97289000047791611023400302CBD0710FF4380B4AE49A140E94F319BE97049FDA8DDC96A8DC437F3BFB02ADC86082E9507934C7ED4FC6F4F678D613F25C09A1DDE927D817F5A824A8012"); break; //ESY.02.47000089"); break; //ESY.37.61211490
    case 234 : return F("b7B445A1415200000023724958C20A0900F002C25B4F60800C0E1D417100AD1BB6EDC72324371005A140102A00050F9B10710112A08DB9869998DE2C0D8614F76213F8682D10A8EF2951413C839461E8E3139EA62193E02B1584E6EC8EDB082AB70C6504F1ADF9E6ABD270E96FE8745AEB93C454FC3C9EAA2D5FCD6679E8A38A3E0818D4B6652993CEE5F8E514867801D"); break; //EBZ.02.00714332"); break; //EBZ.37.00002015
    case 235 : return F("b49449344100549253508227F780DFF5F3500824600007E0007B06EFF2BE9FF000000007F2C000000009E24000000DAC000008000800080008000800080000000E9F10000000000000000002F046D010E8625249880E6"); break; //QDS.08.25490510
    case 236 : return F("b9644A732370335700A045D187A440000200274DD02046D071189260C33FD0638010000446D3B177F2C4C06000000DFDD0084016D3B179F258C010641000000CC0DBA0106000000008C020600000000CC0206B190000000008C030600000000CC030600008B9400008C040600000000CC04060000000099348C050600000000CC0506000000008C0673DA0600000000CC0606000000008C070600E0F80000003C22000000000F0010004FF980E0"); break; //LUG.04.70350337
    case 237 : return F("b4806AC1956030015020363917256030015AC190203D8000000466D00AC9200118625000D78113131363533303030619C3531343530303030308940FD1A014C933B263A494700004C130546000001FD67030D9A8057"); break; //FML.03.15000356"); break; //FML.03.15000356
    case 238 : return F("b9E44A815242292070102881F7F4B0098050B0E989C939C3479F8904137236FE582B853DCACDD8DB48A717A6B42935CB977102079E6397B07AAD2A648E5B65E44D97F9A020B2BFAE433FA37FCB2A2C32711A5986B301D2F6E4A424C1144D808CE9D592C316B117C572689AC6C1322CC05E81F590CAAF457390F6B39DACC946FA314F8E8A34268157AC4338781C3EF5807F9221394DD1FAB5165E1261614B8B85758851295334DF52D9A4DCE2E1E17A555A21007D2DA802B"); break; //EMH.02.07922224
    case 239 : return F("b2E44B05C99010100021B65BE7AC30000002F2F0A6605020AFB1A33041AA002FD971D00002F2F2F2F2F2F2F2F2F2FDF772F2F2F2F2F25EE8008"); break; //WEP.1B.00010199
    case 240 : return F("b2E44B05C75000000041B8EF87A510000002F2F0A6661010AFB1A8905449802FD971D00002F2F2F2F2F2F2F2F2F2FDF772F2F2F2F2F25EE80F8"); break; //WEP.1B.00000075
    case 241 : return F("b1E44C418EA76010001034ECB7A820010A5597AB1CCF2014088590E64BCAB21DC1CC068DAA08035"); break; //FFD.03.000176EA
    case 242 : return F("b4E442423245450514A07545F7AC6004005BDB5EDF3750DF41725EE867C3E39750E20B9F5FB092089B6A5DC7AA586101778BCD5EAD4995B102AC639F0FB4D12403EFE3554F72CAE4F5D348CC374F571CCE4A98634318027AAF3E7DD8600"); break; //HYD.07.51505424
    case 243 : return F("b3C449344454392352337ABFC727972126793442304FF0000200C050321648206004C0530240500426CBF2CCC0805098875630600C2086CDF23326CFFFF046D177FEA11D3242E1F830C"); break; //QDS.04.67127279"); break; //QDS.37.35924345
    case 244 : return F("bY5E449726900634160004728499181897A60030D30040A5D20022C396367C6A868B05FE2E9F70B6878070839C62E69DDC79CB409EBCAD68E950A6958B9B92FE6D1B700065B8BD52CB3035B93653FFDA3C3EF3EF6BBB1B450C7976874216AD188011"); break; //ITW.04.16340690"); break; //ITW.30.18189984
    case 245 : return F("b3444A73228188269040439327A370000202F2F0974040970080C06348AF03500000C14040301000B2D6207000B3BDD4A2322000A5A05090A5E0506F29780E8"); break; //LUG.04.69821828
    case 246 : return F("b9644A732170135700A0476A67A2F00002002747501046D3315952B0C3CF20637000000446D3B177F2C4C06000000F4640084016D3B179F2A8C010636000000CCB85A0106330000008C020633000000CC02067E6A330000008C030633000000CC030600009A1900008C040600000000CC04060000000099348C050600000000CC0506000000008C0673DA0600000000CC0606000000008C070600E0F80000003C22890200000F001000C94A80E8"); break; //LUG.04.70350117
    case 247 : return F("b5E44496A044550108804533A7A19005005B14A145C0EEC7C6FC18A85582753BE2BD08B6F0F9320809C5F28436A3388BB09145DF69DA51ED00DCCFE7BA2D6F86936D17E654489390BB7511CC4DA27A576A9612B1243151C634899A74D3EBDB2CA578CA4444F5740D74D3EC766628013"); break; //ZRI.04.10504504
    case 248 : return F("bD6440186669601001637060C7215067071010660047400C0251CE72725D9364386C4BF8A15AF4C722115F38B66DDE9F4B800D05A14F870E74492B8F57CB2282388B864D71416FDE0A74CA84735B59D1806BBBF8483B6B14AB580F0D5257E8CC06889EBA80AFB032BCEECD849E7E6A4639B5649B0BF5CBB57BF0993AE16EF7D88D784ADD9CE09284880D14D1502967798901C9E24ED6A04A3B26AE580B5D2AEC46804DEC1CBEDCB1E0B3FE1F84679620FD59E297FE8E67374EE9C34CA5A7F45B4B3709C6BD4E0B546F706DDC571C6469CFA90602C0E339A054CDE1D0A0E071651FD49FEBD485D8E561BB36AF7BED2741D35BBC88051"); break; //APA.04.71700615"); break; //APA.37.00019666
    case 249 : return F("bA944C514381355920004A5FC7A3D0090257403AC17355186FF933BAF0D0820041595970BD168489A746DF337AB01D1DBBA3EF7D486D74CCCEE777E57BD12A15D22B89AB4B4266D84B9C5C1DDBC437CD64807AF0B2B232E3A3C1B4DA568C9F8BA39FDF4565C170ACEF1918655B45D82E8BAAD769B88FA917E82542CAD596AB2145433599FFDDD3FCE889F657A8AC07FE5CED3F184244475B59710CFDCA44D7C3AE00CC021FA6EE3F49B77939529B49279FC526803FD0C05010002FD0B01111E248025"); break; //EFE.04.92551338
    case 250 : return F("b6E44A511485268614004B97F7A86006005B8D6D972C8E06EFB5AEC0FC750DB9DFFE62BB59DA183A5709DFC5029CEB2E44FEAF475DD2F3CF6D90544F29372A5C027DACA08536B2457CDEDEB76E36B281F4A0FD4A203FE9F7DFD337BDEA15D02F53A21CB281A3EF01E15D779B3018A99D0E3EF2FF946A63F419E56644BBDF8FB8010"); break; //DME.04.61685248
    case 251 : return F("b3E44A51159351969A004761E7A030030050F0020FC830AABCA7CEE4EC8E44524D98802ED5CA2EF3C89774FF800730951E02569BF91C156331EC9C29730FE2F437128D7E574D29C4E808047"); break; //DME.04.69193559
    case 252 : return F("b7144685009022004593720EB8C00EE900F002C252CA40100F5CDA57367A4436D244E72791670692423FE040600407D7C07108660CA27A4DD53871B12F96FFEF0F84E0CA88A2201BD794859D35CCD051F34327D04EAAE8772675F8E7F28DE2E515FBC9AF3A40E72C078304158D453511F2CF39DBF98DBE1EAD84F0F2CAD975EDDCC4F"); break; //HYD.04.69701679"); break; //TCH.37.04200209
    case 253 : return F("bA944C514381355920004A5FC7A670090251913406458376E6F9CEB817ABA85D9F26E5867EBA222FDFF496F2C8A312E593C80C0604576D7B5269B4A25BD15B314C0598FE3395611400AA7ADB705EE2120686208A449BDC5EE828FF048F669516585611337839C0A40A308F4DE619997F29D2438CE9EC263574FC60D42197CFA7B47528574792BFFF54AFF9193CA678242653A97DEBCB5E5333687846A875A69C4B7371C762C03A2DB5AB738D92623907896AEBF03FD0C05010002FD0B0111C6E48020"); break; //EFE.04.92551338
    case 254 : return F("b5E44C51475051512010425417A1C004025EEA5F744D756D853BB606F5DBB33DD7B26896D7370BD9C50990E605CE03F8F16DEE8AA36EF2C87E3902F933DDD87E1D383B7C647E8D2236E5FF5E1BE6742D65511FE1629B5CD3F6F0F800C000109002F680300035E2001020A0901C98008"); break; //EFE.04.12150575
    case 255 : return F("b5344A5115158536941043C9D8C00CC900F002C25986B0110B13D74311F4D1ACF88797A7E7031071066DEA0D815DFAC31E07763666D4C46C2DEA856FCEC34F2B5CCF3750F72BD8F924544E73C51D7DBC11A1AE31049E5D0E121CE0053AEB8E018805B"); break; //DME.04.69535851
    case 256 : return F("bA944C514381355920004A5FC7A79009025E37121BAC0ACF1A216281AE7A85DA20DF90613A83417418BD9BA7B0F09C192713A65DECD3ECB2DFC5299683A5B2669EE70851493FC9944D7579F7FCD44CCC839E432D498E40EFDF1614BD9B7E6F1B4AD96CC16939C9D061434790765D3883275B80DE0508393A0F00C1B1590A66F94D920BD5E80125C610F7C0B0346D8D0CFE8DD4E8A14370077D90A7BCFA59345DB13D7134E80D5F2F1D50B3B043075126DF42C2A03FD0C05010002FD0B0111ABC78028"); break; //EFE.04.92551338
    case 257 : return F("b4E44EE4D557648251B048F777ABE004025D84601014EEA5EDCC08B007BC8BB1EBB266BADDEC127897EDA4296EEB8EB05A57DFCA5190F54B41C05B1BF0F5EF82B719CD27EA6242263DFB94F589F38843A1146916B872141977275BD8035"); break; //SON.04.25487655
    case 258 : return F("b2644AC488137000050371BF47201813700AC485000D90000002F2F0C32C60E569405002F2F2F2F2F2F2F2FB90E8059"); break; //REL.00.00378101"); break; //REL.37.00003781
    case 259 : return F("b2644AC488137000050371BF47202813700AC48500CE20000002F2F0C068D16635640002F2F2F2F2F2F2F2F72188058"); break; //REL.0C.00378102"); break; //REL.37.00003781
    case 260 : return F("b36446850790133512243C21BA1009F29793C0088230200007E0E4FFD476370544E2D33280000000000000000000CC8A2608214298C81CD45160105964E203B372E"); break; //TCH.43.51330179
    case 261 : return F("b3744685029069372274351B5A2129F27995400305C1200000007400B69792D4C914447BB7D4A2AC86CB37BFAFD59EEC7971B1F070D00000000002C0040004FD085CC"); break; //TCH.43.72930629
    case 262 : return F("b37446850715221622843FE7BA2129F29F827008800000080800D0000DE060000E00739EF48B3861639BC2044115A10CB0C6283102F1830C0080AD861D24C1E8F80E1"); break; //TCH.43.62215271
    case 263 : return F("b374468508475676739430ECDA2109F27793500287F15000000D34D95907D550ADB32A24C9AAAA9922ED6A76075D51C1F7937DB8D4B95F20E4C63EDC5997041E280FA"); break; //TCH.43.67677584
    case 264 : return F("b36446850442620514543A443A1009F279E180038CC0400803680000286DB08F0C00727FCF0C82F92E8128D31165D722873C60C1568910103062C700107EF3F912E"); break; //TCH.43.51202644
    case 265 : return F("b3744685060478860574395F9A20D9F29820500A00000000100060000E34A00000000000000000000000000000000FFFF0000000000000000000000000000FFFF80D4"); break; //TCH.43.60884760
    case 266 : return F("b3644685005012040714351C0A0009F298F560290E10101A40000FFF719F04E99E7B2F9E2E512000000000000000042B000000000000000000000000000FFFF95A6"); break; //TCH.43.40200105
    case 267 : return F("b364468504200545145444FE0A1009F29CA6900B0F00D008023001A68BB4748D28D4F7685755095534E09E187ADE5A7B903D03027041000000100008002CF41D570"); break; //TCH.44.51540042
    case 268 : return F("b37446850341929625744B280A20D9F27C9350058395A00000169848B3EDF2ECEF8A17F008A875B80FCB947E0914351334E69E4A14D72A8E296375279E286AB9B8008"); break; //TCH.44.62291934
    case 269 : return F("b36446850060670527144C1AAA0009F29C41F0190DE0F0081A5A1BF2595D38ABA9F512E89A3471D67F8F0C6206308383B6288298BD0A3D68424A228A9907F8B34B2"); break; //TCH.44.52700606
    case 270 : return F("bY5044972608062002001A7AC90340A517F5A8F83D78281AEFF06FDBDEDEF842336FA663F292D6EEACB0F54CA02FB47C8F587862B352ED30166FEE61753998230C38444F845C9FCC364D3C614095F92D14A28010"); break; //ITW.1A.02200608
    case 271 : return F("bY5E449726900634160004728499181897A60030BA0040A53FB81E378B33F9E23FEDF6A0B1B381F4972C2842041CC7EC8D74D675CE56222039CE82385073750F2B695CBEC9B0E8A48EC3F09B74C26E4194A06B7974203DDD0C7976874216E0D282DE"); break; //ITW.04.16340690"); break; //ITW.30.18189984
    case 272 : return F("bY50449726141331160007728499181897A600307E0030A5500A4109842EF76DD4A2DEDF6722CCB4D0746C8505086D91ED34B41AFD24FED0111715A21E549191B1529EE2AE8229E50E7900000000000070208AD8"); break; //ITW.07.16311314"); break; //ITW.30.18189984
    case 273 : return F("bY5044972625353893071A7A0B0040A5B12EC2E009C467CAD2AF0A38712684FE764C6181D2969A7F0F7B076CB9719FEB21C32E48161EEA5A1E6E92F354FED894994C368F17E6F68E2E6AA1D7C289E3FD7A908015"); break; //ITW.1A.93383525
    case 274 : return F("b2E449726606670194107A6AB8CB0D47ABF0000A00413E84B070004FD3442178001C00004933C00000000333B00000A2D00325A0000CD2E801C"); break; //ITW.07.19706660
    case 275 : return F("bY50449726141331160007728499181897A600307E0030A5500A4109842EF76DD4A2DEDF6722CCB4D0746C8505086D91ED34B41AFD24FED0111715A21E549191B1529EE2AE8229E50E7900000000000070208AD8"); break; //ITW.07.16311314"); break; //ITW.30.18189984

  }
 // *INDENT-ON*
  count = 0;
  return F("");
}

# endif // if P094_DEBUG_OPTIONS

bool P094_data_struct::loop() {
  if (!isInitialized()) {
    return false;
  }
  bool fullSentenceReceived = false;

  if (easySerial != nullptr) {
    int available = easySerial->available();

    unsigned long timeout = millis() + 10;

    while (available > 0 && !fullSentenceReceived) {
      // Look for end marker
      char c = easySerial->read();
      --available;

      if (available == 0) {
        if (!timeOutReached(timeout)) {
          available = easySerial->available();
        }
        delay(0);
      }

      switch (c) {
        case 13:
        {
          const size_t length = sentence_part.length();
          bool valid          = length > 0;

          for (size_t i = 0; i < length && valid; ++i) {
            if ((sentence_part[i] > 127) || (sentence_part[i] < 32)) {
              free_string(sentence_part);
              ++sentences_received_error;
              valid = false;
            }
          }

          if (valid) {
            fullSentenceReceived = true;
          }
          break;
        }
        case 10:

          // Ignore LF
          break;
        default:

          if ((c >= 32) && (c < 127)) {
            sentence_part += c;
          } else {
            current_sentence_errored = true;
          }
          break;
      }

      if (max_length_reached()) { fullSentenceReceived = true; }
    }
  }

  if (fullSentenceReceived) {
    ++sentences_received;
    length_last_received = sentence_part.length();
  }
# if P094_DEBUG_OPTIONS
  else {
    if (debug_generate_CUL_data && (sentence_part.length() == 0)) {
      static uint32_t last_test_sentence = 0;
      static int count                   = 0;

      if (timePassedSince(last_test_sentence) > 1000) {
        count++;

        //        sentence_part = F("b2644AC48585300005037FAB97201585300AC485003150000202F2F0C0AF314213993002F2F2F2F2F2F2F2FAFCA8046");
        sentence_part        = getDebugSentences(count);
        fullSentenceReceived = true;
        last_test_sentence   = millis();
      }
    }
  }
# endif // if P094_DEBUG_OPTIONS
  return fullSentenceReceived;
}

const String& P094_data_struct::peekSentence() const {
  return sentence_part;
}

void P094_data_struct::getSentence(String& string, bool appendSysTime) {
  string        = std::move(sentence_part);

  if (appendSysTime) {
    // Unix timestamp = 10 decimals + separator
    if (string.reserve(sentence_part.length() + 11)) {
      string += ';';
      string += node_time.getUnixTime();
    }
  }
  sentence_part.reserve(string.length());
}

void P094_data_struct::getSentencesReceived(uint32_t& succes, uint32_t& error, uint32_t& length_last) const {
  succes      = sentences_received;
  error       = sentences_received_error;
  length_last = length_last_received;
}

void P094_data_struct::setMaxLength(uint16_t maxlenght) {
  max_length = maxlenght;
}

uint32_t P094_data_struct::getFilterOffWindowTime() const {
  return filterOffWindowTime;
}

void P094_data_struct::setDisableFilterWindowTimer() {
  if (getFilterOffWindowTime() == 0) {
    disable_filter_window = 0;
  }
  else {
    disable_filter_window = millis() + getFilterOffWindowTime();
  }
}

bool P094_data_struct::disableFilterWindowActive() const {
  if (disable_filter_window != 0) {
    if (!timeOutReached(disable_filter_window)) {
      // We're still in the window where filtering is disabled
      return true;
    }
  }
  return false;
}

bool P094_data_struct::parsePacket(const String& received, mBusPacket_t& packet) {
  const size_t strlength = received.length();

  if (strlength == 0) {
    return false;
  }

  const char firstChar = received[0];

  if ((firstChar == 'b')) {
    // Received a data packet in CUL format.
    if (strlength < 21) {
      return false;
    }

    // Decoded packet
    if (!packet.parse(received)) { return false; }

    const mBusPacket_header_t *header = packet.getDeviceHeader();

    if (header == nullptr) {
      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLogMove(LOG_LEVEL_INFO, concat(F("CUL Filter: NO Header "), packet.toString()));
      }

      return false;
    }

    if (mute_messages) {
      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLogMove(LOG_LEVEL_INFO, concat(F("CUL Filter: Muted "), packet.toString()));
      }

      return false; // Mute all messages
    }

    if (!interval_filter.enabled || (_filters.size() == 0)) {
      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLogMove(LOG_LEVEL_INFO, concat(F("CUL Filter: NO Filter "), packet.toString()));
      }

      return true; // No filtering
    }

    for (unsigned int f = 0; f < _filters.size(); ++f) {
      if (_filters[f].matches(*header)) {
        const bool res = interval_filter.filter(packet, _filters[f]);

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          addLogMove(LOG_LEVEL_INFO, concat(F("CUL Filter: Match "), _filters[f].toString()));
          addLogMove(LOG_LEVEL_INFO, concat(res ? F("CUL Filter: Pass ") : F("CUL Filter: Reject "), header->toString()));
        }

        return res;
      }
    }

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, concat(F("CUL Filter: NO Match "), header->toString()));
    }

    // No matching filter, so consider fall-through filter to be:
    // *.*.*;none
    return false;
  } else {
    switch (firstChar) {
      case 'C': // CMODE
      case 'S': // SMODE
      case 'T': // TMODE
      case 'O': // OFF
      case 'V': // Version info

        // FIXME TD-er: Must test the result of the other possible answers.
        return true;
    }
  }

  return false;
}

void P094_data_struct::interval_filter_purgeExpired() {
  interval_filter.purgeExpired();
}

void P094_data_struct::html_show_interval_filter_stats() const
{
  if (interval_filter._mBusFilterMap.empty()) { return; }

  addRowLabel(F("Interval Filter Entries"));
  addHtmlInt(interval_filter._mBusFilterMap.size());

  addFormNote(F("Non expired W-MBus device filters"));
}

bool P094_data_struct::collect_stats_add(const mBusPacket_t& packet, const String& source) {
  if (collect_stats) {
    return mBus_stats[firstStatsIndexActive ? 0 : 1].add(packet, source);
  }
  return false;
}

void P094_data_struct::prepare_dump_stats() {
  firstStatsIndexActive = !firstStatsIndexActive;
}

bool P094_data_struct::dump_next_stats(String& str) {
  const uint8_t dumpStatsIndex = firstStatsIndexActive ? 1 : 0;

  if (mBus_stats[dumpStatsIndex]._mBusStatsMap.empty()) { return false; }

  str = concat(F("stats;"), mBus_stats[dumpStatsIndex].getFront());

  return true;
}

void P094_data_struct::html_show_mBus_stats() const
{
  const uint8_t dumpStatsIndex = firstStatsIndexActive ? 0 : 1;

  if (mBus_stats[dumpStatsIndex]._mBusStatsMap.empty()) { return; }

  addRowLabel(F("W-MBus Devices"));
  addHtmlInt(mBus_stats[dumpStatsIndex]._mBusStatsMap.size());

  addFormNote(F("Devices received since last <tt>culreader,dumpstats</tt>"));

  mBus_stats[dumpStatsIndex].toHtml();
}

bool P094_data_struct::max_length_reached() const {
  if (max_length == 0) { return false; }
  return sentence_part.length() >= max_length;
}

bool P094_data_struct::isDuplicate(const P094_filter& other) const
{
  const String f_str = other.toString();

  for (auto it = _filters.begin(); it != _filters.end(); ++it) {
    if (f_str.equals(it->toString())) {
      return true;
    }
  }
  return false;
}

# if P094_DEBUG_OPTIONS
uint32_t P094_data_struct::getDebugCounter() {
  return debug_counter++;
}

# endif // if P094_DEBUG_OPTIONS

#endif  // USES_P094
#include "../PluginStructs/P093_data_struct.h"

#ifdef USES_P093

/*
 *
 * Bi-directional communication with the heat pump.
 *
 * Should support all Mitsubishi HP units with CN105 connector.
 *
 * Plugin is based on "Arduino library to control Mitsubishi Heat Pumps" from
 * https://github.com/SwiCago/HeatPump.
 *
 * SetRemoteTemperature is based on following Issue and Resolve
 * https://github.com/SwiCago/HeatPump/pull/144#issue-514996963
 * https://github.com/SwiCago/HeatPump/pull/144/commits/c50372c7632b9e7324caf0c0fc0773871645688e
 *
 */

P093_data_struct::P093_data_struct(const ESPEasySerialPort port, const int16_t serialRx, const int16_t serialTx, bool includeStatus) :
  _serial(port, serialRx, serialTx),
  _state(NotConnected),
  _fastBaudRate(false),
  _readPos(0),
  _writeTimeout(0),
  _infoModeIndex(0),
  _statusUpdateTimeout(0),
  _tempMode(false),
  _wideVaneAdj(false),
  _valuesInitialized(false),
  _includeStatus(includeStatus) {}

void P093_data_struct::init() {
  setState(Connecting);
}

bool P093_data_struct::sync() {
  if (_statusUpdateTimeout != 0) {
    if (_writeStatus.isDirty()) {
      cancelWaitingAndTransitTo(ApplyingSettings);
      return false;
    }

    if (timeOutReached(_statusUpdateTimeout)) {
      cancelWaitingAndTransitTo(UpdatingStatus);
      return false;
    }
  }

  return readIncommingBytes();
}

bool P093_data_struct::read(String& result) const {
  if (!_valuesInitialized) {
    return false;
  }

  result.reserve(150);

  // FIXME TD-er: See if this macro can be simpler as it does expand to quite some code which is not changing.
  # define map_list(x, list) findByValue(x, list, sizeof(list) / sizeof(Tuple))

  result  = F("{\"roomTemperature\":");
  result += toString(_currentValues.roomTemperature, 1);
  result += F(",\"wideVane\":\"");
  result += map_list(_currentValues.wideVane, _mappings.wideVane);
  result += F("\",\"power\":\"");
  result += map_list(_currentValues.power, _mappings.power);
  result += F("\",\"mode\":\"");
  result += map_list(_currentValues.mode, _mappings.mode);
  result += F("\",\"fan\":\"");
  result += map_list(_currentValues.fan, _mappings.fan);
  result += F("\",\"vane\":\"");
  result += map_list(_currentValues.vane, _mappings.vane);
  result += F("\",\"iSee\":");
  result += boolToString(_currentValues.iSee);

  if (_includeStatus) {
    result += F(",\"operating\":");
    result += boolToString(_currentValues.operating);
    result += F(",\"compressorFrequency\":");
    result += _currentValues.compressorFrequency;
  }
  result += F(",\"temperature\":");
  result += toString(_currentValues.temperature, 1);
  result += F(",\"remoteTemperature\":");
  result += toString(_currentValues.remoteTemperature, 1) + '}';

  return true;
}

bool P093_data_struct::plugin_get_config_value(struct EventStruct *event,
                                               String            & string) {
  if (!_valuesInitialized) {
    return false;
  }
  bool success         = true;
  const String command = parseString(string, 1);

  if (equals(command, F("roomtemperature"))) {
    string = toString(_currentValues.roomTemperature, 1);
  } else
  if (equals(command, F("widevane"))) {
    string = map_list(_currentValues.wideVane, _mappings.wideVane);
  } else
  if (equals(command, F("power"))) {
    string = map_list(_currentValues.power, _mappings.power);
  } else
  if (equals(command, F("mode"))) {
    string = map_list(_currentValues.mode, _mappings.mode);
  } else
  if (equals(command, F("fan"))) {
    string = map_list(_currentValues.fan, _mappings.fan);
  } else
  if (equals(command, F("vane"))) {
    string = map_list(_currentValues.vane, _mappings.vane);
  } else
  if (equals(command, F("isee"))) {
    string = _currentValues.iSee ? '1' : '0';
  } else
  if (equals(command, F("temperature"))) {
    string = toString(_currentValues.temperature, 1);
  } else
  if (_includeStatus && equals(command, F("operating"))) {
    string = _currentValues.operating ? '1' : '0';
  } else
  if (_includeStatus && equals(command, F("compressorfrequency"))) {
    string = _currentValues.compressorFrequency;
  } else
  if (equals(command, F("remotetemperature"))) {
    string = toString(_currentValues.remoteTemperature, 1);
  } else {
    success = false;
  }

  # undef map_list

  return success;
}

void P093_data_struct::write(const String& command, const String& value) {
  # define lookup(x, list, placeholder) findByMapping(x, list, sizeof(list) / sizeof(Tuple), placeholder)

  if (equals(command, F("temperature"))) {
    float temperature = 0.0f;

    if (validFloatFromString(value, temperature) && (temperature >= 16) && (temperature <= 31)) {
      _wantedSettings.temperature = temperature;
      _writeStatus.set(Temperature);
    }
  } else if ((equals(command, F("power"))) && lookup(value, _mappings.power, _wantedSettings.power)) {
    _writeStatus.set(Power);
  } else if ((equals(command, F("mode"))) && lookup(value, _mappings.mode, _wantedSettings.mode)) {
    _writeStatus.set(Mode);
  } else if ((equals(command, F("fan"))) && lookup(value, _mappings.fan, _wantedSettings.fan)) {
    _writeStatus.set(Fan);
  } else if ((equals(command, F("vane"))) && lookup(value, _mappings.vane, _wantedSettings.vane)) {
    _writeStatus.set(Vane);
  } else if ((equals(command, F("widevane"))) && lookup(value, _mappings.wideVane, _wantedSettings.wideVane)) {
    _writeStatus.set(WideVane);
  } else if (equals(command, F("remotetemperature"))) {
    float remotetemperature = 0.0f;

    if (validFloatFromString(value, remotetemperature)) {
      _wantedSettings.remoteTemperature = remotetemperature;
      _writeStatus.set(RemoteTemperature);
    }
  }

  # undef lookup
}

void P093_data_struct::setState(P093_data_struct::State newState) {
  if ((newState != _state) && shouldTransition(_state, newState)) {
    State currentState = _state;
    _state = newState;
    didTransition(currentState, newState);
  } else {
    # ifdef PLUGIN_093_DEBUG
    addLog(LOG_LEVEL_DEBUG, strformat(F("M-AC: SS - ignoring %s -> %s"),
                                      stateToString(_state).c_str(),
                                      stateToString(newState).c_str()));
    # endif // ifdef PLUGIN_093_DEBUG
  }
}

bool P093_data_struct::shouldTransition(P093_data_struct::State from, P093_data_struct::State to) {
  switch (to) {
    case Connecting:
      return from == NotConnected || from == ReadTimeout;

    case Connected:
      return from == Connecting;

    case UpdatingStatus:
      return from == Connected || from == StatusUpdated || from == WaitingForScheduledStatusUpdate;

    case StatusUpdated:
      return from == UpdatingStatus;

    case ScheduleNextStatusUpdate:
      return from == StatusUpdated || from == SettingsApplied;

    case WaitingForScheduledStatusUpdate:
      return from == ScheduleNextStatusUpdate;

    case ApplyingSettings:
      return from == WaitingForScheduledStatusUpdate;

    case SettingsApplied:
      return from == ApplyingSettings;

    case ReadTimeout:
      return from == UpdatingStatus || from == ApplyingSettings || from == Connecting;

    default:
      return false;
  }
}

void P093_data_struct::didTransition(P093_data_struct::State from, P093_data_struct::State to) {
  # ifdef PLUGIN_093_DEBUG
  addLog(LOG_LEVEL_DEBUG, strformat(F("M-AC: didTransition: %s -> %s"),
                                    stateToString(from).c_str(),
                                    stateToString(to).c_str()));
  # endif // ifdef PLUGIN_093_DEBUG

  switch (to) {
    case ReadTimeout:

      // Try to connect using different boud rate if we don't get response while connecting.
      if (from == Connecting) {
        _fastBaudRate = !_fastBaudRate;
      }
      setState(Connecting);
      break;

    case Connecting:
      connect();
      break;

    case Connected:
      responseReceived();
      _infoModeIndex = 0;
      setState(UpdatingStatus);
      break;

    case UpdatingStatus:
      updateStatus();
      break;

    case StatusUpdated: {
      responseReceived();
      const int infoModeCount = _includeStatus ? sizeof(INFOMODE) : sizeof(INFOMODE) - 1;
      _infoModeIndex = (_infoModeIndex + 1) % infoModeCount;

      if (_infoModeIndex != 0) {
        setState(UpdatingStatus);
      } else {
        _valuesInitialized = true;
        setState(ScheduleNextStatusUpdate);
      }
      break;
    }

    case ScheduleNextStatusUpdate:
      _statusUpdateTimeout = millis() + 1000;
      setState(WaitingForScheduledStatusUpdate);
      break;

    case ApplyingSettings:
      // Let's be optimistic and apply local changes immediately so potential
      // read operation will fetch latest settings even if they are in the
      // process of being applied. If settings will fail to apply for some reason
      // then next status update will re-set correct values (values from AC unit).
      applySettingsLocally();
      applySettings();
      _writeStatus.clear();
      break;

    case SettingsApplied:
      responseReceived();
      setState(ScheduleNextStatusUpdate);
      break;

    default:
      break;
  }
}

void P093_data_struct::applySettingsLocally() {
  if (_writeStatus.isDirty(Power)) {
    _currentValues.power = _wantedSettings.power;
  }

  if (_writeStatus.isDirty(Mode)) {
    _currentValues.mode = _wantedSettings.mode;
  }

  if (_writeStatus.isDirty(Temperature)) {
    _currentValues.temperature = _wantedSettings.temperature;
  }

  if (_writeStatus.isDirty(Fan)) {
    _currentValues.fan = _wantedSettings.fan;
  }

  if (_writeStatus.isDirty(Vane)) {
    _currentValues.vane = _wantedSettings.vane;
  }

  if (_writeStatus.isDirty(WideVane)) {
    _currentValues.wideVane = _wantedSettings.wideVane;
  }

  if (_writeStatus.isDirty(RemoteTemperature)) {
    _currentValues.remoteTemperature = _wantedSettings.remoteTemperature;
  }
}

void P093_data_struct::cancelWaitingAndTransitTo(P093_data_struct::State state) {
  _statusUpdateTimeout = 0;
  setState(state);
}

void P093_data_struct::responseReceived() {
  _writeTimeout = 0;
}

void P093_data_struct::updateStatus() {
  # ifdef PLUGIN_093_DEBUG
  addLog(LOG_LEVEL_DEBUG, concat(F("M-AC: US: "), _infoModeIndex));
  # endif // ifdef PLUGIN_093_DEBUG

  uint8_t packet[PACKET_LEN] = { 0xfc, 0x42, 0x01, 0x30, 0x10 };

  packet[5] = INFOMODE[_infoModeIndex];
  memset(packet + 6, 0, 15);
  packet[21] = checkSum(packet, 21);

  sendPacket(packet, PACKET_LEN);
}

void P093_data_struct::applySettings() {
  uint8_t packet[PACKET_LEN] = { 0xfc, 0x41, 0x01, 0x30, 0x10, 0x01 };

  memset(packet + 6, 0, 15);

  if (_writeStatus.isDirty(Power)) {
    packet[8]  = _wantedSettings.power;
    packet[6] |= 0x01;
  }

  if (_writeStatus.isDirty(Mode)) {
    packet[9]  = _wantedSettings.mode;
    packet[6] |= 0x02;
  }

  if (_writeStatus.isDirty(Temperature)) {
    packet[6] |= 0x04;

    if (_tempMode) {
      packet[19] = static_cast<uint8_t>(_wantedSettings.temperature * 2.0f + 128.0f);
    } else {
      packet[10] = 31 - _wantedSettings.temperature;
    }
  }

  if (_writeStatus.isDirty(Fan)) {
    packet[11] = _wantedSettings.fan;
    packet[6] |= 0x08;
  }

  if (_writeStatus.isDirty(Vane)) {
    packet[12] = _wantedSettings.vane;
    packet[6] |= 0x10;
  }

  if (_writeStatus.isDirty(WideVane)) {
    packet[18] = _wantedSettings.wideVane | (_wideVaneAdj ? 0x80 : 0x00);
    packet[7] |= 0x01;
  }

  if (_writeStatus.isDirty(RemoteTemperature)) {
    memset(packet + 6, 0, 15);
    packet[5] = 0x07;

    if (_wantedSettings.remoteTemperature > 0) {
      packet[6]                        |= 0x01;
      _wantedSettings.remoteTemperature = _wantedSettings.remoteTemperature * 2;
      _wantedSettings.remoteTemperature = round(_wantedSettings.remoteTemperature);
      _wantedSettings.remoteTemperature = _wantedSettings.remoteTemperature / 2;

      if (_tempMode) { // units that don't support 0.5 increment
        packet[8] = static_cast<uint8_t>(_wantedSettings.remoteTemperature * 2.0f + 128.0f);
      } else {         // units that do support 0.5 increment
        packet[7] = static_cast<uint8_t>(3.0f + ((_wantedSettings.remoteTemperature - 10.0f) * 2.0f));
      }
    }
    else {
      packet[6] = 0x00;
      packet[8] = 0x80; // MHK1 send 80, even though it could be 00, since ControlByte is 00
    }
  }
  packet[21] = checkSum(packet, 21);
  sendPacket(packet, PACKET_LEN);
}

void P093_data_struct::connect() {
  const unsigned long baud = getBaudRate();

  # ifdef PLUGIN_093_DEBUG
  addLog(LOG_LEVEL_DEBUG, concat(F("M-AC: Connect "), baud));
  # endif // ifdef PLUGIN_093_DEBUG

  _serial.begin(baud, SERIAL_8E1);
  const uint8_t buffer[] = { 0xfc, 0x5a, 0x01, 0x30, 0x02, 0xca, 0x01, 0xa8 };

  sendPacket(buffer, sizeof(buffer));
}

unsigned long P093_data_struct::getBaudRate() const {
  return _fastBaudRate ? 9600ul : 2400ul;
}

void P093_data_struct::sendPacket(const uint8_t *packet, size_t size) {
  # ifdef PLUGIN_093_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG_MORE)) {
    addLog(LOG_LEVEL_DEBUG_MORE, dumpOutgoingPacket(packet, size));
  }
  # endif // ifdef PLUGIN_093_DEBUG

  _serial.write(packet, size);
  _writeTimeout = millis() + 2000;
}

void P093_data_struct::addByteToReadBuffer(uint8_t value) {
  if (_readPos < READ_BUFFER_LEN) {
    _readBuffer[_readPos] = value;
    ++_readPos;
  } else {
    # ifdef PLUGIN_093_DEBUG
    addLog(LOG_LEVEL_DEBUG, F("M-AC: ABTRB(0)"));
    # endif // ifdef PLUGIN_093_DEBUG
    _readPos = 0;
  }
}

bool P093_data_struct::readIncommingBytes() {
  if ((_writeTimeout != 0) && timeOutReached(_writeTimeout)) {
    _writeTimeout = 0;
    _readPos      = 0;
    setState(ReadTimeout);
    return false;
  }

  static const uint8_t DATA_LEN_INDEX = 4;

  while (_serial.available() > 0) {
    const uint8_t value = _serial.read();

    if (_readPos == 0) {
      // Wait for start uint8_t.
      if (value == 0xfc) {
        addByteToReadBuffer(value);
      } else {
        # ifdef PLUGIN_093_DEBUG
        addLog(LOG_LEVEL_DEBUG, strformat(F("M-AC: RIB(0) 0x%x"), value));
        # endif // ifdef PLUGIN_093_DEBUG
      }
    } else if ((_readPos <= DATA_LEN_INDEX) || (_readPos <= DATA_LEN_INDEX + _readBuffer[DATA_LEN_INDEX])) {
      // Read header + data part - data length is at index 4.
      addByteToReadBuffer(value);
    } else {
      // Done, last uint8_t is checksum.
      const uint8_t length = _readPos;
      _readPos = 0;
      return processIncomingPacket(_readBuffer, length, value);
    }
  }

  return false;
}

bool P093_data_struct::processIncomingPacket(const uint8_t *packet, uint8_t length, uint8_t checksum) {
  const P093_data_struct::State state = checkIncomingPacket(packet, length, checksum);

  if (state == StatusUpdated) {
    static const uint8_t dataPartOffset = 5;

    if (length <= dataPartOffset) {
      return false;
    }

    Values values = _currentValues;

    if (parseValues(_readBuffer + dataPartOffset, length - dataPartOffset)) {
      setState(StatusUpdated);
      return values != _currentValues;
    }
  } else if (state != Invalid) {
    setState(state);
  }

  return false;
}

bool P093_data_struct::parseValues(const uint8_t *data, size_t length) {
  if (length == 0) {
    # ifdef PLUGIN_093_DEBUG
    addLog(LOG_LEVEL_DEBUG, F("M-AC: PV(0)"));
    # endif // ifdef PLUGIN_093_DEBUG
    return false;
  }

  switch (data[0]) {
    case 0x02:

      if (length > 11) {
        _currentValues.power = data[3];
        _currentValues.iSee  = data[4] > 0x08 ? true : false;
        _currentValues.mode  = _currentValues.iSee ? (data[4] - 0x08) : data[4];

        if (data[11] != 0x00) {
          _currentValues.temperature = (static_cast<float>(data[11]) - 128.0f) / 2.0f;
          _tempMode                  = true;
        } else {
          _currentValues.temperature = 31 - data[5];
        }

        _currentValues.fan      = data[6];
        _currentValues.vane     = data[7];
        _currentValues.wideVane = data[10] & 0x0F;
        _wideVaneAdj            = (data[10] & 0xF0) == 0x80 ? true : false;

        return true;
      }
      break;

    case 0x03:

      if (length > 6) {
        if (data[6] != 0x00) {
          _currentValues.roomTemperature = (static_cast<float>(data[6]) - 128.0f) / 2.0f;
        } else {
          _currentValues.roomTemperature = data[3] + 10;
        }
        return true;
      }
      break;

    case 0x06:

      if (length > 4) {
        _currentValues.operating           = data[4];
        _currentValues.compressorFrequency = data[3];
        return true;
      }
      break;
  }
  # ifdef PLUGIN_093_DEBUG
  addLog(LOG_LEVEL_DEBUG, F("M-AC: PV(1)"));
  # endif // ifdef PLUGIN_093_DEBUG
  return false;
}

P093_data_struct::State P093_data_struct::checkIncomingPacket(const uint8_t *packet, uint8_t length, uint8_t checksum) {
  # ifdef PLUGIN_093_DEBUG
  addLog(LOG_LEVEL_DEBUG_MORE, dumpIncomingPacket(packet, length));
  # endif // ifdef PLUGIN_093_DEBUG

  if ((packet[2] != 0x01) || (packet[3] != 0x30)) {
    # ifdef PLUGIN_093_DEBUG
    addLog(LOG_LEVEL_DEBUG, F("M-AC: CIP(0)"));
    # endif // ifdef PLUGIN_093_DEBUG
    return Invalid;
  }

  const uint8_t calculatedChecksum = checkSum(packet, length);

  if (calculatedChecksum != checksum) {
    # ifdef PLUGIN_093_DEBUG
    addLog(LOG_LEVEL_DEBUG, concat(F("M-AC: CIP(1) "), calculatedChecksum));
    # endif // ifdef PLUGIN_093_DEBUG
    return Invalid;
  }

  switch (packet[1]) {
    case 0x61:
      return SettingsApplied;
    case 0x62:
      return StatusUpdated;
    case 0x7a:
      return Connected;
    default:
      return Invalid;
  }
}

uint8_t P093_data_struct::checkSum(const uint8_t *bytes, size_t length) {
  uint8_t sum = 0;

  for (size_t i = 0; i < length; ++i) {
    sum += bytes[i];
  }
  return (0xfc - sum) & 0xff;
}

const __FlashStringHelper * P093_data_struct::findByValue(uint8_t value, const Tuple list[], size_t count) {
  for (size_t index = 0; index < count; ++index) {
    const Tuple& tuple = list[index];

    if (value == tuple.value) {
      return tuple.mapping;
    }
  }
  return list[0].mapping;
}

bool P093_data_struct::findByMapping(const String& mapping, const Tuple list[], size_t count, uint8_t& value) {
  for (size_t index = 0; index < count; ++index) {
    const Tuple& tuple = list[index];

    if (equals(mapping, tuple.mapping)) {
      value = tuple.value;
      return true;
    }
  }
  return false;
}

# ifdef PLUGIN_093_DEBUG
const __FlashStringHelper * P093_data_struct::stateToString_f(P093_data_struct::State state) {
  switch (state) {
    case Invalid: return F("Invalid");
    case NotConnected: return F("NotConnected");
    case Connecting: return F("Connecting");
    case Connected: return F("Connected");
    case UpdatingStatus: return F("UpdatingStatus");
    case StatusUpdated: return F("StatusUpdated");
    case ReadTimeout: return F("ReadTimeout");
    case ScheduleNextStatusUpdate: return F("ScheduleNextStatusUpdate");
    case WaitingForScheduledStatusUpdate: return F("WaitingForScheduledStatusUpdate");
    case ApplyingSettings: return F("ApplyingSettings");
    case SettingsApplied: return F("SettingsApplied");
  }
  return F("");
}

String P093_data_struct::stateToString(P093_data_struct::State state) {
  String res = stateToString_f(state);

  if (res.isEmpty()) {
    return concat(F("<unknown> "), state);
  }
  return res;
}

void P093_data_struct::dumpPacket(const uint8_t *packet, size_t length, String& result) {
  for (size_t idx = 0; idx < length; ++idx) {
    result += strformat(F("%02x "), packet[idx]);
  }
}

String P093_data_struct::dumpOutgoingPacket(const uint8_t *packet, size_t length) {
  String message = F("M-AC - OUT: ");

  dumpPacket(packet, length, message);
  return message;
}

String P093_data_struct::dumpIncomingPacket(const uint8_t *packet, int length) {
  String message = F("M-AC - IN: ");

  dumpPacket(packet, length, message);
  return message;
}

# endif // ifdef PLUGIN_093_DEBUG


#endif // ifdef USES_P093

#include "../PluginStructs/P038_data_struct.h"

#ifdef USES_P038

// **************************************************************************/
// Constructor
// **************************************************************************/
P038_data_struct::P038_data_struct(int8_t   gpioPin,
                                   uint16_t ledCount,
                                   uint8_t  stripType,
                                   uint8_t  brightness,
                                   uint8_t  maxbright)
  : _gpioPin(gpioPin), _maxPixels(ledCount), _stripType(stripType), _brightness(brightness), _maxbright(maxbright) {}

// **************************************************************************/
// Destructor
// **************************************************************************/
P038_data_struct::~P038_data_struct() {
  delete Plugin_038_pixels;
  Plugin_038_pixels = nullptr;
}

bool P038_data_struct::plugin_init(struct EventStruct *event) {
  bool success = false;

  if (!isInitialized()) {
    Plugin_038_pixels = new (std::nothrow) NeoPixelBus_wrapper(_maxPixels,
                                                               _gpioPin,
                                                               (_stripType == P038_STRIP_TYPE_RGBW ? NEO_GRBW : NEO_GRB) + NEO_KHZ800);

    if (Plugin_038_pixels != nullptr) {
      Plugin_038_pixels->begin();                                          // This initializes the NeoPixel library.
      Plugin_038_pixels->setBrightness(std::min(_maxbright, _brightness)); // Set brightness, so we don't get blinded by the light
      success = true;
    }
  }

  return success;
}

bool P038_data_struct::plugin_exit(struct EventStruct *event) {
  delete Plugin_038_pixels;
  Plugin_038_pixels = nullptr;
  return true;
}

const char p038_commands[] PROGMEM =
  "neopixel|"
  "neopixelbright|"
  "neopixelhsv|"
  "neopixelall|"
  "neopixelallhsv|"
  "neopixelline|"
  "neopixellinehsv|"
  # if P038_FEATURE_NEOPIXELFOR
  "neopixelfor|"
  "neopixelforhsv|"
  # endif // if P038_FEATURE_NEOPIXELFOR
;
enum class p038_commands_e : int8_t {
  invalid = -1,
  neopixel,
  neopixelbright,
  neopixelhsv,
  neopixelall,
  neopixelallhsv,
  neopixelline,
  neopixellinehsv,
  # if P038_FEATURE_NEOPIXELFOR
  neopixelfor,
  neopixelforhsv,
  # endif // if P038_FEATURE_NEOPIXELFOR
};

bool P038_data_struct::plugin_write(struct EventStruct *event, const String& string) {
  bool success = false;

  if (isInitialized()) {
    const String cmd = parseString(string, 1);

    const int cmd_i = GetCommandCode(cmd.c_str(), p038_commands);

    if (cmd_i < 0) { return false; } // Fail fast

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, concat(F("P038 : write - "), string));
    }
    const p038_commands_e cmde = static_cast<p038_commands_e>(cmd_i);

    success = true;

    switch (cmde) {
      case p038_commands_e::invalid:
        break;
      case p038_commands_e::neopixel:
        Plugin_038_pixels->setPixelColor(event->Par1 - 1, Plugin_038_pixels->Color(event->Par2, event->Par3, event->Par4, event->Par5));
        break;
      case p038_commands_e::neopixelbright:

        if (parseString(string, 2).isEmpty() || (event->Par1 == 0)) {          // No argument or 0, then
          Plugin_038_pixels->setBrightness(std::min(_maxbright, _brightness)); // use initial brightness
        } else {
          Plugin_038_pixels->setBrightness(std::min(_maxbright, static_cast<uint8_t>(event->Par1)));
        }
        break;
      case p038_commands_e::neopixelhsv:
      {
        // extra function to receive HSV values (i.e. homie controller)
        int rgbw[4];
        rgbw[3] = 0;

        HSV2RGBWorRGBandLog(event->Par2, event->Par3, event->Par4, rgbw);

        Plugin_038_pixels->setPixelColor(event->Par1 - 1, Plugin_038_pixels->Color(rgbw[0], rgbw[1], rgbw[2], rgbw[3]));
        break;
      }
      case p038_commands_e::neopixelall:

        for (int i = 0; i < _maxPixels; ++i) {
          Plugin_038_pixels->setPixelColor(i, Plugin_038_pixels->Color(event->Par1, event->Par2, event->Par3, event->Par4));
        }
        break;
      case p038_commands_e::neopixelallhsv:
      {
        int rgbw[4];
        rgbw[3] = 0;

        HSV2RGBWorRGBandLog(event->Par1, event->Par2, event->Par3, rgbw);

        for (int i = 0; i < _maxPixels; ++i) {
          Plugin_038_pixels->setPixelColor(i, Plugin_038_pixels->Color(rgbw[0], rgbw[1], rgbw[2], rgbw[3]));
        }
        break;
      }
      case p038_commands_e::neopixelline:
      {
        int32_t brightness = 0;
        validIntFromString(parseString(string, 7), brightness); // Get 7th argument aka Par6

        for (int i = event->Par1 - 1; i < event->Par2; ++i) {
          Plugin_038_pixels->setPixelColor(i, Plugin_038_pixels->Color(event->Par3, event->Par4, event->Par5, brightness));
        }
        break;
      }
      case p038_commands_e::neopixellinehsv:
      {
        int rgbw[4];
        rgbw[3] = 0;

        HSV2RGBWorRGBandLog(event->Par3, event->Par4, event->Par5, rgbw);

        for (int i = event->Par1 - 1; i < event->Par2; ++i) {
          Plugin_038_pixels->setPixelColor(i, Plugin_038_pixels->Color(rgbw[0], rgbw[1], rgbw[2], rgbw[3]));
        }
        break;
      }
      # if P038_FEATURE_NEOPIXELFOR
      case p038_commands_e::neopixelfor:
      case p038_commands_e::neopixelforhsv:
      {
        if (event->Par3 != 0) { // endless loop protection
          const bool useHsv = p038_commands_e::neopixelforhsv == cmde;
          int rgbw[4];
          int32_t par6 = 0;
          int32_t par7 = 0;
          int32_t par8 = 0;
          validIntFromString(parseString(string, 7), par6);
          const String str8 = parseString(string, 8);
          const String str9 = parseString(string, 9);

          if (!str9.isEmpty()) { // Check last expected argument first
            validIntFromString(str9, par8);
          }

          if (!str8.isEmpty()) {
            validIntFromString(str8, par7);

            if ((P038_STRIP_TYPE_RGBW != _stripType) || useHsv) {
              par8 = par7;
            }
          }

          if (useHsv) {
            rgbw[3] = 0;
            HSV2RGBWorRGBandLog(event->Par4, event->Par5, par6, rgbw);
          } else {
            rgbw[0] = event->Par4;
            rgbw[1] = event->Par5;
            rgbw[2] = par6;
            rgbw[3] = (P038_STRIP_TYPE_RGBW == _stripType) ? par7 : 0;
          }

          const int inc = event->Par3 < 0 ? -1 : 1;
          int i         = event->Par1 - 1;
          int j         = i;

          for (; (event->Par3 < 0 ? (i >= event->Par2 - 1) : (i < event->Par2)) && i >= 0; i += inc) {
            // addLog(LOG_LEVEL_INFO, strformat(F("P038 : %d/%d %c for: %d; %d; %d; color: 0x%02x %02x %02x %02x"),
            //                                  i, j, i == j ? '*' : ' ', event->Par1, event->Par2, event->Par3,
            //                                  rgbw[0], rgbw[1], rgbw[2], rgbw[3]));

            if (i == j) { // Color pixel
              Plugin_038_pixels->setPixelColor(i, Plugin_038_pixels->Color(rgbw[0], rgbw[1], rgbw[2], rgbw[3]));
              j += event->Par3;
            }
            else if (par8 == 1) { // Clear pixel
              Plugin_038_pixels->setPixelColor(i, Plugin_038_pixels->Color(0, 0, 0, 0));
            }
          }
        } else {
          success = false;
        }
        break;
      }
      # endif // if P038_FEATURE_NEOPIXELFOR
    }

    Plugin_038_pixels->show(); // This sends the updated pixel color to the hardware.
  }
  return success;
}

void P038_data_struct::HSV2RGBWorRGBandLog(float H, float S, float V, int rgbw[4]) {
  if (_stripType == P038_STRIP_TYPE_RGBW) { // RGBW
    HSV2RGBW(H, S, V, rgbw);
  } else {                                  // RGB
    HSV2RGB(H, S, V, rgbw);
  }

  # ifndef LIMIT_BUILD_SIZE

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO,
           strformat(F("P038 HSV converted to RGB(W):%d,%d,%d,%d"), rgbw[0], rgbw[1], rgbw[2], rgbw[3]));
  }
  # endif // ifndef LIMIT_BUILD_SIZE
}

#endif // ifdef USES_P038


#ifdef USES_P127
#include "../PluginStructs/P127_data_struct.h"

// #######################################################################################################
// ################################## Plugin 127 I2C CDM7160 CO2 sensor ##################################
// #######################################################################################################
//
//

P127_data_struct::P127_data_struct(const int8_t   i2caddr,
                                   const uint16_t alt)
  : _i2cAddress(i2caddr), _alt(alt) {}

// Do all required initialization
bool P127_data_struct::init() {
  // delay required to store config byte to EEPROM, device pulls SCL low
  if (initPhase == P127_initPhases::Undefined) {
    setPowerDown();
    timeToWait = 100;
    initPhase  = P127_initPhases::InitDelay1;
    return false;
  }

  uint8_t elev = _alt / 10; // Altitude is as 'finegrained' per 10 meter

  if (elev) {
    setAltitude(elev);
  } else {
    clearAltitude();
  }

  // delay required to store config byte to EEPROM, device pulls SCL low
  if (initPhase == P127_initPhases::InitDelay1) {
    timeToWait = 100;
    initPhase  = P127_initPhases::InitDelay2;
    return false;
  }
  initPhase = P127_initPhases::Ready; // All done, let's go

  // Start reading
  setContinuous();
  return true;
}

// Check status and read data if not busy
bool P127_data_struct::checkData() {
  if (initPhase != P127_initPhases::Ready) {
    return false;
  }

  uint8_t status = getStatus();

  if (!(status & CDM7160_FLAG_BUSY)) {
    _co2 = getCO2();
  }
  return true;
}

// Return the last measured value
uint16_t P127_data_struct::readData() {
  return _co2;
}

uint8_t P127_data_struct::getAltitude() {
  return I2C_read8_reg(_i2cAddress, CDM7160_REG_HIT);
}

uint8_t P127_data_struct::getCompensation() {
  return I2C_read8_reg(_i2cAddress, CDM7160_REG_FUNC);
}

bool P127_data_struct::setPowerDown(void)

// Set power down mode CDM7160 to enable correct settings modification
// Returns true (1) if successful, false (0) if there was an I2C error
{
  // Write 0x06 to control register
  return I2C_write8_reg(_i2cAddress, CDM7160_REG_CTL, CDM7160_FLAG_DOWN);
}

bool P127_data_struct::setContinuous(void)

// Set continuous operation mode CDM7160 to start measurements
// Returns true (1) if successful, false (0) if there was an I2C error
{
  // Write 0x00 to control register
  return I2C_write8_reg(_i2cAddress, CDM7160_REG_CTL, CDM7160_FLAG_CONT);
}

bool P127_data_struct::setReset(void)

// Reset CDM7160
// Returns true (1) if successful, false (0) if there was an I2C error
{
  // Write 0x01 to reset register
  return I2C_write8_reg(_i2cAddress, CDM7160_REG_RESET, CDM7160_FLAG_REST);
}

bool P127_data_struct::setAltitude(uint8_t alt)

// Set altitude compensation on CDM7160
// Returns true (1) if successful, false (0) if there was an I2C error
{
  // Write altitude and enable compensation
  I2C_write8_reg(_i2cAddress, CDM7160_REG_HIT, alt);
  return I2C_write8_reg(_i2cAddress, CDM7160_REG_FUNC, (CDM7160_FLAG_HPAE | CDM7160_FLAG_PWME));
}

bool P127_data_struct::clearAltitude(void)

// Disable altitude compensation on CDM7160
// Returns true (1) if successful, false (0) if there was an I2C error
{
  // Clear altitude and disable compensation
  I2C_write8_reg(_i2cAddress, CDM7160_REG_HIT, 0);
  return I2C_write8_reg(_i2cAddress, CDM7160_REG_FUNC, CDM7160_FLAG_PWME);
}

uint8_t P127_data_struct::getStatus()

// Retrieve CO2 data in ppm
// Returns true (1) if successful, false (0) if there was an I2C error
{
  // Get content of status register
  return I2C_read8_reg(_i2cAddress, CDM7160_REG_STATUS);
}

uint16_t P127_data_struct::getCO2()

// Retrieve CO2 data in ppm
// Returns the value
{
  // Get co2 ppm data out of result registers
  return I2C_read16_LE_reg(_i2cAddress, CDM7160_REG_DATA);
}

bool P127_data_struct::plugin_fifty_per_second() {
  if ((initPhase == P127_initPhases::InitDelay1) ||
      (initPhase == P127_initPhases::InitDelay2)) {
    timeToWait -= 20; // milliseconds

    // String log = F("CDM7160: remaining wait: ");
    // log += timeToWait;
    // addLogMove(LOG_LEVEL_INFO, log);

    if (timeToWait <= 0) {
      timeToWait = 0;
      init(); // Second/Third part
    }
  }
  return true;
}

#endif // ifdef USES_P127

#include "../PluginStructs/P135_data_struct.h"

#ifdef USES_P135

/**************************************************************************
* Constructor
**************************************************************************/
P135_data_struct::P135_data_struct(taskIndex_t taskIndex,
                                   uint8_t     sensorType,
                                   uint16_t    altitude,
                                   float       tempOffset,
                                   bool        autoCalibrate,
                                   bool        lowPowerMeasurement,
                                   bool        useSingleShot)
  : _sensorType(sensorType), _altitude(altitude), _tempOffset(tempOffset), _autoCalibrate(autoCalibrate),
  _lowPowerMeasurement(lowPowerMeasurement), _useSingleShot(useSingleShot), initialized(false)
{}

bool P135_data_struct::init() {
  scd4x = new (std::nothrow) SCD4x(static_cast<scd4x_sensor_type_e>(_sensorType)); // Don't start measurement, we want to set arguments

  if (scd4x != nullptr) {
    if (scd4x->begin(false, _autoCalibrate)) {
      const uint16_t orgAltitude = scd4x->getSensorAltitude();

      if ((_altitude != 0) && (_altitude != orgAltitude)) {
        scd4x->setSensorAltitude(_altitude);
      }
      const float orgTempOffset = scd4x->getTemperatureOffset();

      if (!essentiallyZero(_tempOffset) && !essentiallyEqual(_tempOffset, orgTempOffset)) {
        scd4x->setTemperatureOffset(_tempOffset);
      }
      const bool hasSerial = scd4x->getSerialNumber(serialNumber); // Not yet reading, get serial

      initialized = startPeriodicMeasurements();                   // Start the desired periodic measurement

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        String log = F("SCD4x: Init ");

        if (initialized) {
          log += F("success, serial number: ");

          if (hasSerial) {
            log += String(serialNumber);
          } else {
            log += F("(unknown)");
          }
          log += strformat(F(", org.alt.comp.: %d m, org.temp.offs.: %.2f C"), orgAltitude, orgTempOffset);
        } else {
          log += F("error");
        }
        addLogMove(LOG_LEVEL_INFO, log);
      }
    } else {
      addLog(LOG_LEVEL_ERROR, F("SDC4x: Sensor not detected."));
    }
  }
  return isInitialized();
}

/*****************************************************
* startPeriodicMeasurements
*****************************************************/
bool P135_data_struct::startPeriodicMeasurements() {
  if (_useSingleShot) {
    return true;                                      // Start measurment at first PLUGIN_READ
  } else if (_lowPowerMeasurement) {
    return scd4x->startLowPowerPeriodicMeasurement(); // Reports every 30 seconds
  }
  return scd4x->startPeriodicMeasurement();           // Reports every 5 seconds
}

/*****************************************************
* Destructor
*****************************************************/
P135_data_struct::~P135_data_struct() {
  delete scd4x;
  scd4x = nullptr;
}

/*****************************************************
* plugin_read
*****************************************************/
bool P135_data_struct::plugin_read(struct EventStruct *event)           {
  bool success = false;

  if (isInitialized()) {
    bool getMeasure     = true;
    uint16_t timerDelay = 0;

    if ((_useSingleShot) &&
        !singleShotStarted &&
        scd4x->measureSingleShot()) {
      getMeasure        = false;
      singleShotStarted = true;

      addLog(LOG_LEVEL_INFO, F("SCD4x: SingleShot measurement started."));
    }

    if (getMeasure && scd4x->readMeasurement()) {
      UserVar.setFloat(event->TaskIndex, 0, scd4x->getCO2());
      UserVar.setFloat(event->TaskIndex, 1, scd4x->getHumidity());
      UserVar.setFloat(event->TaskIndex, 2, scd4x->getTemperature());

      success = !firstRead;           // Discard first measurement

      singleShotStarted = false;
      firstRead         = false;      // No longer first read
      errorCount        = 0;          // Reset
    } else {
      if (getMeasure && !firstRead) { // We got delayed somehow, let's wait a little more
        timerDelay = P135_EXTEND_MEASURE_TIME;
        errorCount++;
      } else {
        if (_useSingleShot) { // Single-shot started?
          timerDelay = P135_SINGLE_SHOT_MEASURE_TIME;
        } else if (_lowPowerMeasurement) {
          timerDelay = P135_LOW_POWER_MEASURE_TIME;
        } else {
          timerDelay = P135_NORMAL_MEASURE_TIME;
        }
      }
    }

    if (errorCount > P135_MAX_ERRORS) {
      initialized = false;
      scd4x->stopPeriodicMeasurement();         // Stop measuring, no need to wait for completion
      UserVar.setFloat(event->TaskIndex, 0, 0); // Indicate an error state
      addLog(LOG_LEVEL_ERROR, F("SCD4x: Max. read errors reached, plugin stopped."));
    }

    if (timerDelay != 0) { // Schedule next PLUGIN_READ
      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLog(LOG_LEVEL_INFO, strformat(F("SCD4x: READ Scheduler started: +%d ms."), timerDelay));
      }
      Scheduler.schedule_task_device_timer(event->TaskIndex, millis() + timerDelay);
    }
  } else {
    # if P135_FEATURE_RESET_COMMANDS

    if (operation != SCD4x_Operations_e::None) {
      switch (operation) {
        #  if P135_FEATURE_FACTORYRESET
        case SCD4x_Operations_e::RunFactoryReset: { // May take up to 1200 mSec
          success = scd4x->performFactoryReset();

          String  log = F("SCD4x: Factory reset ");
          uint8_t lvl = LOG_LEVEL_INFO;

          if (success) {
            initialized = startPeriodicMeasurements(); // Select the correct periodic measurement, and start a READ
            Scheduler.schedule_task_device_timer(event->TaskIndex, millis() + P135_STOP_MEASUREMENT_DELAY);
            log += F("success.");
          } else {
            lvl  = LOG_LEVEL_ERROR;
            log += F("failed!");
          }
          addLog(lvl, log);
          break;
        }
        #  endif // if P135_FEATURE_FACTORYRESET
        case SCD4x_Operations_e::RunSelfTest: { // May take up to 10 seconds!
          success = scd4x->performSelfTest();

          String  log = F("SCD4x: Sensor self-test ");
          uint8_t lvl = LOG_LEVEL_INFO;

          if (success) {
            initialized = startPeriodicMeasurements(); // Select the correct periodic measurement, and start a READ
            Scheduler.schedule_task_device_timer(event->TaskIndex, millis() + P135_STOP_MEASUREMENT_DELAY);
            log += F("success.");
          } else {
            lvl  = LOG_LEVEL_ERROR;
            log += F("failed!");
          }
          addLog(lvl, log);
          break;
        }
        case SCD4x_Operations_e::RunForcedRecalibration: { // May take up to 400 mSec
          float frcCorrection = 0.0f;
          success  = scd4x->performForcedRecalibration(frcValue, &frcCorrection);
          frcValue = 0;

          String  log = F("SCD4x: Forced Recalibration ");
          uint8_t lvl = LOG_LEVEL_INFO;

          if (success) {
            initialized = startPeriodicMeasurements(); // Select the correct periodic measurement, and start a READ
            Scheduler.schedule_task_device_timer(event->TaskIndex, millis() + P135_STOP_MEASUREMENT_DELAY);
            log += strformat(F("success. New setting: %d, correction: %.2f"), frcValue, frcCorrection);
          } else {
            lvl  = LOG_LEVEL_ERROR;
            log += F("failed!");
          }
          addLog(lvl, log);
          break;
        }
        case SCD4x_Operations_e::None: { // To keep the compiler and developer happy :-)
          break;
        }
      }
      operation = SCD4x_Operations_e::None;
    }
    # endif // if P135_FEATURE_RESET_COMMANDS
  }
  return success;
}

/*****************************************************
* plugin_write
*****************************************************/
bool P135_data_struct::plugin_write(struct EventStruct *event,
                                    String            & string) {
  bool success = false;

  const String command = parseString(string, 1);

  if (equals(command, F("scd4x"))) {
    const String sub = parseString(string, 2);
    # if P135_FEATURE_RESET_COMMANDS
    const bool doSelftest = equals(sub, F("selftest"));
    # endif // if P135_FEATURE_RESET_COMMANDS

    // scd4x,storesettings : SLOW! Store current Altitude and temperatureCorrection in on-sensor EEPROM
    if (equals(sub, F("storesettings"))) {
      success = scd4x->persistSettings(); // This may take up to 800 mSec
    # if P135_FEATURE_RESET_COMMANDS

      // scd4x,factoryreset[,code] : SLOWER! Restore factory settings for sensor, code logged at ERROR level
      // scd4x,selftest[,code] : SLOWEST! Self-test for sensor, code logged at ERROR level
    } else if (
      #  if P135_FEATURE_FACTORYRESET
      equals(sub, F("factoryreset")) ||
      #  endif // if P135_FEATURE_FACTORYRESET
      doSelftest) {
      if (factoryResetCode.isEmpty()) {
        factoryResetCode = F("Scd4x");

        if (serialNumber[10] != 0x0) {
          if (doSelftest) {
            factoryResetCode += strformat(F("%c%c%c%c"),
                                          char(serialNumber[3]), char(serialNumber[1]), char(serialNumber[10]), char(serialNumber[6]));
          #  if P135_FEATURE_FACTORYRESET
          } else {
            factoryResetCode += strformat(F("%c%c%c%c"),
                                          char(serialNumber[1]), char(serialNumber[3]), char(serialNumber[7]), char(serialNumber[10]));
          #  endif // if P135_FEATURE_FACTORYRESET
          }
        } else {
          factoryResetCode += F("2022");
        }

        if (doSelftest) {
          factoryResetCode += F("SelF");
        #  if P135_FEATURE_FACTORYRESET
        } else {
          factoryResetCode += F("reseT");
        #  endif // if P135_FEATURE_FACTORYRESET
        }
        {
          String log = F("SCD4x: ");

          if (doSelftest) {
            log += F("Selftest");
          #  if P135_FEATURE_FACTORYRESET
          } else {
            log += F("Factory reset");
          #  endif // if P135_FEATURE_FACTORYRESET
          }
          log += concat(F(" code: "), factoryResetCode);
          addLog(LOG_LEVEL_ERROR, log);
        }
        success = true;
      } else {
        const String code = parseStringKeepCase(string, 3); // Case sensitive!

        if (code.equals(factoryResetCode)) {
          if (doSelftest) {
            addLog(LOG_LEVEL_ERROR, F("SCD4x: Selftest starting... (may take up to 11 seconds!)"));
            operation = SCD4x_Operations_e::RunSelfTest;
          #  if P135_FEATURE_FACTORYRESET
          } else {
            addLog(LOG_LEVEL_ERROR, F("SCD4x: Factory reset starting... (may take up to 2.5 seconds!)"));
            operation = SCD4x_Operations_e::RunFactoryReset;
          #  endif // if P135_FEATURE_FACTORYRESET
          }
        }
        factoryResetCode.clear();
      }

      // scd4x,setfrc,co2level : Correct the current level reference to this, externally determined, co2level
    } else if ((equals(sub, F("setfrc"))) && (event->Par2 >= 400) &&
               (((_sensorType == scd4x_sensor_type_e::SCD4x_SENSOR_SCD40) && (event->Par2 <= 2000)) ||
                ((_sensorType == scd4x_sensor_type_e::SCD4x_SENSOR_SCD41) && (event->Par2 <= 5000)))) {
      addLog(LOG_LEVEL_INFO, F("SCD4x: Forced Recalibration starting... (may take up to 1 second!)"));
      operation = SCD4x_Operations_e::RunForcedRecalibration; // Execute FRC
      frcValue  = event->Par2;
    # endif // if P135_FEATURE_RESET_COMMANDS
    }
  }
  # if P135_FEATURE_RESET_COMMANDS

  // Prepare for a special operation, stop measurements and schedule a required delayed READ
  if ((operation != SCD4x_Operations_e::None) && scd4x->stopPeriodicMeasurement()) {
    initialized = false;
    firstRead   = true; // To discard first measurement results
    Scheduler.schedule_task_device_timer(event->TaskIndex, millis() + P135_STOP_MEASUREMENT_DELAY);
    success = true;
  }
  # endif // if P135_FEATURE_RESET_COMMANDS
  return success;
}

/*****************************************************
* plugin_get_config_value
*****************************************************/
bool P135_data_struct::plugin_get_config_value(struct EventStruct *event,
                                               String            & string) {
  if (nullptr == scd4x) { return false; } // Safeguard
  bool success = false;

  const String var = parseString(string, 1);

  if (equals(var, F("getaltitude")) &&
      scd4x->stopPeriodicMeasurement()) { // [<taskname>#getaltitude] = get sensor altitude
    string = scd4x->getSensorAltitude();
    startPeriodicMeasurements();
    success = true;
  } else if (equals(var, F("gettempoffset")) &&
             scd4x->stopPeriodicMeasurement()) { // [<taskname>#gettempoffset] = get sensor temperature offset
    string = toString(scd4x->getTemperatureOffset(), 2);
    startPeriodicMeasurements();
    success = true;
  } else if (equals(var, F("getdataready"))) { // [<taskname>#getdataready] = is data ready? (1/0)
    string  = scd4x->getDataReadyStatus();
    success = true;
  } else if (equals(var, F("getselfcalibration")) &&
             scd4x->stopPeriodicMeasurement()) { // [<taskname>#getselfcalibration] = is self-calibration enabled? (1/0)
    string = scd4x->getAutomaticSelfCalibrationEnabled();
    startPeriodicMeasurements();
    success = true;
  } else if (equals(var, F("serialnumber"))) { // [<taskname>#serialnumber] = the devices electronic serial number
    string  = String(serialNumber);
    success = true;
  }
  return success;
}

#endif // ifdef USES_P135

#include "../PluginStructs/P105_data_struct.h"

#ifdef USES_P105

# include "../Helpers/Convert.h"

struct AHTx_Status {
  inline AHTx_Status(uint8_t stat) : status(stat) {}

  inline bool valid() const {
    return status != 0xFF;
  }

  inline bool calibrated() const {
    return (status & (1 << 3)) != 0;
  }

  inline bool busy() const {
    return (status & (1 << 7)) != 0;
  }

  const uint8_t status;
};

AHTx_Device::AHTx_Device(uint8_t addr, AHTx_device_type type, bool altInit) :
  i2cAddress(addr),
  device_type(type),
  last_hum_val(0.0f),
  last_temp_val(0.0f),
  alt_init(altInit) {}

const __FlashStringHelper * AHTx_Device::getDeviceName() const {
  switch (device_type) {
    case AHTx_device_type::AHT10_DEVICE: return F("AHT10");
    case AHTx_device_type::AHT20_DEVICE: return F("AHT20");
    case AHTx_device_type::AHT21_DEVICE: return F("AHT21");
  }
  return F("AHTx");
}

bool AHTx_Device::initialize() {
  const uint8_t cmd_init = (AHTx_device_type::AHT10_DEVICE == device_type) ? 0xE1 : 0xBE;

  if ((AHTx_device_type::AHT10_DEVICE == device_type) && alt_init) {
    return I2C_write8(i2cAddress, 0xBA); // Soft reset only
  }
  return I2C_write16_reg(i2cAddress, cmd_init, 0x0800);
}

bool AHTx_Device::triggerMeasurement() {
  return I2C_write16_reg(i2cAddress, 0xAC, 0x3300); // measurement time takes over 80 msec
}

bool AHTx_Device::softReset() {
  return I2C_write8(i2cAddress, 0xBA); // soft reset takes less than 20 msec
}

uint8_t AHTx_Device::readStatus() {
  return I2C_read8(i2cAddress, nullptr);
}

bool AHTx_Device::readData() {
  const uint8_t data_len = 6;

  // I2C_read8 len
  if (Wire.requestFrom(i2cAddress, data_len) == 0) {
    return false;
  }

  uint8_t data[data_len];

  for (uint8_t i = 0; i < data_len; ++i) {
    data[i] = Wire.read();
  }

  // check status
  AHTx_Status status = data[0];

  if (!(status.valid() && status.calibrated())) {
    return false;
  }

  // 20 bits humidity value
  uint32_t value = data[1];

  value        = (value << 8) | data[2];
  value        = (value << 4) | (data[3] >> 4);
  last_hum_val = (static_cast<float>(value) / (1 << 20)) * 100.0f;

  // 20 bits temperature value
  value         = data[3] & 0x0F;
  value         = (value << 8) | data[4];
  value         = (value << 8) | data[5];
  last_temp_val = ((static_cast<float>(value) / (1 << 20)) * 200.0f) - 50.0f;

  return true;
}

P105_data_struct::P105_data_struct(uint8_t addr, AHTx_device_type dev, bool altInit) :
  device(addr, dev, altInit),
  state(AHTx_state::AHTx_Uninitialized),
  last_measurement(0),
  trigger_time(0) {}

bool P105_data_struct::initialized() const {
  return state != AHTx_state::AHTx_Uninitialized;
}

void P105_data_struct::setUninitialized() {
  state = AHTx_state::AHTx_Uninitialized;
}

// Perform the measurements with interval
bool P105_data_struct::updateMeasurements(taskIndex_t task_index) {
  const unsigned long current_time = millis();

  if (!initialized()) {
    if (!device.initialize()) {
      addLogMove(LOG_LEVEL_ERROR, strformat(F("%s : unable to initialize"), getDeviceName().c_str()));
      return false;
    }
    addLogMove(LOG_LEVEL_INFO, strformat(F("%s : initialized"), getDeviceName().c_str()));

    trigger_time = current_time;
    state        = AHTx_state::AHTx_Trigger_measurement;
    return false;
  }

  if ((state != AHTx_state::AHTx_Wait_for_samples) && (state != AHTx_state::AHTx_Trigger_measurement)) {
    if (!timeOutReached(last_measurement + (Settings.TaskDeviceTimer[task_index] * 1000))) {
      // Timeout has not yet been reached.
      return false;
    }
    trigger_time = current_time;
    state        = AHTx_state::AHTx_Trigger_measurement;
  }

  // state: AHTx_Wait_for_samples or AHTx_Trigger_measurement
  AHTx_Status status = device.readStatus();

  if (status.valid() && status.calibrated() && !status.busy()) {
    if (state == AHTx_state::AHTx_Trigger_measurement) {
      device.triggerMeasurement();

      trigger_time = current_time;
      state        = AHTx_state::AHTx_Wait_for_samples;
      return false;
    }

    // state: AHTx_Wait_for_samples
    if (!device.readData()) {
      return false;
    }

    last_measurement = current_time;
    state            = AHTx_state::AHTx_New_values;

    # ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { // Log raw measuerd values only on level DEBUG
      addLogMove(LOG_LEVEL_DEBUG, strformat(F("%s : humidity %.2f%% temperature %.2fC"),
                                            getDeviceName().c_str(),
                                            device.getHumidity(),
                                            device.getTemperature()));
    }
    # endif // ifndef BUILD_NO_DEBUG

    return true;
  }

  if (timePassedSince(trigger_time) > 1000) {
    // should not happen
    addLogMove(LOG_LEVEL_ERROR, strformat(F("%s : reset"), getDeviceName().c_str()));
    device.softReset();

    state = AHTx_state::AHTx_Uninitialized;
  }

  return false;
}

#endif // ifdef USES_P105

// ----------------------------------------------------------------------------
// P145 "Gases - MQxxx (MQ135 CO2, MQ3 Alcohol)"
// Implementation of sensor abstraction
// See _P145_MQxxx.ino
// 2023 By flashmark
// ----------------------------------------------------------------------------
#include "../PluginStructs/P145_data_struct.h"

#ifdef USES_P145
# if SOC_ADC_SUPPORTED || defined(ESP8266)

// Enable testing
// #define P145_TEST
// #define P145_CALIBRATION_INTERVAL (5*60*1000)

#  include "../Globals/ESPEasyWiFiEvent.h" // Need to know when WiFi is ruining the ADC measurements

// The table sensorDefs[] contains string items for representation.
// Storage is in PROGMEM where a (fixed format) C-style string does not fit well
// An enum like int is used instead, together with a enum to string conversion function
// Below the defines used to represent these enums

// Gas type identifiers used to condense the sensorDefs table to fixed format
#  define P145_GASUSER    0
#  define P145_GASCO2     1
#  define P145_GASH2      2
#  define P145_GASALCOHOL 3
#  define P145_GASCH4     4
#  define P145_GASLPG     5
#  define P145_GASCO      6

// Sensor type identifier used to condense the SensorDefs table to fixed format
#  define P145_SENSUSER   0
#  define P145_SENSMQ135  1
#  define P145_SENSMQ2    2
#  define P145_SENSMQ3    3
#  define P145_SENSMQ4    4
#  define P145_SENSMQ5    5
#  define P145_SENSMQ6    6
#  define P145_SENSMQ7    7
#  define P145_SENSMQ8    8


#  if FEATURE_MQTT_DISCOVER
int Plugin_145_QueryVType(uint8_t value_nr) {
  const Sensor_VType vtypes[] = {
    Sensor_VType::SENSOR_TYPE_NONE,     // User defined
    Sensor_VType::SENSOR_TYPE_CO2_ONLY, // MQ-135 CO2
    Sensor_VType::SENSOR_TYPE_NONE,     // FIXME MQ-2 H2
    Sensor_VType::SENSOR_TYPE_NONE,     // FIXME MQ-3 Alcohol
    Sensor_VType::SENSOR_TYPE_NONE,     // FIXME MQ-4 CH4
    Sensor_VType::SENSOR_TYPE_NONE,     // FIXME MQ-5 H2
    Sensor_VType::SENSOR_TYPE_NONE,     // FIXME MQ-5 LPG
    Sensor_VType::SENSOR_TYPE_NONE,     // FIXME MQ-6 LPG
    Sensor_VType::SENSOR_TYPE_NONE,     // FIXME MQ-7 CO
    Sensor_VType::SENSOR_TYPE_NONE,     // FIXME MQ-8 H2
  };
  constexpr uint8_t  valueCount = NR_ELEMENTS(vtypes);
  Sensor_VType result           = Sensor_VType::SENSOR_TYPE_NONE;

  if (value_nr < valueCount) {
    result = vtypes[value_nr];
  }
  return static_cast<int>(result);
}

#  endif // if FEATURE_MQTT_DISCOVER


/******************************************************************************/

// Table with sensor type specific data
// This table is stored in PROGMEM to save space in RAM
// The data for the selected sensor type is copied to RAM, see P145_data_struct

/******************************************************************************/
const P145_SENSORDEF sensorDefs[] PROGMEM
{
  // User defined, output plain ratio (Rsensor/Rzero)
  {
    .cleanRatio = 0.0f,
    .para       = 0.0f,
    .parb       = 0.0f,
    .cora       = 0.0f,
    .corb       = 0.0f,
    .corc       = 0.0f,
    .cord       = 0.0f,
    .core       = 0.0f,
    .corf       = 0.0f,
    .corg       = 0.0f,
    .alg        = p145AlgNone,
    .name       = P145_SENSUSER,
    .gas        = P145_GASUSER,
  },

  // *** MQ-135 - CO2 ***
  {
    .cleanRatio = 0.0f,
    .para       = 116.6020682f,
    .parb       = 2.769034857f,
    .cora       = 0.00035f,
    .corb       = 0.02718f,
    .corc       = 1.39538f,
    .cord       = 0.0018f,
    .core       = -0.003333333f,
    .corf       = -0.001923077f,
    .corg       = 1.130128205f,
    .alg        = p145AlgA,
    .name       = P145_SENSMQ135,
    .gas        = P145_GASCO2,
  },

  // *** MQ-2 - H2 ***
  {
    .cleanRatio = 9.83f,
    .para       = 987.99f,
    .parb       = -2.162f,
    .cora       = 0.0f,
    .corb       = 0.0f,
    .corc       = 0.0f,
    .cord       = 0.0f,
    .core       = 0.0f,
    .corf       = 0.0f,
    .corg       = 0.0f,
    .alg        = p145AlgB,
    .name       = P145_SENSMQ2,
    .gas        = P145_GASH2,
  },

  // *** MQ-3 - alcohol ***
  {
    .cleanRatio = 60.0f,
    .para       = 0.3934f,
    .parb       = -1.504f,
    .cora       = 0.0f,
    .corb       = 0.0f,
    .corc       = 0.0f,
    .cord       = 0.0f,
    .core       = 0.0f,
    .corf       = 0.0f,
    .corg       = 0.0f,
    .alg        = p145AlgB,
    .name       = P145_SENSMQ3,
    .gas        = P145_GASALCOHOL,
  },

  // *** MQ-4 - CH4 ***
  {
    .cleanRatio = 4.4f,
    .para       = 1012.7f,
    .parb       = -2.786f,
    .cora       = 0.0f,
    .corb       = 0.0f,
    .corc       = 0.0f,
    .cord       = 0.0f,
    .core       = 0.0f,
    .corf       = 0.0f,
    .corg       = 0.0f,
    .alg        = p145AlgB,
    .name       = P145_SENSMQ4,
    .gas        = P145_GASCH4,
  },

  // *** MQ-5 - H2 ***
  {
    .cleanRatio = 5.5f,
    .para       = 1163.8f,
    .parb       = -3.874f,
    .cora       = 0.0f,
    .corb       = 0.0f,
    .corc       = 0.0f,
    .cord       = 0.0f,
    .core       = 0.0f,
    .corf       = 0.0f,
    .corg       = 0.0f,
    .alg        = p145AlgB,
    .name       = P145_SENSMQ5,
    .gas        = P145_GASCH4,
  },

  // *** MQ-5 - LPG ***
  {
    .cleanRatio = 5.5f,
    .para       = 80.897f,
    .parb       = -2.431f,
    .cora       = 0.0f,
    .corb       = 0.0f,
    .corc       = 0.0f,
    .cord       = 0.0f,
    .core       = 0.0f,
    .corf       = 0.0f,
    .corg       = 0.0f,
    .alg        = p145AlgB,
    .name       = P145_SENSMQ5,
    .gas        = P145_GASLPG,
  },

  // *** MQ-6 - LPG ***
  {
    .cleanRatio = 10.0f,
    .para       = 1009.2f,
    .parb       = -2.35f,
    .cora       = 0.0f,
    .corb       = 0.0f,
    .corc       = 0.0f,
    .cord       = 0.0f,
    .core       = 0.0f,
    .corf       = 0.0f,
    .corg       = 0.0f,
    .alg        = p145AlgB,
    .name       = P145_SENSMQ6,
    .gas        = P145_GASLPG,
  },

  // *** MQ-7 - CO ***
  {
    .cleanRatio = 27.5f,
    .para       = 491204.0f,
    .parb       = -5.826f,
    .cora       = 0.0f,
    .corb       = 0.0f,
    .corc       = 0.0f,
    .cord       = 0.0f,
    .core       = 0.0f,
    .corf       = 0.0f,
    .corg       = 0.0f,
    .alg        = p145AlgB,
    .name       = P145_SENSMQ7,
    .gas        = P145_GASCO,
  },

  // *** MQ-8 - H2 ***
  {
    .cleanRatio = 70.0f,
    .para       = 976.97f,
    .parb       = -0.688f,
    .cora       = 0.0f,
    .corb       = 0.0f,
    .corc       = 0.0f,
    .cord       = 0.0f,
    .core       = 0.0f,
    .corf       = 0.0f,
    .corg       = 0.0f,
    .alg        = p145AlgB,
    .name       = P145_SENSMQ8,
    .gas        = P145_GASH2,
  }
};

/// @brief The number of types stored in the sensorDefs[] table
constexpr const int nbrOfTypes = NR_ELEMENTS(sensorDefs);

// Digital ouput value to swicth heater ON/OFF
#  define P145_HEATER_OFF  LOW
#  define P145_HEATER_ON   HIGH

// Timeout values for Heater control
#  define HEATER_WARMUP_TIME (90 * 1000)
#  define HEATER_ON_TIME (60 * 1000)
#  define HEATER_OFF_TIME (60 * 1000)
#  define HEATER_MEAS_TIME (30 * 1000)

/*****************************************************************************/

/*!
   @brief  Get the resistance of the sensor, ie. the property to be measured
   @param[in] val     Analog input value read from ADC [counts]
   @return The sensor resistance in Ohm
   @note   Uses hard coded constants:
        MAX_ADC_VALUE Max range for ADC
        P145_VMAX     Analog  input voltage corresponding to MAX_ADC_VALUE
        P145_VCC      Standard voltage applied to the sensor-Rload combination
        P145_VCCLOW   Low voltage used to align with ESP input range
 */

/*****************************************************************************/
float P145_data_struct::getResistance(float val) const
{
  if (val > 0.0f)
  {
    float vcc = P145_VCC; // Voltage applied to the sensor

    if (lowvcc)
    {
      vcc = P145_VCCLOW;
    }
    return ((MAX_ADC_VALUE * vcc) / (P145_VMAX * val) - 1.0f) * rload;
  }
  else
  {
    return 1e9f; // Very high resistance value of 1G Ohm
  }
}

/*****************************************************************************/

/*!
   @brief  Get the resistance RZero of the sensor for calibration purposes
   @param[in] rSensor    Sensor resistance value Rs
   @return The sensor resistance RZero in kOhm
 */

/*****************************************************************************/
float P145_data_struct::getRZero(float rSensor) const
{
  float newValue = rSensor;               // Default to current measured Rsensor

  if (newValue < 0.0f) { newValue = 0.0f; // Clip negative values
  }

  switch (algorithm)
  {
    case p145AlgA: // MQ-135
      newValue = (rSensor * powf((refLevel / sensordef.para), (1.0f / sensordef.parb)));
      break;
    case p145AlgB: // Miquel5612 Exponential

      if (sensordef.cleanRatio > 0.0f)
      {
        newValue = (rSensor / sensordef.cleanRatio);
      }
      break;
    case p145AlgC: // Miquel5612 Linear

      if (sensordef.cleanRatio > 0.0f)
      {
        newValue = (rSensor / sensordef.cleanRatio);
      }
      break;
    case p145AlgNone:
      newValue = rzero;
      break;
    default:

      if (sensordef.cleanRatio > 0.0f)
      {
        newValue = (rSensor / sensordef.cleanRatio);
      }
      break;
  }
  return newValue;
}

/*****************************************************************************/

/*!
   @brief  Get the corrected resistance RZero of the sensor for calibration
        purposes
   @param[in] rSensor      Sensor resistance value Rs
   @param[in] temperature  The ambient air temperature
   @param[in] humidity     The relative humidity

   @return The corrected sensor resistance RZero in kOhm
 */

/*****************************************************************************/
float P145_data_struct::getCorrectedRZero(float rSensor, float temperature, float humidity) const
{
  const float c = getTempHumCorrection(temperature, humidity);

  return getRZero(rSensor / c);
}

/*****************************************************************************/

/*!
   @brief  Get the ppm of CO2 sensed (assuming only CO2 in the air)
   @param[in] sensorType   Sensor type identifier
   @param[in] rSensor      Sensor resistance value Rs
   @param[in] rZero        R0 Resistance zero value
   @return The ppm of CO2 in the air
 */

/*****************************************************************************/
float P145_data_struct::getPPM(float rSensor)
{
  switch (algorithm)
  {
    case p145AlgA: // MQ-135
      return sensordef.para * powf((rSensor / rzero), -sensordef.parb);
      break;
    case p145AlgB: // Miquel5612 Exponential
      return sensordef.para * powf((rSensor / rzero), sensordef.parb);
      break;
    case p145AlgC: // Miquel5612 Linear
      return powf(10.0f, (log10f(rSensor / rzero) - sensordef.parb) / sensordef.para);
      break;
    case p145AlgNone:
      return rSensor / rzero; // No conversion, return unconverted ratio
    default:
      return 0.0f;
      break;
  }
  return 0.0f; // Default value should never be returned
}

/*****************************************************************************/

/*!
   @brief  Get the ppm of CO2 sensed (assuming only CO2 in the air), corrected
        for temp/hum
   @param[in] rSensor      Sensor resistance value Rs
   @param[in] temperature  The ambient air temperature
   @param[in] humidity     The relative humidity
   @return The ppm of CO2 in the air
 */

/*****************************************************************************/
float P145_data_struct::getCorrectedPPM(float rSensor, float temperature, float humidity)
{
  const float c = getTempHumCorrection(temperature, humidity);

  return getPPM(rSensor / c);
}

/*****************************************************************************/

/*!
   @brief  Get the correction factor for temperature & humidity correction
   @param[in] temperature  The ambient air temperature
   @param[in] humidity     The relative humidity
   @return The correction factor for Rzero to compensate temperature & humidity
   @note Datasheet suggests temp & hum relation to Rzero is a multiplication factor c
      here we try to calculate this factor.
 */

/*****************************************************************************/
float P145_data_struct::getTempHumCorrection(float temperature, float humidity) const
{
  float c = 1.0f; // Correction factor, default no correction, factor is 1.0

  if (compensation)
  {
    switch (algorithm)
    {
      case p145AlgA:

        if (temperature < 20.0f)
        {
          c = sensordef.cora * temperature * temperature - sensordef.corb * temperature + sensordef.corc - (humidity - 33.0f) *
              sensordef.cord;
        }
        else
        {
          c = sensordef.core * temperature + sensordef.corf * humidity + sensordef.corg;
        }
        break;
      case p145AlgB:
      case p145AlgC:
        // Assume a 2nd order polynomial for temperature and 1st order poynomial for humidit
        // A + B*t + C*t*t + D*h
        c = (((sensordef.corc * temperature) + sensordef.corb) * temperature) + sensordef.cora + (sensordef.cord * humidity);
        break;
      case p145AlgNone:
      default:
        // No correction
        c = 1.0f;
    }
  }

  if (c <= 0.0f) // In case no or wrong correction factors are specified disable correction (multiplier = 1.0)
  {
    c = 1.0f;
  }
#  ifdef P145_DEBUG
  addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: correction= "), c));
#  endif // ifdef P145_DEBUG
  return c;
}

/*****************************************************************************/

/*!
   @brief  Get the analog input value
   @param[in] event   Plugin context
   @return The the filtered value of the analog input over the sampling interval
   @note   The sensor is expected to be sampled ten times per second.
        Lowest and highest value is dropped to remove outliers.
        All other values are averaged to a single float value.
        This algorithm is copied from P002_ADC.
 */

/*****************************************************************************/
float P145_data_struct::getAnalogValue()
{
  float value{};

  ovs.peek(value);
  ovs.resetKeepLast();
  return value;
}

/*****************************************************************************/

/*!
   @brief  Set the configuration data for the sensor
   @param[in] stype   Sensor type [enum]
   @param[in] comp    Temperature/humidity compensation enable/disable
   @param[in] cal     Autocalibration enable/disable
   @param[in] load    Load resistence Rload
   @param[in] zero    Rzero, Sensor resistor value for reference level
   @param[in] ref     Reference level for calibration
   @note   These parameters must be set before the plugin can calculate the level
        They are determined by the plugin configuration
 *****************************************************************************/
void P145_data_struct::setSensorData(int stype, bool comp, bool cal, bool vcclow, float load, float zero, float ref)
{
  /* Each MQ-xxx sensor comes with its own set of constants */
  /* Copy the correct set from program meory space          */
  if ((stype != sensorType) && (stype < nbrOfTypes) && (stype >= 0))
  {
    memcpy_P(&sensordef, &sensorDefs[stype], sizeof(struct P145_SENSORDEF));
    algorithm  = sensordef.alg;
    sensorType = stype;  // Selected sensor type, index in sensor data table
  }
  lowvcc       = vcclow; // Low power supply value
  compensation = comp;   // Compensation selection flag
  calibration  = cal;    // Calibration selection flag
  rload        = load;   // Rload, load resistor [Ohm]
  rzero        = zero;   // R0, reference resistance [Ohm]
  refLevel     = ref;    // Reference level for calibration [ppm]
  rcal_act     = 0.0f;   // Reset Rzero estimation
  cal_data     = 0.0f;   // Reset autocalibration
}

/*****************************************************************************/

/*!
   @brief  Set the connection pins for the sensor
   @param[in] aPin   Analog pin for the analog sensor input value
   @param[in] hPin   Ootput pin for heater control
   @note   These values must be set before the plugin can measure the level
        They are determined by the plugin configuration
 *****************************************************************************/
void P145_data_struct::setSensorPins(int aPin, int hPin)
{
  analogPin = aPin;
  heaterPin = hPin;
}

/*****************************************************************************/

/*!
   @brief  Return the current/latest value acquired from the sensor
   @param[in] temperature  Temperature for compensation
   @param[in] humidity     Humidity for compensation
   @return The calculated gas concentration [ppm or mg/l]
   @note   This is the entry point to start collecting the oversampling data,
        convert the analog value to Rsensor and convert Rsensor to the gas
        concentration level. It also performs autocalibration if selected.
        Oversampling data will be reset as a side effec.
        This function shall be called at regular interval for proper performance.
 */

/*****************************************************************************/
float P145_data_struct::readValue(float temperature, float humidity)
{
  float ain     = 0.0f; // Analog measured and filtered data
  float rSensor = 0.0f; // Sensor resistance Rs
  float value   = 0.0f; // Return value

#  ifdef P145_DEBUG
  uint32_t ovs_cnt = 0; // Oversampling count (for debugging)
#  endif // ifdef P145_DEBUG
#  ifdef P145_TEST
  static float injector = 50.0f;
  static float injstep  = 50.0f;
  ain       = injector;
  injector += injstep;

  if (injector >= 400.0f)
  {
    injstep = -50.0f;
  }

  if (injector <= 100)
  {
    injstep = 50.0f;
  }
#  else // ifdef P145_TEST

  if (validGpio(heaterPin))   // Check if heater control is enabled
  {
    ain = latchedAnalogInput; // Use value measured by heater control cycle
  }
  else
  {
    ain = getAnalogValue(); // Use acually being measured value
#   ifdef P145_DEBUG
    ovs_cnt = ovs.getCount();
#   endif // ifdef P145_DEBUG
    resetOversampling(); // Reset the oversampling variables.
  }
#  endif // ifdef P145_TEST

  if (ain > 0.0f)                  // Skip unsuccesful measurements
  {
    rSensor  = getResistance(ain); // Convert to Rsensor value
    value    = getCorrectedPPM(rSensor, temperature, humidity);
    rcal_act = getCorrectedRZero(rSensor, temperature, humidity);

    // Perform calibration if functionality is enabled
    if (calibration && (rcal_act < 1.0e6f))
    {
      calibrate(rcal_act);
    }
  }

  if (loglevelActiveFor(LOG_LEVEL_INFO))
  {
#  ifdef P145_DEBUG
    addLog(LOG_LEVEL_INFO, concat(concat(F("MQ-xx: Sensor type= "), sensorType), concat(F(": "), getTypeName(sensorType))));
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: algorithm= "), algorithm));     // Conversion algorithm
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: Rload= "), rload));             // Load resistor Rload
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: Rzero= "), rzero));             // Rerefernce resistance Rzero
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: Ratio= "), (rSensor / rzero))); // Ratio, input for non-linear conversion
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: RS= "), rSensor));              // Calculated sensor resistance Rsensor
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: Ref= "), refLevel));            // Reference level for calibration
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: Temp= "), temperature));        // Temperature for compensation algorithm
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: Hum= "), humidity));            // Humidity for compensation algorithm
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: ain= "), ain));                 // Measured analog input value
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: ovs= "), ovs_cnt));             // Oversampling count

    if (calibration)
    {
      addLog(LOG_LEVEL_INFO, F("MQ-xx: Calibration enabled"));
    }
#  endif // ifdef P145_DEBUG
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: level= "), value));   // Calculated sensor value
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: Rcal= "), rcal_act)); // Fresh calibrated Rzero when at ref level
  }

  return value;
}

/*****************************************************************************/

/*!
   @brief  Reset the input oversampling algorithm
 */

/*****************************************************************************/
void P145_data_struct::resetOversampling()
{
  ovs.reset();
}

/*****************************************************************************/

/*!
   @brief  Initialize the pluging stuct/class
   @return always true
   @note   This function is expected to be called as part of the plugin
        PLUGIN_INIT functionality
 */

/*****************************************************************************/
bool P145_data_struct::plugin_init()
{
  resetOversampling();
  last_cal = millis(); // Initialise the calibration interval
  return true;
}

/*****************************************************************************/

/*!
   @brief  Handle the 10Hz processing for the plugin struct/class
   @return Always true
   @note   This function is expected to be called as part of the plugin
        PLUGIN_TEN_PER_SECOND functionality
 */

/*****************************************************************************/
bool P145_data_struct::plugin_ten_per_second()
{
  // Measure the analog input value for oversampling
  // Algorithm uses mean value with exeption of max and min values
  // See declaraion of the global variables for their usage
  // Skip measurement in analog input is used to calibrate WiFi
  if (!WiFiEventData.wifiConnectInProgress)
  {
    ovs.add(espeasy_analogRead(analogPin));
  }
  return true;
}

/**************************************************************************/

/*!
   @brief  Return the actual Calibration value.
   @return Rzero associated with the current measurement
   @note   The value depends on the conversion algorithm associated with the
        sensor type.
 */

/**************************************************************************/
float P145_data_struct::getCalibrationValue() const
{
  return rcal_act;
}

/**************************************************************************/

/*!
   @brief  Return the Rzero value determined by autocalibration.
   @return Rzero as calculated by the autocalibration algorithm
   @note   This is the internally corrected Rzero and will be unaltered when
        Autocalibration is switched off.
 */

/**************************************************************************/
float P145_data_struct::getAutoCalibrationValue() const
{
  return rzero;
}

/**************************************************************************/

/*!
   @brief  Dump data from the internal plugin struct/class structure to the log
   @note   This function is only used for debugging
 */

/**************************************************************************/
void P145_data_struct::dump() const
{
#  ifdef P145_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO))
  {
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: NAME "), getTypeName(sensordef.name)));
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: GAS  "), getGasName(sensordef.name)));
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: CleanRatio "), sensordef.cleanRatio));
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: Algorithm "), sensordef.alg));
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: PARA "), sensordef.para));
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: PARB "), sensordef.parb));
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: CORA "), sensordef.cora));
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: CORB "), sensordef.corb));
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: CORC "), sensordef.corc));
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: CORD "), sensordef.cord));
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: CORE "), sensordef.core));
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: CORF "), sensordef.corf));
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: CORG "), sensordef.corg));
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: rzero "), rzero));
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: rcal  "), rcal_act));
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: analog PIN: "), analogPin));
    addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: heater PIN: "), heaterPin));
  }
#  endif // ifdef P145_DEBUG
}

/**************************************************************************/

/* @brief Provide the sensor name associated with a sensor type
   @param[in] stype Sensor type (index in sensor table)
   @return Pointer to sensor name as stored in flash memory
   @note Returns "invalid" for invalid sensor types
         This is a static member function
 */

/**************************************************************************/
const String P145_data_struct::getTypeName(int stype)
{
  switch (sensorDefs[stype].name)
  {
    case P145_SENSMQ135: return F("MQ135");
    case P145_SENSMQ2:   return F("MQ-2");
    case P145_SENSMQ3:   return F("MQ-3");
    case P145_SENSMQ4:   return F("MQ-4");
    case P145_SENSMQ5:   return F("MQ-5");
    case P145_SENSMQ6:   return F("MQ-6");
    case P145_SENSMQ7:   return F("MQ-7");
    case P145_SENSMQ8:   return F("MQ-8");
    case P145_SENSUSER:  return F("user");
    default:             return F("invalid");
  }
}

/**************************************************************************/

/* @brief Provide the name of the gas measured by a sensor type
   @param[in] stype Sensor type (index in sensor table)
   @return Pointer to gas name as stored in flash memory
   @note Returns "invalid" for invalid sensor types
         This is a static member function
 */

/**************************************************************************/
const String P145_data_struct::getGasName(int stype)
{
  switch (sensorDefs[stype].gas)
  {
    case P145_GASUSER:    return F("user");
    case P145_GASCH4:     return F("CH4");
    case P145_GASCO2:     return F("CO2");
    case P145_GASCO:      return F("CO");
    case P145_GASH2:      return F("H2");
    case P145_GASLPG:     return F("LPG");
    case P145_GASALCOHOL: return F("alcohol");
    default:              return F("invalid");
  }
}

/**************************************************************************/

/* @brief Returns the number of predefined sensor/gas entries in the table
   @return Number of predefined entries
   @note This is a static member function
 */

/**************************************************************************/
int P145_data_struct::getNbrOfTypes()
{
  return nbrOfTypes;
}

/*****************************************************************************/

/*!
   @brief  Perform the calibration algorithm
   @param[in] currentRcal  Value for Rzero determined with current measurement
   @return Void
   @note   Collect the highest Rzero during the calibration period
        At the end of the period use it to set the Rzero for next calculations
 */

/*****************************************************************************/
void P145_data_struct::calibrate(float currentRcal)
{
  unsigned long now = millis();
  long  time        = timePassedSince(last_cal);
  float lastRcal    = cal_data; // Last calculated Rcal this calibration sequence

  if (currentRcal > lastRcal)
  {
    cal_data = currentRcal; // Remember highest estimated Rzero during calibration period
  }
  const bool doit = (time > P145_CALIBRATION_INTERVAL) && (cal_data > 0.0f);

  if (doit)
  {
    rzero    = cal_data; // Update Rzero as determined by calibration
    cal_data = 0.0f;     // Restart calibration cycle with no value
    last_cal = now;      // Remember calibration moment
  }

  if (loglevelActiveFor(LOG_LEVEL_INFO))
  {
#  ifdef P145_DEBUG

    // Calculated Rzero if calibration concentration is applied
    // Rcal as calculated previous sample
    addLog(LOG_LEVEL_INFO, concat(concat(F("MQ-xx: Calibration with Rcal =  "), currentRcal), concat(F(" Rlast = "), lastRcal)));
#  endif // ifdef P145_DEBUG

    if (doit)
    {
      addLog(LOG_LEVEL_INFO, concat(F("MQ-xx: ***Calibrating*** Rzero =  "), cal_data));
    }
  }
}

/**************************************************************************/

/* @brief Evaluate the heater control algorithm
   @return void
   @note Heater control switches the heater pin depending on a statemachine
        The algorithm is tuned for MQ-7, see datasheet
        Heater is assumed to be sued when the associated pin is in use
 */

/**************************************************************************/
void P145_data_struct::heaterControl(void)
{
  unsigned long now = millis();
  long time         = timePassedSince(heaterChangeTime); // Time a state is active

  #  ifdef P145_DEBUG
  P145_heaterState lastState = heaterState;              // To detect a state change
  #  endif // ifdef P145_DEBUG

  // Check if the heaterPin has been changed => change in controller
  if (heaterPin != lastHeaterPin)
  {
    if (validGpio(lastHeaterPin))     // Disable last used pin
    {
      pinMode(heaterPin, INPUT);      // Safe value when pin is not ours?
    }

    if (validGpio(heaterPin))         // Enable new pin
    {
      pinMode(heaterPin, OUTPUT);     // Safe value when pin is not ours?
      digitalWrite(heaterPin, P145_HEATER_ON);
      heaterState = P145HeaterWarmup; // Start warming up the sensor
    }
    else
    {
      heaterState = P145HeaterDisabled; // Disable teh controller
    }
    heaterChangeTime = now;             // Reset state timer
    lastHeaterPin    = heaterPin;       // Remember selection
  }

  // State machine evaluation
  switch (heaterState)
  {
    // Heater control is disabled (default)
    case P145HeaterDisabled:
      // State transition only based upon pin selection above
      break;

    // Heater switched on, warming up
    case P145HeaterWarmup:

      if (time >= HEATER_WARMUP_TIME)
      {
        heaterState      = P145HeaterHighVolt;
        heaterChangeTime = now;
      }
      break;

    // Apply high voltage to heater
    case P145HeaterHighVolt:

      if (time >= HEATER_ON_TIME)
      {
        digitalWrite(heaterPin, P145_HEATER_OFF); // Switch heater
        heaterState      = P145HeaterLowVolt;
        heaterChangeTime = now;
      }
      break;

    // Apply low voltage to heater
    case P145HeaterLowVolt:

      if (time >= HEATER_OFF_TIME)
      {
        resetOversampling(); // Start oversampling sequence
        heaterState      = P145HeaterMeasure;
        heaterChangeTime = now;
      }
      break;

    // Measurement phase
    case P145HeaterMeasure:

      if (time >= HEATER_MEAS_TIME)
      {
        latchedAnalogInput = getAnalogValue();   // Read the sensor value
        digitalWrite(heaterPin, P145_HEATER_ON); // Switch heater
        heaterState      = P145HeaterHighVolt;
        heaterChangeTime = now;
      }
      break;
    default:
      heaterState = P145HeaterDisabled; // Back to a known state
      break;
  }

#  ifdef P145_DEBUG

  if (heaterState != lastState)
  {
    int x = time / 1000;
    addLog(LOG_LEVEL_INFO, concat(concat(F("MQ-xx: $$$ Heater state: "), heaterState), concat(F(", time: "), x)));
  }
#  endif  // P145_DEBUG
}

# endif // if SOC_ADC_SUPPORTED || defined(ESP8266)
#endif // USES_P145

#include "../PluginStructs/P169_data_struct.h"

#ifdef USES_P169

# include "../ESPEasyCore/ESPEasyGPIO.h"

# include <GPIO_Direct_Access.h>


# ifndef CORE_POST_3_0_0
#  ifdef ESP8266
#   define IRAM_ATTR ICACHE_RAM_ATTR
#  endif // ifdef ESP8266
# endif  // ifndef CORE_POST_3_0_0

# define P169_AS3935_TIMEOUT_USEC  2000


P169_data_struct::P169_data_struct(struct EventStruct *event) :
  _irqPin(P169_IRQ_PIN)
{
  // Do not try to construct the sensor if not needed as it will set the pinmode of the pin
  if ((_irqPin >= 0) && (Settings.TaskDeviceDataFeed[event->TaskIndex] == 0)) {
    _sensor = new (std::nothrow) AS3935I2C(P169_I2C_ADDRESS, P169_IRQ_PIN);
  }
}

P169_data_struct::~P169_data_struct()
{
  if (_sensor != nullptr) {
    _sensor->writePowerDown(true);
    delete _sensor;
    _sensor = nullptr;
  }
}

bool P169_data_struct::loop(struct EventStruct *event)
{
  if (_sensor == nullptr) {
    return false;
  }

  if (_sensor->getInterruptMode() == AS3935MI::AS3935_INTERRUPT_NORMAL) {
    // FIXME TD-er: Should also check for state of IRQ pin as it may still be high if the interrupt souce isn't checked.
    const uint32_t timestamp = _sensor->getInterruptTimestamp();

    if ((timestamp != 0ul) || DIRECT_pinRead(_irqPin)) {
      if ((timestamp != 0ul) && (timePassedSince(timestamp) < 2)) {
        // Check to make sure the sensor isn't still outputting a high freq. signal to the interrupt pin.
        // Count should be 1 at most.
        if (!_sensor->checkProperlySetToListenMode()) {
          // Sensor not yet ready to report some data
          addLog(LOG_LEVEL_ERROR, F("AS3935: Sensor was still showing LCO frequency on IRQ pin"));
          return false;
        }

        // Wait for the sensor to be ready to read the interrupt source
        if (timePassedSince(_sensor->getInterruptTimestamp()) < 2) {
          delay(1);
        }
      }

      // query the interrupt source from the AS3935
      switch (_sensor->readInterruptSource()) {
        case AS3935MI::AS3935_INT_NH:

          // Noise floor too high
          adjustForNoise(event);
          break;
        case AS3935MI::AS3935_INT_D:

          // Disturbance detected
          // N.B. can be disabled with _sensor->writeMaskDisturbers(true);
          adjustForDisturbances(event);
          break;
        case AS3935MI::AS3935_INT_L:
        {
          // Lightning detected
          ++_lightningCount;

          // FIXME TD-er: What to do with the "Lightning Threshold" ?
          // If it was > 15 minutes ago since the last detected lightning,
          // or cleared statistics, then we should set _lightningCount to this
          // threshold value and also increment the total counter accordingly.

          const int totalStrikes = UserVar.getFloat(event->TaskIndex, 3) + 1;
          const uint32_t energy  = getEnergy();

          if (energy > _highestEnergy) { _highestEnergy = energy; }

          if (energy < _lowestEnergy) { _lowestEnergy = energy; }
          UserVar.setFloat(event->TaskIndex, 0, computeDistanceFromEnergy(_highestEnergy, NAN));
          UserVar.setFloat(event->TaskIndex, 1, computeDistanceFromEnergy(_lowestEnergy, NAN));
          UserVar.setFloat(event->TaskIndex, 2, _lightningCount);
          UserVar.setFloat(event->TaskIndex, 3, totalStrikes);

          if (loglevelActiveFor(LOG_LEVEL_INFO)) {
            addLog(LOG_LEVEL_INFO, strformat(
                     F("AS3935: Lightning detected. DistNear: %.1f, DistFar: %.1f, Count: %u, Total: %d"),
                     computeDistanceFromEnergy(_highestEnergy, -1.0f),
                     computeDistanceFromEnergy(_lowestEnergy,  -1.0f),
                     _lightningCount,
                     totalStrikes));
          }

          if (Settings.UseRules) {
            // Lightning detected, Send event
            // Eventvalues:
            // - Distance
            // - Energy
            // - Lightning count since last PLUGIN_READ
            // - Total Lightning count since this was reset (or power cycle of ESP)
            eventQueue.addMove(
              strformat(
                F("%s#LightningDetected=%.1f,%.1f,%u,%d"),
                getTaskDeviceName(event->TaskIndex).c_str(),
                computeDistanceFromEnergy(_highestEnergy, -1.0f),
                computeDistanceFromEnergy(_lowestEnergy,  -1.0f),
                _lightningCount,
                totalStrikes));
          }

          return true;
        }
        case AS3935MI::AS3935_INT_DUPDATE:
        {
          // FIXME TD-er: No longer needed?


          // Distance updated
          const float distance = getDistance();

          if (_lightningCount > 0) {
            // Do not update until after this task interval or else the reported distance will be too low.
            UserVar.setFloat(event->TaskIndex, 0, distance);
          } else {
            _sensor->clearStatistics();
          }

          if (Settings.UseRules) {
            // Distance updated, Send event
            // Eventvalue:
            // - Distance
            eventQueue.addMove(
              strformat(
                F("%s#DistanceUpdated=%.2f"),
                getTaskDeviceName(event->TaskIndex).c_str(),
                distance));
          }
          break;
        }
      }
    }
    tryIncreasedSensitivity(event);
  }
  return false;
}

void P169_data_struct::html_show_sensor_info(struct EventStruct *event)
{
  if (_sensor == nullptr) {
    return;
  }

  addFormSubHeader(F("Current Sensor Data"));
  addRowLabel(F("Calibration"));
  const int8_t ant_cap = _sensor->getCalibratedAntCap();

  if (ant_cap != -1) {
    const float deviation_pct = computeDeviationPct(_sensor->getAntCapFrequency(ant_cap));

    if (fabs(deviation_pct) < (100 * AS3935MI_ALLOWED_DEVIATION)) {
      addEnabled(true);
    } else {
      if (P169_GET_TOLERANT_CALIBRATION_RANGE) {
        addEnabled(true);
        addHtml(F(HTML_SYMBOL_WARNING));
      } else {
        addEnabled(false);
      }
    }

    addRowLabel(F("Best Antenna cap"));
    addHtml(strformat(F("%d (%.2f%%)"), ant_cap, deviation_pct));
  } else {
    addEnabled(false);
  }

  addRowLabel(F("Error % per cap"));
# if FEATURE_CHART_JS
  addCalibrationChart(event);
# else // if FEATURE_CHART_JS

  for (uint8_t i = 0; i < 16; ++i) {
    const int32_t freq = _sensor->getAntCapFrequency(i);

    if (i != 0) {
      addHtml(',');
      addHtml(' ');
    }

    if (freq > 0) {
      addHtml(strformat(F("%.2f%%"), computeDeviationPct(freq)));
    } else {
      addHtml('-');
    }
  }
# endif // if FEATURE_CHART_JS

  addRowLabel(F("Current AFE gain"));
  addHtmlFloat(_afeGain, 2);
  addHtml('x');

  addRowLabel(F("Current Noise Floor Threshold"));
  addHtmlInt(_sensor->readNoiseFloorThreshold());

  addRowLabel(F("Current Watchdog Threshold"));
  addHtmlInt(_sensor->readWatchdogThreshold());

  addRowLabel(F("Current Spike Rejection"));
  addHtmlInt(_sensor->readSpikeRejection());
}

bool P169_data_struct::plugin_init(struct EventStruct *event)
{
  if (_sensor == nullptr) {
    return false;
  }

  _sensor->setInterruptMode(AS3935MI::AS3935_INTERRUPT_DETACHED);

  if (!(_sensor->begin() && _sensor->checkConnection()))
  {
    addLog(LOG_LEVEL_ERROR, F("AS3935: Sensor not detected"));
    return false;
  }
  addLog(LOG_LEVEL_INFO, F("AS3935: Sensor detected"));

  /*
     if (!_sensor->checkIRQ())
     {
      addLog(LOG_LEVEL_ERROR, F("AS3935: IRQ pin connection check failed"));

      //    return false;
     }
   */

  calibrate(event);

# ifdef ESP32

  #  ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG))
  {
    // Short test checking effect of nr samples during calibration
    {
      String log = F("AS3935: Calibration test: ");

      for (size_t i = 0; i < 7; ++i) {
        const uint32_t nrSamples = 2048 >> i;
        log += strformat(F(",%d samples"), nrSamples);
      }
      addLogMove(LOG_LEVEL_DEBUG, log);
    }

    for (int antcap = 0; antcap < 16; ++antcap) {
      float deviation_pct[7]{};

      for (size_t i = 0; i < 7; ++i) {
        const uint32_t nrSamples = 2048 >> i;
        _sensor->setFrequencyMeasureNrSamples(nrSamples);
        const uint32_t freq = _sensor->measureResonanceFrequency(AS3935MI::display_frequency_source_t::LCO, antcap);

        if (freq > 0) {
          deviation_pct[i] = computeDeviationPct(freq);
        } else {
          deviation_pct[i] = 0.0f;
        }
      }
      String log = strformat(F("AS3935: LCO: cap %d "), antcap);

      for (size_t i = 0; i < 7; ++i) {
        log += strformat(F(",%.2f%%"), deviation_pct[i]);
      }
      addLogMove(LOG_LEVEL_DEBUG, log);
    }
  }
  #  endif // ifndef BUILD_NO_DEBUG
# endif // ifdef ESP32


  // set the analog front end gain
  _sensor->writeNoiseFloorThreshold(AS3935MI::AS3935_NFL_2);
  _sensor->writeWatchdogThreshold(AS3935MI::AS3935_WDTH_2);
  _sensor->writeSpikeRejection(AS3935MI::AS3935_SREJ_2);
  setAFE_gain(event, P169_AFE_GAIN_LOW);
  {
    AS3935MI::min_num_lightnings_t min_num_lightnings = AS3935MI::AS3935_MNL_1;

    if ((P169_LIGHTNING_THRESHOLD >= AS3935MI::AS3935_MNL_1) && (P169_LIGHTNING_THRESHOLD <= AS3935MI::AS3935_MNL_16)) {
      min_num_lightnings = static_cast<AS3935MI::min_num_lightnings_t>(P169_LIGHTNING_THRESHOLD);
    }
    _sensor->writeMinLightnings(min_num_lightnings);
  }

  _sensor->writeMaskDisturbers(P169_GET_MASK_DISTURBANCE);

  _sensor->setInterruptMode(AS3935MI::AS3935_INTERRUPT_NORMAL);
  return true;
}

const char P169_subcommands[] PROGMEM = "clearstats|calibrate|setgain|setnf|setwd|setsrej";

enum class P169_subcmd_e : int8_t {
  invalid    = -1,
  clearstats = 0,
  calibrate,
  setgain,
  setnf,  // Set noise floor
  setwd,  // Set Watchdog Threshold
  setsrej // Set Spike Rejection
};

/*****************************************************
* plugin_write
*****************************************************/
bool P169_data_struct::plugin_write(struct EventStruct *event,
                                    String            & string) {
  if (_sensor == nullptr) {
    return false;
  }

  bool success = false;

  const String command = parseString(string, 1);

  if (equals(command, F("as3935"))) {
    const String subcommand   = parseString(string, 2);
    const int    subcommand_i = GetCommandCode(subcommand.c_str(), P169_subcommands);

    if (subcommand_i < 0) { return false; } // Fail fast

    const P169_subcmd_e subcmd = static_cast<P169_subcmd_e>(subcommand_i);
    uint32_t   value{};
    const bool hasValue = validUIntFromString(parseString(string, 3), value);

    switch (subcmd) {
      case P169_subcmd_e::invalid:
        break;
      case P169_subcmd_e::clearstats:
        clearStatistics();
        success = true;
        break;
      case P169_subcmd_e::calibrate:
        calibrate(event);
        _sensor->setInterruptMode(AS3935MI::AS3935_INTERRUPT_NORMAL);

        success = true;
        break;
      case P169_subcmd_e::setgain:

        if (hasValue) {
          success = true;

          // First check if it is a register value or gain factor.
          setAFE_gain(event, AFE_gain_to_regValue(value));
        }
        break;
      case P169_subcmd_e::setnf:

        if (hasValue) {
          success = true;
          setNoiseFloorThreshold(event, value);
        }
        break;
      case P169_subcmd_e::setwd:

        if (hasValue) {
          success = true;
          _sensor->writeWatchdogThreshold(value);
          sendChangeEvent(event);
        }
        break;
      case P169_subcmd_e::setsrej:

        if (hasValue) {
          success = true;
          _sensor->writeSpikeRejection(value);
          sendChangeEvent(event);
        }
        break;
    }
  }
  return success;
}

const char P169_get_config[] PROGMEM = "noisefloor|watchdog|srej|gain";

enum class P169_get_config_e : int8_t {
  invalid    = -1,
  noisefloor = 0, // [<taskname>#noisefloor]
  watchdog,       // [<taskname>#watchdog]
  srej,           // [<taskname>#srej] = current spike rejection
  gain            // [<taskname>#gain]
};


/*****************************************************
* plugin_get_config_value
*****************************************************/
bool P169_data_struct::plugin_get_config_value(struct EventStruct *event,
                                               String            & string) {
  if (_sensor == nullptr) {
    return false;
  }

  const String var      = parseString(string, 1);
  const int    config_i = GetCommandCode(var.c_str(), P169_get_config);

  if (config_i < 0) { return false; } // Fail fast
  const P169_get_config_e config = static_cast<P169_get_config_e>(config_i);

  switch (config)
  {
    case P169_get_config_e::invalid:
      return false;
    case P169_get_config_e::noisefloor:
      string = _sensor->readNoiseFloorThreshold();
      break;
    case P169_get_config_e::watchdog:
      string = _sensor->readWatchdogThreshold();
      break;
    case P169_get_config_e::srej:
      string = _sensor->readSpikeRejection();
      break;
    case P169_get_config_e::gain:
      string = toString(_afeGain, 2);
      break;
  }
  return true;
}

float P169_data_struct::getDistance()
{
  return computeDistanceFromEnergy(getEnergy(), -1.0f);
}

uint32_t P169_data_struct::getEnergy()
{
  if (_sensor == nullptr) {
    return 0u;
  }

  const uint32_t rawEnergy = _sensor->readEnergy();

  if ((rawEnergy == 0) || (rawEnergy == 0xFFFFFFFF)) {
    return 0u;
  }

  const int8_t ant_cap = _sensor->getCalibratedAntCap();

  if (ant_cap != -1) {
    const float deviation_pct = computeDeviationPct(_sensor->getAntCapFrequency(ant_cap));

    // Compute correction factor for loss in reported energy due to offset from perfect calibration.
    // Formula derived by TD-er using chart on this site: (section "Is Tuning Important?")
    // https://sites.google.com/view/as3935workbook/home#h.n9qonjaydsbd
    const float loss =
      (0.0321f * deviation_pct * deviation_pct) +
      (0.0279f * deviation_pct) +
      1.0f;

    return static_cast<uint32_t>((rawEnergy * loss) / _afeGain);
  }

  // No antenna calibration present, so no compensation possible
  return static_cast<uint32_t>(rawEnergy / _afeGain);
}

uint32_t P169_data_struct::getAndClearLightningCount()
{
  const uint32_t res = _lightningCount;

  _lightningCount = 0;
  _highestEnergy  = 0;
  _lowestEnergy   = 0xFFFFFFFF;
  return res;
}

void P169_data_struct::clearStatistics()
{
  if (_sensor != nullptr) {
    _sensor->clearStatistics();
  }
}

float P169_data_struct::computeDeviationPct(uint32_t LCO_freq)
{
  return (LCO_freq / 5000.0f) - 100.0f;
}

float P169_data_struct::computeDistanceFromEnergy(uint32_t energy, float errorValue)
{
  if ((energy == 0) || (energy == 0xFFFFFFFF)) { return errorValue; }

  // TD-er: Distance vs Energy attenuation is roughly X / sqrt(energy) for some factor X.
  // Factor of 2100 was determined experimentally evaluating a number of thunder storms
  // by Michael Gasperi, the author of this site: https://sites.google.com/view/as3935workbook/home
  // Verified by TD-er comparing live data mapped on https://map.blitzortung.org/ and the sensor.
  // LCO calibration offset was taken into account.
  return 2100.0f / sqrtf(energy);
}

bool P169_data_struct::calibrate(struct EventStruct *event)
{
  if (_sensor == nullptr) {
    return false;
  }

  _sensor->setInterruptMode(AS3935MI::AS3935_INTERRUPT_DETACHED);


  // calibrate the resonance frequency. failing the resonance frequency could indicate an issue
  // of the sensor.
  int32_t frequency = 0;

  _sensor->setCalibrateAllAntCap(P169_GET_SLOW_LCO_CALIBRATION);

  _sensor->setFrequencyMeasureNrSamples(P169_GET_SLOW_LCO_CALIBRATION ? AS3935MI_NR_CALIBRATION_SAMPLES : (AS3935MI_NR_CALIBRATION_SAMPLES /
                                                                                                           2));

  if (!_sensor->calibrateResonanceFrequency(frequency))
  {
    if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
      addLog(LOG_LEVEL_ERROR,
             strformat(F("AS3935: Resonance Frequency Calibration failed: %d Hz not in range 482500 Hz ... 517500 Hz"), frequency));
    }
  } else {
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO,
             strformat(F("AS3935: Resonance Frequency Calibration passed: ant_cap: %d, %d Hz, deviation: %.2f%%"),
                       _sensor->readAntennaTuning(),
                       frequency, computeDeviationPct(frequency)));
    }
  }

  // calibrate the RCO.
  if (!_sensor->calibrateRCO())
  {
    addLog(LOG_LEVEL_ERROR, F("AS3935: RCO Calibration failed."));
  } else {
    addLog(LOG_LEVEL_INFO, F("AS3935: RCO Calibration passed."));
  }

  // stop displaying LCO on IRQ
  _sensor->displayLcoOnIrq(false);

  return frequency != 0;
}

void P169_data_struct::adjustForNoise(struct EventStruct *event)
{
  if (_sensor == nullptr) {
    return;
  }

  // if the noise floor threshold setting is not yet maxed out, increase the setting.
  // note that noise floor threshold events can also be triggered by an incorrect
  // analog front end setting.
  uint8_t nf_lev{};

  if (_sensor->increaseNoiseFloorThreshold(nf_lev)) {
    addLog(LOG_LEVEL_INFO, strformat(F("AS3935: Increased noise floor threshold to: %u"), nf_lev));
    sendChangeEvent(event);
  }
  else {
    addLog(LOG_LEVEL_ERROR, F("AS3935: Noise floor threshold already at maximum"));
  }
}

void P169_data_struct::adjustForDisturbances(struct EventStruct *event)
{
  if (_sensor == nullptr) {
    return;
  }

  // increasing the Watchdog Threshold and / or Spike Rejection setting improves the AS3935s resistance
  // against disturbers but also decrease the lightning detection efficiency (see AS3935 datasheet)
  const uint8_t wdth  = _sensor->readWatchdogThreshold();
  const uint8_t srej  = _sensor->readSpikeRejection();
  const uint8_t noise = _sensor->readNoiseFloorThreshold();

  if ((wdth == AS3935MI::AS3935_WDTH_5) ||
      (srej == AS3935MI::AS3935_SREJ_5) ||
      (noise == AS3935MI::AS3935_NFL_5))
  {
    int32_t frequency{};
    const bool valid = _sensor->validateCurrentResonanceFrequency(frequency);

    if (valid || P169_GET_TOLERANT_CALIBRATION_RANGE) {
      // Resonance frequency is still OK, try lowering gain
      uint8_t curGain = _sensor->readAFE();

      if (curGain > P169_AFE_GAIN_LOW) {
        --curGain;

        // Since we change the gain, reset the other values to default
        _sensor->writeNoiseFloorThreshold(AS3935MI::AS3935_NFL_2);
        _sensor->writeWatchdogThreshold(AS3935MI::AS3935_WDTH_2);
        _sensor->writeSpikeRejection(AS3935MI::AS3935_SREJ_2);
        setAFE_gain(event, curGain);
        _sense_adj_last = millis();
      } else
      if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
        addLog(LOG_LEVEL_ERROR, strformat(
                 F("AS3935: Watchdog Threshold and Spike Rejection settings are already maxed out. Freq = %d"),
                 frequency));
      }
    } else if (timePassedSince(_sense_adj_last) > static_cast<long>(_sense_increase_interval))
    {
      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLog(LOG_LEVEL_INFO, strformat(
                 F("AS3935: Calibrate Resonance freq. Current frequency: %d"),
                 frequency));
      }

      if (_sensor->calibrateResonanceFrequency(frequency)) {
        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          addLog(LOG_LEVEL_INFO, strformat(
                   F("AS3935: Calibrate Resonance freq. Current frequency: %d"),
                   frequency));
        }

        // calibrate the RCO.
        if (!_sensor->calibrateRCO())
        {
          addLog(LOG_LEVEL_ERROR, F("AS3935: RCO Calibration failed."));
        } else {
          addLog(LOG_LEVEL_INFO, F("AS3935: RCO Calibration passed."));
        }
      }

      // FIXME TD-er: Should we do anything else here?
    }
    _sensor->setInterruptMode(AS3935MI::AS3935_INTERRUPT_NORMAL);
  }

  // FIXME TD-er: Is this a good threshold for auto adjust algorithm?
  if ((wdth < AS3935MI::AS3935_WDTH_5) ||
      (srej < AS3935MI::AS3935_SREJ_5)

      //      || (noise < AS3935MI::AS3935_NFL_5)
      )
  {
    _sense_adj_last = millis();

    // alternatively increase spike rejection and watchdog threshold
    if (srej < wdth)
    {
      if (_sensor->increaseSpikeRejection()) {
        sendChangeEvent(event);

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          addLog(LOG_LEVEL_INFO, strformat(F("AS3935: Increased spike rejection ratio to: %d"), (srej + 1)));
        }
      }
      else {
        addLog(LOG_LEVEL_ERROR, F("AS3935: Spike rejection ratio already at maximum"));
      }
    }
    else
    {
      if (_sensor->increaseWatchdogThreshold()) {
        sendChangeEvent(event);

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          addLog(LOG_LEVEL_INFO, strformat(F("AS3935: Increased watchdog threshold to %d"), (wdth + 1)));
        }
      }
      else {
        addLog(LOG_LEVEL_ERROR, F("AS3935: Watchdog threshold already at maximum"));
      }
    }
  }
}

void P169_data_struct::tryIncreasedSensitivity(struct EventStruct *event)
{
  if (_sensor == nullptr) {
    return;
  }

  // increase sensor sensitivity every once in a while. _sense_increase_interval controls how quickly the code
  // attempts to increase sensitivity.
  if (timePassedSince(_sense_adj_last) > static_cast<long>(_sense_increase_interval))
  {
    _sense_adj_last = millis();

    addLog(LOG_LEVEL_INFO, F("AS3935: No disturber detected, attempting to decrease noise floor threshold."));

    const uint8_t wdth  = _sensor->readWatchdogThreshold();
    const uint8_t srej  = _sensor->readSpikeRejection();
    const uint8_t noise = _sensor->readNoiseFloorThreshold();

    if ((wdth == AS3935MI::AS3935_WDTH_0) ||
        (srej == AS3935MI::AS3935_SREJ_0) ||
        (noise == AS3935MI::AS3935_NFL_0))
    {
      uint8_t curGain = _sensor->readAFE();

      if (curGain < P169_AFE_GAIN_HIGH) {
        ++curGain;

        // Since we change the gain, reset the other values to default
        _sensor->writeNoiseFloorThreshold(AS3935MI::AS3935_NFL_2);
        _sensor->writeWatchdogThreshold(AS3935MI::AS3935_WDTH_2);
        _sensor->writeSpikeRejection(AS3935MI::AS3935_SREJ_2);

        setAFE_gain(event, curGain);
        return;
      }
    }

    if ((noise > srej) && (noise > wdth) && _sensor->decreaseNoiseFloorThreshold()) {
      sendChangeEvent(event);

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLog(LOG_LEVEL_INFO, strformat(F("AS3935: Decreased noise floor to %d"), (noise - 1)));
      }
    }

    // alternatively decrease spike rejection and watchdog threshold
    if (srej > wdth)
    {
      if (_sensor->decreaseSpikeRejection()) {
        sendChangeEvent(event);

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          addLog(LOG_LEVEL_INFO, strformat(F("AS3935: Decreased spike rejection ratio to %d"), (srej - 1)));
        }
      }
        # ifndef BUILD_NO_DEBUG
      else {
        addLog(LOG_LEVEL_DEBUG, F("AS3935: Spike rejection ratio already at minimum"));
      }
        # endif // ifndef BUILD_NO_DEBUG
    }
    else
    {
      if (_sensor->decreaseWatchdogThreshold()) {
        sendChangeEvent(event);

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          addLog(LOG_LEVEL_INFO, strformat(F("AS3935: Decreased watchdog threshold to: %d"), (wdth - 1)));
        }
      }
        # ifndef BUILD_NO_DEBUG
      else {
        addLog(LOG_LEVEL_DEBUG, F("AS3935: Watchdog threshold already at minimum"));
      }
        # endif // ifndef BUILD_NO_DEBUG
    }
  }
}

void P169_data_struct::setAFE_gain(struct EventStruct *event, uint8_t gain)
{
  if (_sensor == nullptr) {
    return;
  }

  _afeGain       = regValue_AFE_gain_toFloat(gain);
  _afeGainRegval = gain;
  _sensor->writeAFE(gain);

  sendChangeEvent(event);
}

void P169_data_struct::setNoiseFloorThreshold(struct EventStruct *event, uint8_t noiseFloor)
{
  if (_sensor == nullptr) {
    return;
  }

  _sensor->writeNoiseFloorThreshold(noiseFloor);
  sendChangeEvent(event);
}

float P169_data_struct::regValue_AFE_gain_toFloat(uint8_t gain)
{
  // Source: https://sites.google.com/view/as3935workbook/home
  float afeGain = 1.0f;

  switch (gain)
  {
    case 10: afeGain = 0.30f; break;
    case 11: afeGain = 0.40f; break;
    case 12: afeGain = 0.55f; break;
    case 13: afeGain = 0.74f; break;
    case 14: afeGain = 1.00f; break; // Datasheet: "Outdoor"
    case 15: afeGain = 1.35f; break;
    case 16: afeGain = 1.83f; break;
    case 17: afeGain = 2.47f; break;
    case 18: afeGain = 3.34f; break; // Datasheet: "Indoor"
  }
  return afeGain;
}

uint8_t P169_data_struct::AFE_gain_to_regValue(float gain)
{
  {
    uint8_t regval = static_cast<uint8_t>(roundf(gain));

    if (regval >= 10) {
      if (regval <= 18) {
        return regval;
      }
      return AS3935MI::AS3935_OUTDOORS;
    }
  }

  float prevAFE_Gain = regValue_AFE_gain_toFloat(10);

  if (gain < prevAFE_Gain) {
    return 10;
  }

  for (uint8_t regval = 11; regval <= 18; ++regval)
  {
    const float afeGain = regValue_AFE_gain_toFloat(regval);

    if (gain < afeGain) {
      // See which is closest, prev or current
      if ((gain - prevAFE_Gain) < (afeGain - gain)) {
        return regval - 1;
      }
      return regval;
    }
    prevAFE_Gain = afeGain;
  }

  return AS3935MI::AS3935_INDOORS;
}

void P169_data_struct::sendChangeEvent(struct EventStruct *event)
{
  if (_sensor == nullptr) {
    return;
  }

  if (Settings.UseRules) {
    const   uint8_t noiseFloor = _sensor->readNoiseFloorThreshold();
    const   uint8_t watchdog   = _sensor->readWatchdogThreshold();
    const   uint8_t srej       = _sensor->readSpikeRejection();

    if (((_lastEvent_noiseFloor != noiseFloor)  && ((_lastEvent_noiseFloor > 1) || (noiseFloor > 1))) ||
        ((_lastEvent_watchdog != watchdog) && ((_lastEvent_watchdog > 1) || (watchdog > 1))) ||
        (_lastEvent_srej != srej) ||
        (_lastEvent_gain != _afeGainRegval)) {
      // Some value was updated, send event
      // Eventvalue:
      // - Gain
      // - Noise Level
      // - Watchdog Threshold
      // - Spike Rejection
      eventQueue.addMove(
        strformat(
          F("%s#ParamUpdate=%.2f,%u,%u,%u"),
          getTaskDeviceName(event->TaskIndex).c_str(),
          _afeGain,
          noiseFloor,
          watchdog,
          srej));
    }

    _lastEvent_noiseFloor = noiseFloor;
    _lastEvent_watchdog   = watchdog;
    _lastEvent_srej       = srej;
    _lastEvent_gain       = _afeGainRegval;
  }
}

# if FEATURE_CHART_JS
void P169_data_struct::addCalibrationChart(struct EventStruct *event)
{
  if (_sensor == nullptr) {
    return;
  }

  const int valueCount = 16;
  int   xAxisValues[valueCount]{};
  float values[valueCount]{};

  int actualValueCount = 0;

  for (int i = 0; i < valueCount; ++i) {
    const int32_t freq = _sensor->getAntCapFrequency(i);

    if (freq > 0) {
      values[actualValueCount]      = computeDeviationPct(freq);
      xAxisValues[actualValueCount] = i;
      ++actualValueCount;
    }
  }

  String axisOptions;

  {
    ChartJS_options_scales scales;
    scales.add({ F("x"), F("Antenna capacitor") });
    scales.add({ F("y"), F("Error (%)") });
    axisOptions = scales.toString();
  }

  add_ChartJS_chart_header(
    F("line"),
    F("lcoCapErrorCurve"),
    { F("LCO Resonance Frequency") },
    500,
    500,
    axisOptions);

  add_ChartJS_chart_labels(
    actualValueCount,
    xAxisValues);

  {
    const ChartJS_dataset_config config(
      F("Error %"),
      F("rgb(255, 99, 132)"));


    add_ChartJS_dataset(
      config,
      values,
      actualValueCount,
      2);
  }
  add_ChartJS_chart_footer();
}

# endif // if FEATURE_CHART_JS


#endif  // ifdef USES_P169

#include "../PluginStructs/P107_data_struct.h"

#ifdef USES_P107

// Needed also here for PlatformIO's library finder as the .h file 
// is in a directory which is excluded in the src_filter
# include <Adafruit_SI1145.h>

bool P107_data_struct::begin()
{
  return uv.begin();
}

#endif // ifdef USES_P107

#include "../PluginStructs/P049_data_struct.h"

#ifdef USES_P049


// 9 uint8_t commands:
// mhzCmdReadPPM[]              = {0xFF,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79};
// mhzCmdCalibrateZero[]        = {0xFF,0x01,0x87,0x00,0x00,0x00,0x00,0x00,0x78};
// mhzCmdABCEnable[]            = {0xFF,0x01,0x79,0xA0,0x00,0x00,0x00,0x00,0xE6};
// mhzCmdABCDisable[]           = {0xFF,0x01,0x79,0x00,0x00,0x00,0x00,0x00,0x86};
// mhzCmdReset[]                = {0xFF,0x01,0x8d,0x00,0x00,0x00,0x00,0x00,0x72};

/* It seems the offsets [3]..[4] for the detection range setting (command uint8_t 0x99) are wrong in the latest
 * online data sheet: http://www.winsen-sensor.com/d/files/infrared-gas-sensor/mh-z19b-co2-ver1_0.pdf
 * According to the MH-Z19B datasheet version 1.2, valid from: 2017.03.22 (received 2018-03-07)
 * the offset should be [6]..[7] instead.
 * 0x99 - Detection range setting, send command:
 * /---------+---------+---------+---------+---------+---------+---------+---------+---------\
 * | Byte 0  | Byte 1  | Byte 2  | Byte 3  | Byte 4  | Byte 5  | Byte 6  | Byte 7  | Byte 8  |
 * |---------+---------+---------+---------+---------+---------+---------+---------+---------|
 * | Start   | Reserved| Command | Reserved|Detection|Detection|Detection|Detection| Checksum|
 * | Byte    |         |         |         |range    |range    |range    |range    |         |
 * |         |         |         |         |24~32 bit|16~23 bit|8~15 bit |0~7 bit  |         |
 * |---------+---------+---------+---------+---------+---------+---------+---------+---------|
 * | 0xFF    | 0x01    | 0x99    | 0x00    | Data 1  | Data 2  | Data 3  | Data 4  | Checksum|
 * \---------+---------+---------+---------+---------+---------+---------+---------+---------/
 * Note: Detection range should be 0~2000, 0~5000, 0~10000 ppm.
 * For example: set 0~2000 ppm  detection range, send command: FF 01 99 00 00 00 07 D0 8F
 *              set 0~10000 ppm detection range, send command: FF 01 99 00 00 00 27 10 8F
 * The latter, updated version above is implemented here.
 */

// mhzCmdMeasurementRange1000[] = {0xFF,0x01,0x99,0x00,0x00,0x00,0x03,0xE8,0x7B};
// mhzCmdMeasurementRange2000[] = {0xFF,0x01,0x99,0x00,0x00,0x00,0x07,0xD0,0x8F};
// mhzCmdMeasurementRange3000[] = {0xFF,0x01,0x99,0x00,0x00,0x00,0x0B,0xB8,0xA3};
// mhzCmdMeasurementRange5000[] = {0xFF,0x01,0x99,0x00,0x00,0x00,0x13,0x88,0xCB};
// Removing redundant data, just keeping offsets [2], [6]..[7]:
const PROGMEM uint8_t mhzCmdData[][3] = {
  { 0x86, 0x00, 0x00 },
  { 0x87, 0x00, 0x00 },
  { 0x79, 0xA0, 0x00 },
  { 0x79, 0x00, 0x00 },
  { 0x8d, 0x00, 0x00 },
# ifdef ENABLE_DETECTION_RANGE_COMMANDS
  { 0x99, 0x03, 0xE8 },
  { 0x99, 0x07, 0xD0 },
  { 0x99, 0x0B, 0xB8 },
  { 0x99, 0x13, 0x88 }
# endif // ifdef ENABLE_DETECTION_RANGE_COMMANDS
};



P049_data_struct::~P049_data_struct() {
  if (easySerial != nullptr) {
    delete easySerial;
    easySerial = nullptr;
  }
}

void P049_data_struct::reset() {
  if (easySerial != nullptr) {
    delete easySerial;
    easySerial = nullptr;
  }
  linesHandled       = 0;
  checksumFailed     = 0;
  nrUnknownResponses = 0;
  ++sensorResets;

  // Default of the sensor is to run ABC
  ABC_Disable     = false;
  ABC_MustApply   = false;
  modelA_detected = false;
}

bool P049_data_struct::init(ESPEasySerialPort port, const int16_t serial_rx, const int16_t serial_tx, bool setABCdisabled) {
  if ((serial_rx < 0) || (serial_tx < 0)) {
    return false;
  }
  reset();
  easySerial = new (std::nothrow) ESPeasySerial(port, serial_rx, serial_tx);

  if (easySerial == nullptr) {
    return false;
  }
  easySerial->begin(9600);
  ABC_Disable = setABCdisabled;

  if (ABC_Disable) {
    // No guarantee the correct state is active on the sensor after reboot.
    ABC_MustApply = true;
  }
  lastInitTimestamp = millis();
  initTimePassed    = false;
  return isInitialized();
}

bool P049_data_struct::plugin_write(struct EventStruct *event, const String& string)
{
  String command = parseString(string, 1);

  if (equals(command, F("mhzcalibratezero")))
  {
    send_mhzCmd(mhzCmdCalibrateZero);
    addLog(LOG_LEVEL_INFO, F("MHZ19: Calibrated zero point!"));
    return true;
  }
  else if (equals(command, F("mhzreset")))
  {
    send_mhzCmd(mhzCmdReset);
    addLog(LOG_LEVEL_INFO, F("MHZ19: Sent sensor reset!"));
    return true;
  }
  else if (equals(command, F("mhzabcenable")))
  {
    send_mhzCmd(mhzCmdABCEnable);
    addLog(LOG_LEVEL_INFO, F("MHZ19: Sent sensor ABC Enable!"));
    return true;
  }
  else if (equals(command, F("mhzabcdisable")))
  {
    send_mhzCmd(mhzCmdABCDisable);
    addLog(LOG_LEVEL_INFO, F("MHZ19: Sent sensor ABC Disable!"));
    return true;
  }

# ifdef ENABLE_DETECTION_RANGE_COMMANDS
  else if (command.startsWith(F("mhzmeasurementrange"))) {
    if (equals(command, F("mhzmeasurementrange1000")))
    {
      send_mhzCmd(mhzCmdMeasurementRange1000);
      addLog(LOG_LEVEL_INFO, F("MHZ19: Sent measurement range 0-1000PPM!"));
      return true;
    }
    else if (equals(command, F("mhzmeasurementrange2000")))
    {
      send_mhzCmd(mhzCmdMeasurementRange2000);
      addLog(LOG_LEVEL_INFO, F("MHZ19: Sent measurement range 0-2000PPM!"));
      return true;
    }
    else if (equals(command, F("mhzmeasurementrange3000")))
    {
      send_mhzCmd(mhzCmdMeasurementRange3000);
      addLog(LOG_LEVEL_INFO, F("MHZ19: Sent measurement range 0-3000PPM!"));
      return true;
    }
    else if (equals(command, F("mhzmeasurementrange5000")))
    {
      send_mhzCmd(mhzCmdMeasurementRange5000);
      addLog(LOG_LEVEL_INFO, F("MHZ19: Sent measurement range 0-5000PPM!"));
      return true;
    }
  }
# endif // ENABLE_DETECTION_RANGE_COMMANDS
  return false;
}

void P049_data_struct::setABCmode(int abcDisableSetting) {
  const bool new_ABC_disable = (abcDisableSetting == P049_ABC_disabled);

  if (ABC_Disable != new_ABC_disable) {
    // Setting changed in the webform.
    ABC_MustApply = true;
    ABC_Disable   = new_ABC_disable;
  }
}

uint8_t P049_data_struct::calculateChecksum() const {
  uint8_t checksum = 0;

  for (uint8_t i = 1; i < 8; ++i) {
    checksum += mhzResp[i];
  }
  checksum = 0xFF - checksum;
  return checksum + 1;
}

size_t P049_data_struct::send_mhzCmd(uint8_t CommandId)
{
  if (!isInitialized()) { return 0; }

  // The receive buffer "mhzResp" is re-used to send a command here:
  mhzResp[0] = 0xFF; // Start uint8_t, fixed
  mhzResp[1] = 0x01; // Sensor number, 0x01 by default
  memcpy_P(&mhzResp[2], mhzCmdData[CommandId], sizeof(mhzCmdData[0]));
  mhzResp[6] = mhzResp[3]; mhzResp[7] = mhzResp[4];
  mhzResp[3] = mhzResp[4] = mhzResp[5] = 0x00;
  mhzResp[8] = calculateChecksum();

  if (!initTimePassed) {
    // Allow for 3 minutes of init time.
    initTimePassed = timePassedSince(lastInitTimestamp) > 180000;
  }

  return easySerial->write(mhzResp, sizeof(mhzResp));
}

bool P049_data_struct::read_ppm(unsigned int& ppm, signed int& temp, unsigned int& s, float& u) {
  if (!isInitialized()) { return false; }

  // send read PPM command
  uint8_t nbBytesSent = send_mhzCmd(mhzCmdReadPPM);

  if (nbBytesSent != 9) {
    return false;
  }

  // get response
  memset(mhzResp, 0, sizeof(mhzResp));

  long timer   = millis() + PLUGIN_READ_TIMEOUT;
  int  counter = 0;

  while (!timeOutReached(timer) && (counter < 9)) {
    if (easySerial->available() > 0) {
      uint8_t value = easySerial->read();

      if (((counter == 0) && (value == 0xFF)) || (counter > 0)) {
        mhzResp[counter++] = value;
      }
    } else {
      delay(10);
    }
  }

  if (counter < 9) {
    // Timeout
    return false;
  }
  ++linesHandled;

  if (!(mhzResp[8] == calculateChecksum())) {
    ++checksumFailed;
    return false;
  }

  if ((mhzResp[0] == 0xFF) && (mhzResp[1] == 0x86)) {
    // calculate CO2 PPM
    ppm = (static_cast<unsigned int>(mhzResp[2]) << 8) + mhzResp[3];

    // set temperature (offset 40)
    unsigned int mhzRespTemp = (unsigned int)mhzResp[4];
    temp = mhzRespTemp - 40;

    // set 's' (stability) value
    s = mhzResp[5];

    if (s != 0) {
      modelA_detected = true;
    }

    // calculate 'u' value
    u = (static_cast<unsigned int>(mhzResp[6]) << 8) + mhzResp[7];
    return true;
  }
  return false;
}

bool P049_data_struct::receivedCommandAcknowledgement(bool& expectReset) {
  expectReset = false;

  if (mhzResp[0] == 0xFF)  {
    switch (mhzResp[1]) {
      case 0x86: // Read CO2 concentration
      case 0x79: // ON/OFF Auto Calibration
        break;
      case 0x87: // Calibrate Zero Point (ZERO)
      case 0x88: // Calibrate Span Point (SPAN)
      case 0x99: // Detection range setting
        expectReset = true;
        break;
      default:
        ++nrUnknownResponses;
        return false;
    }
    uint8_t checksum = calculateChecksum();
    return mhzResp[8] == checksum;
  }
  ++nrUnknownResponses;
  return false;
}

String P049_data_struct::getBufferHexDump() const {
  String result;

  result.reserve(27);

  for (int i = 0; i < 9; ++i) {
    result += ' ';
    result += String(mhzResp[i], HEX);
  }
  return result;
}

MHZ19Types P049_data_struct::getDetectedDevice() const {
  if (linesHandled > checksumFailed) {
    return modelA_detected ? MHZ19_A : MHZ19_B;
  }
  return MHZ19_notDetected;
}

bool Plugin_049_Check_and_ApplyFilter(unsigned int prevVal, unsigned int& newVal, uint32_t s, const int filterValue, String& log) {
  if (s == 1) {
    // S==1 => "A" version sensor bootup, do not use values.
    return false;
  }

  if ((prevVal < 400) || (prevVal > 5000)) {
    // Prevent unrealistic values during start-up with filtering enabled.
    // Just assume the entered value is correct.
    return true;
  }
  bool filterApplied = filterValue > PLUGIN_049_FILTER_OFF_ALLSAMPLES;
  int32_t difference = newVal - prevVal;

  if ((s > 0) && (s < 64) && (filterValue != PLUGIN_049_FILTER_OFF)) {
    // Not the "B" version of the sensor, S value is used.
    // S==0 => "B" version, else "A" version
    // The S value is an indication of the stability of the reading.
    // S == 64 represents a stable reading and any lower value indicates (unusual) fast change.
    // Now we increase the delay filter for low values of S and increase response time when the
    // value is more stable.
    // This will make the reading useful in more turbulent environments,
    // where the sensor would report more rapid change of measured values.
    difference    = difference * s;
    difference   /= 64;
    log          += F("Compensate Unstable ");
    filterApplied = true;
  }

  switch (filterValue) {
    case PLUGIN_049_FILTER_OFF: {
      if ((s != 0) && (s != 64)) {
        log += F("Skip Unstable ");
        return false;
      }
      filterApplied = false;
      break;
    }

    // #Samples to reach >= 75% of step response
    case PLUGIN_049_FILTER_OFF_ALLSAMPLES: filterApplied = false; break; // No Delay
    case PLUGIN_049_FILTER_FAST:    difference          /= 2; break;     // Delay: 2 samples
    case PLUGIN_049_FILTER_MEDIUM:  difference          /= 4; break;     // Delay: 5 samples
    case PLUGIN_049_FILTER_SLOW:    difference          /= 8; break;     // Delay: 11 samples
  }

  if (filterApplied) {
    log += strformat(F("Raw PPM: %d Filtered "), newVal);
  }
  newVal = static_cast<unsigned int>(prevVal + difference);
  return true;
}

void P049_html_show_stats(struct EventStruct *event) {
  P049_data_struct *P049_data =
    static_cast<P049_data_struct *>(getPluginTaskData(event->TaskIndex));

  if (nullptr == P049_data) {
    return;
  }

  addRowLabel(F("Checksum (pass/fail/reset)"));
  addHtmlInt(P049_data->linesHandled);
  addHtml('/');
  addHtmlInt(P049_data->checksumFailed);
  addHtml('/');
  addHtmlInt(P049_data->sensorResets);

  addRowLabel(F("Detected"));

  switch (P049_data->getDetectedDevice()) {
    case MHZ19_A: addHtml(F("MH-Z19A")); break;
    case MHZ19_B: addHtml(F("MH-Z19B")); break;
    default: addHtml(F("---")); break;
  }
}

bool P049_perform_init(struct EventStruct *event) {
  bool success                 = false;
  const int16_t serial_rx      = CONFIG_PIN1;
  const int16_t serial_tx      = CONFIG_PIN2;
  const ESPEasySerialPort port = static_cast<ESPEasySerialPort>(CONFIG_PORT);
  P049_data_struct *P049_data  =
    static_cast<P049_data_struct *>(getPluginTaskData(event->TaskIndex));

  if (nullptr != P049_data) {
    if (P049_data->init(port, serial_rx, serial_tx, (PCONFIG(0) == P049_ABC_disabled))) {
      success = true;
      addLog(LOG_LEVEL_INFO, F("MHZ19: Init OK "));

      // delay first read, because hardware needs to initialize on cold boot
      // otherwise we get a weird value or read error
      Scheduler.schedule_task_device_timer(event->TaskIndex, millis() + 15000);
    }
  }
  return success;
}

#endif // ifdef USES_P049

#include "../PluginStructs/P110_data_struct.h"

#ifdef USES_P110

P110_data_struct::P110_data_struct(uint8_t i2c_addr, int timing, bool range) :
  _i2cAddress(i2c_addr),
  _timing(timing),
  _range(range) {}

// **************************************************************************/
// Initialize VL53L0X
// **************************************************************************/
bool P110_data_struct::begin(uint32_t interval_ms) {
  _timeToWait = 0;
  _initPhase  = P110_initPhases::Undefined;
  sensor.setAddress(_i2cAddress); // Initialize for configured address

  if (!sensor.init()) {
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, strformat(F("VL53L0X: Sensor not found, init failed for 0x%02x"), _i2cAddress));
      addLog(LOG_LEVEL_INFO, sensor.getInitResult());
    }
    return false;
  }

  sensor.setTimeout(500);

  if (_range) {
    // lower the return signal rate limit (default is 0.25 MCPS)
    sensor.setSignalRateLimit(0.1);

    // increase laser pulse periods (defaults are 14 and 10 PCLKs)
    sensor.setVcselPulsePeriod(VL53L0X::VcselPeriodPreRange,   18);
    sensor.setVcselPulsePeriod(VL53L0X::VcselPeriodFinalRange, 14);
  }

  sensor.setMeasurementTimingBudget(_timing * 1000);

  _initPhase  = P110_initPhases::InitDelay;
  _timeToWait = millis() + _timing + 50;

  sensor.startContinuous(interval_ms);

  return true;
}

bool P110_data_struct::check_reading_ready(struct EventStruct *event) {
  if (_initPhase == P110_initPhases::InitDelay) {
    if ((_timeToWait != 0) && timeOutReached(_timeToWait)) {
      _timeToWait = 0;
      _initPhase  = P110_initPhases::Ready;
    }
  } else {
    if (readDistance() >= 0) {
      Scheduler.schedule_task_device_timer(event->TaskIndex, millis());
    }
  }
  return true;
}

bool P110_data_struct::plugin_read(struct EventStruct *event) {
  bool success = false;

  if (isReadSuccessful()) {
    const float new_distance  = getDistance();
    const float prev_distance = _prev_distance;

    const bool first_sample = (prev_distance < 0.0f);

    const float estimator = (_filtered < 0.0f)
            ? new_distance
            : _filtered;

    const float ratio_filtered = 16;
    const float ratio_newVal = _prev_newval_ratio;
    _prev_newval_ratio = std::abs(estimator - new_distance);

    _filtered = 
      ((ratio_filtered * estimator) + (ratio_newVal * new_distance))
      / (ratio_filtered + ratio_newVal);

    const float dist   = _filtered;
    const float p_dist = prev_distance;

    // Check trend:
    //  0 = equal
    // -1 = move closer
    //  1 = move away

    const int16_t displacement = first_sample ? 0 : roundf(dist - p_dist);
    const int16_t disp_dir     = (displacement == 0)
            ? 0
            : (displacement > 0) ? 1 : -1;

    //const bool direction_changed = disp_dir != static_cast<int16_t>(UserVar.getFloat(event->TaskIndex, 1));
    const bool triggered         =
      //      direction_changed ||
      (std::abs(displacement) > P110_DELTA);

        # ifdef P110_INFO_LOG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO, strformat(F("VL53L0x: Perform read: trig: %d, prev: %d, dist: %d"), triggered, p_dist, dist));
    }
        # endif // ifdef P110_INFO_LOG
    // Value is classified as invalid when > 8190, so no conversion or 'split' needed
    UserVar.setFloat(event->TaskIndex, 0, _filtered);
    UserVar.setFloat(event->TaskIndex, 1, disp_dir);       // Trend of value

    if (first_sample || triggered || (P110_SEND_ALWAYS == 1)) {
      // Update the "previous" distance.
      _prev_distance = _filtered;
      success        = true;
    }
  }
  return success;
}

int16_t P110_data_struct::getDistance() {
  const int res = _distance;

  _distance = P110_DISTANCE_WAITING;
  return res;
}

int16_t P110_data_struct::readDistance() {
  if (_initPhase != P110_initPhases::Ready) {
    return P110_DISTANCE_UNINITIALIZED;
  }

  int16_t dist{};

  if (sensor.asyncReadRangeContinuousMillimeters(dist)) {
    if ((dist >= 0) && (dist < 8192)) {
      // Only keep a copy of valid distance readings.
      // Since the distance reading is later called from PLUGIN_READ,
      // we might have had a new reading inbetween which could be a "still waiting"
      // value and then we lost the actual reading.

      _distance = dist;
      return _distance;
    }
  }

  if (dist == VL53L0X_WAITING) {
    // Just waiting
    // No need to keep sending many logs per second
    return P110_DISTANCE_WAITING;
  }


# ifdef P110_DEBUG_LOG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLogMove(LOG_LEVEL_DEBUG, strformat(F("VL53L0X: idx: 0x%02x init: %u"),
                                          _i2cAddress, static_cast<uint8_t>(_initPhase)));
  }
# endif // P110_DEBUG_LOG

  if (sensor.timeoutOccurred()) {
# ifdef P110_DEBUG_LOG
    addLog(LOG_LEVEL_DEBUG, F("VL53L0X: TIMEOUT"));
# endif // P110_DEBUG_LOG
    return P110_DISTANCE_READ_TIMEOUT;
  } else if (dist >= 8190) {
# ifdef P110_DEBUG_LOG
    addLog(LOG_LEVEL_DEBUG, concat(F("VL53L0X: NO MEASUREMENT: "), dist));
# endif // P110_DEBUG_LOG
    return P110_DISTANCE_OUT_OF_RANGE;
  }

# ifdef P110_DEBUG_LOG
  addLog(LOG_LEVEL_DEBUG, F("VL53L0X: NO MEASUREMENT: 0xFFFF"));
# endif // P110_DEBUG_LOG
  return P110_DISTANCE_READ_ERROR;
}

bool P110_data_struct::isReadSuccessful() const {
  return _distance >= 0;
}

#endif // ifdef USES_P110

#include "../PluginStructs/P092_data_struct.h"

#ifdef USES_P092

//
// DLBus reads and decodes the DL-Bus.
// The DL-Bus is used in heating control units e.g. sold by Technische Alternative (www.ta.co.at).
// Author uwekaditz

// #define DLbus_DEBUG

// Flags for pulse width (bit 0 is the content!)
# define DLbus_FlagSingleWidth                 0x02
# define DLbus_FlagDoubleWidth                 0x04
# define DLbus_FlagShorterThanSingleWidth      0x10
# define DLbus_FlagBetweenDoubleSingleWidth    0x20
# define DLbus_FlagLongerThanDoubleWidth       0x40
# define DLbus_FlagLongerThanTwiceDoubleWidth  0x80
# define DLbus_FlagsWrongTiming                (DLbus_FlagLongerThanTwiceDoubleWidth | DLbus_FlagLongerThanDoubleWidth | \
                                                DLbus_FlagBetweenDoubleSingleWidth | DLbus_FlagShorterThanSingleWidth)

// Helper for ISR call
DLBus *DLBus::__instance                         = nullptr;
volatile  static uint8_t *ISR_PtrChangeBitStream = nullptr; // pointer to received bit change stream

DLBus::DLBus()
{
  if (__instance == nullptr)
  {
    __instance             = this;
    ISR_PtrChangeBitStream = DLbus_ChangeBitStream;
# ifndef P092_LIMIT_BUILD_SIZE
    addLog(LOG_LEVEL_INFO, F("Class DLBus created"));
# endif // ifndef P092_LIMIT_BUILD_SIZE
  }
}

DLBus::~DLBus()
{
  if (__instance == this)
  {
    __instance             = nullptr;
    ISR_PtrChangeBitStream = nullptr;
# ifndef P092_LIMIT_BUILD_SIZE
    addLog(LOG_LEVEL_INFO, F("Class DLBus destroyed"));
# endif // ifndef P092_LIMIT_BUILD_SIZE
  }
}

# ifndef P092_LIMIT_BUILD_SIZE
void DLBus::AddToInfoLog(const String& string)
{
  if ((IsLogLevelInfo) && (LogLevelInfo != 0xff)) {
    addLog(LogLevelInfo, string);
  }
}

# endif // ifndef P092_LIMIT_BUILD_SIZE

void DLBus::AddToErrorLog(const String& string)
{
  if (LogLevelError != 0xff) {
    addLog(LogLevelError, string);
  }
}

void DLBus::attachDLBusInterrupt(void)
{
  ISR_Receiving = false;
  IsISRset      = true;
  IsNoData      = false;
  attachInterrupt(digitalPinToInterrupt(ISR_DLB_Pin), ISR, CHANGE);
}

void DLBus::StartReceiving(void)
{
  ISR_noInterrupts(); // make sure we don't get interrupted before we are ready
  ISR_PulseCount      = 0;
  ISR_Receiving       = (ISR_PtrChangeBitStream != nullptr);
  ISR_AllBitsReceived = false;
  ISR_interrupts(); // interrupts allowed now, next instruction WILL be executed
}

void IRAM_ATTR DLBus::ISR(void)
{
  if (__instance)
  {
    __instance->ISR_PinChanged();
  }
}

void IRAM_ATTR DLBus::ISR_PinChanged(void)
{
  //  long TimeDiff = usecPassedSince(ISR_TimeLastBitChange); // time difference to previous pulse in Âµs
  uint32_t _now     = micros();
  int32_t  TimeDiff = (int32_t)(_now - ISR_TimeLastBitChange);

  //  ISR_TimeLastBitChange = micros();                           // save last pin change time
  ISR_TimeLastBitChange = _now;             // save last pin change time

  if (ISR_Receiving) {
    uint8_t val = digitalRead(ISR_DLB_Pin); // read state

    // check pulse width
    if (TimeDiff >= 2 * ISR_MinDoublePulseWidth) {
      val |= DLbus_FlagLongerThanTwiceDoubleWidth; // longer then 2x double pulse width
    }
    else if (TimeDiff > ISR_MaxDoublePulseWidth) {
      val |= DLbus_FlagLongerThanDoubleWidth;      // longer then double pulse width
    }
    else if (TimeDiff >= ISR_MinDoublePulseWidth) {
      val |= DLbus_FlagDoubleWidth;                // double pulse width
    }
    else if (TimeDiff > ISR_MaxPulseWidth) {
      val |= DLbus_FlagBetweenDoubleSingleWidth;   // between double and single pulse width
    }
    else if (TimeDiff < ISR_MinPulseWidth) {
      val |= DLbus_FlagShorterThanSingleWidth;     // shorter then single pulse width
    }
    else {
      val |= DLbus_FlagSingleWidth;                // single pulse width
    }

    if (ISR_PulseCount < 2) {
      // check if sync is received
      if (val & DLbus_FlagLongerThanTwiceDoubleWidth) {
        // sync received
        *ISR_PtrChangeBitStream       = !(val & 0x01);
        *(ISR_PtrChangeBitStream + 1) = val;
        ISR_PulseCount                = 2;
      }
      else {
        ISR_PulseCount = 1; // flag that interrupt is receiving
      }
    }
    else {
      *(ISR_PtrChangeBitStream + ISR_PulseCount) = val;         // set bit
      ISR_PulseCount++;
      ISR_Receiving       = (ISR_PulseCount < ISR_PulseNumber); // stop P092_receiving when data frame is complete
      ISR_AllBitsReceived = !ISR_Receiving;
    }
  }
}

boolean DLBus::CheckTimings(void) {
  uint8_t rawval, val;
  uint8_t WrongTimeCnt = 0;
  int     i;

# ifdef DLbus_DEBUG
  uint16_t WrongTimingArray[5][6];
# endif // DLbus_DEBUG

  //  AddToInfoLog(F("Receive stopped."));

  ISR_PulseCount = 0;

  for (i = 0; i <= ISR_PulseNumber; ++i) {
    // store DLbus_ChangeBitStream into ByteStream
    rawval = *(ISR_PtrChangeBitStream + i);

    if (rawval & DLbus_FlagsWrongTiming) {
      // wrong DLbus_time_diff
      if (ISR_PulseCount > 0) {
# ifdef DLbus_DEBUG
        WrongTimingArray[WrongTimeCnt][0] = i;
        WrongTimingArray[WrongTimeCnt][1] = ISR_PulseCount;
        WrongTimingArray[WrongTimeCnt][2] = BitNumber;
        WrongTimingArray[WrongTimeCnt][3] = rawval;
# endif // DLbus_DEBUG

        if ((rawval == DLbus_FlagLongerThanTwiceDoubleWidth) && (*(ISR_PtrChangeBitStream + i - 1) == (DLbus_FlagDoubleWidth | 0x01))) {
          // Add two additional short pulses (low and high), previous bit is High and contains DLbus_FlagDoubleWidth
          ProcessBit(0);
          ProcessBit(1);
# ifdef DLbus_DEBUG
          WrongTimingArray[WrongTimeCnt][4] = DLbus_FlagSingleWidth;
          WrongTimingArray[WrongTimeCnt][5] = DLbus_FlagSingleWidth + 1;
# endif // DLbus_DEBUG
        }
# ifdef DLbus_DEBUG
        else {
          WrongTimingArray[WrongTimeCnt][4] = 0xff;
          WrongTimingArray[WrongTimeCnt][5] = 0xff;
        }
# endif // DLbus_DEBUG
        WrongTimeCnt++;

        if (WrongTimeCnt >= 5) {
          return false;
        }
      }
    }
    else {
      val = rawval & 0x01;

      if ((rawval & DLbus_FlagDoubleWidth) == DLbus_FlagDoubleWidth) {
        // double pulse width
        ProcessBit(!val);
        ProcessBit(val);
      }
      else {
        // single pulse width
        ProcessBit(val);
      }
    }
  }

  //  AddToInfoLog(F("DLbus_ChangeBitStream copied."));

# ifdef DLbus_DEBUG

  if (WrongTimeCnt > 0) {
    if (IsLogLevelInfo) {
      String log = F("Wrong Timings: ");
      AddToInfoLog(log);

      for (i = 0; i < WrongTimeCnt; ++i) {
        log  = i + 1;
        log += F(": PulseCount:");
        log += WrongTimingArray[i][1];
        log += F(": BitCount:");
        log += WrongTimingArray[i][2];
        log += F(" Value:0x");
        log += String(WrongTimingArray[i][3], HEX);
        log += F(" ValueBefore:0x");
        log += String(*(ISR_PtrChangeBitStream + WrongTimingArray[i][0] - 1), HEX);
        log += F(" ValueAfter:0x");
        log += String(*(ISR_PtrChangeBitStream + WrongTimingArray[i][0] + 1), HEX);

        if (WrongTimingArray[i][4] != 0xff) {
          log += F(" Added:0x");
          log += String(WrongTimingArray[i][4], HEX);
        }

        if (WrongTimingArray[i][5] != 0xff) {
          log += F(" Added:0x");
          log += String(WrongTimingArray[i][5], HEX);
        }
        AddToInfoLog(log);
      }
    }
  }
# endif // DLbus_DEBUG
  return true;
}

void DLBus::ProcessBit(uint8_t b) {
  // ignore first pulse
  ISR_PulseCount++;

  if (ISR_PulseCount % 2) {
    return;
  }
  BitNumber = (ISR_PulseCount / 2);

  if (b) {
    ByteStream[BitNumber / 8] |= (1 << (BitNumber % 8));  // set bit
  }
  else {
    ByteStream[BitNumber / 8] &= ~(1 << (BitNumber % 8)); // clear bit
  }
}

boolean DLBus::Processing(void) {
  boolean inverted = false;
  int16_t StartBit; // first bit of data frame (-1 not recognized)

# ifndef P092_LIMIT_BUILD_SIZE
  AddToInfoLog(F("Processing..."));
# endif // ifndef P092_LIMIT_BUILD_SIZE
  StartBit = Analyze(); // find the data frame's beginning

  // inverted signal?
  while (StartBit == -1) {
    if (inverted) {
      AddToErrorLog(F("Error: Already inverted!"));
      return false;
    }
    Invert(); // invert again
    inverted = true;
    StartBit = Analyze();

    if (StartBit == -1) {
      AddToErrorLog(F("Error: No data frame available!"));
      return false;
    }
    uint16_t RequiredBitStreamLength = (ISR_PulseNumber - DLBus_ReserveBytes) / DLBus_BitChangeFactor;

    if ((BitNumber - StartBit) < RequiredBitStreamLength) {
      // no complete data frame available (difference between start_bit and received bits is < RequiredBitStreamLength)
      AddToErrorLog(F("Start bit too close to end of stream!"));

# ifndef P092_LIMIT_BUILD_SIZE

      if (IsLogLevelInfo) {
        AddToInfoLog(strformat(
                       F("# Required bits: %d StartBit: %d / EndBit: %d"),
                       RequiredBitStreamLength,
                       StartBit,
                       BitNumber));
      }
# endif // ifndef P092_LIMIT_BUILD_SIZE
      return false;
    }
  }

# ifndef P092_LIMIT_BUILD_SIZE

  if (IsLogLevelInfo) {
    AddToInfoLog(strformat(
                   F("StartBit: %d / EndBit: %d"),
                   StartBit, BitNumber));
  }
# endif // ifndef P092_LIMIT_BUILD_SIZE
  Trim(StartBit);      // remove start and stop bits

  if (CheckDevice()) { // check connected device
    return true;
  }
  else {
    AddToErrorLog(F("Error: Device not found!"));
    return false;
  }
}

int DLBus::Analyze(void) {
  uint8_t sync = 0;

  // find SYNC (16 * sequential 1)
  for (int i = 0; i < BitNumber; ++i) {
    if (ReadBit(i)) {
      sync++;
    }
    else {
      sync = 0;
    }

    if (sync == DLBus_SyncBits) {
      // finde erste 0 // find first 0
      while (ReadBit(i) == 1) {
        i++;
      }
      return i; // beginning of data frame
    }
  }

  // no data frame available. check signal?
  return -1;
}

void DLBus::Invert(void) {
# ifndef P092_LIMIT_BUILD_SIZE
  AddToInfoLog(F("Invert bit stream..."));
# endif // ifndef P092_LIMIT_BUILD_SIZE

  for (int i = 0; i < BitNumber; ++i) {
    WriteBit(i, ReadBit(i) ? 0 : 1); // invert every bit
  }
}

uint8_t DLBus::ReadBit(int pos) {
  int row = pos / 8;                          // detect position in bitmap
  int col = pos % 8;

  return ((ByteStream[row]) >> (col)) & 0x01; // return bit
}

void DLBus::WriteBit(int pos, uint8_t set) {
  int row = pos / 8; // detect position in bitmap
  int col = pos % 8;

  if (set) {
    ByteStream[row] |= 1 << col;    // set bit
  }
  else {
    ByteStream[row] &= ~(1 << col); // clear bit
  }
}

void DLBus::Trim(int start_bit) {
  for (int i = start_bit, bit = 0; i < BitNumber; ++i) {
    int offset = i - start_bit;

    // ignore start and stop bits:
    // start bits: 0 10 20 30, also  x    % 10 == 0
    // stop bits:  9 19 29 39, also (x+1) % 10 == 0
    if (offset % 10 && (offset + 1) % 10) {
      WriteBit(bit, ReadBit(i));
      bit++;
    }
  }
}

boolean DLBus::CheckDevice(void) {
  // Data frame of a device?
  if (ByteStream[0] == DeviceBytes[0]) {
    if ((DeviceBytes[1] == 0) || (ByteStream[1] == DeviceBytes[1])) {
      return true;
    }
  }

# ifndef P092_LIMIT_BUILD_SIZE

  if (IsLogLevelInfo) {
    String log = strformat(F("# Received DeviceByte(s): 0x%02x"), ByteStream[0]);

    if (DeviceBytes[1] != 0) {
      log += strformat(F("%02x"), ByteStream[1]);
    }
    log += strformat(F(" Requested: 0x%02x"), DeviceBytes[0]);

    if (DeviceBytes[1] != 0) {
      log += strformat(F("%02x"), DeviceBytes[1]);
    }
    AddToInfoLog(log);
  }
# endif // ifndef P092_LIMIT_BUILD_SIZE
  return false;
}

boolean DLBus::CheckCRC(uint8_t IdxCRC) {
  // CRC check sum
  if (IdxCRC == 0) {
    return true;
  }
# ifndef P092_LIMIT_BUILD_SIZE
  AddToInfoLog(F("Check CRC..."));
# endif // ifndef P092_LIMIT_BUILD_SIZE
  uint16_t dataSum = 0;

  for (int i = 0; i < IdxCRC; ++i) {
    dataSum = dataSum + ByteStream[i];
  }
  dataSum = dataSum & 0xff;

  if (dataSum == ByteStream[IdxCRC]) {
    return true;
  }
  AddToErrorLog(F("Check CRC failed!"));

# ifndef P092_LIMIT_BUILD_SIZE

  if (IsLogLevelInfo) {
    AddToInfoLog(strformat(
                   F("# Calculated CRC: 0x%x Received: 0x%x"),
                   dataSum,
                   ByteStream[IdxCRC]));
  }
# endif // ifndef P092_LIMIT_BUILD_SIZE
  return false;
}

// sensor types
# define DLbus_UNUSED              0b000
# define DLbus_Sensor_DIGITAL      0b001
# define DLbus_Sensor_TEMP         0b010
# define DLbus_Sensor_VOLUME_FLOW  0b011
# define DLbus_Sensor_RAYS         0b110
# define DLbus_Sensor_ROOM         0b111

// room sensor modes
# define DLbus_RSM_AUTO            0b00
# define DLbus_RSM_NORMAL          0b01
# define DLbus_RSM_LOWER           0b10
# define DLbus_RSM_STANDBY         0b11


P092_data_struct::~P092_data_struct() {
  if (DLbus_Data != nullptr) {
    if (DLbus_Data->ISR_DLB_Pin != 0xFF) {
      detachInterrupt(digitalPinToInterrupt(DLbus_Data->ISR_DLB_Pin));
    }
    delete DLbus_Data;
    DLbus_Data = nullptr;
  }
}

bool P092_data_struct::init(int8_t pin1, int P092DeviceIndex, eP092pinmode P092pinmode) {
  DLbus_Data = new (std::nothrow) DLBus;

  if (DLbus_Data == nullptr) {
    return false;
  }
  DLbus_Data->LogLevelInfo   = LOG_LEVEL_INFO;
  DLbus_Data->LogLevelError  = LOG_LEVEL_ERROR;
  DLbus_Data->IsLogLevelInfo = loglevelActiveFor(LOG_LEVEL_INFO);
  DLbus_Data->ISR_DLB_Pin    = pin1;

  // interrupt is detached in PLUGIN_WEBFORM_SAVE and attached in PLUGIN_ONCE_A_SECOND
  // to ensure that new interrupt is attached after new pin is configured, setting
  // IsISRset to false is done here.
  DLbus_Data->IsISRset = false;

  switch (P092pinmode) {
    case eP092pinmode::ePPM_InputPullUp:
# ifndef P092_LIMIT_BUILD_SIZE
      addLog(LOG_LEVEL_INFO, F("P092_init: Set input pin with pullup"));
# endif // ifndef P092_LIMIT_BUILD_SIZE
      pinMode(pin1, INPUT_PULLUP);
      break;
    default:
# ifndef P092_LIMIT_BUILD_SIZE
      addLog(LOG_LEVEL_INFO, F("P092_init: Set input pin"));
# endif // ifndef P092_LIMIT_BUILD_SIZE
      pinMode(pin1, INPUT);
  }

  // on a CHANGE on the data pin P092_Pin_changed is called
  // DLbus_Data->attachDLBusInterrupt();
  return true;
}

void P092_data_struct::Plugin_092_SetIndices(int P092DeviceIndex) {
  // Set the indices for the DL bus packet
  int iDeviceBytes, iDontCareBytes, iTimeStampBytes;

  // default settings for ESR21
  P092_DataSettings.DataBytes                 = 31;
  P092_DataSettings.DLbus_MinPulseWidth       = P092_min_width_488;
  P092_DataSettings.DLbus_MaxPulseWidth       = P092_max_width_488;
  P092_DataSettings.DLbus_MinDoublePulseWidth = P092_double_min_width_488;
  P092_DataSettings.DLbus_MaxDoublePulseWidth = P092_double_max_width_488;

  P092_DataSettings.DeviceByte0    = 0x70;
  P092_DataSettings.DeviceByte1    = 0x8F;
  iDeviceBytes                     = 2;
  iDontCareBytes                   = 0;
  iTimeStampBytes                  = 0;
  P092_DataSettings.MaxSensors     = 3;
  P092_DataSettings.MaxExtSensors  = 6;
  P092_DataSettings.OutputBytes    = 1;
  P092_DataSettings.SpeedBytes     = 1;
  P092_DataSettings.MaxAnalogOuts  = 1;
  P092_DataSettings.AnalogBytes    = 1;
  P092_DataSettings.VolumeBytes    = 0;
  P092_DataSettings.MaxHeatMeters  = 1;
  P092_DataSettings.CurrentHmBytes = 2;
  P092_DataSettings.MWhBytes       = 2;
  P092_DataSettings.IdxCRC         = 30;

  switch (P092DeviceIndex) {
    case 31:                       // UVR31
    case 42:                       // UVR42

      if (P092DeviceIndex == 31) { // UVR31
        P092_DataSettings.DataBytes   = 8;
        P092_DataSettings.DeviceByte0 = 0x30;
      } else {                     // UVR42
        P092_DataSettings.DataBytes   = 10;
        P092_DataSettings.DeviceByte0 = 0x10;
      }
      P092_DataSettings.DLbus_MinPulseWidth       = P092_min_width_50;
      P092_DataSettings.DLbus_MaxPulseWidth       = P092_max_width_50;
      P092_DataSettings.DLbus_MinDoublePulseWidth = P092_double_min_width_50;
      P092_DataSettings.DLbus_MaxDoublePulseWidth = P092_double_max_width_50;

      P092_DataSettings.DeviceByte1    = 0;
      iDeviceBytes                     = 1;
      P092_DataSettings.MaxExtSensors  = 0;
      P092_DataSettings.SpeedBytes     = 0;
      P092_DataSettings.AnalogBytes    = 0;
      P092_DataSettings.MaxAnalogOuts  = 0;
      P092_DataSettings.MaxHeatMeters  = 0;
      P092_DataSettings.CurrentHmBytes = 0;
      P092_DataSettings.MWhBytes       = 0;
      P092_DataSettings.IdxCRC         = 0;
      break;
    case 1611: // UVR1611
      P092_DataSettings.DataBytes = 64;

      P092_DataSettings.DeviceByte0    = 0x80;
      P092_DataSettings.DeviceByte1    = 0x7F;
      iDontCareBytes                   = 1;
      iTimeStampBytes                  = 5;
      P092_DataSettings.MaxSensors     = 16;
      P092_DataSettings.MaxExtSensors  = 0;
      P092_DataSettings.OutputBytes    = 2;
      P092_DataSettings.SpeedBytes     = 4;
      P092_DataSettings.AnalogBytes    = 0;
      P092_DataSettings.MaxAnalogOuts  = 0;
      P092_DataSettings.MaxHeatMeters  = 2;
      P092_DataSettings.CurrentHmBytes = 4;
      P092_DataSettings.IdxCRC         = P092_DataSettings.DataBytes - 1;

      break;
    case 6132: // UVR 61-3 (up to V8.2)
      P092_DataSettings.DataBytes = 35;

      P092_DataSettings.DeviceByte0   = 0x90;
      P092_DataSettings.DeviceByte1   = 0x6F;
      iDontCareBytes                  = 1;
      iTimeStampBytes                 = 5;
      P092_DataSettings.MaxSensors    = 6;
      P092_DataSettings.MaxExtSensors = 0;
      P092_DataSettings.MaxAnalogOuts = 1;
      P092_DataSettings.VolumeBytes   = 2;
      P092_DataSettings.MWhBytes      = 4;
      P092_DataSettings.IdxCRC        = P092_DataSettings.DataBytes - 1;

      break;
    case 6133: // UVR 61-3 (from V8.3)
      P092_DataSettings.DataBytes = 62;

      P092_DataSettings.DeviceByte0   = 0x90;
      P092_DataSettings.DeviceByte1   = 0x9F;
      iDontCareBytes                  = 1;
      iTimeStampBytes                 = 5;
      P092_DataSettings.MaxSensors    = 6;
      P092_DataSettings.MaxExtSensors = 9;
      P092_DataSettings.MaxAnalogOuts = 2;
      P092_DataSettings.MaxHeatMeters = 3;
      P092_DataSettings.IdxCRC        = P092_DataSettings.DataBytes - 1;

      break;
  }
  P092_DataSettings.IdxSensor     = iDeviceBytes + iDontCareBytes + iTimeStampBytes;
  P092_DataSettings.IdxExtSensor  = P092_DataSettings.IdxSensor + 2 * P092_DataSettings.MaxSensors;
  P092_DataSettings.IdxOutput     = P092_DataSettings.IdxExtSensor + 2 * P092_DataSettings.MaxExtSensors;
  P092_DataSettings.IdxDrehzahl   = P092_DataSettings.IdxOutput + P092_DataSettings.OutputBytes;
  P092_DataSettings.IdxAnalog     = P092_DataSettings.IdxDrehzahl + P092_DataSettings.SpeedBytes;
  P092_DataSettings.IdxHmRegister = P092_DataSettings.IdxAnalog + (P092_DataSettings.AnalogBytes * P092_DataSettings.MaxAnalogOuts);
  P092_DataSettings.IdxVolume     = P092_DataSettings.IdxHmRegister + 1;
  P092_DataSettings.IdxHeatMeter1 = P092_DataSettings.IdxVolume + P092_DataSettings.VolumeBytes;
  P092_DataSettings.IdxkWh1       = P092_DataSettings.IdxHeatMeter1 + P092_DataSettings.CurrentHmBytes;
  P092_DataSettings.IdxMWh1       = P092_DataSettings.IdxkWh1 + 2;
  P092_DataSettings.IdxHeatMeter2 = P092_DataSettings.IdxMWh1 + P092_DataSettings.MWhBytes;
  P092_DataSettings.IdxkWh2       = P092_DataSettings.IdxHeatMeter2 + P092_DataSettings.CurrentHmBytes;
  P092_DataSettings.IdxMWh2       = P092_DataSettings.IdxkWh2 + 2;
  P092_DataSettings.IdxHeatMeter3 = P092_DataSettings.IdxMWh2 + P092_DataSettings.MWhBytes;
  P092_DataSettings.IdxkWh3       = P092_DataSettings.IdxHeatMeter3 + P092_DataSettings.CurrentHmBytes;
  P092_DataSettings.IdxMWh3       = P092_DataSettings.IdxkWh3 + 2;
}

/****************\
   DLBus P092_receiving
\****************/
void P092_data_struct::Plugin_092_StartReceiving(taskIndex_t taskindex) {
  DLbus_Data->ISR_Receiving   = false;
  DLbus_Data->DeviceBytes[0]  = P092_DataSettings.DeviceByte0;
  DLbus_Data->DeviceBytes[1]  = P092_DataSettings.DeviceByte1;
  DLbus_Data->ISR_PulseNumber =
    (((P092_DataSettings.DataBytes + DLbus_AdditionalRecBytes) * (DLbus_StartBits + 8 +  DLbus_StopBits) + DLBus_SyncBits) *
     DLBus_BitChangeFactor) + DLBus_ReserveBytes;
  DLbus_Data->ISR_MinPulseWidth       = P092_DataSettings.DLbus_MinPulseWidth;
  DLbus_Data->ISR_MaxPulseWidth       = P092_DataSettings.DLbus_MaxPulseWidth;
  DLbus_Data->ISR_MinDoublePulseWidth = P092_DataSettings.DLbus_MinDoublePulseWidth;
  DLbus_Data->ISR_MaxDoublePulseWidth = P092_DataSettings.DLbus_MaxDoublePulseWidth;
  DLbus_Data->StartReceiving();
  uint32_t start = millis();

# ifndef P092_LIMIT_BUILD_SIZE

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO, concat(F("P092_receiving ... TaskIndex:"), taskindex));
  }
# endif // ifndef P092_LIMIT_BUILD_SIZE

  while ((timePassedSince(start) < 100) && (DLbus_Data->ISR_PulseCount == 0)) {
    // wait for first pulse received (timeout 100ms)
    delay(0);
  }

  if (DLbus_Data->ISR_PulseCount == 0) {
    // nothing received
    DLbus_Data->ISR_Receiving = false;
    DLbus_Data->IsNoData      = true; // stop receiving until next PLUGIN_092_READ
    addLog(LOG_LEVEL_ERROR, F("## StartReceiving: Error: Nothing received! No DL bus connected!"));
  }
}

/****************\
   DLBus get data
\****************/
boolean P092_data_struct::P092_GetData(int OptionIdx, int CurIdx, sP092_ReadData *ReadData) {
# ifndef P092_LIMIT_BUILD_SIZE
  String log;
# endif // ifndef P092_LIMIT_BUILD_SIZE
  boolean result = false;

  switch (OptionIdx) {
    case 1: // F("Sensor")
# ifndef P092_LIMIT_BUILD_SIZE
      log = F("Get Sensor");
# endif // ifndef P092_LIMIT_BUILD_SIZE

      if (CurIdx > P092_DataSettings.MaxSensors) {
        result = false;
        break;
      }
      ReadData->Idx = P092_DataSettings.IdxSensor;
      result        = P092_fetch_sensor(CurIdx, ReadData);
      break;
    case 2: // F("Sensor")
# ifndef P092_LIMIT_BUILD_SIZE
      log = F("Get ExtSensor");
# endif // ifndef P092_LIMIT_BUILD_SIZE

      if (CurIdx > P092_DataSettings.MaxExtSensors) {
        result = false;
        break;
      }
      ReadData->Idx = P092_DataSettings.IdxExtSensor;
      result        = P092_fetch_sensor(CurIdx, ReadData);
      break;
    case 3: // F("Digital output")
# ifndef P092_LIMIT_BUILD_SIZE
      log = F("Get DigitalOutput");
# endif // ifndef P092_LIMIT_BUILD_SIZE

      if (CurIdx > (8 * P092_DataSettings.OutputBytes)) {
        result = false;
        break;
      }
      result = P092_fetch_output(CurIdx, ReadData);
      break;
    case 4: // F("Speed step")
# ifndef P092_LIMIT_BUILD_SIZE
      log = F("Get SpeedStep");
# endif // ifndef P092_LIMIT_BUILD_SIZE

      if (CurIdx > P092_DataSettings.SpeedBytes) {
        result = false;
        break;
      }
      result = P092_fetch_speed(CurIdx, ReadData);
      break;
    case 5: // F("Analog output")
# ifndef P092_LIMIT_BUILD_SIZE
      log = F("Get AnalogOutput");
# endif // ifndef P092_LIMIT_BUILD_SIZE

      if (CurIdx > P092_DataSettings.AnalogBytes) {
        result = false;
        break;
      }
      result = P092_fetch_analog(CurIdx, ReadData);
      break;
    case 6: // F("Heat power (kW)")
# ifndef P092_LIMIT_BUILD_SIZE
      log = F("Get HeatPower");
# endif // ifndef P092_LIMIT_BUILD_SIZE

      if (CurIdx > P092_DataSettings.MaxHeatMeters) {
        result = false;
        break;
      }
      result = P092_fetch_heatpower(CurIdx, ReadData);
      break;
    case 7: // F("Heat meter (MWh)"
# ifndef P092_LIMIT_BUILD_SIZE
      log = F("Get HeatMeter");
# endif // ifndef P092_LIMIT_BUILD_SIZE

      if (CurIdx > P092_DataSettings.MaxHeatMeters) {
        result = false;
        break;
      }
      result = P092_fetch_heatmeter(CurIdx, ReadData);
      break;
  }

# ifndef P092_LIMIT_BUILD_SIZE

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    log += strformat(F("%d: "), CurIdx);

    if (result) {
      log += String(ReadData->value, 1);
    }
    else {
      log += F("nan");
    }
    addLogMove(LOG_LEVEL_INFO, log);
  }
# endif // ifndef P092_LIMIT_BUILD_SIZE
  return result;
}

boolean P092_data_struct::P092_fetch_sensor(int number, sP092_ReadData *ReadData) {
  float value = 0.0f;

  ReadData->mode = -1;
  number         = ReadData->Idx + (number - 1) * 2;
  int32_t sensorvalue = (DLbus_Data->ByteStream[number + 1] << 8) | DLbus_Data->ByteStream[number];

  if (sensorvalue == 0) {
    return false;
  }
  uint8_t sensortype = (sensorvalue & 0x7000) >> 12;

  if (!(sensorvalue & 0x8000)) { // sign positive
    sensorvalue &= 0xfff;

    // calculations for different sensor types
    switch (sensortype) {
      case DLbus_Sensor_DIGITAL:
        value = false;
        break;
      case DLbus_Sensor_TEMP:
        value = static_cast<float>(sensorvalue) * 0.1f;
        break;
      case DLbus_Sensor_RAYS:
        value = sensorvalue;
        break;
      case DLbus_Sensor_VOLUME_FLOW:
        value = sensorvalue * 4;
        break;
      case DLbus_Sensor_ROOM:
        ReadData->mode = (sensorvalue & 0x600) >> 9;
        value          = static_cast<float>(sensorvalue & 0x1ff) * 0.1f;
        break;
      default:
        return false;
    }
  }
  else { // sign negative
    sensorvalue |= 0xf000;

    // calculations for different sensor types
    switch (sensortype) {
      case DLbus_Sensor_DIGITAL:
        value = true;
        break;
      case DLbus_Sensor_TEMP:
        value = static_cast<float>(sensorvalue - 0x10000) * 0.1f;
        break;
      case DLbus_Sensor_RAYS:
        value = sensorvalue - 0x10000;
        break;
      case DLbus_Sensor_VOLUME_FLOW:
        value = (sensorvalue - 0x10000) * 4;
        break;
      case DLbus_Sensor_ROOM:
        ReadData->mode = (sensorvalue & 0x600) >> 9;
        value          = static_cast<float>((sensorvalue & 0x1ff) - 0x10000) * 0.1f;
        break;
      default:
        return false;
    }
  }
  ReadData->value = value;
  return true;
}

boolean P092_data_struct::P092_fetch_output(int number, sP092_ReadData *ReadData) {
  int32_t outputs;

  if (P092_DataSettings.OutputBytes > 1) {
    outputs = (DLbus_Data->ByteStream[P092_DataSettings.IdxOutput + 1] << 8) | DLbus_Data->ByteStream[P092_DataSettings.IdxOutput];
  }
  else {
    outputs = DLbus_Data->ByteStream[P092_DataSettings.IdxOutput];
  }

  if (outputs & (1 << (number - 1))) {
    ReadData->value = 1;
  }
  else {
    ReadData->value = 0;
  }
  return true;
}

boolean P092_data_struct::P092_fetch_speed(int number, sP092_ReadData *ReadData) {
  uint8_t speedbyte;

  if ((P092_DataSettings.IdxDrehzahl + (number - 1)) >= P092_DataSettings.IdxAnalog) {
    // wrong index for speed, overlapping next index (IdxAnalog)
    return false;
  }
  speedbyte = DLbus_Data->ByteStream[P092_DataSettings.IdxDrehzahl + (number - 1)];

  if (speedbyte & 0x80) {
    return false;
  }
  ReadData->value = (speedbyte & 0x1f);
  return true;
}

boolean P092_data_struct::P092_fetch_analog(int number, sP092_ReadData *ReadData) {
  uint8_t analogbyte;

  if ((P092_DataSettings.IdxAnalog + (number - 1)) >= P092_DataSettings.IdxHmRegister) {
    // wrong index for analog, overlapping next index (IdxHmRegister)
    return false;
  }
  analogbyte = DLbus_Data->ByteStream[P092_DataSettings.IdxAnalog + (number - 1)];

  if (analogbyte & 0x80) {
    return false;
  }
  ReadData->value = (analogbyte * 0.1);
  return true;
}

P092_data_struct::sDLbus_HMindex P092_data_struct::P092_CheckHmRegister(int number) {
  sDLbus_HMindex result;

  result.IndexIsValid = 0;

  switch (number) {
    case 1:

      if ((DLbus_Data->ByteStream[P092_DataSettings.IdxHmRegister] & 0x1) == 0) {
        return result;
      }
      result.power_index = P092_DataSettings.IdxHeatMeter1;
      result.kwh_index   = P092_DataSettings.IdxkWh1;
      result.mwh_index   = P092_DataSettings.IdxMWh1;
      break;
    case 2:

      if ((DLbus_Data->ByteStream[P092_DataSettings.IdxHmRegister] & 0x2) == 0) {
        return result;
      }
      result.power_index = P092_DataSettings.IdxHeatMeter2;
      result.kwh_index   = P092_DataSettings.IdxkWh2;
      result.mwh_index   = P092_DataSettings.IdxMWh2;
      break;
    case 3:

      if ((DLbus_Data->ByteStream[P092_DataSettings.IdxHmRegister] & 0x4) == 0) {
        return result;
      }
      result.power_index = P092_DataSettings.IdxHeatMeter3;
      result.kwh_index   = P092_DataSettings.IdxkWh3;
      result.mwh_index   = P092_DataSettings.IdxMWh3;
      break;
    default:
      return result;
  }
  result.IndexIsValid = 1;
  return result;
}

boolean P092_data_struct::P092_fetch_heatpower(int number, sP092_ReadData *ReadData) {
  // current power
  int32_t high;
  sDLbus_HMindex HMindex = P092_CheckHmRegister(number);

  if (HMindex.IndexIsValid == 0) {
    return false;
  }
  uint8_t b1 = DLbus_Data->ByteStream[HMindex.power_index];
  uint8_t b2 = DLbus_Data->ByteStream[HMindex.power_index + 1];

  if (P092_DataSettings.CurrentHmBytes > 2) {
    uint8_t b3 = DLbus_Data->ByteStream[HMindex.power_index + 2];
    uint8_t b4 = DLbus_Data->ByteStream[HMindex.power_index + 3];
    high = 0x10000 * b4 + 0x100 * b3 + b2;
    int low = (b1 * 10) / 0x100;

    if (!(b4 & 0x80)) { // sign positive
      ReadData->value = static_cast<float>(10 * high + low) / 100.0f;
    }
    else {              // sign negative
      ReadData->value = static_cast<float>(10 * (high - 0x10000) - low) / 100.0f;
    }
  }
  else {
    high = (b2 << 8) | b1;

    if ((b2 & 0x80) == 0) { // sign positive
      ReadData->value = static_cast<float>(high) / 10.0f;
    }
    else {                  // sign negative
      ReadData->value = static_cast<float>(high - 0x10000) / 10.0f;
    }
  }
  return true;
}

boolean P092_data_struct::P092_fetch_heatmeter(int number, sP092_ReadData *ReadData) {
  // heat meter
  int32_t heat_meter;
  float   heat_meter_mwh;

  sDLbus_HMindex HMindex = P092_CheckHmRegister(number);

  if (HMindex.IndexIsValid == 0) {
    return false;
  }
  heat_meter     = (DLbus_Data->ByteStream[HMindex.kwh_index + 1] << 8) | DLbus_Data->ByteStream[HMindex.kwh_index];
  heat_meter_mwh = (heat_meter * 0.1f) / 1000.0f; // in MWh

  if (heat_meter_mwh > 1.0f) {
    // in kWh
    heat_meter      = heat_meter_mwh;
    heat_meter_mwh -= heat_meter;
  }

  // MWh
  heat_meter      = (DLbus_Data->ByteStream[HMindex.mwh_index + 1] << 8) | DLbus_Data->ByteStream[HMindex.mwh_index];
  ReadData->value = heat_meter_mwh + heat_meter;
  return true;
}

#endif // ifdef USES_P092

#include "../PluginStructs/P109_data_struct.h"

#ifdef USES_P109

# include "../src/src/Globals/ESPEasy_time.h"

/**************************************************************************
 * Constructor
 *************************************************************************/
P109_data_struct::P109_data_struct():
  _display(nullptr), _taskIndex(0), _varIndex(0)
{
  for (int i = 0; i < P109_Nlines; ++i) {
    ZERO_FILL(_deviceTemplate[i]);
  }
}

/**************************************************************************
 * Destructor
 *************************************************************************/
P109_data_struct::~P109_data_struct() {
  if (nullptr != _display) {
    _display->end();
  }
  delete _display;
  _display = nullptr;
}

/**************************************************************************
 * Load ExtraTaskSettings data
 *************************************************************************/
bool P109_data_struct::plugin_webform_load(struct EventStruct *event) {
  bool success = true;

  LoadCustomTaskSettings(event->TaskIndex, reinterpret_cast<uint8_t *>(&_deviceTemplate), sizeof(_deviceTemplate));

  for (int varNr = 0; varNr < P109_Nlines; ++varNr) {
    addFormTextBox(concat(varNr == 0 ? F("Temperature source ") : F("Line "), varNr + 1),
                   getPluginCustomArgName(varNr + 1),
                   _deviceTemplate[varNr],
                   P109_Nchars);
  }

  return success;
}

/**************************************************************************
 * Save ExtraTaskSetting data
 *************************************************************************/
bool P109_data_struct::plugin_webform_save(struct EventStruct *event) {
  bool success = false;

  for (uint8_t varNr = 0; varNr < P109_Nlines; ++varNr) {
    strncpy(_deviceTemplate[varNr],
            web_server.arg(getPluginCustomArgName(varNr + 1)).c_str(),
            sizeof(_deviceTemplate[varNr]) - 1);
    _deviceTemplate[varNr][sizeof(_deviceTemplate[varNr]) - 1] = 0;
  }

  String error = SaveCustomTaskSettings(event->TaskIndex, reinterpret_cast<uint8_t *>(&_deviceTemplate), sizeof(_deviceTemplate));

  if (!error.isEmpty()) {
    addHtmlError(error);
  }

  return success;
}

/**************************************************************************
 * Initialize plugin
 *************************************************************************/
bool P109_data_struct::plugin_init(struct EventStruct *event) {
  _lastWiFiState = P109_WIFI_STATE_UNSET;

  // Load the custom settings from flash
  LoadCustomTaskSettings(event->TaskIndex, reinterpret_cast<uint8_t *>(&_deviceTemplate), sizeof(_deviceTemplate));

  //      Init the display and turn it on
  if (_display) {
    _display->end();
    delete _display;
    _display = nullptr;
  }
  _taskIndex       = event->TaskIndex;
  _varIndex        = event->BaseVarIndex;
  _relaypin        = P109_CONFIG_RELAYPIN;
  _relayInverted   = P109_GET_RELAY_INVERT;
  _setpointTimeout = P109_CONFIG_SETPOINT_DELAY - P109_SETPOINT_OFFSET;

  #if FEATURE_I2C_MULTIPLE
  const uint8_t i2cBus = Settings.getI2CInterface(event->TaskIndex);
  #else
  const uint8_t i2cBus = 0;
  #endif // if FEATURE_I2C_MULTIPLE
  if (P109_CONFIG_DISPLAYTYPE == 1) {
    _display = new (std::nothrow) SSD1306Wire(P109_CONFIG_I2CADDRESS, Settings.getI2CSdaPin(i2cBus), Settings.getI2CSclPin(i2cBus));
  } else {
    _display = new (std::nothrow) SH1106Wire(P109_CONFIG_I2CADDRESS, Settings.getI2CSdaPin(i2cBus), Settings.getI2CSclPin(i2cBus));
  }

  if (nullptr == _display) {
    return false; // Premature exit
  }

  if (P109_GET_TASKNAME_IN_TITLE == 1) {
    _title = getTaskDeviceName(event->TaskIndex);
  } else {
    _title = Settings.getHostname();
  }
  _alternateTitle = !P109_GET_ALTERNATE_HEADER;

  _display->init(); // call to local override of init function
  _display->displayOn();

  OLedSetContrast(_display, P109_CONFIG_CONTRAST);

  # ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO,
               strformat(F("Thermo : Btn L:%d, R:%d, M:%d"),
                         CONFIG_PIN1, CONFIG_PIN2, CONFIG_PIN3));
  }
  # endif // ifndef BUILD_NO_DEBUG

  for (uint8_t pin = 0; pin < 3; ++pin) {
    if (validGpio(PIN(pin))) {
      pinMode(PIN(pin), INPUT_PULLUP);
    }
  }

  _prev_temp = P109_TEMP_STATE_UNSET;

  String fileName = strformat(
    F("thermo%d.dat"),
    _taskIndex + 1); // Settings per task index
  fs::File f = tryOpenFile(fileName, String('r'));

  if (!f) {          // Not found? Then open previous default filename
    fileName = F("thermo.dat");
    f        = tryOpenFile(fileName, String('r'));
  }

  if (f) {
    f.read(reinterpret_cast<uint8_t *>(UserVar.getRawTaskValues_Data(event->TaskIndex)), 16);
    f.close();
  }
  _save_setpoint = UserVar[event->BaseVarIndex];
  _prev_setpoint = UserVar[event->BaseVarIndex];

  if (UserVar[event->BaseVarIndex] < 1) {
    UserVar.setFloat(event->TaskIndex, 0, P109_SETPOINT_STATE_INITIAL); // setpoint
  }
  UserVar.setFloat(event->TaskIndex, 1, 0.5f);                          // Unitialize relay state
  UserVar.setFloat(event->TaskIndex, 2, P109_MODE_STATE_INITIAL);       // mode (X=0,A=1,M=2)
  UserVar.setFloat(event->TaskIndex, 3, 0);                             // Reset

  # ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO, strformat(
                 F("Thermo : Starting status S:%s, R:%d"),
                 formatUserVarNoCheck(event, 0).c_str(),
                 static_cast<int>(UserVar[event->BaseVarIndex + 1])));
  }
  # endif // ifndef BUILD_NO_DEBUG

  _changed    = 0; // No need to save, settings have just been restored
  _buttons[0] = 0;
  _buttons[1] = 0;
  _buttons[2] = 0;

  // flip screen if required
  if (P109_CONFIG_ROTATION == 2) { _display->flipScreenVertically(); }

  // Display the device name, logo, time and wifi
  display_header();
  display_page();
  _display->display();
  _initialized = true;

  check_auto_mode(event);

  return _initialized;
}

/**************************************************************************
 * De-initialize, pre-destructor
 *************************************************************************/
bool P109_data_struct::plugin_exit(struct EventStruct *event) {
  if (_saveneeded == 1) { // Can't wait for timeout any longer
    saveThermoSettings(event);
  }
  _initialized = false;

  return true;
}

/**************************************************************************
 * Check button state
 *************************************************************************/
bool P109_data_struct::plugin_ten_per_second(struct EventStruct *event) {
  if (_initialized) {
    uint32_t current_time;

    if (validGpio(CONFIG_PIN1) && !digitalRead(CONFIG_PIN1)) {
      current_time = millis();

      if (_buttons[0] + P109_BUTTON_DEBOUNCE_TIME_MS < current_time) {
        _buttons[0] = current_time;
        actionLeft(event);
      }
    }

    if (validGpio(CONFIG_PIN2) && !digitalRead(CONFIG_PIN2)) {
      current_time = millis();

      if (_buttons[1] + P109_BUTTON_DEBOUNCE_TIME_MS < current_time) {
        _buttons[1] = current_time;
        actionRight(event);
      }
    }

    if (validGpio(CONFIG_PIN3) && !digitalRead(CONFIG_PIN3)) {
      current_time = millis();

      if (_buttons[2] + P109_BUTTON_DEBOUNCE_TIME_MS < current_time) {
        _buttons[2] = current_time;
        actionMode(event);
      }
    }
  }

  return _initialized;
}

/**************************************************************************
 * Update display
 *************************************************************************/
bool P109_data_struct::plugin_once_a_second(struct EventStruct *event) {
  if (_initialized) {
    if (_display && display_wifibars()) {
      // WiFi symbol was updated.
      _display->display();
    }

    if (UserVar.getFloat(event->TaskIndex, 2) == 2) { // manual timeout
      if (UserVar[event->BaseVarIndex + 3] > 0) {
        UserVar.setFloat(event->TaskIndex, 3, UserVar[event->BaseVarIndex + 3] - 1);
        display_timeout();
      } else {
        UserVar.setFloat(event->TaskIndex, 3, 0);
        setMode(F("a"), F("0")); // heater to auto
        display_setpoint_temp(1);
        check_auto_mode(event);  // Check now to avoid a double save
      }
    }

    if (_setpointDelay > 0) {
      _setpointDelay--;

      if ((_setpointDelay == 0) && !_last_heater.isEmpty()) {
        setHeater(_last_heater); // Last requested status, applied after delay
      }
    } else {
      check_auto_mode(event);
    }

    if (_changed == 1) {
      sendData(event);
      bool isSpDif = !essentiallyEqual(UserVar[event->BaseVarIndex], _save_setpoint);

      // Don't save when in manual mode
      _saveneeded     = (isSpDif && UserVar[event->BaseVarIndex + 2] != 2) ? 1 : 0;
      _changed        = 0;
      _lastchangetime = millis(); // Let's wait before actually saving
    }

    if ((_saveneeded == 1) && ((_lastchangetime + P109_DELAY_BETWEEN_SAVE) < millis())) {
      _saveneeded    = 0;
      _save_setpoint = UserVar[event->BaseVarIndex]; // Save this for next save-check

      saveThermoSettings(event);
    }
  }
  return _initialized;
}

/**************************************************************************
 * Save thermo settings unconditionally
 *************************************************************************/
void P109_data_struct::saveThermoSettings(struct EventStruct *event) {
  const String fileName(strformat(F("thermo%d.dat"), static_cast<int>(event->TaskIndex + 1)));
  fs::File     f = tryOpenFile(fileName, F("w"));

  if (f) {
    f.write(reinterpret_cast<const uint8_t *>(UserVar.getRawTaskValues_Data(event->TaskIndex)), 16);
    f.close();
    flashCount();
  }
  # ifndef BUILD_NO_DEBUG
  addLogMove(LOG_LEVEL_INFO, strformat(
               F("Thermo : (delayed) Save UserVars to %s"), fileName.c_str()));
  # endif // ifndef BUILD_NO_DEBUG
}

/**************************************************************************
 * Handle data
 *************************************************************************/
bool P109_data_struct::plugin_read(struct EventStruct *event) {
  if (_initialized) {
    //      Update display
    display_header();

    check_auto_mode(event);

    display_current_temp();
    display_timeout();

    _display->display();
  }
  return _initialized;
}

void P109_data_struct::check_auto_mode(struct EventStruct *event) {
  if (UserVar.getFloat(event->TaskIndex, 2) == 1) {
    String atempstr2 = _deviceTemplate[0];
    String atempstr  = parseTemplate(atempstr2);

    if (!atempstr.isEmpty() &&
        (_prev_temp != P109_TEMP_STATE_UNSET)) { // do not switch until the first temperature data arrives
      float atemp = atempstr.toFloat();

      if (atemp != 0.0f) {
        if ((UserVar[event->BaseVarIndex] > atemp) &&
            (UserVar[event->BaseVarIndex + 1] < 1.0f)) {
          if (_prev_heating != 1) {
            _changed = 1;
          }
          setHeater(F("1"));
        } else if (((atemp - P109_CONFIG_HYSTERESIS) >= UserVar[event->BaseVarIndex]) &&
                   (UserVar[event->BaseVarIndex + 1] > 0.0f)) {
          if (_prev_heating != 0) {
            _changed = 1;
          }
          setHeater(F("0"));
        } else {
          display_heat();
        }
      }
    } else {
      display_heat();
    }
  }
}

/**************************************************************************
 * Handle commands
 *************************************************************************/
bool P109_data_struct::plugin_write(struct EventStruct *event,
                                    String            & string) {
  bool   success = false;
  String command = parseString(string, 1);

  if (_initialized) {
    String subcommand = parseString(string, 2);

    if (equals(command, F("oledframedcmd"))) {
      success = true;

      if (equals(subcommand, F("off"))) {
        OLedSetContrast(_display, OLED_CONTRAST_OFF);
      }
      else if (equals(subcommand, F("on"))) {
        _display->displayOn();
      }
      else if (equals(subcommand, F("low"))) {
        OLedSetContrast(_display, OLED_CONTRAST_LOW);
      }
      else if (equals(subcommand, F("med"))) {
        OLedSetContrast(_display, OLED_CONTRAST_MED);
      }
      else if (equals(subcommand, F("high"))) {
        OLedSetContrast(_display, OLED_CONTRAST_HIGH);
      } else {
        success = false;
      }
    }

    if (!success && equals(command, F("thermo"))) {
      success = true;
      String par3 = parseString(string, 3);

      if (equals(subcommand, F("setpoint"))) {
        setSetpoint(par3);
        check_auto_mode(event);
      }
      else if (equals(subcommand, F("down"))) {    // Emulate Left button action
        actionLeft(event);
      }
      else if (equals(subcommand, F("up"))) {      // Emulate Right button action
        actionRight(event);
      }
      else if (equals(subcommand, F("modebtn"))) { // Emulate Mode button action
        actionMode(event);
      }
      else if (equals(subcommand, F("heating"))) {
        int prev = UserVar[event->BaseVarIndex + 1];
        setHeater(par3);

        if (prev != UserVar[event->BaseVarIndex + 1]) {
          _changed = 1; // Only if actually changed
        }
      }
      else if (equals(subcommand, F("mode"))) {
        setMode(par3, parseString(string, 4));
      } else {
        success = false;
      }
    }

    if (success) {
      SendStatus(event, F("\nOk")); // FIXME: Will cause duplicate Ok message, why?
    }
  }
  return success;
}

/*******************
 * Private methods
 ******************/
/**************************************************************************
 * Left button action
 *************************************************************************/
void P109_data_struct::actionLeft(struct EventStruct *event) {
  switch (int(UserVar[event->BaseVarIndex + 2])) {
    case 0: { // off mode, no func
      break;
    }
    case 1: { // auto mode, setpoint dec
      setSetpoint(F("-0.5"));
      break;
    }
    case 2: { // manual on mode, timer dec
      UserVar.setFloat(event->TaskIndex, 3, UserVar[event->BaseVarIndex + 3] - P109_BUTTON_DEBOUNCE_TIME_MS);

      if (UserVar[event->BaseVarIndex + 3] < 0) {
        UserVar.setFloat(event->TaskIndex, 3, 5400);
      }
      _prev_timeout = P109_TIMEOUT_STATE_UNSET;
      break;
    }
  }
}

/**************************************************************************
 * Right button action
 *************************************************************************/
void P109_data_struct::actionRight(struct EventStruct *event) {
  switch (int(UserVar[event->BaseVarIndex + 2])) {
    case 0: { // off mode, no func
      break;
    }
    case 1: { // auto mode, setpoint inc
      setSetpoint(F("+0.5"));
      break;
    }
    case 2: { // manual on mode, timer dec
      UserVar.setFloat(event->TaskIndex, 3, UserVar[event->BaseVarIndex + 3] + P109_BUTTON_DEBOUNCE_TIME_MS);

      if (UserVar[event->BaseVarIndex + 3] > 5400) {
        UserVar.setFloat(event->TaskIndex, 3, 60);
      }
      _prev_timeout = P109_TIMEOUT_STATE_UNSET;
      break;
    }
  }
}

/**************************************************************************
 * Rotate mode
 *************************************************************************/
void P109_data_struct::actionMode(struct EventStruct *event) {
  switch (int(UserVar[event->BaseVarIndex + 2])) {
    case 0: { // off mode, next
      setMode(F("a"), F("0"));
      break;
    }
    case 1: { // auto mode, next
      setMode(F("m"), F("5"));
      break;
    }
    case 2: { // manual on mode, next
      setMode(F("x"), F("0"));
      break;
    }
  }
}

/**
 * Display header, alternating between WiFi AP SSID and Sysname
 */
void P109_data_struct::display_header() {
  if (_alternateTitle && _showWiFiName && WiFiEventData.WiFiServicesInitialized()) {
    // String newString = ;
    display_title(WiFi.SSID());
  } else {
    display_title(_title);
  }
  _showWiFiName = !_showWiFiName;

  // Display time and wifibars both clear area below, so paint them after the title.
  display_time();
  display_wifibars();
}

/**
 * Display the current time
 */
void P109_data_struct::display_time() {
  String newString = node_time.getTimeString(':', false); // Avoid the overhead of calling parseTemplate()

  _display->setTextAlignment(TEXT_ALIGN_LEFT);
  displayBigText(0, 0, 28, 13, getDialog_plain_12(), 0, 0, newString.substring(0, 5));
}

/**
 * Display the title centered
 */
void P109_data_struct::display_title(const String& title) {
  _display->setTextAlignment(TEXT_ALIGN_CENTER);
  displayBigText(0, 0, 128, 15, getDialog_plain_12(), 64, 0, title);
}

/**
 * Draw Signal Strength Bars, return true when there was an update.
 */
bool P109_data_struct::display_wifibars() {
  const bool connected    = WiFiEventData.WiFiServicesInitialized();
  const int  nbars_filled = (WiFi.RSSI() + 100) / 8;
  const int  newState     = connected ? nbars_filled : P109_WIFI_STATE_UNSET;

  if (newState == _lastWiFiState) {
    return false; // nothing to do.
  }
  const int x         = 105;
  const int y         = 0;
  int size_x          = 15;
  const int size_y    = 10;
  const int nbars     = 5;
  const int16_t width = (size_x / nbars);

  size_x = width * nbars - 1; // Correct for round errors.

  //  x,y are the x,y locations
  //  sizex,sizey are the sizes (should be a multiple of the number of bars)
  //  nbars is the number of bars and nbars_filled is the number of filled bars.

  //  We leave a 1 pixel gap between bars
  _display->setColor(BLACK);
  _display->fillRect(x, y, size_x, size_y);
  _display->setColor(WHITE);

  if (WiFiEventData.WiFiServicesInitialized()) {
    for (uint8_t ibar = 0; ibar < nbars; ++ibar) {
      const int16_t height = size_y * (ibar + 1) / nbars;
      const int16_t xpos   = x + ibar * width;
      const int16_t ypos   = y + size_y - height;

      if (ibar <= nbars_filled) {
        // Fill complete bar
        _display->fillRect(xpos, ypos, width - 1, height);
      } else {
        // Only draw top and bottom.
        _display->fillRect(xpos, ypos,           width - 1, 1);
        _display->fillRect(xpos, y + size_y - 1, width - 1, 1);
      }
    }
  } else {
    // Draw a not connected sign.
  }
  return true;
}

/**
 * Display current temperature
 */
void P109_data_struct::display_current_temp() {
  String tmpString = _deviceTemplate[0];
  String atempstr  = parseTemplate(tmpString);

  atempstr.trim();

  if (atempstr.length() > 0) {
    float atemp = atempstr.toFloat();
    atemp = (roundf(atemp * 10.0f)) / 10.0f;

    if (!essentiallyEqual(_prev_temp, atemp)) {
      tmpString = toString(atemp, 1);
      displayBigText(3, 19, 47, 25, getArialMT_Plain_24(), 3, 19, tmpString.substring(0, 5));

      _prev_temp = atemp;
    }
  }
}

/**
 * Display the Setpoint temperature
 */
void P109_data_struct::display_setpoint_temp(const uint8_t& force) {
  if (UserVar.getFloat(_taskIndex,  2) == 1) {
    const float stemp = (roundf(UserVar[_varIndex] * 10.0f)) / 10.0f;
    const bool  isDif = !essentiallyEqual(_prev_setpoint, stemp);

    if (isDif || (force == 1)) {
      String tmpString = toString(stemp, 1);
      displayBigText(86, 35, 41, 21, getDialog_plain_18(), 86, 35, tmpString.substring(0, 5));

      _prev_setpoint = stemp;

      if (isDif) {
        _changed       = 1;
        _setpointDelay = _setpointTimeout; // Start delay
      }
    }
  }
}

/**
 * Display the remaining timeout, if any is set
 */
void P109_data_struct::display_timeout() {
  if (UserVar.getFloat(_taskIndex,  2) == 2) {
    if (_prev_timeout >= (UserVar.getFloat(_taskIndex,  3) + 60.0f)) {
      const String thour = minutesToHourColonMinute(static_cast<int>(UserVar.getFloat(_taskIndex,  3) / 60.0f));
      displayBigText(86, 35, 41, 21, getDialog_plain_18(), 89, 35, thour.substring(1, 5));

      _prev_timeout = UserVar.getFloat(_taskIndex,  3);
    }
  }
}

/**
 * Display the current mode
 */
void P109_data_struct::display_mode() {
  if (_prev_mode != UserVar.getFloat(_taskIndex,  2)) {
    const String   tmpString = F("XAM");
    const uint16_t xamIdx    = min(static_cast<int>(UserVar.getFloat(_taskIndex,  2)), 2);

    displayBigText(61, 49, 12, 17, getArialMT_Plain_16(), 61, 49, tmpString.substring(xamIdx, xamIdx + 1));

    _prev_mode = UserVar.getFloat(_taskIndex,  2);
  }
}

void P109_data_struct::displayBigText(int16_t       x1,
                                      int16_t       y1,
                                      int16_t       w1,
                                      int16_t       h1,
                                      const char   *font,
                                      int16_t       x2,
                                      int16_t       y2,
                                      const String& text) {
  _display->setColor(BLACK);
  _display->fillRect(x1, y1, w1, h1);
  _display->setColor(WHITE);
  _display->setFont(font);
  _display->drawString(x2, y2, text);
}

/**
 * Display the Heater status (flame)
 */
void P109_data_struct::display_heat() {
  if (_prev_heating != UserVar.getFloat(_taskIndex,  1)) {
    _display->setColor(BLACK);
    _display->fillRect(54, 19, 24, 27);
    _display->setColor(WHITE);

    if (UserVar.getFloat(_taskIndex,  1) == 1) {
      _display->drawXbm(54, 19, 24, 27, flameimg);
    }
    _prev_heating = UserVar.getFloat(_taskIndex,  1);
  }
}

/**
 * Setup the initial page, draw lines etc.
 */
void P109_data_struct::display_page() {
  // init with full clear

  String tstr      = F("{D}C");
  String newString = parseTemplate(tstr);

  _display->setTextAlignment(TEXT_ALIGN_LEFT);
  displayBigText(0, 15, 128, 49, getDialog_plain_12(), 18, 46, newString.substring(0, 5));

  display_heat();

  _display->drawHorizontalLine(0, 15, 128);
  _display->drawVerticalLine(52, 14, 49);

  _display->drawCircle(109, 47, 26);
  _display->drawHorizontalLine(78, 47, 8);
  _display->drawVerticalLine(109, 19, 10);

  display_mode();
  display_setpoint_temp(1);
  display_current_temp();
}

/**
 * Change the setpoint value, and update the display
 */
void P109_data_struct::setSetpoint(const String& sptemp) {
  float stemp = (roundf(UserVar[_varIndex] * 10.0f)) / 10.0f;

  if ((sptemp.charAt(0) == '+') || (sptemp.charAt(0) == 'p'))  {
    stemp = stemp + sptemp.substring(1).toFloat();
  } else if ((sptemp.charAt(0) == '-') || (sptemp.charAt(0) == 'm')) {
    stemp = stemp - sptemp.substring(1).toFloat();
  } else {
    stemp = sptemp.toFloat();
  }
  UserVar.setFloat(_taskIndex, 0, stemp);
  display_setpoint_temp();
}

/**
 * Set the heater relay state
 */
void P109_data_struct::setHeatRelay(const uint8_t& state) {
  if (validGpio(_relaypin)) {
    # ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO,
                 strformat(F("Thermo : Set Relay%d=%d"),
                           _relaypin,
                           _relayInverted ? !state : state));
    }
    # endif // ifndef BUILD_NO_DEBUG

    pinMode(_relaypin, OUTPUT);
    digitalWrite(_relaypin, _relayInverted ? !state : state);
  }
}

/**
 * Change the heater state and update the display
 */
void P109_data_struct::setHeater(const String& heater) {
  if (_setpointDelay == 0) {
    if ((heater.charAt(0) == '1') || (equals(heater, F("on"))) ||
        ((heater.length() == 0) && (UserVar.getFloat(_taskIndex, 1) == 0))) {
      UserVar.setFloat(_taskIndex, 1, 1);
      setHeatRelay(HIGH);
    } else {
      UserVar.setFloat(_taskIndex, 1, 0);
      setHeatRelay(LOW);
    }
    display_heat();
  }
  _last_heater = heater;
}

/**
 * Set the mode and update the display
 */
void P109_data_struct::setMode(const String& amode,
                               const String& atimeout) {
  UserVar.setFloat(_taskIndex, 3, 0.0f); // Reset timeout

  if ((amode[0] == '0') || (amode[0] == 'x')) {
    UserVar.setFloat(_taskIndex, 2, 0);
    setHeater(F("0"));
    _display->setColor(BLACK);
    _display->fillRect(86, 35, 41, 21);
    _prev_setpoint = P109_SETPOINT_STATE_UNSET;
  } else if ((amode[0] == '1') || (amode[0] == 'a')) {
    UserVar.setFloat(_taskIndex, 2, 1);
    display_setpoint_temp(1);
  } else if ((amode[0] == '2') || (amode[0] == 'm')) {
    UserVar.setFloat(_taskIndex, 2, 2);
    UserVar.setFloat(_taskIndex, 3, (atimeout.toFloat() * 60.0f));
    _prev_timeout = P109_TIMEOUT_STATE_UNSET;
    display_timeout();
    setHeater(F("1"));
  } else {
    UserVar.setFloat(_taskIndex, 2, 0);
  }

  // _changed = 1;
  display_mode();
}

#endif // ifdef USES_P109

#include "../PluginStructs/P062_data_struct.h"

// Needed also here for PlatformIO's library finder as the .h file
// is in a directory which is excluded in the src_filter
#ifdef USES_P062

# include <Adafruit_MPR121.h>
# include "../Helpers/ESPEasy_Storage.h"

P062_data_struct::P062_data_struct() {
  # ifdef PLUGIN_062_DEBUG
  addLog(LOG_LEVEL_INFO, F("P062_data_struct constructor"));
  # endif // ifdef PLUGIN_062_DEBUG
  clearCalibrationData(); // Reset
}

P062_data_struct::~P062_data_struct() {
  delete keypad;
  keypad = nullptr;
}

bool P062_data_struct::init(taskIndex_t taskIndex,
                            uint8_t     i2c_addr,
                            bool        scancode,
                            bool        keepCalibrationData,
                            uint8_t     sensitivity) {
  # ifdef PLUGIN_062_DEBUG
  addLog(LOG_LEVEL_INFO, F("P062_data_struct init()"));
  # endif // ifdef PLUGIN_062_DEBUG
  _i2c_addr            = i2c_addr;
  _use_scancode        = scancode;
  _keepCalibrationData = keepCalibrationData;
  _sensitivity         = sensitivity;

  if (!keypad) {
    keypad = new (std::nothrow) Adafruit_MPR121();
  }

  if (keypad) {
    keypad->begin(_i2c_addr, _sensitivity);
    loadTouchObjects(taskIndex);
    return true;
  }
  return false;
}

void P062_data_struct::updateCalibration(uint8_t t) {
  if (t >= P062_MaxTouchObjects) { return; }

  if (_keepCalibrationData) {
    uint16_t current = keypad->filteredData(t);
    CalibrationData.CalibrationValues[t].current = current;

    if ((CalibrationData.CalibrationValues[t].min == 0) || (current < CalibrationData.CalibrationValues[t].min)) {
      CalibrationData.CalibrationValues[t].min = current;
    }

    if ((CalibrationData.CalibrationValues[t].max == 0) || (current > CalibrationData.CalibrationValues[t].max)) {
      CalibrationData.CalibrationValues[t].max = current;
    }
  }
}

bool P062_data_struct::readKey(uint16_t& key) {
  if (!keypad) { return false; }
  key = keypad->touched();

  if (key)
  {
    uint16_t colMask = 0x01;

    for (uint8_t col = 0; col < P062_MaxTouchObjects; ++col)
    {
      if (key & colMask) // this key pressed?
      {
        updateCalibration(col);

        if (_use_scancode) {
          key = col + 1;
          break;
        }
      }
      colMask <<= 1;
    }
  }

  if (keyLast != key)
  {
    keyLast = key;
    return true;
  }
  return false;
}

/**
 * Set all tresholds at once
 */
void P062_data_struct::setThresholds(uint8_t touch, uint8_t release) {
  keypad->setThresholds(touch, release);
}

/**
 * Set a single treshold
 */
void P062_data_struct::setThreshold(uint8_t t, uint8_t touch, uint8_t release) {
  keypad->setThreshold(t, touch, release);
}

/**
 * Load the touch objects from the settings, and initialize then properly where needed.
 */
void P062_data_struct::loadTouchObjects(taskIndex_t taskIndex) {
  # ifdef PLUGIN_062_DEBUG
  addLogMove(LOG_LEVEL_INFO, concat(F("P062 DEBUG loadTouchObjects size: "), sizeof(StoredSettings)));
  # endif // PLUGIN_062_DEBUG
  LoadCustomTaskSettings(taskIndex, reinterpret_cast<uint8_t *>(&StoredSettings), sizeof(StoredSettings));
}

/**
 * Get the Calibration data for 1 touch object, return false if all zeroes or invalid input for t.
 */
bool P062_data_struct::getCalibrationData(uint8_t t, uint16_t *current, uint16_t *min, uint16_t *max) {
  if (t >= P062_MaxTouchObjects) { return false; }
  *current = CalibrationData.CalibrationValues[t].current;
  *min     = CalibrationData.CalibrationValues[t].min;
  *max     = CalibrationData.CalibrationValues[t].max;
  return (*current + *min + *max) > 0;
}

/**
 * Reset the touch data.
 */
void P062_data_struct::clearCalibrationData() {
  for (uint8_t t = 0; t < P062_MaxTouchObjects; ++t) {
    CalibrationData.CalibrationValues[t].current = 0;
    CalibrationData.CalibrationValues[t].min     = 0;
    CalibrationData.CalibrationValues[t].max     = 0;
  }
}

#endif // ifdef USES_P062

#include "../PluginStructs/P176_data_struct.h"

#ifdef USES_P176

# include <GPIO_Direct_Access.h>

/**************************************************************************
* Constructor
**************************************************************************/
P176_data_struct::P176_data_struct(struct EventStruct *event) {
  _port         = static_cast<ESPEasySerialPort>(CONFIG_PORT);
  _baud         = P176_SERIAL_BAUDRATE;
  _config       = P176_SERIAL_CONFIG;
  _serialBuffer = P176_SERIAL_BUFFER;
  _rxPin        = CONFIG_PIN1;
  _txPin        = CONFIG_PIN2;
  _ledPin       = P176_GET_LED_PIN;
  _ledInverted  = P176_GET_LED_INVERTED;
  # if P176_HANDLE_CHECKSUM && P176_FAIL_CHECKSUM
  _failChecksum = P176_GET_FAIL_CHECKSUM;
  # endif // if P176_HANDLE_CHECKSUM && P176_FAIL_CHECKSUM
  # if P176_HANDLE_CHECKSUM
  _readUpdated = P176_GET_READ_UPDATED;
  # endif // if P176_HANDLE_CHECKSUM
  # if P176_DEBUG
  _debugLog = P176_GET_DEBUG_LOG;
  # endif // if P176_DEBUG
  _logQuiet = P176_GET_QUIET_LOG;
  _dataLine.reserve(44); // Should be max. line-length sent, including CR/LF
}

P176_data_struct::~P176_data_struct() {
  delete _serial;
}

bool P176_data_struct::init() {
  if (ESPEasySerialPort::not_set != _port) {
    _serial = new (std::nothrow) ESPeasySerial(_port, _rxPin, _txPin, false, _serialBuffer);

    if (nullptr != _serial) {
      # if defined(ESP8266)
      _serial->begin(_baud, (SerialConfig)_config);
      # elif defined(ESP32)
      _serial->begin(_baud, _config);
      # endif // if defined(ESP8266)
      addLog(LOG_LEVEL_INFO, F("Victron: Serial port started"));

      if (validGpio(_ledPin)) {
        DIRECT_PINMODE_OUTPUT(_ledPin);
        DIRECT_pinWrite(_ledPin, _ledInverted ? 1 : 0); // Led off
      } else {
        _ledPin = -1;
      }
    }
  }

  return isInitialized();
}

/*****************************************************
* plugin_read
*****************************************************/
bool P176_data_struct::plugin_read(struct EventStruct *event) {
  bool success = false;

  if (isInitialized()) {
    for (uint8_t i = 0; i < VARS_PER_TASK; ++i) {
      String key = getTaskValueName(event->TaskIndex, i);
      key.toLowerCase();
      VictronValue value;

      if (getReceivedValue(key, value) && value.isNumeric()) {
        UserVar.setFloat(event->TaskIndex, i, value.getNumValue());
        success = true;
      }
    }
    # if P176_HANDLE_CHECKSUM

    if (success && (!_readUpdated || (_readUpdated &&
                                      (_successCounter > 0) && (_successCounter != _lastReadCounter)))) {
      _lastReadCounter = _successCounter;
    } else {
      success = false;
    }
    # endif // if P176_HANDLE_CHECKSUM
  }

  return success;
}

// In order of most values included
// mV, mAh
const char p176_factor_1000[] PROGMEM =
  "v|v2|v3|vs|vm|vpv|i|i2|i3|il|ce|h1|h2|h3|h6|h7|h8|h15|h16"
;

// 0.01 kWh, 0.01 V
const char p176_factor_0_01[] PROGMEM =
  "h17|h18|h19|h20|h22|ac_out_v|dc_in_v|"
;

// promille
const char p176_factor_10[] PROGMEM =
  "dm|soc|"
;

// 0.1 A
const char p176_factor_0_1[] PROGMEM =
  "ac_out_i|dc_in_i|"
;

float P176_data_struct::getKeyFactor(const String& key,
                                     int32_t     & nrDecimals) const {
  nrDecimals = 0;

  if (key.isEmpty()) { return 1.0f; }

  if (GetCommandCode(key.c_str(), p176_factor_1000) > -1) {
    nrDecimals = 3;
    return 0.001f;
  }

  if (GetCommandCode(key.c_str(), p176_factor_0_01) > -1) {
    return 100.0f;
  }

  if (GetCommandCode(key.c_str(), p176_factor_10) > -1) {
    nrDecimals = 1;
    return 0.1f;
  }

  if (GetCommandCode(key.c_str(), p176_factor_0_1) > -1) {
    return 10.0f;
  }

  return 1.0f;
}

/*****************************************************
* plugin_fifty_per_second
*****************************************************/
bool P176_data_struct::plugin_fifty_per_second(struct EventStruct *event) {
  bool success = false;

  if (isInitialized()) {
    if (handleSerial()) { // Read and process data
      plugin_read(event); // Update task values
    }
    success = true;
  }

  return success;
}

/*****************************************************
* plugin_get_config_value
*****************************************************/
bool P176_data_struct::plugin_get_config_value(struct EventStruct *event,
                                               String            & string) {
  bool success = false;

  if (isInitialized()) {
    const String key = parseString(string, 1, '.'); // Decimal point as separator, by convention
    VictronValue value;

    # if P176_HANDLE_CHECKSUM

    if (equals(key, F("successcount"))) {
      string  = _successCounter;
      success = true;
    } else
    if (equals(key, F("updated"))) {
      string              = _successCounter != _lastSuccessCounter;
      _lastSuccessCounter = _successCounter;
      success             = true;
    } else
    if (equals(key, F("errorcount"))) {
      string  = _checksumErrors;
      success = true;
    } else
    # endif // if P176_HANDLE_CHECKSUM

    if (getReceivedValue(key, value)) {
      string  = value.getValue();
      success = true;
    }
  }

  return success;
}

/*****************************************************
* getCurrentDataSize
*****************************************************/
size_t P176_data_struct::getCurrentDataSize() const {
  return _data.size();
}

/*****************************************************
* showCurrentData
*****************************************************/
bool P176_data_struct::showCurrentData() const {
  bool success = false;

  if (_data.size() > 0) {
    html_table(EMPTY_STRING); // Sub-table
    html_table_header(F("Name"),  125);
    html_table_header(F("Data"),  250);
    html_table_header(F("Value"), 125);

    for (auto it = _data.begin(); it != _data.end(); ++it) {
      html_TR_TD();
      addHtml(it->second.getName());
      html_TD();
      addHtml(it->second.getRawValue());
      html_TD();

      if (it->second.isNumeric()) {
        addHtml(it->second.getValue());
      }
    }
    html_end_table();
  }
  return success;
}

// Support functions

/*****************************************************
* getReceivedValue
*****************************************************/
bool P176_data_struct::getReceivedValue(const String& key,
                                        VictronValue& value) const {
  bool success = false;

  if (!key.isEmpty()) { // Find is case-sensitive
    # ifdef ESP8266
    const auto it = _data.find(key);

    if (it != _data.end()) {
      value = it->second;
    # else // ifdef ESP8266

    if (_data.contains(key)) {
      value = _data.at(key);
    # endif // ifdef ESP8266
      success = true;
    }

    # if P176_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO) && _debugLog) {
      addLog(LOG_LEVEL_INFO, strformat(F("P176 : getReceivedValue Key:%s, value:%s"),
                                       key.c_str(), value.getRawValue().c_str()));
    }
    # endif // if P176_DEBUG
  }
  return success;
}

/*****************************************************
* handleSerial
*****************************************************/
bool P176_data_struct::handleSerial() {
  bool enough    = false;
  bool result    = false; // True for a successfully received packet, with a correct checksum or _failChecksum = false
  int  available = _serial->available();
  uint8_t ch;

  do {
    if (available > 0) {
      if (_ledPin != -1) {
        DIRECT_pinWrite(_ledPin, _ledInverted ? 0 : 1);
      }

      ch = static_cast<uint8_t>(_serial->read());
      available--;

      # if P176_HANDLE_CHECKSUM
      _checksum += ch;
      # endif // if P176_HANDLE_CHECKSUM

      switch (ch) {
        case '\r': // Ignore as it'll be stripped off
          break;
        case '\n':
          // no need to append before processing, as it's stripped off
          processBuffer(_dataLine); // Store data
          _dataLine.clear();
          enough = true;

          # if P176_HANDLE_CHECKSUM
          #  if P176_DEBUG

          if (loglevelActiveFor(LOG_LEVEL_INFO) && _debugLog) {
            addLog(LOG_LEVEL_INFO, strformat(F("P176 : Checksum state: %d"),
                                             static_cast<uint8_t>(_checksumState)));
          }
          #  endif // if P176_DEBUG

          if (Checksum_state_e::ValidateNext == _checksumState) {
            _checksumState = Checksum_state_e::Validating;
          } else
          if (Checksum_state_e::Starting == _checksumState) { // Start counting after a Checksum (aka 'end of packet') was received
            _checksumState = Checksum_state_e::Counting;
            _checksum      = 0;
            #  if P176_DEBUG

            if (loglevelActiveFor(LOG_LEVEL_INFO) && _debugLog) {
              addLog(LOG_LEVEL_INFO, F("P176 : Start counting for checksum"));
            }
            #  endif // if P176_DEBUG
          }
          # endif // if P176_HANDLE_CHECKSUM
          break;
        case '\t':
          # if P176_HANDLE_CHECKSUM

          if (equals(_dataLine, F("Checksum"))) {
            if (Checksum_state_e::Counting == _checksumState) {
              _checksumState = Checksum_state_e::ValidateNext;
              #  if P176_DEBUG

              if (loglevelActiveFor(LOG_LEVEL_INFO) && _debugLog) {
                addLog(LOG_LEVEL_INFO, F("P176 : Validate next checksum"));
              }
              #  endif // if P176_DEBUG
            } else {
              _checksumState = Checksum_state_e::Starting;
              #  if P176_FAIL_CHECKSUM

              commitTempData(!_failChecksum); // Discard any data received so far, as we don't know their checksum status
              #  endif // if P176_FAIL_CHECKSUM
            }
          }
          # endif // if P176_HANDLE_CHECKSUM
          _dataLine += static_cast<char>(ch); // append after
          break;
        default:
          _dataLine += static_cast<char>(ch);
          break;
      }

      # if P176_HANDLE_CHECKSUM

      if (Checksum_state_e::Validating == _checksumState) {
        if (_checksum != 0) {
          _checksumState = Checksum_state_e::Error; // Error if resulting checksum isn't 0
          _checksumErrors++;
          _checksumDelta = 0;

          if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
            addLog(LOG_LEVEL_ERROR, strformat(F("Victron: Checksum error, expected 0 but got %d (success: %d errors: %d)"),
                                              _checksum, _successCounter, _checksumErrors));
          }
        } else {
          _checksumState = Checksum_state_e::Counting; // New packet is expected, start counting immediately
          _successCounter++;
          _checksumDelta++;
          result = true;

          if (_checksumDelta >= 50) {
            _checksumErrors = 0;
          }


          if (loglevelActiveFor(LOG_LEVEL_INFO) && !_logQuiet) {
            addLog(LOG_LEVEL_INFO, F("Victron: Checksum validated Ok"));
          }
        }
        _checksum = 0; // Clean start

        #  if P176_FAIL_CHECKSUM

        const bool checksumSuccess = (Checksum_state_e::Error != _checksumState) || !_failChecksum;
        commitTempData(checksumSuccess);

        if (checksumSuccess) {
          result = true; // In case of a failed checksum
        }
        #  endif // if P176_FAIL_CHECKSUM
      }
      # endif // if P176_HANDLE_CHECKSUM

      if (_ledPin != -1) {
        DIRECT_pinWrite(_ledPin, _ledInverted ? 1 : 0);
      }
    } else {
      available = _serial->available();
      enough    = available <= 0;
    }
  } while (!enough);
  return result;
}

/*****************************************************
* processBuffer
*****************************************************/
void P176_data_struct::processBuffer(const String& message) {
  if (message.isEmpty()) { return; }
  const String name  = parseStringKeepCase(message, 1, '\t');
  const String value = parseStringToEndKeepCase(message, 2, '\t');
  String key(name);
  key.toLowerCase();

  # if P176_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO) && _debugLog) {
    addLog(LOG_LEVEL_INFO, strformat(F("P176 : Processing data '%s\t%s'"
                                       #  if P176_HANDLE_CHECKSUM
                                       ", checksum: %d"
                                       #  endif // if P176_HANDLE_CHECKSUM
                                       ), name.c_str(), value.c_str()
                                     #  if P176_HANDLE_CHECKSUM
                                     , _checksum
                                     #  endif // if P176_HANDLE_CHECKSUM
                                     ));
  }
  # endif // if P176_DEBUG

  if (!key.isEmpty() && !value.isEmpty() && !equals(key, F("checksum"))) {
    auto it = _data.find(key);

    if (it == _data.end()) {
      int32_t nrDecimals{};
      float   factor = getKeyFactor(key, nrDecimals);
      VictronValue val(name, value, factor, nrDecimals);
      _data[key] = std::move(val);
    } else {
      it->second.set(value);
    }
  }
}

# if P176_FAIL_CHECKSUM

/*****************************************************
* commitTempData
*****************************************************/
bool P176_data_struct::commitTempData(bool checksumSuccess) {
  size_t nrChanged = 0;

  for (auto it = _data.begin(); it != _data.end(); ++it) {
    if (it->second.commitTempData(checksumSuccess)) {
      ++nrChanged;
    }
  }
  #  if P176_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO) && _debugLog) {
    addLog(LOG_LEVEL_INFO, strformat(F("P176 : Moving %d _temp items to _data"), nrChanged));
  }
  #  endif // if P176_DEBUG
  return nrChanged != 0;
}

# endif // if P176_FAIL_CHECKSUM

#endif // ifdef USES_P176

#include "../PluginStructs/P140_data_struct.h"

#ifdef USES_P140

# include "../Commands/ExecuteCommand.h"

P140_data_struct::P140_data_struct(struct EventStruct *event) {
  _events = 1 == P140_SEND_EVENTS;
  _exec   = 1 == P140_EXEC_COMMAND;
  _input  = 1 == P140_GET_INPUT;
}

P140_data_struct::~P140_data_struct() {}

bool P140_data_struct::plugin_read(struct EventStruct *event) {
  return false;
}

bool P140_data_struct::plugin_write(struct EventStruct *event, String& string) {
  const String cmd = parseString(string, 1);

  if (equals(cmd, F("cardkb"))) {
    const String subcmd = parseString(string, 2);

    if (equals(subcmd, F("events")) && ((0 == event->Par2) || (1 == event->Par2))) {
      P140_SEND_EVENTS = event->Par2;
      _events          = 1 == event->Par2;
      return true;
    } else
    if (equals(subcmd, F("exec")) && ((0 == event->Par2) || (1 == event->Par2))) {
      P140_EXEC_COMMAND = event->Par2;
      _exec             = 1 == event->Par2;

      if (!(_exec || _input)) {
        clear(); // Clear when both turned off
      }
      return true;
    } else
    if (equals(subcmd, F("input")) && ((0 == event->Par2) || (1 == event->Par2))) {
      P140_GET_INPUT = event->Par2;
      _input         = 1 == event->Par2;

      if (!(_exec || _input)) {
        clear(); // Clear when both turned off
      }
      return true;
    } else
    if (equals(subcmd, F("clear"))) {
      clear(); // Clear buffer
      return true;
    }
  }
  return false;
}

bool P140_data_struct::getBufferValue(String& string) {
  if (_inCounter) {
    if (_inCounter < P140_INPUT_BUFFER_SIZE) {
      _buffer[_inCounter] = 0; // terminate string
    }
    const String tmp(_buffer);
    string = std::move(tmp);
    return true;
  }
  return false;
}

bool P140_data_struct::plugin_ten_per_second(struct EventStruct *event) {
  bool result = false;
  bool update = false;
  bool ok;
  const uint8_t inChar = I2C_read8(P140_I2C_ADDR, &ok);

  if (!ok || (0 == inChar)) {
    return false; // Shortcut
  }

  if ((_exec || _input) && isprint(inChar))
  {
    if (_inCounter < P140_INPUT_BUFFER_SIZE) { // add char to buffer if it still fits
      _buffer[_inCounter++] = inChar;
    }
    update = true;
    # if P140_DEBUG

    if (_inCounter < P140_INPUT_BUFFER_SIZE) {
      _buffer[_inCounter] = 0; // terminate string
    }
    addLog(LOG_LEVEL_INFO, strformat(F("CardKB: Key %d (0x%02X) '%c' command: %s"), inChar, inChar, inChar, _buffer));
    # endif // if P140_DEBUG
  } else

  if ((inChar == '\b') && (_inCounter > 0)) // Correct a typo using BackSpace
  {
    _buffer[--_inCounter] = 0;              // shorten input
    update                = true;
    # if P140_DEBUG
    addLog(LOG_LEVEL_INFO, strformat(F("CardKB: Backspace : %s"), _buffer));
    # endif // if P140_DEBUG
  } else
  if ((_exec || _input) && ((inChar == '\r') || (inChar == '\n'))) { // CR or LF completes input
    // Ignore empty input
    if (_inCounter != 0) {
      _buffer[_inCounter] = 0;                                       // keyboard data completed
      const String cmd(_buffer);

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLog(LOG_LEVEL_INFO, strformat(F("CardKB: Input: %s"), _buffer));
      }

      if (_exec) {
        // Act like we entered a command via serial
        ExecuteCommand_all({ EventValueSource::Enum::VALUE_SOURCE_SERIAL, std::move(cmd) }, true);
      }
      update = true;
      result = true;
    }
  } else
  if (inChar != 0) {
    update = true;
    # if P140_DEBUG
    addLog(LOG_LEVEL_INFO, strformat(F("CardKB: Key: %d (0x%02X) (ignored)"), inChar, inChar));
    # endif // if P140_DEBUG
  }


  if (update) {
    UserVar.setFloat(event->TaskIndex, 0, inChar);
    UserVar.setFloat(event->TaskIndex, 1, _inCounter);

    if (_events || (result && _input)) { // Send events also when using Input mode after pressing Enter
      sendData(event);
    }

    if (result) {
      clear(); // keyboard data processed, clear buffer
    }
  }
  return result;
}

void P140_data_struct::clear() {
  _inCounter = 0;
  _buffer[0] = 0;
}

#endif // ifdef USES_P140

#include "../PluginStructs/P058_data_struct.h"

#ifdef USES_P058

// Needed also here for PlatformIO's library finder as the .h file 
// is in a directory which is excluded in the src_filter
# include <HT16K33.h>



P058_data_struct::P058_data_struct(uint8_t i2c_addr) {
  keyPad.Init(i2c_addr);
}

bool P058_data_struct::readKey(uint8_t& key) {
  key = keyPad.ReadKeys();

  if (keyLast != key)
  {
    keyLast = key;
    return true;
  }
  return false;
}

#endif // ifdef USES_P058

#include "../PluginStructs/P035_data_struct.h"

#ifdef USES_P035

// **************************************************************************/
// Constructor
// **************************************************************************/
P035_data_struct::P035_data_struct(int8_t gpioPin, bool inverted)
  : _gpioPin(gpioPin), _inverted(inverted) {}

// **************************************************************************/
// Destructor
// **************************************************************************/
P035_data_struct::~P035_data_struct() {
  if (Plugin_035_irSender != nullptr) {
    delete Plugin_035_irSender;
    Plugin_035_irSender = nullptr;
  }
  # ifdef P016_P035_Extended_AC

  if (Plugin_035_commonAc != nullptr) {
    delete Plugin_035_commonAc;
    Plugin_035_commonAc = nullptr;
  }
  # endif // ifdef P016_P035_Extended_AC
}

bool P035_data_struct::plugin_init(struct EventStruct *event) {
  bool success = false;

  if ((Plugin_035_irSender == nullptr) && validGpio(_gpioPin)) {
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO, F("INIT: IR TX"));
      addLog(LOG_LEVEL_INFO, F("IR lib Version: " _IRREMOTEESP8266_VERSION_STR));
      # ifdef P035_DEBUG_LOG
      addLog(LOG_LEVEL_INFO, concat(F("Supported Protocols by IRSEND: "), listProtocols()));
      # endif // ifdef P035_DEBUG_LOG
    }
    Plugin_035_irSender = new (std::nothrow) IRsend(_gpioPin, _inverted);

    if (Plugin_035_irSender != nullptr) {
      Plugin_035_irSender->begin(); // Start the sender
      success = true;
    }
  }

  # ifdef P016_P035_Extended_AC

  if (success && (Plugin_035_commonAc == nullptr) && validGpio(_gpioPin)) {
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO, F("INIT AC: IR TX"));
      #  ifdef P035_DEBUG_LOG
      addLog(LOG_LEVEL_INFO, concat(F("Supported Protocols by IRSENDAC: "), listACProtocols()));
      #  endif // ifdef P035_DEBUG_LOG
    }
    Plugin_035_commonAc = new (std::nothrow) IRac(_gpioPin);
    success             = nullptr != Plugin_035_commonAc;
  }

  # endif // ifdef P016_P035_Extended_AC
  return success;
}

bool P035_data_struct::plugin_write(struct EventStruct *event, const String& string) {
  bool success = false;

  const String cmdCode = parseString(string, 1);

  if (equals(cmdCode, F("irsend")) && (Plugin_035_irSender != nullptr)) {
    success = true;
    enableIR_RX(false);

    # ifdef P016_P035_USE_RAW_RAW2
    handleRawRaw2Encoding(string);
    # endif // P016_P035_USE_RAW_RAW2

    handleIRremote(string);
  }

  # ifdef P016_P035_Extended_AC
  else if (equals(cmdCode, F("irsendac")) && (Plugin_035_commonAc != nullptr)) {
    success = true;
    enableIR_RX(false);
    handle_AC_IRremote(parseStringToEnd(string, 2));
  }
  # endif // P016_P035_Extended_AC

  enableIR_RX(true);

  return success;
}

bool P035_data_struct::handleIRremote(const String& cmd) {
  String IrType;
  String TmpStr1;

  uint64_t IrCode   = 0;
  uint16_t IrBits   = 0;
  uint16_t IrRepeat = 0;
  String   ircodestr;

  StaticJsonDocument<200> docTemp;
  DeserializationError    error = deserializeJson(docTemp, parseStringToEnd(cmd, 2));

  if (!error) {                                  // If the command is in JSON format
    IrType    =  docTemp[F("protocol")].as<String>();
    ircodestr = docTemp[F("data")].as<String>(); // JSON does not support hex values, thus we use command representation
    IrCode    = strtoull(ircodestr.c_str(), nullptr, 16);
    IrBits    = docTemp[F("bits")] | 0;
    IrRepeat  = docTemp[F("repeats")] | 0;
  } else { // If the command is NOT in JSON format (legacy)
    IrType = parseString(cmd, 2);

    if (IrType.length() > 0) {
      ircodestr = parseString(cmd, 3);

      if (ircodestr.length() > 0) {
        IrCode = strtoull(ircodestr.c_str(), nullptr, 16);
      }
      IrBits   = parseString(cmd, 4).toInt(); // Number of bits to be sent. USE 0 for default protocol bits
      IrRepeat = parseString(cmd, 5).toInt(); // Nr. of times the message is to be repeated
    }
  }

  bool IRsent = sendIRCode(strToDecodeType(IrType.c_str()), IrCode, ircodestr.c_str(), IrBits, IrRepeat); // Send the IR command

  if (IRsent) {
    printToLog(IrType, ircodestr, IrBits, IrRepeat);
  }
  return IRsent;
}

# ifdef P016_P035_Extended_AC
bool P035_data_struct::handle_AC_IRremote(const String& irData) {
  StaticJsonDocument<JSON_OBJECT_SIZE(18) + 190> doc;
  DeserializationError error = deserializeJson(doc, irData); // Deserialize the JSON document

  if (error) {                                               // Test if parsing succeeds.
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO, concat(F("IRTX: Deserialize Json failed: "), error.c_str()));
    }
    return false; // do not continue with sending the signal.
  }
  String sprotocol = doc[F("protocol")];

  st.protocol = strToDecodeType(sprotocol.c_str());

  if (!IRac::isProtocolSupported(st.protocol)) { // Check if we support the protocol
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO, concat(F("IRTX: Protocol not supported:"), sprotocol));
    }
    return false; // do not continue with sending of the signal.
  }

  String tempstr;

  tempstr  = doc[F("model")].as<String>();
  st.model = IRac::strToModel(tempstr.c_str(), -1);                             // The specific model of A/C if applicable. //strToModel();.
                                                                                // Defaults to -1 (unknown) if missing from JSON
  tempstr    = doc[F("power")].as<String>();
  st.power   = IRac::strToBool(tempstr.c_str(), false);                         // POWER ON or OFF. Defaults to false if missing from JSON
  st.degrees = doc[F("temp")] | 22.0f;                                          // What temperature should the unit be set to?. Defaults to
                                                                                // 22c if missing from JSON
  tempstr    = doc[F("use_celsius")].as<String>();
  st.celsius = IRac::strToBool(tempstr.c_str(), true);                          // Use degreees Celsius, otherwise Fahrenheit. Defaults to
                                                                                // true if missing from JSON
  tempstr = doc[F("mode")].as<String>();
  st.mode = IRac::strToOpmode(tempstr.c_str(), stdAc::opmode_t::kAuto);         // What operating mode should the unit perform? e.g. Cool.
                                                                                // Defaults to auto if missing from JSON
  tempstr     = doc[F("fanspeed")].as<String>();
  st.fanspeed = IRac::strToFanspeed(tempstr.c_str(), stdAc::fanspeed_t::kAuto); // Fan Speed setting. Defaults to auto if missing from JSON
  tempstr     = doc[F("swingv")].as<String>();
  st.swingv   = IRac::strToSwingV(tempstr.c_str(), stdAc::swingv_t::kAuto);     // Vertical swing setting. Defaults to auto if missing from
                                                                                // JSON
  tempstr   = doc[F("swingh")].as<String>();
  st.swingh = IRac::strToSwingH(tempstr.c_str(), stdAc::swingh_t::kAuto);       // Horizontal Swing setting. Defaults to auto if missing
                                                                                // from JSON
  tempstr  = doc[F("quiet")].as<String>();
  st.quiet = IRac::strToBool(tempstr.c_str(), false);                           // Quiet setting ON or OFF. Defaults to false if missing
                                                                                // from JSON
  tempstr  = doc[F("turbo")].as<String>();
  st.turbo = IRac::strToBool(tempstr.c_str(), false);                           // Turbo setting ON or OFF. Defaults to false if missing
                                                                                // from JSON
  tempstr  = doc[F("econo")].as<String>();
  st.econo = IRac::strToBool(tempstr.c_str(), false);                           // Economy setting ON or OFF. Defaults to false if missing
                                                                                // from JSON
  tempstr  = doc[F("light")].as<String>();
  st.light = IRac::strToBool(tempstr.c_str(), true);                            // Light setting ON or OFF. Defaults to true if missing from
                                                                                // JSON
  tempstr   = doc[F("filter")].as<String>();
  st.filter = IRac::strToBool(tempstr.c_str(), false);                          // Filter setting ON or OFF. Defaults to false if missing
                                                                                // from JSON
  tempstr  = doc[F("clean")].as<String>();
  st.clean = IRac::strToBool(tempstr.c_str(), false);                           // Clean setting ON or OFF. Defaults to false if missing
                                                                                // from JSON
  tempstr = doc[F("beep")].as<String>();
  st.beep = IRac::strToBool(tempstr.c_str(), false);                            // Beep setting ON or OFF. Defaults to false if missing from
                                                                                // JSON
  st.sleep = doc[F("sleep")] | -1;                                              // Nr. of mins of sleep mode, or use sleep mode. (<= 0 means
                                                                                // off.). Defaults to -1 if missing from JSON
  st.clock = doc[F("clock")] | -1;                                              // Nr. of mins past midnight to set the clock to. (< 0 means
                                                                                // off.). Defaults to -1 if missing from JSON

  // Send the IR command
  const bool IRsent = Plugin_035_commonAc->sendAc(st, &prev);

  if (IRsent) {
    printToLog(typeToString(st.protocol), irData, 0, 0);
  }
  return IRsent;
}

# endif // ifdef P016_P035_Extended_AC


bool P035_data_struct::handleRawRaw2Encoding(const String& cmd) {
  bool raw            = true;
  const String IrType = parseString(cmd, 2);

  if (IrType.isEmpty())           { return false; }

  if (equals(IrType, F("raw"))) {
    raw = true;
  } else if (equals(IrType, F("raw2"))) {
    raw = false;
  }

  String   IrRaw      = parseString(cmd, 3);         // Get the "Base32" encoded/compressed Ir signal
  uint16_t IrHz       = parseString(cmd, 4).toInt(); // Get the base freguency of the signal (allways 38)
  unsigned int IrPLen = parseString(cmd, 5).toInt(); // Get the Pulse Length in ms
  unsigned int IrBLen = parseString(cmd, 6).toInt(); // Get the Blank Pulse Length in ms

  uint16_t  idx = 0;                                 // If this goes above the buf.size then the esp will throw a 28 EXCCAUSE
  uint16_t *buf;

  buf = new (std::nothrow) uint16_t[P35_Ntimings];   // The Raw Timings that we can buffer.

  if (buf == nullptr) {                              // error assigning memory.
    return false;
  }


  if (raw) {
    unsigned int c0 = 0; // count consecutives 0s
    unsigned int c1 = 0; // count consecutives 1s

    // printWebString += F("Interpreted RAW Code: ");  //print the number of 1s and 0s just for debugging/info purposes
    // Loop throught every char in RAW string
    for (unsigned int i = 0; i < IrRaw.length(); i++) {
      // Get the decimal value from base32 table
      // See: https://en.wikipedia.org/wiki/Base32#base32hex
      char c = from_32hex(IrRaw[i]);

      // Loop through 5 LSB (bits 16, 8, 4, 2, 1)
      for (unsigned int shft = 1; shft < 6; shft++) {
        // if bit is 1 (5th position - 00010000 = 16)
        if ((c & 16) != 0) {
          // add 1 to counter c1
          c1++;

          // if we already have any 0s in counting (the previous
          // bit was 0)
          if (c0 > 0) {
            // add the total ms into the buffer (number of 0s multiplied
            // by defined blank length ms)
            buf[idx++] = c0 * IrBLen;

            // print the number of 0s just for debuging/info purpouses
            // for (uint t = 0; t < c0; t++)
            // printWebString += '0';
          }

          // So, as we receive a "1", and processed the counted 0s
          // sending them as a ms timing into the buffer, we clear
          // the 0s counter
          c0 = 0;
        } else {
          // So, bit is 0

          // On first call, ignore 0s (suppress left-most 0s)
          if (c0 + c1 != 0) {
            // add 1 to counter c0
            c0++;

            // if we already have any 1s in counting (the previous
            // bit was 1)
            if (c1 > 0) {
              // add the total ms into the buffer (number of 1s
              // multiplied by defined pulse length ms)
              buf[idx++] = c1 * IrPLen;

              // print the number of 1s just for debugging/info purposes
              //                          for (uint t = 0; t < c1; t++)
              //                            printWebString += '1';
            }

            // So, as we receive a "0", and processed the counted 1s
            // sending them as a ms timing into the buffer, we clear
            // the 1s counter
            c1 = 0;
          }
        }

        // shift to left the "c" variable to process the next bit that is
        // in 5th position (00010000 = 16)
        c <<= 1;
      }
    }

    // Finally, we need to process the last counted bit that we were
    // processing

    // If we have pendings 0s
    if (c0 > 0) {
      buf[idx++] = c0 * IrBLen;

      // for (uint t = 0; t < c0; t++)
      // printWebString += '0';
    }

    // If we have pendings 1s
    if (c1 > 0) {
      buf[idx++] = c1 * IrPLen;

      // for (uint t = 0; t < c1; t++)
      // printWebString += '1';
    }

    // printWebString += F("<BR>");
  }

  if (!raw) { // RAW2
    for (unsigned int i = 0, total = IrRaw.length(), gotRep = 0, rep = 0; i < total;) {
      char c = IrRaw[i++];

      if (c == '*') {
        if (((i + 2) >= total) || ((idx + (rep = from_32hex(IrRaw[i++])) * 2) > (sizeof(buf[0]) * P35_Ntimings))) {
          delete[] buf;
          buf = nullptr;
          return addErrorTrue();
        }
        gotRep = 2;
      } else {
        if (((c == '^') && ((i + 1u) >= total)) || ((idx + 2u) >= (sizeof(buf[0]) * P35_Ntimings))) {
          delete[] buf;
          buf = nullptr;
          return addErrorTrue();
        }

        uint16_t irLen = (idx & 1) ? IrBLen : IrPLen;

        if (c == '^') {
          buf[idx++] = (from_32hex(IrRaw[i]) * 32 + from_32hex(IrRaw[i + 1])) * irLen;
          i         += 2;
        } else {
          buf[idx++] = from_32hex(c) * irLen;
        }

        if (--gotRep == 0) {
          while (--rep) {
            buf[idx]     = buf[idx - 2];
            buf[idx + 1] = buf[idx - 1];
            idx         += 2;
          }
        }
      }
    }
  } // End RAW2

  Plugin_035_irSender->sendRaw(buf, idx, IrHz);

  // printWebString += IrType + String(F(": Base32Hex RAW Code: ")) + IrRaw + String(F("<BR>kHz: ")) + IrHz + String(F("<BR>Pulse Len: ")) +
  // IrPLen + String(F("<BR>Blank Len: ")) + IrBLen + String(F("<BR>"));
  printToLog(F(": Base32Hex RAW Code Send "), IrRaw, 0, 0);

  // printWebString += String(F(": Base32Hex RAW Code Send "));
  delete[] buf;
  buf = nullptr;
  return true;
}

void P035_data_struct::printToLog(const String& protocol, const String& data, int bits, int repeats) {
  if (!loglevelActiveFor(LOG_LEVEL_INFO) && !printToWeb) {
    return;
  }
  String tmp = F("IRTX: IR Code Sent: ");

  tmp += protocol;
  tmp += F(" Data: ");
  tmp += data;

  if (bits > 0) {
    tmp += F(" Bits: ");
    tmp += bits;
  }

  if (repeats > 0) {
    tmp += F(" Repeats: ");
    tmp += repeats;
  }

  if (printToWeb) {
    printWebString = tmp;
  }
  addLogMove(LOG_LEVEL_INFO, tmp);
}

# ifdef P035_DEBUG_LOG
String P035_data_struct::listProtocols() {
  String temp;

  if (temp.reserve(1024)) {
    for (uint32_t i = 0; i <= kLastDecodeType; i++) {
      if (IRsend::defaultBits((decode_type_t)i) > 0) {
        const String typ = typeToString((decode_type_t)i);

        if (typ.length() > 1) {
          temp += typ;
          temp += ' ';
        }
      }
    }
  }
  return temp;
}

# endif // ifdef P035_DEBUG_LOG

# if defined(P016_P035_Extended_AC) && defined(P035_DEBUG_LOG)
String P035_data_struct::listACProtocols() {
  String temp;

  if (temp.reserve(1024)) {
    for (uint32_t i = 0; i <= kLastDecodeType; i++) {
      if (hasACState((decode_type_t)i)) {
        const String typ = typeToString((decode_type_t)i);

        if (typ.length() > 1) {
          temp +=  typ;
          temp += ' ';
        }
      }
    }
  }
  return temp;
}

# endif // if defined(P016_P035_Extended_AC) && defined(P035_DEBUG_LOG)

bool P035_data_struct::addErrorTrue() {
  addLog(LOG_LEVEL_ERROR, F("RAW2: Invalid encoding!"));
  return true;
}

// A lot of the following code has been taken directly (with permission) from the IRMQTTServer.ino example code
// of the IRremoteESP8266 library. (https://github.com/markszabo/IRremoteESP8266)

// Transmit the given IR message.
//
// Args:
//   irsend:   A pointer to a IRsend object to transmit via.
//   irtype:  enum of the protocol to be sent.
//   code:     Numeric payload of the IR message. Most protocols use this.
//   code_str: The unparsed code to be sent. Used by complex protocol encodings.
//   bits:     Nr. of bits in the protocol. 0 means use the protocol's default.
//   repeat:   Nr. of times the message is to be repeated. (Not all protcols.)
// Returns:
//   bool: Successfully sent or not.
bool P035_data_struct::sendIRCode(const int      irtype,
                                  const uint64_t code,
                                  const char    *code_str,
                                  uint16_t       bits,
                                  uint16_t       repeat) {
  decode_type_t irType = static_cast<decode_type_t>(irtype);
  bool success         = true; // Assume success.

  repeat = std::max(IRsend::minRepeats(irType), repeat);

  if (bits == 0) {
    bits = IRsend::defaultBits(irType);
  }

  // send the IR message.

  if (hasACState(irType)) { // protocols with > 64 bits
    success = parseStringAndSendAirCon(irType, code_str);
  } else {                  // protocols with <= 64 bits
    success = Plugin_035_irSender->send(irType, code, bits, repeat);
  }

  return success;
}

// Parse an Air Conditioner A/C Hex String/code and send it.
// Args:
//   irtype: Nr. of the protocol we need to send.
//   str: A hexadecimal string containing the state to be sent.
bool P035_data_struct::parseStringAndSendAirCon(const int irtype, const String str) {
  decode_type_t irType          = (decode_type_t)irtype;
  uint8_t  strOffset            = 0;
  uint8_t  state[kStateSizeMax] = { 0 }; // All array elements are set to 0.
  uint16_t stateSize            = 0;

  if (str.startsWith("0x") || str.startsWith("0X")) {
    strOffset = 2;
  }

  // Calculate how many hexadecimal characters there are.
  const uint16_t inputLength = str.length() - strOffset;

  if (inputLength == 0) {
    // debug("Zero length AirCon code encountered. Ignored.");
    return false;   // No input. Abort.
  }

  switch (irType) { // Get the correct state size for the protocol.
    case DAIKIN:
      // Daikin has 2 different possible size states.
      // (The correct size, and a legacy shorter size.)
      // Guess which one we are being presented with based on the number of
      // hexadecimal digits provided. i.e. Zero-pad if you need to to get
      // the correct length/uint8_t size.
      // This should provide backward compatiblity with legacy messages.
      stateSize = inputLength / 2; // Every two hex chars is a uint8_t.
      // Use at least the minimum size.
      stateSize = std::max(stateSize, kDaikinStateLengthShort);

      // If we think it isn't a "short" message.
      if (stateSize > kDaikinStateLengthShort) {
        // Then it has to be at least the version of the "normal" size.
        stateSize = std::max(stateSize, kDaikinStateLength);
      }

      // Lastly, it should never exceed the "normal" size.
      stateSize = std::min(stateSize, kDaikinStateLength);
      break;
    case FUJITSU_AC:
      // Fujitsu has four distinct & different size states, so make a best guess
      // which one we are being presented with based on the number of
      // hexadecimal digits provided. i.e. Zero-pad if you need to to get
      // the correct length/uint8_t size.
      stateSize = inputLength / 2; // Every two hex chars is a uint8_t.
      // Use at least the minimum size.
      stateSize = std::max(stateSize,
                           (uint16_t)(kFujitsuAcStateLengthShort - 1));

      // If we think it isn't a "short" message.
      if (stateSize > kFujitsuAcStateLengthShort) {
        // Then it has to be at least the smaller version of the "normal" size.
        stateSize = std::max(stateSize, (uint16_t)(kFujitsuAcStateLength - 1));
      }

      // Lastly, it should never exceed the maximum "normal" size.
      stateSize = std::min(stateSize, kFujitsuAcStateLength);
      break;
    case HITACHI_AC3:
      // HitachiAc3 has two distinct & different size states, so make a best
      // guess which one we are being presented with based on the number of
      // hexadecimal digits provided. i.e. Zero-pad if you need to to get
      // the correct length/uint8_t size.
      stateSize = inputLength / 2; // Every two hex chars is a uint8_t.
      // Use at least the minimum size.
      stateSize = std::max(stateSize,
                           (uint16_t)(kHitachiAc3MinStateLength));

      // If we think it isn't a "short" message.
      if (stateSize > kHitachiAc3MinStateLength) {
        // Then it probably the "normal" size.
        stateSize = std::max(stateSize,
                             (uint16_t)(kHitachiAc3StateLength));
      }

      // Lastly, it should never exceed the maximum "normal" size.
      stateSize = std::min(stateSize, kHitachiAc3StateLength);
      break;
    case MWM:
      // MWM has variable size states, so make a best guess
      // which one we are being presented with based on the number of
      // hexadecimal digits provided. i.e. Zero-pad if you need to to get
      // the correct length/uint8_t size.
      stateSize = inputLength / 2; // Every two hex chars is a uint8_t.
      // Use at least the minimum size.
      stateSize = std::max(stateSize, (uint16_t)3);

      // Cap the maximum size.
      stateSize = std::min(stateSize, kStateSizeMax);
      break;
    case SAMSUNG_AC:
      // Samsung has two distinct & different size states, so make a best guess
      // which one we are being presented with based on the number of
      // hexadecimal digits provided. i.e. Zero-pad if you need to to get
      // the correct length/uint8_t size.
      stateSize = inputLength / 2; // Every two hex chars is a uint8_t.
      // Use at least the minimum size.
      stateSize = std::max(stateSize, (uint16_t)(kSamsungAcStateLength));

      // If we think it isn't a "normal" message.
      if (stateSize > kSamsungAcStateLength) {
        // Then it probably the extended size.
        stateSize = std::max(stateSize,
                             (uint16_t)(kSamsungAcExtendedStateLength));
      }

      // Lastly, it should never exceed the maximum "extended" size.
      stateSize = std::min(stateSize, kSamsungAcExtendedStateLength);
      break;
    default: // Everything else.
      stateSize = (IRsend::defaultBits(irType) + 7) / 8;

      if (!stateSize || !hasACState(irType)) {
        // Not a protocol we expected. Abort.
        // debug("Unexpected AirCon protocol detected. Ignoring.");
        return false;
      }
  }

  if (inputLength > stateSize * 2) {
    // debug("AirCon code to large for the given protocol.");
    return false;
  }

  // Ptr to the least significant uint8_t of the resulting state for this protocol.
  uint8_t *statePtr = &state[stateSize - 1];

  // Convert the string into a state array of the correct length.
  for (uint16_t i = 0; i < inputLength; i++) {
    // Grab the next least sigificant hexadecimal digit from the string.
    uint8_t c = tolower(str[inputLength + strOffset - i - 1]);

    if (isxdigit(c)) {
      if (isdigit(c)) {
        c -= '0';
      } else {
        c = c - 'a' + 10;
      }
    } else {
      // debug("Aborting! Non-hexadecimal char found in AirCon state:");
      // debug(str.c_str());
      return false;
    }

    if (i % 2 == 1) { // Odd: Upper half of the uint8_t.
      *statePtr += (c << 4);
      statePtr--;     // Advance up to the next least significant uint8_t of state.
    } else {          // Even: Lower half of the uint8_t.
      *statePtr = c;
    }
  }

  if (!Plugin_035_irSender->send(irType, state, stateSize)) {
    // debug("Unexpected AirCon type in send request. Not sent.");
    return false;
  }
  return true; // We were successful as far as we can tell.
}

#endif // ifdef USES_P035

#include "../PluginStructs/P163_data_struct.h"

#ifdef USES_P163

/**************************************************************************
* Constructor
**************************************************************************/
P163_data_struct::P163_data_struct(struct EventStruct *event) {
  _readIncrement = P163_GET_READ_INCREMENT;
  _resetOnRead   = P163_GET_RESET_ON_READ;
  _lowPowerMode  = P163_GET_LOW_POWER;
  _ledState      = P163_GET_LED_STATE;
  _threshold     = P163_CFG_THRESHOLD;
  _changeOnly    = _threshold < 0;
  # if FEATURE_PLUGIN_STATS
  _countAvg = P163_CFG_COUNT_AVG;
  # endif // if FEATURE_PLUGIN_STATS
}

P163_data_struct::~P163_data_struct() {
  delete sensor;
}

/*****************************************************
* init
*****************************************************/
bool P163_data_struct::init(struct EventStruct *event) {
  sensor = new (std::nothrow) CG_RadSens(RS_DEFAULT_I2C_ADDRESS);

  if ((nullptr != sensor) && sensor->init()) {
    initialized = true;

    sensor->setLPmode(_lowPowerMode);
    sensor->setLedState(_ledState);

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO, strformat(F("RadSens: Initialized, ChipID: 0x%02x, Firmware: %d"),
                                       sensor->getChipId(), sensor->getFirmwareVersion()));
    }
  } else {
    delete sensor; // Clean up if initialization fails
    addLog(LOG_LEVEL_ERROR, F("RadSens: Initialization failed!"));
  }
  return isInitialized();
}

/*****************************************************
* plugin_read
*****************************************************/
bool P163_data_struct::plugin_read(struct EventStruct *event) {
  if (isInitialized()) {
    if (setOutputValues(event)) {
      return true;
    }
  }
  return false;
}

bool P163_data_struct::setOutputValues(struct EventStruct *event) {
  bool result = false;

  const uint32_t count    = _readIncrement ? sensor->getNumberOfPulses() : sensor->getNumberOfNewPulses();
  const uint32_t incCount = sensor->getNumberOfPulses();
  const float    iDynamic = sensor->getRadIntensyDynamic();
  const float    iStatic  = sensor->getRadIntensyStatic();
  int32_t delta           = -1; // Invalid/unset

  if (_resetOnRead) {
    sensor->resetPulses();
  }

  # if FEATURE_PLUGIN_STATS

  if (hasPluginStats()) {
    PluginStats *stats = getPluginStats(0);                // Get stats for Count value

    if ((nullptr != stats) && (stats->getNrSamples() > 0)) {
      delta = abs(count - stats->getSampleAvg(_countAvg)); // Average count of last n values
    }
  }

  if (-1 == delta) // No stats available
  # endif // if FEATURE_PLUGIN_STATS
  {
    delta = abs(count - UserVar.getFloat(event->TaskIndex, 0));
  }

  result = !_changeOnly || (delta >= _threshold);

  UserVar.setFloat(event->TaskIndex, 0, count);

  UserVar.setFloat(event->TaskIndex, 1, iDynamic);
  UserVar.setFloat(event->TaskIndex, 2, iStatic);
  UserVar.setFloat(event->TaskIndex, 3, incCount);

  return result;
}

/*****************************************************
* plugin_write
*****************************************************/
const char P163_subcommands[] PROGMEM =
  "calibration|"
  "highvoltage|"
  "resetcount|"
;

enum class P163_subcmd_e : int8_t {
  invalid     = -1,
  calibration = 0,
  highvoltage = 1,
  resetcount  = 2,
};

bool P163_data_struct::plugin_write(struct EventStruct *event,
                                    String            & string) {
  bool success = false;

  const String command = parseString(string, 1);

  if (isInitialized() && equals(command, F("radsens"))) {
    const String subcommand   = parseString(string, 2);
    const int    subcommand_i = GetCommandCode(subcommand.c_str(), P163_subcommands);

    if (subcommand_i < 0) { return false; } // Fail fast

    const P163_subcmd_e subcmd = static_cast<P163_subcmd_e>(subcommand_i);

    switch (subcmd) {
      case P163_subcmd_e::invalid:
        break;
      case P163_subcmd_e::calibration:

        if (event->Par2 >= 0) {
          sensor->setSensitivity(event->Par2);
          success = true;
        }
        break;
      case P163_subcmd_e::highvoltage:

        if ((event->Par2 == 0) || (event->Par2 == 1)) {
          sensor->setHVGeneratorState(event->Par2 == 1);
          success = true;
        }
        break;
      case P163_subcmd_e::resetcount:
        sensor->resetPulses();
        success = true;
        break;
    }
  }
  return success;
}

#endif // ifdef USES_P163


#include "../PluginStructs/P178_data_struct.h"

#ifdef USES_P178

P178_data_struct::P178_data_struct(uint8_t address, uint16_t freq) : _address(address)
{
  // Reset
  I2C_write8_reg(_address, 0xFB, 0xFB);
  setFrequency(freq);
  update();
}

bool P178_data_struct::enablePin(uint8_t pin, bool enable)
{
  if (pin == LU9685_ALL_PINS) {
    constexpr uint32_t mask = LU9685_ALL_PIN_ENABLE_MASK;
    _enabledPin = enable ? mask : 0;
    return update();
  }

  if (pin >= LU9685_MAX_PINS) {
    return false;
  }
  bitWrite(_enabledPin, pin, enable);
  return setAngle(pin, _data[pin]);
}

bool P178_data_struct::setAngle(uint8_t pin, int angle)
{
  if ((pin != LU9685_ALL_PINS) && (pin >= LU9685_MAX_PINS)) {
    return false;
  }

  setAngle_internal(pin, angle);

  if (pin == LU9685_ALL_PINS) {
    return update();
  }
  return I2C_write8_reg(_address, pin, getAngle(pin));
}

bool P178_data_struct::setAngle(
  uint8_t pin,
  int     angle,
  bool    enable)
{
  if ((pin != LU9685_ALL_PINS) && (pin >= LU9685_MAX_PINS)) {
    return false;
  }

  if (pin == LU9685_ALL_PINS) {
    constexpr uint32_t mask = LU9685_ALL_PIN_ENABLE_MASK;
    _enabledPin = enable ? mask : 0;
  } else {
    bitWrite(_enabledPin, pin, enable);
  }
  return setAngle(pin, constrain(angle, 0, LU9685_MAX_ANGLE));
}

bool P178_data_struct::setRange(uint8_t startPin, uint32_t startVarIndex, int nrPins)
{
  uint8_t endPin = LU9685_MAX_PINS;

  if (startPin == LU9685_ALL_PINS) {
    startPin = 0;
    nrPins   = -1;
  }

  if (startPin < LU9685_MAX_PINS) {
    if ((nrPins > 0) && (nrPins < LU9685_MAX_PINS) && ((startPin + nrPins) < LU9685_MAX_PINS)) {
      endPin = startPin + nrPins;
    }

    for (uint8_t pin = startPin; pin < endPin; ++pin) {
      const int angle = getCustomFloatVar(String(startVarIndex), -1);
      setAngle_internal(pin, angle);
      ++startVarIndex;
    }
    return update();
  }
  return false;
}

bool P178_data_struct::update()
{
  // Write all 20 values in a single I2C call
  Wire.beginTransmission(_address);
  Wire.write(0xFD);

  for (uint8_t pin = 0; pin < LU9685_MAX_PINS; ++pin) {
    Wire.write(getAngle(pin));
  }
  return Wire.endTransmission() == 0;
}

void P178_data_struct::setFrequency(uint16_t freq)
{
  I2C_write16_reg(
    _address,
    0xFC,
    constrain(freq, LU9685_MIN_FREQUENCY, LU9685_MAX_FREQUENCY));
}

String P178_data_struct::logPrefix() {
  return concat(formatToHex(_address, F("LU9685 0x"), 2),  F(": "));
}

String P178_data_struct::logPrefix(const __FlashStringHelper *poststr)
{
  return concat(logPrefix(), poststr);
}

void P178_data_struct::setAngle_internal(uint8_t pin, int angle)
{
  if (pin < LU9685_MAX_PINS) {
    if ((angle < 0) || (angle > LU9685_MAX_ANGLE)) {
      bitClear(_enabledPin, pin);
    } else {
      // Do not change the enabled state here
      _data[pin] = angle;
    }
  } else if (pin == LU9685_ALL_PINS) {
    for (uint8_t i = 0; i < LU9685_MAX_PINS; ++i) {
      setAngle_internal(i, angle);
    }
  }
}

uint8_t P178_data_struct::getAngle(uint8_t pin) const
{
  if ((pin < LU9685_MAX_PINS) && bitRead(_enabledPin, pin)) {
    return constrain(_data[pin], 0, LU9685_MAX_ANGLE);
  }
  return LU9685_OUTPUT_DISABLED;
}

#endif // ifdef USES_P178

#include "../PluginStructs/P073_data_struct.h"

#ifdef USES_P073

# ifdef ESP32
#  include <GPIO_Direct_Access.h>
# endif // ifdef ESP32

uint8_t P073_getDefaultDigits(uint8_t displayModel,
                              uint8_t digits) {
  const uint8_t digitsSet[] = { 4, 4, 6, 8, 0 }; // Fixed except 74HC595
  uint8_t bufLen{};

  if (displayModel < NR_ELEMENTS(digitsSet)) {
    bufLen = digitsSet[displayModel];
  }
  # if P073_USE_74HC595

  if (P073_74HC595_2_8DGT == displayModel) {
    bufLen = digits;
  }
  # endif // if P073_USE_74HC595

  return bufLen;
}

/**
 * Maps an ASCII character to a generic 7-segment usable smaller characterset,
 * for easy mapping to different font-sets, see P073_getFontChar()
 */
uint8_t P073_mapCharToFontPosition(char    character,
                                   uint8_t fontset) {
  uint8_t position = 10;

  # if P073_EXTRA_FONTS
  const String specialChars = F(" -^=/_%@.,;:+*#!?'\"<>\\()|");
  const String chnorux      = F("CHNORUX");

  switch (fontset)
  {
    case 1: // Siekoo
    case 2: // Siekoo with uppercase 'CHNORUX'

      if ((fontset == 2) && (chnorux.indexOf(character) > -1)) {
        position = chnorux.indexOf(character) + 35;
      } else if (isDigit(character)) {
        position = character - '0';
      } else if (isAlpha(character)) {
        position = character - (isLowerCase(character) ? 'a' : 'A') + 42;
      } else {
        const int idx = specialChars.indexOf(character);

        if (idx > -1) {
          position = idx + 10;
        }
      }
      break;
    case 3:  // dSEG7 (same table size as 7Dgt)
    default: // Original fontset (7Dgt)
  # endif // if P073_EXTRA_FONTS

  if (isDigit(character)) {
    position = character - '0';
  }
  else if (isAlpha(character)) {
    position = character - (isLowerCase(character) ? 'a' : 'A') + 16;
  } else {
    switch (character)
    {
      case ' ': position = 10;
        break;
      case '-': position = 11;
        break;
      case '^': position = 12;
        break; // degree
      case '=': position = 13;
        break;
      case '/': position = 14;
        break;
      case '_': position = 15;
        break;
    }
  }
  # if P073_EXTRA_FONTS
}

  # endif // if P073_EXTRA_FONTS
  return position;
}

/**
 * Get the 7-segment representation for an index into a specific 7-segment font
 * Fonts available depend on the build, but the 'DefaultCharTable' is always available
 */
uint8_t P073_getFontChar(uint8_t index,
                         uint8_t fontset) {
  # if P073_EXTRA_FONTS

  switch (fontset)
  {
    case 1:  // Siekoo
    case 2:  // Siekoo uppercase CHNORUX
      return pgm_read_byte(&(SiekooCharTable[index]));
    case 3:  // dSEG7
      return pgm_read_byte(&(Dseg7CharTable[index]));
    default: // Standard fontset
      return pgm_read_byte(&(DefaultCharTable[index]));
  }
  # else // if P073_EXTRA_FONTS
  return pgm_read_byte(&(DefaultCharTable[index]));
  # endif // if P073_EXTRA_FONTS
}

int32_t P073_parse_7dfont(struct EventStruct *event,
                          const String      & text) {
  int32_t fontNr = 0;

  # if P073_EXTRA_FONTS

  if (!text.isEmpty()) {
    const String fontArg = parseString(text, 1);
    fontNr = -1;

    if ((equals(fontArg, F("default"))) || (equals(fontArg, F("7dgt")))) {
      fontNr = 0;
    } else if (equals(fontArg, F("siekoo"))) {
      fontNr = 1;
    } else if (equals(fontArg, F("siekoo_upper"))) {
      fontNr = 2;
    } else if (equals(fontArg, F("dseg7"))) {
      fontNr = 3;
    } else if (!validIntFromString(text, fontNr)) {
      fontNr = -1; // reset if invalid
    }
  }
  # endif // if P073_EXTRA_FONTS
  return fontNr;
}

void P073_display_output_selector(const __FlashStringHelper *id, int16_t value) {
  const __FlashStringHelper *displout[] = {
    F("Manual"),
    F("Clock 24h - Blink"),
    F("Clock 24h - No Blink"),
    F("Clock 12h - Blink"),
    F("Clock 12h - No Blink"),
    F("Date"),
  };
  const int disploutOptions[] = {
    P073_DISP_MANUAL,
    P073_DISP_CLOCK24BLNK,
    P073_DISP_CLOCK24,
    P073_DISP_CLOCK12BLNK,
    P073_DISP_CLOCK12,
    P073_DISP_DATE,
  };
  const FormSelectorOptions selector(NR_ELEMENTS(disploutOptions), displout, disploutOptions);

  selector.addFormSelector(F("Display Output"), id, value);
}

# if P073_EXTRA_FONTS

void P073_font_selector(const __FlashStringHelper *id, int16_t value) {
  const __FlashStringHelper *fontset[] = {
    F("Default"),
    F("Siekoo"),
    F("Siekoo with uppercase 'CHNORUX'"),
    F("dSEG7"),
  };
  const FormSelectorOptions  selector(NR_ELEMENTS(fontset), fontset);

  selector.addFormSelector(F("Font set"), id, value);
  addFormNote(F("Check documentation for examples of the font sets."));
}

# endif // if P073_EXTRA_FONTS

/**
 * This function reverts the 7 databits/segmentbits so TM1637 and 74HC595 displays work with fonts designed for MAX7219.
 * Dot/colon bit is still bit 8
 */
uint8_t P073_revert7bits(uint8_t character) {
  uint8_t dpBit = character & 0x80; // Keep dot-bit if passed in
  uint8_t b     = character << 1;   // Pre-shift as only 7 bits to revert

  // Source: https://graphics.stanford.edu/~seander/bithacks.html#BitReverseObvious
  # ifdef ESP8266

  // 32 bit 7 operations variant, 64 bit operations is quite slow on ESP8266, but fast on most ESP32 variants
  b = ((b * 0x0802LU & 0x22110LU) | (b * 0x8020LU & 0x88440LU)) * 0x10101LU >> 16;
  # endif // ifdef ESP8266
  # ifdef ESP32

  // 64 bit 4 operations variant
  b = ((b * 0x80200802ULL) & 0x0884422110ULL) * 0x0101010101ULL >> 32;
  # endif // ifdef ESP32

  return b | dpBit; // Restore dot-bit
}

void P073_data_struct::init(struct EventStruct *event)
{
  ClearBuffer();
  pin1         = CONFIG_PIN1;
  pin2         = CONFIG_PIN2;
  pin3         = CONFIG_PIN3;
  displayModel = P073_CFG_DISPLAYTYPE;
  output       = P073_CFG_OUTPUTTYPE;
  brightness   = P073_CFG_BRIGHTNESS;
  periods      = bitRead(P073_CFG_FLAGS, P073_OPTION_PERIOD);
  hideDegree   = bitRead(P073_CFG_FLAGS, P073_OPTION_HIDEDEGREE);
  # if P073_SCROLL_TEXT
  txtScrolling = bitRead(P073_CFG_FLAGS, P073_OPTION_SCROLLTEXT);
  scrollFull   = bitRead(P073_CFG_FLAGS, P073_OPTION_SCROLLFULL);
  setScrollSpeed(P073_CFG_SCROLLSPEED);
  # endif // if P073_SCROLL_TEXT
  rightAlignTempMAX7219 = bitRead(P073_CFG_FLAGS, P073_OPTION_RIGHTALIGN);
  suppressLeading0      = bitRead(P073_CFG_FLAGS, P073_OPTION_SUPPRESS0);
  # if P073_BLINK_DOT
  blinkdot = bitRead(P073_CFG_FLAGS, P073_OPTION_BLINK_DOT);
  # endif // if P073_BLINK_DOT
  timesep = true;
  # if P073_EXTRA_FONTS
  fontset = P073_CFG_FONTSET;
  # endif // if P073_EXTRA_FONTS
  digits = P073_CFG_DIGITS;
  # if P073_USE_74HC595

  if ((digits > 0) && ((digits < 4) || (5 == digits) || (7 == digits) || (9 == digits) || (10 == digits) || (11 == digits))) {
    isSequential = true;

    if (1 == digits) {  // 2+2
      digits = 4;
    } else
    if (9 == digits) {  // 4 sequential
      digits = 4;
    } else
    if (10 == digits) { // 4+4 sequential
      digits = 8;
    } else
    if (7 == digits) {  // 3+3
      digits = 6;
    } else
    if (11 == digits) { // 3+4/4+3 sequential
      digits = 7;
    }
  }
  # endif // if P073_USE_74HC595

  if (0 == digits) {
    digits = P073_getDefaultDigits(P073_CFG_DISPLAYTYPE);
  }

  if ((digits > 0) && (digits < 4)) {
    hideDegree = true; // Hide degree symbol on small displays
  }

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, strformat(F("P073 : Digits: %d, model: %d, pins: %d, %d, %d"), digits, displayModel, pin1, pin2, pin3));
  }

  switch (displayModel)
  {
    case P073_TM1637_4DGTCOLON:
    case P073_TM1637_4DGTDOTS:
    case P073_TM1637_6DGT:
      tm1637_InitDisplay();
      tm1637_SetPowerBrightness(brightness / 2, true);

      if (output == P073_DISP_MANUAL) {
        tm1637_ClearDisplay();
      }
      break;
    case P073_MAX7219_8DGT:
      max7219_InitDisplay();
      delay(10); // small poweroff/poweron delay
      max7219_SetPowerBrightness(brightness, true);

      if (output == P073_DISP_MANUAL) {
        max7219_ClearDisplay();
      }
      break;
    # if P073_USE_74HC595
    case P073_74HC595_2_8DGT:
      hc595_InitDisplay();

      if (output == P073_DISP_MANUAL) {
        ClearBuffer();
        hc595_ToOutputBuffer();

        if (hc595_Sequential()) { // Sequential displays don't need continuous refreshing
          hc595_ShowBuffer();
        }
      }
      break;
    # endif // if P073_USE_74HC595
  }
}

# if P073_USE_74HC595

bool P073_data_struct::plugin_fifty_per_second(struct EventStruct *event) {
  #  ifdef P073_DEBUG
  counter50++;
  #  endif // ifdef P073_DEBUG

  if (P073_74HC595_2_8DGT == displayModel) {
    if (P073_HC595_MULTIPLEX) {
      hc595_ShowBuffer();
    }
    return true;
  }
  return false;
}

bool P073_data_struct::is74HC595Matrix() { return P073_74HC595_2_8DGT == displayModel && P073_HC595_MULTIPLEX; }

// ====================================
// ---- 74HC595 specific functions ----
// ====================================

void P073_data_struct::hc595_ShowBuffer() {
  #  if P073_USE_74HCMULTIPLEX
  const uint8_t hc595digit4[] = {
    0b00001000, // left segment
    0b00000100,
    0b00000010,
    0b00000001, // right segment
  };

  const uint8_t hc595digit8[] = {
    0b00010000, // left segment
    0b00100000,
    0b01000000,
    0b10000000,
    0b00000001,
    0b00000010,
    0b00000100,
    0b00001000, // right segment
  };
  #  endif // if P073_USE_74HCMULTIPLEX

  int8_t i    = digits - 1;
  int8_t stop = -1;
  int8_t incr = -1;

  #  if P073_USE_74HCMULTIPLEX

  if (P073_HC595_MULTIPLEX) {
    i    =  dspDgt;
    stop =  dspDgt + 1;
    incr =  1;
  }
  #  endif // if P073_USE_74HCMULTIPLEX

  for (; i != stop && i >= 0; i += incr) {
    shiftOut(pin1, pin2, MSBFIRST, outputbuffer[i]); // Digit data out

    // 2, 3 and some 4 digit modules use sequential digit values (in reversed order)
    // 4, 6 and 8 digit modules use multiplexing in LTR order
    #  if P073_USE_74HCMULTIPLEX
    uint8_t digit = 0xFF;

    if (P073_HC595_MULTIPLEX) {
      if (4 == digits) {
        digit = hc595digit4[i];
      } else
      if (6 == digits) {
        digit = hc595digit8[i + (i > 2 ? 1 : 0)];
      } else
      if (8 == digits) {
        digit = hc595digit8[i];
      }
    }

    if (digit != 0xFF) { // Select multiplexer digit, 0xFF is invalid
      shiftOut(pin1, pin2, MSBFIRST, digit);
    }
    #  endif // if P073_USE_74HCMULTIPLEX

    if ((P073_HC595_SEQUENTIAL && (0 == i)) || P073_HC595_MULTIPLEX) {
      digitalWrite(pin3, LOW); // Clock data
      digitalWrite(pin3, HIGH);
    }
  }

  if (i >= digits) {
    dspDgt = 0;
  } else {
    dspDgt = i;
  }

  #  ifdef P073_DEBUG

  // TODO disable log
  // if ((counter50 % 200 == 0) || P073_HC595_SEQUENTIAL) {
  //   addLog(LOG_LEVEL_INFO, strformat(F("P073: hc595_ShowBuffer (end) dgt:%d i:%d stop:%d incr:%d pin1: %d pin2: %d pin3: %d"),
  //                                    digits, i, stop, incr, pin1, pin2, pin3));
  // }
  #  endif // ifdef P073_DEBUG
}

void P073_data_struct::hc595_ToOutputBuffer() {
  for (uint8_t i = 0; i < 8; ++i) {
    uint8_t value;

    // 74HC595 uses inverted data, compared to MAX7219/TM1637
    value = ~P073_getFontChar(showbuffer[i], fontset);

    if (showperiods[i]) {
      value &= 0x7F;
    }
    outputbuffer[i] = P073_revert7bits(value); // Rotate bits 6..0
  }
}

void P073_data_struct::hc595_AdjustBuffer() {
  if (digits < 8) {
    const uint8_t delta = 8 - digits;

    for (uint8_t i = 0; i < digits; ++i) {
      showbuffer[i] = showbuffer[i + delta];
    }
  }
}

void P073_data_struct::hc595_InitDisplay() {
  pinMode(pin1, OUTPUT);
  pinMode(pin2, OUTPUT);
  pinMode(pin3, OUTPUT);
  digitalWrite(pin3, HIGH);
}

# endif // if P073_USE_74HC595

void P073_data_struct::FillBufferWithTime(bool    sevendgt_now,
                                          uint8_t sevendgt_hours,
                                          uint8_t sevendgt_minutes,
                                          uint8_t sevendgt_seconds,
                                          bool    flag12h,
                                          bool    suppressLeading0) {
  ClearBuffer();

  if (sevendgt_now) {
    sevendgt_hours   = node_time.hour();
    sevendgt_minutes = node_time.minute();
    sevendgt_seconds = node_time.second();
  }

  if (flag12h && (sevendgt_hours > 12)) {
    sevendgt_hours -= 12; // if flag 12h is TRUE and h>12 adjust subtracting 12
  }

  if (flag12h && (sevendgt_hours == 0)) {
    sevendgt_hours = 12; // if flag 12h is TRUE and h=0  adjust to h=12
  }
  Put4NumbersInBuffer(sevendgt_hours, sevendgt_minutes, sevendgt_seconds, -1
                      # if P073_SUPPRESS_ZERO
                      , suppressLeading0
                      # endif // if P073_SUPPRESS_ZERO
                      );
}

void P073_data_struct::FillBufferWithDate(bool    sevendgt_now,
                                          uint8_t sevendgt_day,
                                          uint8_t sevendgt_month,
                                          int     sevendgt_year,
                                          bool    suppressLeading0) {
  ClearBuffer();
  int sevendgt_year0 = sevendgt_year;

  if (sevendgt_now) {
    sevendgt_day   = node_time.day();
    sevendgt_month = node_time.month();
    sevendgt_year0 = node_time.year();
  } else if (sevendgt_year0 < 100) {
    sevendgt_year0 += 2000;
  }
  const uint8_t sevendgt_year1 = static_cast<uint8_t>(sevendgt_year0 / 100);
  const uint8_t sevendgt_year2 = static_cast<uint8_t>(sevendgt_year0 % 100);

  Put4NumbersInBuffer(sevendgt_day, sevendgt_month, sevendgt_year1, sevendgt_year2
                      # if P073_SUPPRESS_ZERO
                      , suppressLeading0
                      # endif // if P073_SUPPRESS_ZERO
                      );
}

void P073_data_struct::Put4NumbersInBuffer(const uint8_t nr1,
                                           const uint8_t nr2,
                                           const uint8_t nr3,
                                           const int8_t  nr4
                                           # if          P073_SUPPRESS_ZERO
                                           , const bool  suppressLeading0
                                           # endif // if P073_SUPPRESS_ZERO
                                           ) {
  showbuffer[0] = static_cast<uint8_t>(nr1 / 10);
  showbuffer[1] = nr1 % 10;
  showbuffer[2] = static_cast<uint8_t>(nr2 / 10);
  showbuffer[3] = nr2 % 10;
  showbuffer[4] = static_cast<uint8_t>(nr3 / 10);
  showbuffer[5] = nr3 % 10;

  if (nr4 > -1) {
    showbuffer[6] = static_cast<uint8_t>(nr4 / 10);
    showbuffer[7] = nr4 % 10;
  }
  # if P073_SUPPRESS_ZERO

  if (suppressLeading0 && (showbuffer[0] == 0)) { showbuffer[0] = 10; } // set to space
  # endif // if P073_SUPPRESS_ZERO
}

void P073_data_struct::FillBufferWithNumber(const String& number) {
  ClearBuffer();

  if (number.length() == 0) {
    return;
  }
  int8_t p073_index = 7;

  dotpos = -1; // -1 means no dot to display

  for (int i = number.length() - 1; i >= 0 && p073_index >= 0; --i) {
    const char p073_tmpchar = number.charAt(i);

    if (p073_tmpchar == '.') { // dot
      dotpos = p073_index;
    } else {
      showbuffer[p073_index] = P073_mapCharToFontPosition(p073_tmpchar, fontset);
      p073_index--;
    }
  }
}

void P073_data_struct::FillBufferWithTemp(int temperature) {
  ClearBuffer();
  char p073_digit[8];
  const bool between10and0      = ((temperature < 10) && (temperature >= 0)); // To have a zero prefix (0.x and -0.x) display between 0.9
  const bool between0andMinus10 = ((temperature < 0) && (temperature > -10)); // and -0.9 degrees,as all display types use 1 digit for
                                                                              // temperatures between 10.0 and -10.0
  String format;

  if (hideDegree) {
    format = (between10and0 ? F("      %02d") : (between0andMinus10 ? F("     %03d") : F("%8d")));
  } else {
    format = (between10and0 ? F("     %02d") : (between0andMinus10 ? F("    %03d") : F("%7d")));
  }
  sprintf_P(p073_digit, format.c_str(), temperature);
  const size_t p073_numlenght = strlen(p073_digit);

  for (size_t i = 0; i < p073_numlenght; ++i) {
    showbuffer[i] = P073_mapCharToFontPosition(p073_digit[i], fontset);
  }

  if (!hideDegree) {
    showbuffer[7] = 12; // degree "°"
  }
}

# if P073_7DDT_COMMAND

/**
 * FillBufferWithDualTemp()
 * leftTemperature or rightTempareature < -100.0 then shows dashes
 */
void P073_data_struct::FillBufferWithDualTemp(int  leftTemperature,
                                              bool leftWithDecimal,
                                              int  rightTemperature,
                                              bool rightWithDecimal) {
  ClearBuffer();
  char   p073_digit[8];
  String format;
  const bool leftBetween10and0 = (leftWithDecimal && (leftTemperature < 10) && (leftTemperature >= 0));

  // To have a zero prefix (0.x and -0.x) display between 0.9 and -0.9 degrees,
  // as all display types use 1 digit for temperatures between 10.0 and -10.0
  const bool leftBetween0andMinus10 = (leftWithDecimal && (leftTemperature < 0) && (leftTemperature > -10));

  if (hideDegree) {
    // Include a space for compensation of the degree symbol
    format = (leftBetween10and0 ? F("  %02d") : (leftBetween0andMinus10 ? F(" %03d") : leftTemperature < -1000 ? F("----") : F("%4d")));
  } else {
    // Include a space for compensation of the degree symbol
    format = (leftBetween10and0 ? F(" %02d ") : (leftBetween0andMinus10 ? F("%03d ") : leftTemperature < -100 ? F("----") : F("%3d ")));
  }
  bool rightBetween10and0 = (rightWithDecimal && (rightTemperature < 10) && (rightTemperature >= 0));

  // To have a zero prefix (0.x and -0.x) display between 0.9 and -0.9 degrees,
  // as all display types use 1 digit for temperatures between 10.0 and -10.0
  const bool rightBetween0andMinus10 = (rightWithDecimal && (rightTemperature < 0) && (rightTemperature > -10));

  if (hideDegree) {
    format += (rightBetween10and0 ? F("  %02d") : (rightBetween0andMinus10 ? F(" %03d") : rightTemperature < -1000 ? F("----") : F("%4d")));
  } else {
    format += (rightBetween10and0 ? F(" %02d") : (rightBetween0andMinus10 ? F("%03d") : rightTemperature < -100 ? F("----") : F("%3d")));
  }
  sprintf_P(p073_digit, format.c_str(), leftTemperature, rightTemperature);
  const size_t p073_numlenght = strlen(p073_digit);

  for (size_t i = 0; i < p073_numlenght; ++i) {
    showbuffer[i] = P073_mapCharToFontPosition(p073_digit[i], fontset);
  }

  if (!hideDegree) {
    if (leftTemperature  > -100.0) {
      showbuffer[3] = 12; // degree "°"
    }

    if (rightTemperature > -100.0) {
      showbuffer[7] = 12; // degree "°"
    }
  }

  // addLog(LOG_LEVEL_INFO, concat(F("7dgt format: "), format));
}

# endif // if P073_7DDT_COMMAND

void P073_data_struct::FillBufferWithString(const String& textToShow,
                                            bool          useBinaryData) {
  # if P073_7DBIN_COMMAND
  binaryData = useBinaryData;
  # endif // if P073_7DBIN_COMMAND
  ClearBuffer();
  const int p073_txtlength = textToShow.length();

  int p = 0;

  for (int i = 0; i < p073_txtlength && p <= 8; ++i) { // p <= 8 to allow a period after last digit
    if (periods
        && textToShow.charAt(i) == '.'
        # if P073_7DBIN_COMMAND
        && !binaryData
        # endif // if P073_7DBIN_COMMAND
        ) {         // If setting periods true
      if (p == 0) { // Text starts with a period, becomes a space with a dot
        showperiods[p] = true;
        p++;
      } else {
        // if (p > 0) {
        showperiods[p - 1] = true;                        // The period displays as a dot on the previous digit!
      }

      if ((i > 0) && (textToShow.charAt(i - 1) == '.')) { // Handle consecutive periods
        p++;

        if ((p - 1) < 8) {
          showperiods[p - 1] = true; // The period displays as a dot on the previous digit!
        }
      }
    } else if (p < 8) {
      # if P073_7DBIN_COMMAND
      showbuffer[p] = useBinaryData ? textToShow.charAt(i) : P073_mapCharToFontPosition(textToShow.charAt(i), fontset);
      # else // if fP073_7DBIN_COMMAND
      showbuffer[p] = P073_mapCharToFontPosition(textToShow.charAt(i), fontset);
      # endif // if P073_7DBIN_COMMAND
      p++;
    }
  }
  # ifdef P073_DEBUG
  LogBufferContent(F("7dtext"));
  # endif // ifdef P073_DEBUG
}

# if P073_SCROLL_TEXT

int P073_data_struct::getEffectiveTextLength(const String& text) {
  const int textLength = text.length();
  int p                = 0;

  for (int i = 0; i < textLength; ++i) {
    if (periods && (text.charAt(i) == '.')) { // If setting periods true
      if (p == 0) {                           // Text starts with a period, becomes a space with a dot
        p++;
      }

      if ((i > 0) && (text.charAt(i - 1) == '.')) { // Handle consecutive periods
        p++;
      }
    } else {
      p++;
    }
  }
  return p;
}

bool P073_data_struct::NextScroll() {
  bool result = false;

  if (isScrollEnabled() && (!_textToScroll.isEmpty())) {
    if ((scrollCount > 0) && (scrollCount < 0xFFFF)) { scrollCount--; }

    if (scrollCount == 0) {
      scrollCount = 0xFFFF; // Max value to avoid interference when scrolling long texts
      result      = true;
      const int bufToFill      = P073_getDefaultDigits(displayModel, digits);
      const int p073_txtlength = _textToScroll.length();
      ClearBuffer();

      int p = 0;

      for (int i = scrollPos; i < p073_txtlength && p <= bufToFill; ++i) { // p <= bufToFill to allow a period after last digit
        if (periods
            && _textToScroll.charAt(i) == '.'
            #  if P073_7DBIN_COMMAND
            && !binaryData
            #  endif // if P073_7DBIN_COMMAND
            ) {         // If setting periods true
          if (p == 0) { // Text starts with a period, becomes a space with a dot
            showperiods[p] = true;
            p++;
          } else {
            showperiods[p - 1] = true;                                   // The period displays as a dot on the previous digit!
          }

          if ((i > scrollPos) && (_textToScroll.charAt(i - 1) == '.')) { // Handle consecutive periods
            showperiods[p - 1] = true;                                   // The period displays as a dot on the previous digit!
            p++;
          }
        } else if (p < bufToFill) {
          #  if P073_7DBIN_COMMAND
          showbuffer[p] = binaryData ?
                          _textToScroll.charAt(i) :
                          P073_mapCharToFontPosition(_textToScroll.charAt(i), fontset);
          #  else // if P073_7DBIN_COMMAND
          showbuffer[p] = P073_mapCharToFontPosition(_textToScroll.charAt(i), fontset);
          #  endif // if P073_7DBIN_COMMAND
          p++;
        }
      }
      scrollPos++;

      if (scrollPos > _textToScroll.length() - bufToFill) {
        scrollPos = 0;            // Restart when all text displayed
      }
      scrollCount = _scrollSpeed; // Restart countdown
      #  ifdef P073_DEBUG
      LogBufferContent(F("nextScroll"));
      #  endif // ifdef P073_DEBUG
    }
  }
  return result;
}

void P073_data_struct::setTextToScroll(const String& text) {
  free_string(_textToScroll);

  if (!text.isEmpty()) {
    const int bufToFill = P073_getDefaultDigits(displayModel, digits);
    _textToScroll.reserve(text.length() + bufToFill + (scrollFull ? bufToFill : 0));

    for (int i = 0; scrollFull && i < bufToFill; ++i) { // Scroll text in from the right, so start with all spaces
      _textToScroll +=
        #  if P073_7DBIN_COMMAND
        binaryData ? (char)0x00 :
        #  endif // if P073_7DBIN_COMMAND
        ' ';
    }
    _textToScroll += text;

    for (int i = 0; i < bufToFill; ++i) { // Scroll text off completely before restarting
      _textToScroll +=
        #  if P073_7DBIN_COMMAND
        binaryData ? (char)0x00 :
        #  endif // if P073_7DBIN_COMMAND
        ' ';
    }
  }
  scrollCount = _scrollSpeed;
  scrollPos   = 0;
  #  if P073_7DBIN_COMMAND
  binaryData = false;
  #  endif // if P073_7DBIN_COMMAND
}

void P073_data_struct::setScrollSpeed(uint8_t speed) {
  _scrollSpeed = speed;
  scrollCount  = _scrollSpeed;
  scrollPos    = 0;
}

bool P073_data_struct::isScrollEnabled()             { return txtScrolling && scrollAllowed; }

void P073_data_struct::setScrollEnabled(bool scroll) { scrollAllowed = scroll; }

# endif // if P073_SCROLL_TEXT

# if P073_7DBIN_COMMAND

void P073_data_struct::setBinaryData(const String& data) {
  binaryData = true;
  #  if P073_SCROLL_TEXT
  setTextToScroll(data);
  binaryData  = true; // is reset in setTextToScroll
  scrollCount = _scrollSpeed;
  scrollPos   = 0;
  #  else // if P073_SCROLL_TEXT
  _textToScroll = data;
  #  endif // if P073_SCROLL_TEXT
}

# endif // if P073_7DBIN_COMMAND

# ifdef P073_DEBUG

void P073_data_struct::LogBufferContent(String prefix) {
  String log;

  if (loglevelActiveFor(LOG_LEVEL_INFO) &&
      log.reserve(48)) {
    log = strformat(F("%s buffer: periods: %c "), prefix.c_str(), periods ? 't' : 'f');

    for (uint8_t i = 0; i < 8; ++i) {
      if (i > 0) { log += ','; }
      log += formatToHex(showbuffer[i]);
      log += ',';
      log += showperiods[i] ? F(".") : F("");
    }
    addLogMove(LOG_LEVEL_INFO, log);
  }
}

# endif // ifdef P073_DEBUG

// in case of error show all dashes
void P073_data_struct::FillBufferWithDash() { memset(showbuffer, 11, sizeof(showbuffer)); }

void P073_data_struct::ClearBuffer() {
  memset(showbuffer,
         # if P073_7DBIN_COMMAND
         binaryData ? 0 :
         # endif // if P073_7DBIN_COMMAND
         10, sizeof(showbuffer));

  for (uint8_t i = 0; i < 8; ++i) {
    showperiods[i] = false;
  }
}

uint8_t P073_data_struct::tm1637_getFontChar(uint8_t index,
                                             uint8_t fontset) { return P073_revert7bits(P073_getFontChar(index, fontset)); }

bool    P073_data_struct::plugin_once_a_second(struct EventStruct *event) {
  if (output == P073_DISP_MANUAL) {
    return false;
  }

  if ((output == P073_DISP_CLOCK24BLNK) ||
      (output == P073_DISP_CLOCK12BLNK)) {
    timesep = !timesep;
  } else {
    timesep = true;
  }

  if (output == P073_DISP_DATE) {
    FillBufferWithDate(true, 0, 0, 0,
                       # if P073_SUPPRESS_ZERO
                       suppressLeading0
                       # else // if P073_SUPPRESS_ZERO
                       false
                       # endif // if P073_SUPPRESS_ZERO
                       );
  } else {
    FillBufferWithTime(true, 0, 0, 0, !((output == P073_DISP_CLOCK24BLNK) ||
                                        (output == P073_DISP_CLOCK24)),
                       # if P073_SUPPRESS_ZERO
                       suppressLeading0
                       # else // if P073_SUPPRESS_ZERO
                       false
                       # endif // if P073_SUPPRESS_ZERO
                       );
  }

  # if P073_BLINK_DOT

  if (blinkdot &&
      ((output == P073_DISP_CLOCK24BLNK) ||
       (output == P073_DISP_CLOCK12BLNK))) {
    showperiods[1] = timesep; // Blink dot on second digit
  }
  # endif // if P073_BLINK_DOT

  switch (displayModel)
  {
    case P073_TM1637_4DGTCOLON:
    case P073_TM1637_4DGTDOTS:
      tm1637_ShowTimeTemp4(timesep, 0);
      break;
    case P073_TM1637_6DGT:

      if (P073_CFG_OUTPUTTYPE == P073_DISP_DATE) {
        tm1637_ShowDate6();
      } else {
        tm1637_ShowTime6();
      }
      break;
    case P073_MAX7219_8DGT:

      if (P073_CFG_OUTPUTTYPE == P073_DISP_DATE) {
        max7219_ShowDate();
      } else {
        max7219_ShowTime(timesep);
      }
      break;
    # if P073_USE_74HC595
    case P073_74HC595_2_8DGT:
      hc595_AdjustBuffer();
      hc595_ToOutputBuffer();

      if (hc595_Sequential()) { // Sequential displays don't need continuous refreshing
        hc595_ShowBuffer();
      }
      break;
    # endif // if P073_USE_74HC595
  }
  return true;
}

# if P073_SCROLL_TEXT

bool P073_data_struct::plugin_ten_per_second(struct EventStruct *event) {
  if ((output != P073_DISP_MANUAL) || !isScrollEnabled()) {
    return false;
  }

  if (NextScroll()) {
    switch (displayModel)
    {
      case P073_TM1637_4DGTCOLON:
      case P073_TM1637_4DGTDOTS:
      {
        tm1637_ShowBuffer(0, 4
                          #  if P073_7DBIN_COMMAND
                          , binaryData
                          #  endif // if P073_7DBIN_COMMAND
                          );
        break;
      }
      case P073_TM1637_6DGT:
      {
        tm1637_SwapDigitInBuffer(0); // only needed for 6-digits displays
        tm1637_ShowBuffer(0, 6
                          #  if P073_7DBIN_COMMAND
                          , binaryData
                          #  endif // if P073_7DBIN_COMMAND
                          );
        break;
      }
      case P073_MAX7219_8DGT:
      {
        dotpos = -1; // avoid to display the dot
        max7219_ShowBuffer();
        break;
      }
      #  if P073_USE_74HC595
      case P073_74HC595_2_8DGT:
      {
        hc595_ToOutputBuffer();

        if (hc595_Sequential()) { // Sequential displays don't need continuous refreshing
          hc595_ShowBuffer();
        }
        break;
      }
      #  endif // if P073_USE_74HC595
    }
  }
  return true;
}

# endif // if P073_SCROLL_TEXT

const char p073_commands[] PROGMEM =
  "7dn|7dt|"
  # if P073_7DDT_COMMAND
  "7ddt|"
  # endif // if P073_7DDT_COMMAND
  "7dst|7dsd|7dtext|"
  # if P073_EXTRA_FONTS
  "7dfont|"
  # endif // if P073_EXTRA_FONTS
  # if P073_7DBIN_COMMAND
  "7dbin|"
  # endif // if P073_7DBIN_COMMAND
  "7don|7doff|7db|7output|"
;
enum class p073_commands_e : int8_t {
  invalid = -1,
  c7dn    = 0,
  c7dt,
  # if P073_7DDT_COMMAND
  c7ddt,
  # endif // if P073_7DDT_COMMAND
  c7dst,
  c7dsd,
  c7dtext,
  # if P073_EXTRA_FONTS
  c7dfont,
  # endif // if P073_EXTRA_FONTS
  # if P073_7DBIN_COMMAND
  c7dbin,
  # endif // if P073_7DBIN_COMMAND
  c7don,
  c7doff,
  c7db,
  c7output,

};

bool P073_data_struct::plugin_write(struct EventStruct *event,
                                    const String      & string) {
  const String cmd_s = parseString(string, 1);

  if ((cmd_s.length() < 3) || (cmd_s[0] != '7')) { return false; }

  # if P073_SCROLL_TEXT
  const bool currentScroll = isScrollEnabled(); // Save current state
  bool newScroll           = false;             // disable scroll if command changes
  setScrollEnabled(false);
  # endif // if P073_SCROLL_TEXT

  const int cmd_i = GetCommandCode(cmd_s.c_str(), p073_commands);

  if (cmd_i < 0) { return false; } // Fail fast

  const p073_commands_e cmd = static_cast<p073_commands_e>(cmd_i);

  const String text = parseStringToEndKeepCase(string, 2);
  bool success      = false;
  bool displayon    = false;

  switch (cmd)
  {
    case p073_commands_e::c7dn:
      return plugin_write_7dn(event, text);
    case p073_commands_e::c7dt:
      return plugin_write_7dt(text);
    # if P073_7DDT_COMMAND
    case p073_commands_e::c7ddt:
      return plugin_write_7ddt(text);
    # endif // if P073_7DDT_COMMAND
    case p073_commands_e::c7dst:
      return plugin_write_7dst(event);
    case p073_commands_e::c7dsd:
      return plugin_write_7dsd(event);
    case p073_commands_e::c7dtext:
      # if P073_SCROLL_TEXT
      setScrollEnabled(true); // Scrolling allowed for 7dtext command
      # endif // if P073_SCROLL_TEXT
      return plugin_write_7dtext(text);
    # if P073_EXTRA_FONTS
    case p073_commands_e::c7dfont:
      #  if P073_SCROLL_TEXT
      setScrollEnabled(currentScroll); // Restore state
      #  endif // if P073_SCROLL_TEXT
      return plugin_write_7dfont(event, text);
    # endif // if P073_EXTRA_FONTS
    # if P073_7DBIN_COMMAND
    case p073_commands_e::c7dbin:
      #  if P073_SCROLL_TEXT
      setScrollEnabled(true); // Scrolling allowed for 7dbin command
      #  endif // if P073_SCROLL_TEXT
      return plugin_write_7dbin(text);
    # endif // if P073_7DBIN_COMMAND
    case p073_commands_e::c7don:
      # if P073_SCROLL_TEXT
      newScroll = currentScroll; // Restore state
      # endif // if P073_SCROLL_TEXT
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_INFO, F("7DGT : Display ON"));
      # endif // ifndef BUILD_NO_DEBUG
      displayon = true;
      success   = true;
      break;
    case p073_commands_e::c7doff:
      # if P073_SCROLL_TEXT
      newScroll = currentScroll; // Restore state
      # endif // if P073_SCROLL_TEXT
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_INFO, F("7DGT : Display OFF"));
      # endif // ifndef BUILD_NO_DEBUG
      displayon = false;
      success   = true;
      break;
    case p073_commands_e::c7db:
      # if P073_SCROLL_TEXT
      newScroll = currentScroll; // Restore state
      # endif // if P073_SCROLL_TEXT

      if ((event->Par1 >= 0) && (event->Par1 < 16)) {
        # ifndef BUILD_NO_DEBUG

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          addLog(LOG_LEVEL_INFO, concat(F("7DGT : Brightness="), event->Par1));
        }
        # endif // ifndef BUILD_NO_DEBUG
        brightness          = event->Par1;
        P073_CFG_BRIGHTNESS = event->Par1;
        displayon           = true;
        success             = true;
      }
      break;
    case p073_commands_e::c7output:

      if ((event->Par1 >= 0) && (event->Par1 < 6)) { // 0:"Manual",1:"Clock 24h - Blink",2:"Clock 24h - No Blink",
                                                     // 3:"Clock 12h - Blink",4:"Clock 12h - No Blink",5:"Date"
        # ifndef BUILD_NO_DEBUG

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          addLog(LOG_LEVEL_INFO, concat(F("7DGT : Display output="), event->Par1));
        }
        # endif // ifndef BUILD_NO_DEBUG
        output              = event->Par1;
        P073_CFG_OUTPUTTYPE = event->Par1;
        displayon           = true;
        success             = true;
        # if P073_SCROLL_TEXT

        if (event->Par1 == 0) { newScroll = currentScroll; } // Restore state
        # endif // if P073_SCROLL_TEXT
      }
      break;
    case p073_commands_e::invalid:
      break;
  }

  if (success) {
    # if P073_SCROLL_TEXT
    setScrollEnabled(newScroll);
    # endif // if P073_SCROLL_TEXT

    switch (displayModel)
    {
      case P073_TM1637_4DGTCOLON:
      case P073_TM1637_4DGTDOTS:
      case P073_TM1637_6DGT:
        tm1637_SetPowerBrightness(brightness / 2, displayon);
        break;
      case P073_MAX7219_8DGT:
        max7219_SetPowerBrightness(brightness, displayon);
        break;
      # if P073_USE_74HC595
      case P073_74HC595_2_8DGT:
        // 74HC595 don't have a brightness setting
        break;
      # endif // if P073_USE_74HC595
    }
  }
  return success;
}

void P073_data_struct::getDisplayLimits(int32_t& lLimit,
                                        int32_t& uLimit,
                                        int8_t   offset,
                                        uint8_t  digits) {
  uint8_t dgts = P073_getDefaultDigits(displayModel);

  # if P073_USE_74HC595

  if (P073_74HC595_2_8DGT == displayModel) {
    dgts = digits;
  }
  # endif // if P073_USE_74HC595
  dgts  -= offset;           // Subtract an offset, used for extra symbol
  lLimit = -pow10(dgts - 1); // Lowest value we can display - 1
  uLimit = pow10(dgts);      // Highest value we can display + 1
  // TODO disable log
  // addLog(LOG_LEVEL_INFO, strformat(F("P073: limits: %d digits(%d), lower: %d, upper: %d"), dgts, offset, lLimit, uLimit));
}

bool P073_data_struct::plugin_write_7dn(struct EventStruct *event,
                                        const String      & text) {
  if (output != P073_DISP_MANUAL) {
    return false;
  }

  # ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, concat(F("7DGT : Show Number="), event->Par1));
  }
  # endif // ifndef BUILD_NO_DEBUG

  int32_t lLimit = 0;
  int32_t uLimit = 0;
  getDisplayLimits(lLimit, uLimit, 0, digits);

  if (!text.isEmpty()) {
    if ((event->Par1 > lLimit) && (event->Par1 < uLimit)) {
      FillBufferWithNumber(text.c_str());
    } else {
      FillBufferWithDash();
    }
  }

  switch (displayModel)
  {
    case P073_TM1637_4DGTCOLON:
    case P073_TM1637_4DGTDOTS:
      tm1637_ShowBuffer(TM1637_4DIGIT, 8);
      break;
    case P073_TM1637_6DGT:
      tm1637_SwapDigitInBuffer(2); // only needed for 6-digits displays
      tm1637_ShowBuffer(TM1637_6DIGIT, 8);
      break;
    case P073_MAX7219_8DGT:
      max7219_ShowBuffer();
      break;
    # if P073_USE_74HC595
    case P073_74HC595_2_8DGT:
      hc595_AdjustBuffer();
      hc595_ToOutputBuffer();

      if (hc595_Sequential()) { // Sequential displays don't need continuous refreshing
        hc595_ShowBuffer();
      }
      break;
    # endif // if P073_USE_74HC595
  }
  return true;
}

bool P073_data_struct::plugin_write_7dt(const String& text) {
  if  (output != P073_DISP_MANUAL) {
    return false;
  }

  float p073_temptemp    = 0;
  bool  p073_tempflagdot = false;

  if (!text.isEmpty()) {
    validFloatFromString(text, p073_temptemp);
  }

  # ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, concat(F("7DGT : Show Temperature="), p073_temptemp));
  }
  # endif // ifndef BUILD_NO_DEBUG

  int32_t lLimit = 0;
  int32_t uLimit = 0;
  getDisplayLimits(lLimit, uLimit, hideDegree ? 0 : 1, digits);
  float lLimitErr = lLimit + 0.1f;
  float uLimitErr = uLimit - 1.0f;
  float lLimitDec = lLimit / 10.0f;
  float uLimitDec = uLimit / 10.0f;

  // TODO disable log
  // addLog(LOG_LEVEL_INFO, strformat(F("P073: 7dt: lErr: %.1f, uErr: %.1f, lDec: %.1f, uDec: %.1f"),
  //                                  lLimitErr, uLimitErr, lLimitDec, uLimitDec));

  if ((p073_temptemp > uLimitErr) || (p073_temptemp < lLimitErr)) {
    FillBufferWithDash();
  } else {
    if ((p073_temptemp < uLimitDec) && (p073_temptemp > lLimitDec)) {
      p073_temptemp    = roundf(p073_temptemp * 10.0f);
      p073_tempflagdot = true;
    }
    FillBufferWithTemp(p073_temptemp);
  }

  switch (displayModel)
  {
    case P073_TM1637_4DGTCOLON:
    case P073_TM1637_4DGTDOTS:
    case P073_TM1637_6DGT:

      if ((p073_temptemp == 0) && p073_tempflagdot) {
        showbuffer[5] = 0;
      }

      if (P073_TM1637_6DGT == displayModel) {
        tm1637_ShowTemp6(p073_tempflagdot);
      } else {
        tm1637_ShowTimeTemp4(p073_tempflagdot, 4);
      }
      break;
    case P073_MAX7219_8DGT:
      # ifdef P073_DEBUG

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLogMove(LOG_LEVEL_INFO, concat(F("7DGT : 7dt preprocessed ="), p073_temptemp));
      }
      # endif // ifdef P073_DEBUG

      max7219_ShowTemp(hideDegree ? 6 : 5, -1);
      break;
    # if P073_USE_74HC595
    case P073_74HC595_2_8DGT:
      hc595_AdjustBuffer();
      hc595_ToOutputBuffer();

      if (hc595_Sequential()) { // Sequential displays don't need continuous refreshing
        hc595_ShowBuffer();
      }
      break;
    # endif // if P073_USE_74HC595
  }
  # ifdef P073_DEBUG
  LogBufferContent(F("7dt"));
  # endif // ifdef P073_DEBUG
  return true;
}

# if P073_7DDT_COMMAND

bool P073_data_struct::plugin_write_7ddt(const String& text) {
  if (output != P073_DISP_MANUAL) {
    return false;
  }

  float p073_lefttemp    = 0.0f;
  float p073_righttemp   = 0.0f;
  bool  p073_tempflagdot = false;

  if (!text.isEmpty()) {
    validFloatFromString(parseString(text, 1), p073_lefttemp);

    if (text.indexOf(',') > -1) {
      validFloatFromString(parseString(text, 2), p073_righttemp);
    }
  }

  #  ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, strformat(F("7DGT : Dual Temperature 1st=%.2f 2nd=%.2f"), p073_lefttemp, p073_righttemp));
  }
  #  endif // ifndef BUILD_NO_DEBUG

  switch (displayModel)
  {
    case P073_TM1637_4DGTCOLON:
    case P073_TM1637_4DGTDOTS:
    case P073_TM1637_6DGT:
    {
      FillBufferWithDash();

      if (displayModel == P073_TM1637_6DGT) {
        tm1637_ShowTemp6(p073_tempflagdot);
      } else {
        tm1637_ShowTimeTemp4(p073_tempflagdot, 4);
      }
      break;
    }
    case P073_MAX7219_8DGT:
    case P073_74HC595_2_8DGT:
    {
      uint8_t firstDot   = -1; // No decimals is no dots
      uint8_t secondDot  = -1;
      float   hideFactor = hideDegree ? 10.0f : 1.0f;

      if ((p073_lefttemp > 999.99f * hideFactor) || (p073_lefttemp < -99.99f * hideFactor)) {
        p073_lefttemp = -101.0f * hideFactor; // Triggers on -100
      } else {
        if ((p073_lefttemp < 100.0f * hideFactor) && (p073_lefttemp > -10.0f * hideFactor)) {
          p073_lefttemp = roundf(p073_lefttemp * 10.0f);
          firstDot      = hideDegree ? 2 : 1;
        }
      }

      if ((p073_righttemp > 999.99f * hideFactor) || (p073_righttemp < -99.99f * hideFactor)) {
        p073_righttemp = -101.0f * hideFactor;
      } else {
        if ((p073_righttemp < 100.0f * hideFactor) && (p073_righttemp > -10.0f * hideFactor)) {
          p073_righttemp = roundf(p073_righttemp * 10.0f);
          secondDot      = hideDegree ? 6 : 5;
        }
      }

      #  ifdef P073_DEBUG

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLog(LOG_LEVEL_INFO, strformat(F("7DGT : 7ddt preprocessed 1st=%.2f 2nd=%.2f"), p073_lefttemp, p073_righttemp));
      }
      #  endif // ifdef P073_DEBUG

      FillBufferWithDualTemp(p073_lefttemp, firstDot > -1, p073_righttemp, secondDot > -1);

      if (P073_MAX7219_8DGT == displayModel) {
        bool alignSave = rightAlignTempMAX7219; // Save setting
        rightAlignTempMAX7219 = true;

        max7219_ShowTemp(firstDot, secondDot);

        rightAlignTempMAX7219 = alignSave; // Restore
      #  if P073_USE_74HC595
      } else

      // if (P073_74HC595_2_8DGT == P073_data->displayModel)
      {
        if (digits < 8) {
          FillBufferWithDash();
        }
        hc595_ToOutputBuffer();

        if (hc595_Sequential()) { // Sequential displays don't need continuous refreshing
          hc595_ShowBuffer();
        }
      #  endif // if P073_USE_74HC595
      }

      break;
    }
  }
  #  ifdef P073_DEBUG
  LogBufferContent(F("7ddt"));
  #  endif // ifdef P073_DEBUG
  return true;
}

# endif // if P073_7DDT_COMMAND

bool P073_data_struct::plugin_write_7dst(struct EventStruct *event) {
  if (output != P073_DISP_MANUAL) {
    return false;
  }

  # ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, strformat(F("7DGT : Show Time=%02d:%02d:%02d"), event->Par1, event->Par2, event->Par3));
  }
  # endif // ifndef BUILD_NO_DEBUG
  timesep = true;
  FillBufferWithTime(false, event->Par1, event->Par2, event->Par3, false,
                     # if P073_SUPPRESS_ZERO
                     suppressLeading0
                     # else // if P073_SUPPRESS_ZERO
                     false
                     # endif // if P073_SUPPRESS_ZERO
                     );

  switch (displayModel)
  {
    case P073_TM1637_4DGTCOLON:
    case P073_TM1637_4DGTDOTS:
      tm1637_ShowTimeTemp4(timesep, 0);
      break;
    case P073_TM1637_6DGT:
      tm1637_ShowTime6();
      break;
    case P073_MAX7219_8DGT:
      max7219_ShowTime(timesep);
      break;
    # if P073_USE_74HC595
    case P073_74HC595_2_8DGT:
      hc595_AdjustBuffer();
      hc595_ToOutputBuffer();

      if (hc595_Sequential()) { // Sequential displays don't need continuous refreshing
        hc595_ShowBuffer();
      }
      break;
    # endif // if P073_USE_74HC595
  }
  return true;
}

bool P073_data_struct::plugin_write_7dsd(struct EventStruct *event) {
  if (output != P073_DISP_MANUAL) {
    return false;
  }

  # ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, strformat(F("7DGT : Show Date=%02d:%02d:%02d"), event->Par1, event->Par2, event->Par3));
  }
  # endif // ifndef BUILD_NO_DEBUG
  FillBufferWithDate(false, event->Par1, event->Par2, event->Par3,
                     # if P073_SUPPRESS_ZERO
                     suppressLeading0
                     # else // if P073_SUPPRESS_ZERO
                     false
                     # endif // if P073_SUPPRESS_ZERO
                     );

  switch (displayModel)
  {
    case P073_TM1637_4DGTCOLON:
    case P073_TM1637_4DGTDOTS:
      tm1637_ShowTimeTemp4(timesep, 0);
      break;
    case P073_TM1637_6DGT:
      tm1637_ShowDate6();
      break;
    case P073_MAX7219_8DGT:
      max7219_ShowDate();
      break;
    # if P073_USE_74HC595
    case P073_74HC595_2_8DGT:
      hc595_AdjustBuffer();
      hc595_ToOutputBuffer();

      if (hc595_Sequential()) { // Sequential displays don't need continuous refreshing
        hc595_ShowBuffer();
      }
      break;
    # endif // if P073_USE_74HC595
  }
  return true;
}

bool P073_data_struct::plugin_write_7dtext(const String& text) {
  if (output != P073_DISP_MANUAL) {
    return false;
  }

  # ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO, concat(F("7DGT : Show Text="), text));
  }
  # endif // ifndef BUILD_NO_DEBUG
  # if P073_SCROLL_TEXT
  setTextToScroll(EMPTY_STRING);
  const uint8_t bufLen = P073_getDefaultDigits(displayModel, digits);

  if (isScrollEnabled() && (getEffectiveTextLength(text) > bufLen)) {
    setTextToScroll(text);
  } else
  # endif // if P073_SCROLL_TEXT
  {
    FillBufferWithString(text);

    switch (displayModel)
    {
      case P073_TM1637_4DGTCOLON:
      case P073_TM1637_4DGTDOTS:
        tm1637_ShowBuffer(0, 4);
        break;
      case P073_TM1637_6DGT:
        tm1637_SwapDigitInBuffer(0); // only needed for 6-digits displays
        tm1637_ShowBuffer(0, 6);
        break;
      case P073_MAX7219_8DGT:
        dotpos = -1; // avoid to display the dot
        max7219_ShowBuffer();
        break;
      # if P073_USE_74HC595
      case P073_74HC595_2_8DGT:
        hc595_ToOutputBuffer();

        if (hc595_Sequential()) { // Sequential displays don't need continuous refreshing
          hc595_ShowBuffer();
        }
        break;
      # endif // if P073_USE_74HC595
    }
  }
  return true;
}

# if P073_EXTRA_FONTS

bool P073_data_struct::plugin_write_7dfont(struct EventStruct *event,
                                           const String      & text) {
  if (!text.isEmpty()) {
    int32_t fontNr = P073_parse_7dfont(event, text);
    #  ifdef P073_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO, strformat(F("P073 7dfont,%s -> %d"), parseString(text, 1).c_str(), fontNr));
    }
    #  endif // ifdef P073_DEBUG

    if ((fontNr >= 0) && (fontNr <= 3)) {
      fontset          = fontNr;
      P073_CFG_FONTSET = fontNr;
      return true;
    }
  }
  return false;
}

# endif // if P073_EXTRA_FONTS

# if P073_7DBIN_COMMAND

bool P073_data_struct::plugin_write_7dbin(const String& text) {
  if (!text.isEmpty()) {
    String  data;
    int32_t byteValue{};
    int     arg      = 1;
    String  argValue = parseString(text, arg);

    while (!argValue.isEmpty()) {
      if (validIntFromString(argValue, byteValue) && (byteValue < 256) && (byteValue > -1)) {
        data += static_cast<char>(displayModel == P073_MAX7219_8DGT ?
                                  byteValue :
                                  P073_revert7bits(byteValue));
      }
      arg++;
      argValue = parseString(text, arg);
    }
    #  if P073_SCROLL_TEXT
    const uint8_t bufLen = P073_getDefaultDigits(displayModel, digits);
    #  endif // if P073_SCROLL_TEXT

    if (!data.isEmpty()) {
      #  if P073_SCROLL_TEXT
      setTextToScroll(EMPTY_STRING); // Clear any scrolling text

      if (isScrollEnabled() && (data.length() > bufLen)) {
        setBinaryData(data);
      } else
      #  endif // if P073_SCROLL_TEXT
      {
        FillBufferWithString(data, true);

        switch (displayModel)
        {
          case P073_TM1637_4DGTCOLON:
          case P073_TM1637_4DGTDOTS:
            tm1637_ShowBuffer(0, 4);
            break;
          case P073_TM1637_6DGT:
            tm1637_SwapDigitInBuffer(0); // only needed for 6-digits displays
            tm1637_ShowBuffer(0, 6, true);
            break;
          case P073_MAX7219_8DGT:
            dotpos = -1; // avoid to display the dot
            max7219_ShowBuffer();
            break;
          #  if P073_USE_74HC595
          case P073_74HC595_2_8DGT:
            hc595_ToOutputBuffer();

            if (hc595_Sequential()) { // Sequential displays don't need continuous refreshing
              hc595_ShowBuffer();
            }
            break;
          #  endif // if P073_USE_74HC595
        }
      }
      return true;
    }
  }
  return false;
}

# endif // if P073_7DBIN_COMMAND

// ===================================
// ---- TM1637 specific functions ----
// ===================================

# ifdef ESP32
#  define CLK_HIGH() DIRECT_pinWrite(this->pin1, HIGH)
#  define CLK_LOW() DIRECT_pinWrite(this->pin1, LOW)
#  define DIO_HIGH() DIRECT_PINMODE_INPUT(this->pin2)
#  define DIO_LOW() DIRECT_PINMODE_OUTPUT(this->pin2) // ; DIRECT_pinWrite(this->pin2, LOW)
# else // ifdef ESP32
#  define CLK_HIGH() digitalWrite(this->pin1, HIGH)
#  define CLK_LOW() digitalWrite(this->pin1, LOW)
#  define DIO_HIGH() pinMode(this->pin2, INPUT)
#  define DIO_LOW() pinMode(this->pin2, OUTPUT)
# endif // ifdef ESP32

void P073_data_struct::tm1637_i2cStart() {
  # ifdef P073_DEBUG
  addLog(LOG_LEVEL_DEBUG, F("7DGT : Comm Start"));
  # endif // ifdef P073_DEBUG
  DIO_HIGH();
  CLK_HIGH();
  delayMicroseconds(TM1637_CLOCKDELAY);
  DIO_LOW();
}

void P073_data_struct::tm1637_i2cStop() {
  # ifdef P073_DEBUG
  addLog(LOG_LEVEL_DEBUG, F("7DGT : Comm Stop"));
  # endif // ifdef P073_DEBUG
  CLK_LOW();
  delayMicroseconds(TM1637_CLOCKDELAY);
  DIO_LOW();
  delayMicroseconds(TM1637_CLOCKDELAY);
  CLK_HIGH();
  delayMicroseconds(TM1637_CLOCKDELAY);
  DIO_HIGH();
}

void P073_data_struct::tm1637_i2cAck() {
  CLK_LOW();
  pinMode(this->pin2, INPUT_PULLUP);

  // DIO_HIGH();
  delayMicroseconds(TM1637_CLOCKDELAY);

  // while(digitalRead(dio_pin));
  # ifdef P073_DEBUG
  const bool dummyAck =
  # endif // ifdef P073_DEBUG
  # ifdef ESP32
  DIRECT_pinRead(this->pin2);
  # else
  digitalRead(this->pin2);
  # endif // ifdef ESP32

  # ifdef P073_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    String log = F("7DGT : Comm ACK=");

    if (dummyAck == 0) {
      log += F("TRUE");
    } else {
      log += F("FALSE");
    }
    addLogMove(LOG_LEVEL_DEBUG, log);
  }
  # endif // ifdef P073_DEBUG
  CLK_HIGH();
  delayMicroseconds(TM1637_CLOCKDELAY);
  CLK_LOW();
  DIO_LOW();
}

void P073_data_struct::tm1637_i2cWrite_ack(uint8_t bytesToPrint[],
                                           uint8_t length) {
  # ifdef P073_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, concat(F("7DGT : TM1637 databuffer: 0x"), formatToHex_array(bytesToPrint, length)));
  }
  # endif // ifdef P073_DEBUG
  tm1637_i2cStart();

  for (uint8_t i = 0; i < length; ++i) {
    tm1637_i2cWrite_ack(bytesToPrint[i]);
  }
  tm1637_i2cStop();
}

void P073_data_struct::tm1637_i2cWrite_ack(uint8_t bytetoprint) {
  tm1637_i2cWrite(bytetoprint);
  tm1637_i2cAck();
}

void P073_data_struct::tm1637_i2cWrite(uint8_t bytetoprint) {
  # ifdef P073_DEBUG
  addLog(LOG_LEVEL_DEBUG, F("7DGT : WriteByte"));
  # endif // ifdef P073_DEBUG

  for (uint8_t i = 0; i < 8; ++i) {
    CLK_LOW();

    if (bytetoprint & 0b00000001) {
      DIO_HIGH();
    } else {
      DIO_LOW();
    }
    delayMicroseconds(TM1637_CLOCKDELAY);
    bytetoprint = bytetoprint >> 1;
    CLK_HIGH();
    delayMicroseconds(TM1637_CLOCKDELAY);
  }
}

void P073_data_struct::tm1637_ClearDisplay() {
  uint8_t bytesToPrint[7]{};

  bytesToPrint[0] = 0xC0;
  tm1637_i2cWrite_ack(bytesToPrint, 7);
}

void P073_data_struct::tm1637_SetPowerBrightness(uint8_t brightlvl,
                                                 bool    poweron) {
  # ifdef P073_DEBUG
  addLog(LOG_LEVEL_INFO, F("7DGT : Set BRIGHT"));
  # endif // ifdef P073_DEBUG
  brightlvl &= 0b111;

  if (poweron) {
    brightlvl |= TM1637_POWER_ON;
  } else {
    brightlvl |= TM1637_POWER_OFF;
  }

  tm1637_i2cWrite_ack(brightlvl);
}

void P073_data_struct::tm1637_InitDisplay() {
  # ifdef ESP32
  DIRECT_PINMODE_OUTPUT(this->pin1);
  DIRECT_PINMODE_OUTPUT(this->pin2);
  # else // ifdef ESP32
  pinMode(this->pin1, OUTPUT);
  pinMode(this->pin2, OUTPUT);
  # endif // ifdef ESP32
  CLK_HIGH();
  DIO_HIGH();

  const uint8_t byteToPrint = 0x40;

  tm1637_i2cWrite_ack(byteToPrint);
  tm1637_ClearDisplay();
}

uint8_t P073_data_struct::tm1637_separator(uint8_t value,
                                           bool    sep) {
  if (sep) {
    value |= 0b10000000;
  }
  return value;
}

void P073_data_struct::tm1637_ShowTime6() {
  tm1637_ShowDate6(true); // deduplicated
}

void P073_data_struct::tm1637_ShowDate6(bool showTime) {
  uint8_t bytesToPrint[7]{};

  bytesToPrint[0] = 0xC0;
  bytesToPrint[1] = tm1637_getFontChar(showbuffer[2], fontset);
  bytesToPrint[2] = tm1637_separator(tm1637_getFontChar(showbuffer[1], fontset), timesep);
  bytesToPrint[3] = tm1637_getFontChar(showbuffer[0], fontset);

  if (showTime) {
    bytesToPrint[4] = tm1637_getFontChar(showbuffer[5], fontset);
    bytesToPrint[5] = tm1637_getFontChar(showbuffer[4], fontset);
  } else {
    bytesToPrint[4] = tm1637_getFontChar(showbuffer[7], fontset);
    bytesToPrint[5] = tm1637_getFontChar(showbuffer[6], fontset);
  }
  bytesToPrint[6] = tm1637_separator(tm1637_getFontChar(showbuffer[3], fontset), timesep);

  tm1637_i2cWrite_ack(bytesToPrint, 7);
}

void P073_data_struct::tm1637_ShowTemp6(bool sep) {
  uint8_t bytesToPrint[7]{};

  bytesToPrint[0] = 0xC0;
  bytesToPrint[1] = tm1637_separator(tm1637_getFontChar(showbuffer[5], fontset), sep);
  bytesToPrint[2] = tm1637_getFontChar(showbuffer[4], fontset);
  bytesToPrint[3] = tm1637_getFontChar(10, fontset);
  bytesToPrint[4] = tm1637_getFontChar(10, fontset);
  bytesToPrint[5] = tm1637_getFontChar(showbuffer[7], fontset);
  bytesToPrint[6] = tm1637_getFontChar(showbuffer[6], fontset);

  tm1637_i2cWrite_ack(bytesToPrint, 7);
}

void P073_data_struct::tm1637_ShowTimeTemp4(bool    sep,
                                            uint8_t bufoffset) {
  uint8_t bytesToPrint[5]{};

  bytesToPrint[0] = 0xC0;
  bytesToPrint[1] = tm1637_getFontChar(showbuffer[0 + bufoffset], fontset);
  bytesToPrint[2] = tm1637_separator(tm1637_getFontChar(showbuffer[1 + bufoffset], fontset), sep);
  bytesToPrint[3] = tm1637_getFontChar(showbuffer[2 + bufoffset], fontset);
  bytesToPrint[4] = tm1637_getFontChar(showbuffer[3 + bufoffset], fontset);

  tm1637_i2cWrite_ack(bytesToPrint, 5);
}

void P073_data_struct::tm1637_SwapDigitInBuffer(uint8_t startPos) {
  std::swap(showbuffer[2 + startPos],  showbuffer[0 + startPos]);
  std::swap(showbuffer[3 + startPos],  showbuffer[5 + startPos]);

  std::swap(showperiods[2 + startPos], showperiods[0 + startPos]);
  std::swap(showperiods[3 + startPos], showperiods[5 + startPos]);

  if (dotpos > -1) {
    const uint8_t dotPositionSwap[] = { 0, 1, 4, 3, 2, 7, 6, 5, 8 };

    dotpos = dotPositionSwap[dotpos];
  }
}

void P073_data_struct::tm1637_ShowBuffer(uint8_t firstPos,
                                         uint8_t lastPos,
                                         bool    useBinaryData) {
  uint8_t bytesToPrint[8]{};

  bytesToPrint[0] = 0xC0;
  uint8_t length = 1;

  if (dotpos > -1) {
    showperiods[dotpos] = true;
  }

  uint8_t p073_datashowpos1;

  for (int i = firstPos; i < lastPos; ++i) {
    if (useBinaryData) {
      bytesToPrint[length] = showbuffer[i];
    } else {
      p073_datashowpos1 = tm1637_separator(
        tm1637_getFontChar(showbuffer[i], fontset),
        showperiods[i]);
      bytesToPrint[length] = p073_datashowpos1;
    }
    length++;
  }
  # ifdef P073_DEBUG
  addLog(LOG_LEVEL_INFO, strformat(F("TM1673: Write bytes: %d buffer %d to %d"), length, firstPos, lastPos));
  # endif // ifdef P073_DEBUG
  tm1637_i2cWrite_ack(bytesToPrint, length);
}

// ====================================
// ---- MAX7219 specific functions ----
// ====================================

# define OP_DECODEMODE   9
# define OP_INTENSITY   10
# define OP_SCANLIMIT   11
# define OP_SHUTDOWN    12
# define OP_DISPLAYTEST 15

void P073_data_struct::max7219_spiTransfer(ESPEASY_VOLATILE(uint8_t) opcode,
                                           ESPEASY_VOLATILE(uint8_t) data) {
  spidata[1] = opcode;
  spidata[0] = data;
  digitalWrite(pin3, LOW);
  shiftOut(pin1, pin2, MSBFIRST, spidata[1]);
  shiftOut(pin1, pin2, MSBFIRST, spidata[0]);
  digitalWrite(pin3, HIGH);
}

void P073_data_struct::max7219_ClearDisplay() {
  for (int i = 0; i < 8; i++) {
    max7219_spiTransfer(i + 1, 0);
  }
}

void P073_data_struct::max7219_SetPowerBrightness(uint8_t brightlvl,
                                                  bool    poweron) {
  max7219_spiTransfer(OP_INTENSITY, brightlvl);
  max7219_spiTransfer(OP_SHUTDOWN,  poweron ? 1 : 0);
}

void P073_data_struct::max7219_SetDigit(int     dgtpos,
                                        uint8_t dgtvalue,
                                        bool    showdot,
                                        bool    binaryData) {
  uint8_t p073_tempvalue;

  if (binaryData) {
    p073_tempvalue = dgtvalue; // Overwrite if binary data
  } else
  {
    p073_tempvalue = P073_getFontChar(dgtvalue, fontset);

    if (showdot) {
      p073_tempvalue |= 0b10000000;
    }
  }
  max7219_spiTransfer(dgtpos + 1, p073_tempvalue);
}

void P073_data_struct::max7219_InitDisplay() {
  pinMode(pin1, OUTPUT);
  pinMode(pin2, OUTPUT);
  pinMode(pin3, OUTPUT);
  digitalWrite(pin3, HIGH);
  max7219_spiTransfer(OP_DISPLAYTEST, 0);
  max7219_spiTransfer(OP_SCANLIMIT,   7); // scanlimit setup to max at Init
  max7219_spiTransfer(OP_DECODEMODE,  0);
  max7219_ClearDisplay();
  max7219_SetPowerBrightness(0, false);
}

void P073_data_struct::max7219_ShowTime(bool sep) {
  const uint8_t idx_list[] = { 7, 6, 4, 3, 1, 0 }; // Digits in reversed order, as the loop is backward

  for (int8_t i = 5; i >= 0; --i) {
    max7219_SetDigit(idx_list[i], showbuffer[i], false);
  }

  const uint8_t sepChar = P073_mapCharToFontPosition(sep ? '-' : ' ', fontset);

  max7219_SetDigit(2, sepChar, false);
  max7219_SetDigit(5, sepChar, false);
}

void P073_data_struct::max7219_ShowTemp(int8_t firstDot,
                                        int8_t secondDot) {
  max7219_SetDigit(0, 10, false);

  if (firstDot  > -1) { showperiods[firstDot] = true; }

  if (secondDot > -1) { showperiods[secondDot] = true; }

  const int alignRight = rightAlignTempMAX7219 ? 0 : 1;

  for (int i = alignRight; i < 8; ++i) {
    const int bufIndex = (7 + alignRight) - i;

    if (bufIndex < 8) {
      max7219_SetDigit(i,
                       showbuffer[bufIndex],
                       showperiods[bufIndex]);
    }
  }
}

void P073_data_struct::max7219_ShowDate() {
  const uint8_t dotflags[8] = { false, true, false, true, false, false, false, false };

  for (int i = 0; i < 8; ++i) {
    max7219_SetDigit(i,
                     showbuffer[7 - i],
                     dotflags[7 - i]);
  }
}

void P073_data_struct::max7219_ShowBuffer() {
  if (dotpos > -1) {
    showperiods[dotpos] = true;
  }

  for (int i = 0; i < 8; i++) {
    max7219_SetDigit(i,
                     showbuffer[7 - i],
                     showperiods[7 - i]
                     # if P073_7DBIN_COMMAND
                     , binaryData
                     # endif // if P073_7DBIN_COMMAND
                     );
  }
}

#endif    // ifdef USES_P073

#include "../PluginStructs/P020_data_struct.h"

#ifdef USES_P020

# include "../ESPEasyCore/Serial.h"
# include "../ESPEasyCore/ESPEasyNetwork.h"

# include "../Globals/EventQueue.h"

# include "../Helpers/ESPEasy_Storage.h"
# include "../Helpers/Misc.h"

P020_Task::P020_Task(struct EventStruct *event) : _taskIndex(event->TaskIndex) {
  if (P020_GET_LED_ENABLED) {
    _ledPin = P020_LED_PIN; // Default pin (12) is already initialized in P020_Task
  }
  _ledEnabled   = P020_GET_LED_ENABLED == 1;
  _ledInverted  = P020_GET_LED_INVERTED == 1;
  _space        = static_cast<char>(P020_REPLACE_SPACE);
  _newline      = static_cast<char>(P020_REPLACE_NEWLINE);
  _port         = static_cast<ESPEasySerialPort>(CONFIG_PORT);
  _serialId     = P020_GET_EVENT_SERIAL_ID;
  _appendTaskId = P020_GET_APPEND_TASK_ID;
  _rxBufferSize = P020_RX_BUFFER;
  _eventAsHex   = P020_GET_EVENT_AS_HEX;
  # if P020_USE_PROTOCOL
  _protocol = static_cast<P020_Protocol_e>(P020_GET_PROTOCOL);
  _udpport  = P020_GET_SERVER_PORT;
  # endif // if P020_USE_PROTOCOL
  clearBuffer(); // Depends on _rxBufferSize
}

P020_Task::~P020_Task() {
  if (ser2netServer != nullptr) {
    delete ser2netServer;
    ser2netServer = nullptr;
  }
  # if P020_USE_PROTOCOL

  if (ser2netUdp != nullptr) {
    delete ser2netUdp;
    ser2netUdp = nullptr;
  }
  # endif // if P020_USE_PROTOCOL

  if (ser2netSerial != nullptr) {
    delete ser2netSerial;
    ser2netSerial = nullptr;
  }
}

bool P020_Task::serverActive(WiFiServer *server) {
# if defined(ESP8266)
  return nullptr != server && server->status() != CLOSED;
# elif defined(ESP32)
  return nullptr != server && *server;
# endif // if defined(ESP8266)
}

void P020_Task::startServer(uint16_t portnumber) {
  # if P020_USE_PROTOCOL

  if ((P020_Protocol_e::TCP == _protocol) || (P020_Protocol_e::TCP_UDP == _protocol))
  # endif // if P020_USE_PROTOCOL
  {
    if ((gatewayPort == portnumber) && serverActive(ser2netServer)) {
      // server is already listening on this port
      return;
    }
    stopServer();
    gatewayPort   = portnumber;
    ser2netServer = new (std::nothrow) WiFiServer(portnumber);

    if ((nullptr != ser2netServer) && NetworkConnected()) {
      ser2netServer->begin();

      if (serverActive(ser2netServer)) {
        addLog(LOG_LEVEL_INFO, strformat(F("Ser2Net: WiFi server started at port %d"), portnumber));
      } else {
        addLog(LOG_LEVEL_ERROR, strformat(F("Ser2Net: WiFi server start FAILED at port %d, retrying..."), portnumber));
      }
    }
  }
  # if P020_USE_PROTOCOL

  if ((P020_Protocol_e::UDP == _protocol) || (P020_Protocol_e::TCP_UDP == _protocol)) {
    ser2netUdp = new (std::nothrow) WiFiUDP();
  }
  # endif // if P020_USE_PROTOCOL
}

void P020_Task::checkServer() {
  if ((nullptr != ser2netServer) && !serverActive(ser2netServer) && NetworkConnected()) {
    ser2netServer->close();
    ser2netServer->begin();

    if (serverActive(ser2netServer)) {
      addLog(LOG_LEVEL_INFO, F("Ser2Net: WiFi server started"));
    }
  }
  # if P020_USE_PROTOCOL

  if ((nullptr != ser2netUdp) && !_udpInit && NetworkConnected()) {
    _udpInit = true; // Init only once

    if (ser2netUdp->begin(_udpport) == 0) {
      if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
        addLog(LOG_LEVEL_ERROR, strformat(F("Ser2Net: Cannot bind UDP at port %d"), _udpport));
      }
    } else {
      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLog(LOG_LEVEL_INFO, strformat(F("Ser2Net: UDP receiver started at port %d"), _udpport));
      }
    }
  }
  # endif // if P020_USE_PROTOCOL
}

void P020_Task::stopServer() {
  if (nullptr != ser2netServer) {
    if (ser2netClient) { ser2netClient.stop(); }
    clientConnected = false;
    ser2netServer->close();
    addLog(LOG_LEVEL_INFO, F("Ser2Net: WiFi server closed"));
    delete ser2netServer;
    ser2netServer = nullptr;
  }
  # if P020_USE_PROTOCOL

  if (nullptr != ser2netUdp) {
    ser2netUdp->stop();
    addLog(LOG_LEVEL_INFO, F("Ser2Net: UDP receiver stopped"));
    delete ser2netUdp;
    ser2netUdp = nullptr;
  }
  # endif // if P020_USE_PROTOCOL
}

bool P020_Task::hasClientConnected() {
  if ((nullptr != ser2netServer) && ser2netServer->hasClient())
  {
    if (ser2netClient) { ser2netClient.stop(); }
    # if ESP_IDF_VERSION_MAJOR >= 5
    ser2netClient = ser2netServer->accept();
    # else // if ESP_IDF_VERSION_MAJOR >= 5
    ser2netClient = ser2netServer->available();
    # endif // if ESP_IDF_VERSION_MAJOR >= 5

    # ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS

    // See: https://github.com/espressif/arduino-esp32/pull/6676
    ser2netClient.setTimeout((CONTROLLER_CLIENTTIMEOUT_DFLT + 500) / 1000); // in seconds!!!!
    Client *pClient = &ser2netClient;
    pClient->setTimeout(CONTROLLER_CLIENTTIMEOUT_DFLT);
    # else // ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS
    ser2netClient.setTimeout(CONTROLLER_CLIENTTIMEOUT_DFLT); // in msec as it should be!
    # endif // ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS

    sendConnectedEvent(true);
    addLog(LOG_LEVEL_INFO, F("Ser2Net: Client connected!"));
  }

  if (ser2netClient.connected())
  {
    clientConnected = true;
  }
  else
  {
    if (clientConnected) // there was a client connected before...
    {
      clientConnected = false;
      sendConnectedEvent(false);
      addLog(LOG_LEVEL_INFO, F("Ser2Net: Client disconnected!"));
    }
  }
  return clientConnected;
}

void P020_Task::discardClientIn() {
  // flush all data received from the WiFi gateway
  // as a P1 meter does not receive data
  while (ser2netClient.available()) {
    ser2netClient.read();
  }
}

void P020_Task::clearBuffer() {
  free_string(serial_buffer);
  _maxDataGramSize = serial_processing == P020_Events::P1WiFiGateway
                    ? P020_P1_DATAGRAM_MAX_SIZE
                    : _rxBufferSize;
  serial_buffer.reserve(_maxDataGramSize);
}

void P020_Task::serialBegin(const ESPEasySerialPort port, int16_t rxPin, int16_t txPin, unsigned long baud, uint8_t config) {
  serialEnd();

  if (ESPEasySerialPort::not_set != port) {
    ser2netSerial = new (std::nothrow) ESPeasySerial(port, rxPin, txPin);

    if (nullptr != ser2netSerial) {
      # if defined(ESP8266)
      ser2netSerial->begin(baud, (SerialConfig)config);
      # elif defined(ESP32)
      ser2netSerial->begin(baud, config);
      # endif // if defined(ESP8266)
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_DEBUG, F("Ser2Net: Serial opened"));
      # endif // ifndef BUILD_NO_DEBUG
    }
  }
}

void P020_Task::serialEnd() {
  if (nullptr != ser2netSerial) {
    delete ser2netSerial;
    clearBuffer();
    ser2netSerial = nullptr;
    # ifndef BUILD_NO_DEBUG
    addLog(LOG_LEVEL_DEBUG, F("Ser2Net: Serial closed"));
    # endif // ifndef BUILD_NO_DEBUG
  }
}

void P020_Task::handleClientIn(struct EventStruct *event) {
  # if P020_USE_PROTOCOL

  if (((P020_Protocol_e::TCP == _protocol) || (P020_Protocol_e::TCP_UDP == _protocol)) &&
      (nullptr != ser2netServer))
  # endif // if P020_USE_PROTOCOL
  {
    int count = ser2netClient.available();

    if (count > 0) {
      uint8_t net_buf[_maxDataGramSize];

      if (count > static_cast<int>(_maxDataGramSize)) { count = _maxDataGramSize; }
      const int bytes_read = ser2netClient.read(net_buf, count);

      if (bytes_read) {
        # ifndef LIMIT_BUILD_SIZE

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          addLog(LOG_LEVEL_INFO, strformat(F("Ser2Net: Passing through %d bytes from %s to serial."), bytes_read, FsP(F("TCP"))));
        }
        # endif // ifndef LIMIT_BUILD_SIZE
        ser2netSerial->write(net_buf, bytes_read);
        ser2netSerial->flush();           // Waits for the transmission of outgoing serial data to
      }

      while (ser2netClient.available()) { // flush overflow data if available
        ser2netClient.read();
      }
      delay(0);
    }
  }
  # if P020_USE_PROTOCOL

  if (((P020_Protocol_e::UDP == _protocol) || (P020_Protocol_e::TCP_UDP == _protocol)) &&
      (nullptr != ser2netUdp)) {
    const int packetSize = ser2netUdp->parsePacket();

    if (packetSize > 0) {
      std::vector<uint8_t> packetBuffer;
      packetBuffer.resize(packetSize + 1);

      if (packetBuffer.size() >= static_cast<size_t>(packetSize)) {
        memset(&packetBuffer[0], 0, packetSize + 1);
        const int bytes_read = ser2netUdp->read(&packetBuffer[0], packetSize);

        if (bytes_read > 0) {
          #  ifndef LIMIT_BUILD_SIZE

          if (loglevelActiveFor(LOG_LEVEL_INFO)) {
            addLog(LOG_LEVEL_INFO, strformat(F("Ser2Net: Passing through %d bytes from %s to serial."), bytes_read, FsP(F("UDP"))));
          }
          #  endif // ifndef LIMIT_BUILD_SIZE
          ser2netSerial->write(&packetBuffer[0], bytes_read);
          ser2netSerial->flush();

          while (ser2netUdp->available()) {
            ser2netUdp->read();
          }
        }
      }
      delay(0);
    }
  }
  # endif // if P020_USE_PROTOCOL
}

void P020_Task::handleSerialIn(struct EventStruct *event) {
  if (nullptr == ser2netSerial) { return; }
  int  RXWait    = P020_RX_WAIT;
  int  timeOut   = RXWait;
  int  maxExtend = 5;
  bool done      = false;
  char ch;

  do {
    if (ser2netSerial->available()) {
      if ((serial_processing != P020_Events::P1WiFiGateway) // P1 handling without this check
          && (serial_buffer.length() > static_cast<size_t>(P020_RX_BUFFER))) {
        # ifndef BUILD_NO_DEBUG
        addLog(LOG_LEVEL_DEBUG, F("Ser2Net: Error: Buffer overflow, discarded input."));
        # endif // ifndef BUILD_NO_DEBUG
        ser2netSerial->read();
      }
      else {
        if (_ledEnabled) {
          digitalWrite(_ledPin, _ledInverted ? 0 : 1);
        }

        ch = static_cast<char>(ser2netSerial->read());

        if (serial_processing == P020_Events::P1WiFiGateway) {
          done = handleP1Char(ch);
        } else {
          addChar(ch);
        }

        if (_ledEnabled) {
          digitalWrite(_ledPin, _ledInverted ? 1 : 0);
        }
      }

      if (done) {
        break;
      }
      timeOut = RXWait; // if serial received, reset timeout counter
    } else {
      if (timeOut <= 0) {
        if ((RXWait > 0) && (serial_processing == P020_Events::P1WiFiGateway) &&
            ((_state == ParserState::READING) ||
             (_state == ParserState::CHECKSUM)) &&
            (maxExtend > 0)) {
          timeOut = RXWait;
          maxExtend--;
        } else {
          break;
        }
      }
      delay(1);
      --timeOut;
    }
  } while (true);

  if (serial_buffer.length() > 0) {
    if (ser2netClient.connected()) { // Only send out if a client is connected
      if ((serial_processing == P020_Events::P1WiFiGateway) && !serial_buffer.endsWith(F("\r\n"))) {
        serial_buffer += F("\r\n");
      }

      // Might include non-printable characters, so use write instead of print
      ser2netClient.write(reinterpret_cast<const uint8_t *>(serial_buffer.c_str()), serial_buffer.length());
    }

    blinkLED();

    rulesEngine(serial_buffer);
    ser2netClient.PR_9453_FLUSH_TO_CLEAR();
    clearBuffer();
    # ifndef BUILD_NO_DEBUG
    addLog(LOG_LEVEL_DEBUG, F("Ser2Net: data sent!"));
    # endif // ifndef BUILD_NO_DEBUG
  } // done
}

void P020_Task::discardSerialIn() {
  if (nullptr != ser2netSerial) {
    while (ser2netSerial->available()) {
      ser2netSerial->read();
    }
  }
}

// We can also use the rules engine for local control!
void P020_Task::rulesEngine(const String& message) {
  if (!Settings.UseRules || message.isEmpty() || (P020_Events::None == serial_processing)) { return; }
  int NewLinePos    = 0;
  uint16_t StartPos = 0;

  NewLinePos = handleMultiLine ? message.indexOf('\n', StartPos) : message.length();

  do {
    if (NewLinePos < 0) {
      NewLinePos = message.length();
    }

    String eventString;

    // Remove preceeding CR also
    if ((message[NewLinePos] == '\n') && (message[NewLinePos - 1] == '\r')) {
      NewLinePos--;
    }

    switch (serial_processing) {
      case P020_Events::None: { break; }
      case P020_Events::Generic: { // Generic
        if (NewLinePos > StartPos) {
          eventString = '!';       // F("!Serial");

          if (_serialId) {
            eventString += ESPEasySerialPort_toString(_port, true);
          } else {
            eventString += F("Serial");
          }

          if (_appendTaskId) {
            eventString += (_taskIndex + 1);
          }
          eventString += '#';

          if (_eventAsHex) {
            eventString += F("0x");
            eventString += formatToHex_array(reinterpret_cast<const uint8_t *>(message.substring(StartPos, NewLinePos).c_str()),
                                             NewLinePos - StartPos);
          } else {
            eventString += message.substring(StartPos, NewLinePos);
          }
        }
        break;
      }
      case P020_Events::RFLink: { // RFLink
        StartPos += 6;            // RFLink, strip 20;xx; from incoming message

        if (((NewLinePos - StartPos) >= 8) &&
            message.substring(StartPos, StartPos + 8)
            .startsWith(F("ESPEASY"))) { // Special treatment for gpio values, strip unneeded parts...
          StartPos   += 8;               // Strip "ESPEASY;"
          eventString = F("RFLink");
        } else {
          eventString = F("!RFLink");    // default event as it comes in, literal match needed in rules, using '!'
        }

        if (_appendTaskId) {
          eventString += (_taskIndex + 1);
        }
        eventString += '#';

        if (NewLinePos > StartPos) {
          eventString += message.substring(StartPos, NewLinePos);
        }
        eventQueue.addMove(std::move(eventString));
        break;
      }
      case P020_Events::P1WiFiGateway: // P1 WiFi Gateway
        eventString  = getTaskDeviceName(_taskIndex);
        eventString += F("#Data");

        if (_P1EventData) {
          eventString += '=';
          eventString += message;               // Include entire message, may cause memory overflow!
          eventString.replace(F("\n"), F(",")); // Make it a single line, comma-separated, as much as possible
          eventString.replace(F("\r"), F(""));  // We don't need no st*n carriage returns :)
        }
        break;
    } // switch

    // Skip CR/LF
    StartPos = NewLinePos; // Continue after what was already handled

    while (StartPos < message.length() && (message[StartPos] == '\n' || message[StartPos] == '\r')) {
      StartPos++;
    }

    if (!eventString.isEmpty()) {
      eventQueue.add(eventString);
    }
    NewLinePos = message.indexOf('\n', StartPos);

    if (handleMultiLine && (NewLinePos < 0)) {
      NewLinePos = message.length();
    }
  } while (handleMultiLine && NewLinePos > StartPos);
}

bool P020_Task::isInit() const {
  return (nullptr != ser2netServer
          # if P020_USE_PROTOCOL
          || nullptr != ser2netUdp
          # endif // if P020_USE_PROTOCOL
          ) && nullptr != ser2netSerial;
}

void P020_Task::sendConnectedEvent(bool connected)
{
  eventQueue.add(_taskIndex, F("Client"), (connected ? 1 : 0));
}

void P020_Task::blinkLED() {
  if (_ledEnabled) {
    _blinkLEDStartTime = millis();
    digitalWrite(_ledPin, _ledInverted ? 0 : 1);
  }
}

void P020_Task::checkBlinkLED() {
  if (_ledEnabled && (_blinkLEDStartTime > 0) && (timePassedSince(_blinkLEDStartTime) >= 500)) {
    digitalWrite(_ledPin, _ledInverted ? 1 : 0);
    _blinkLEDStartTime = 0;
  }
}

void P020_Task::addChar(char ch) {
  if ((ch == 0x20) && (_space > 0)) { ch = _space; }

  if (_newline > 0) {
    if (ch == '\n') { ch = _newline; }

    if (ch == '\r') { return; } // Ignore CR if LF is replaced
  }

  serial_buffer += ch;
}

/*  checkDatagram
    checks whether the P020_CHECKSUM of the data received from P1 matches the P020_CHECKSUM
    attached to the telegram
 */
bool P020_Task::checkDatagram() const {
  int endChar = serial_buffer.length() - 1;

  if (_CRCcheck) {
    endChar -= P020_CHECKSUM_LENGTH;
  }

  if ((endChar < 0) || (serial_buffer[0] != P020_DATAGRAM_START_CHAR) ||
      (serial_buffer[endChar] != P020_DATAGRAM_END_CHAR)) {
    return false;
  }

  if (!_CRCcheck) {
    return true;
  }

  const int checksumStartIndex = endChar + 1;

  # if PLUGIN_020_DEBUG

  for (unsigned int cnt = 0; cnt < serial_buffer.length(); ++cnt) {
    serialPrint(serial_buffer.substring(cnt, 1));
  }
  # endif // if PLUGIN_020_DEBUG

  // calculate the CRC and check if it equals the hexadecimal one attached to the datagram
  unsigned int crc = CRC16(serial_buffer, checksumStartIndex);
  return strtoul(serial_buffer.substring(checksumStartIndex).c_str(), nullptr, 16) == crc;
}

/*
   CRC16
      based on code written by Jan ten Hove
     https://github.com/jantenhove/P1-Meter-ESP8266
 */
unsigned int P020_Task::CRC16(const String& buf, int len) {
  unsigned int crc = 0;

  for (int pos = 0; pos < len; ++pos) {
    crc ^= static_cast<const unsigned int>(buf[pos]); // XOR byte into least sig. byte of crc

    for (int i = 8; i != 0; --i) {                    // Loop over each bit
      if ((crc & 0x0001) != 0) {                      // If the LSB is set
        crc >>= 1;                                    // Shift right and XOR 0xA001
        crc  ^= 0xA001;
      } else {                                        // Else LSB is not set
        crc >>= 1;                                    // Just shift right
      }
    }
  }

  return crc;
}

/*
   validP1char
       Checks if the character is valid as part of the P1 datagram contents and/or checksum.
       Returns false on a datagram start ('/'), end ('!') or invalid character
 */
bool P020_Task::validP1char(char ch) {
  return
    isAlphaNumeric(ch) ||
    ch == '.' ||
    ch == ' ' ||
    ch == '\\' || // Single backslash, but escaped in C++
    ch == '\r' ||
    ch == '\n' ||
    ch == '(' ||
    ch == ')' ||
    ch == '-' ||
    ch == '*' ||
    ch == ':' ||
    ch == '_';
}

bool P020_Task::handleP1Char(char ch) {
  if (serial_buffer.length() >= _maxDataGramSize - 2) { // room for cr/lf
    # ifndef BUILD_NO_DEBUG
    addLog(LOG_LEVEL_DEBUG, F("P1   : Error: Buffer overflow, discarded input."));
    # endif // ifndef BUILD_NO_DEBUG
    _state = ParserState::WAITING; // reset
  }
  ch &= 0x7F;                      // Strip off occasional 8th bit for now

  bool done    = false;
  bool invalid = false;

  switch (_state) {
    case ParserState::WAITING:

      if (ch == P020_DATAGRAM_START_CHAR)  {
        clearBuffer();
        addChar(ch);
        _state = ParserState::READING;
      } // else ignore data
      break;
    case ParserState::READING:

      if (validP1char(ch)) {
        addChar(ch);
      } else if (ch == P020_DATAGRAM_END_CHAR) {
        addChar(ch);

        if (_CRCcheck) {
          checkI = 0;
          _state = ParserState::CHECKSUM;
        } else {
          done = true;
        }
      } else if (ch == P020_DATAGRAM_START_CHAR) {
        # ifndef BUILD_NO_DEBUG
        addLog(LOG_LEVEL_DEBUG, F("P1   : Error: Start detected, discarded input."));
        # endif // ifndef BUILD_NO_DEBUG
        _state = ParserState::WAITING; // reset
        return handleP1Char(ch);
      } else {
        addLog(LOG_LEVEL_ERROR, strformat(F("P1   : Receiving unknown: %d,'%c'"), ch, ch));
        invalid = true;
      }
      break;
    case ParserState::CHECKSUM:

      if (validP1char(ch)) {
        addChar(ch);
        ++checkI;

        if (checkI == P020_CHECKSUM_LENGTH) {
          done = true;
        }
      } else {
        invalid = true;
      }
      break;
  } // switch

  if (invalid) {
    // input is not a datagram char
    # ifndef BUILD_NO_DEBUG
    addLog(LOG_LEVEL_DEBUG, F("P1   : Error: DATA corrupt, discarded input."));
    # endif // ifndef BUILD_NO_DEBUG

    # if PLUGIN_020_DEBUG
    serialPrint(F("faulty char>"));
    serialPrint(String(ch));
    serialPrintln("<");
    # endif // if PLUGIN_020_DEBUG
    _state = ParserState::WAITING; // reset
  }

  if (done) {
    done = checkDatagram();

    if (done) {
      // add the cr/lf pair to the datagram ahead of reading both
      // from serial as the datagram has already been validated
      addChar('\r');
      addChar('\n');
    } else if (_CRCcheck) {
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_DEBUG, F("P1   : Error: Invalid CRC, dropped data"));
      # endif // ifndef BUILD_NO_DEBUG
    } else {
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_DEBUG, F("P1   : Error: Invalid datagram, dropped data"));
      # endif // ifndef BUILD_NO_DEBUG
    }
    _state = ParserState::WAITING; // prepare for next one
  }

  return done;
}

#endif // ifdef USES_P020

#include "../PluginStructs/P111_data_struct.h"

#ifdef USES_P111

# include "../PluginStructs/P111_data_struct.h"
# include "../Helpers/PrintToString.h"

// Needed also here for PlatformIO's library finder as the .h file
// is in a directory which is excluded in the src_filter

# include <MFRC522.h>

P111_data_struct::P111_data_struct(int8_t csPin,
                                   int8_t rstPin,
                                   int8_t irqPin)
  : mfrc522(nullptr), _csPin(csPin), _rstPin(rstPin), _irqPin(irqPin)
{}

P111_data_struct::~P111_data_struct() {
  if (validGpio(_irqPin)) {
    detachInterrupt(digitalPinToInterrupt(_irqPin));
  }

  delete mfrc522;
  mfrc522 = nullptr;
}

void P111_data_struct::init() {
  delete mfrc522;

  mfrc522 = new (std::nothrow) MFRC522(_csPin, _rstPin); // Instantiate a MFRC522

  if (mfrc522 != nullptr) {
    mfrc522->PCD_Init();                                 // Initialize MFRC522 reader
    mfrc522->PCD_WriteRegister(MFRC522::ComIEnReg, 0b10100000); // enable receiver interrupt
    mfrc522->PCD_WriteRegister(MFRC522::DivIEnReg, 0x80); // Set as CMOS output pin
    initPhase = P111_initPhases::Ready;

    if (validGpio(_irqPin)) {
      pinMode(_irqPin, INPUT);

      attachInterruptArg(
        digitalPinToInterrupt(_irqPin),
        reinterpret_cast<void (*)(void *)>(mfrc522_interrupt),
        this, FALLING);
    }
  }
}

/**
 * read status and tag
 */
uint8_t P111_data_struct::readCardStatus(uint32_t *key,
                                         bool     *removedTag) {
  if (initPhase != P111_initPhases::Ready) { // No read during reset
    return P111_ERROR_RESET_BUSY;
  }

  uint8_t error = P111_NO_ERROR;
  uint8_t uid[] = { 0, 0, 0, 0, 0, 0, 0 };
  uint8_t uidLength;

  error = readPassiveTargetID(uid, &uidLength);

  switch (error) {
    case P111_ERROR_READ: // Read error
    {
      errorCount++;
      removedState = false;

      if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
        String log = F("MFRC522: Read error: ");
        log += errorCount;
        addLogMove(LOG_LEVEL_ERROR, log);
      }
      break;
    }
    case P111_ERROR_NO_TAG: // No tag found

      if (!removedState) {
        removedState = true;
        *removedTag  = true;
        error        = 0; // pass through that removal just once
      }
      errorCount = 0;
      break;
    default:                // Read a tag correctly
      errorCount   = 0;
      removedState = false; // No longer removed
      break;
  }

  if (errorCount > 2) { // if three consecutive read errors, reset MFRC522
    if (!reset(_csPin, _rstPin)) {
      return P111_ERROR_RESET_BUSY;
    }
  }
  uint32_t tmpKey = uid[0];

  for (uint8_t i = 1; i < 4; i++) {
    tmpKey <<= 8;
    tmpKey  += uid[i];
  }
  *key = tmpKey;

  return error;
}

/**
 * Returns last read card (type) name
 */
String P111_data_struct::getCardName() {
  return mfrc522->PICC_GetTypeName(mfrc522->PICC_GetType(mfrc522->uid.sak));
}

/*********************************************************************************************\
* MFRC522 init
* Procedure when resetPin != -1:
* - pull reset pin low
* - set timer for 100 msec, phase = ResetTimer1
* - exit false
* - plugin_fifty_per_second counts down the timer, when done calls reset() again
* - pull reset pin high
* - set timer for 10 msec, phase = ResetTimer2
* - exit false
* - plugin_fifty_per_second counts down the timer, when done calls reset() again
* - reset initializes the cardreader and checks status
* - phase = Ready
* - exit true, assuming initialization succeeds
\*********************************************************************************************/
bool P111_data_struct::reset(int8_t csPin,
                             int8_t resetPin) {
  if ((resetPin != -1) &&
      (initPhase == P111_initPhases::Ready)) {
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(
        LOG_LEVEL_INFO, 
        concat(F("MFRC522: Reset on pin: "), resetPin));
    }

    init();
    return true;

    // FIXME TD-er: Remove the rest of this function.
    pinMode(resetPin, OUTPUT);
    digitalWrite(resetPin, LOW);
    timeToWait = 100;
    initPhase  = P111_initPhases::ResetDelay1; // Start 1st timer
    return false;
  }

  if ((resetPin != -1) &&
      (initPhase == P111_initPhases::ResetDelay1)) {
    digitalWrite(resetPin, HIGH);
    pinMode(resetPin, INPUT_PULLUP);
    timeToWait = 10;                           // Effectively 20 msec
    initPhase  = P111_initPhases::ResetDelay2; // Start 2nd timer
    return false;
  }

  pinMode(csPin, OUTPUT);
  digitalWrite(csPin, LOW);

  mfrc522->PCD_Init(csPin, resetPin); // Init MFRC522 module
  mfrc522->PCD_WriteRegister(MFRC522::ComIEnReg, 0b10100000); // enable receiver interrupt
  mfrc522->PCD_WriteRegister(MFRC522::DivIEnReg, 0x80); // Set as CMOS output pin

  // If you set Antenna Gain to Max it will increase reading distance
  mfrc522->PCD_SetAntennaGain(mfrc522->RxGain_max);

  bool result = mfrc522->PCD_PerformSelfTest(); // perform the test

  if (result) {
    // String log = F("RC522: Found");
    // Get the MFRC522 software version
    uint8_t v = mfrc522->PCD_ReadRegister(mfrc522->VersionReg);

    // When 0x00 or 0xFF is returned, communication probably failed
    if ((v == 0x00) || (v == 0xFF)) {
      addLog(LOG_LEVEL_ERROR, F("MFRC522: Communication failure, is the MFRC522 properly connected?"));
      result = false;
    } else {
      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        String log = F("MFRC522: Software Version: ");

        if (v == 0x91) {
          log += F(" = v1.0");
        }
        else if (v == 0x92) {
          log += F(" = v2.0");
        }
        else {
          log += F(" (unknown),probably a chinese clone?");
        }

        addLogMove(LOG_LEVEL_INFO, log);
      }
    }
  }

  if ((resetPin != -1) &&
      (initPhase == P111_initPhases::ResetDelay1)) { // Last phase, done
    initPhase = P111_initPhases::Ready;              // Reading can commence again
  }

  return result;
}

/*********************************************************************************************\
* RC522 read tag ID
\*********************************************************************************************/
uint8_t P111_data_struct::readPassiveTargetID(uint8_t *uid,
                                              uint8_t *uidLength) { // needed ? see above (not PN532)
  // Getting ready for Reading PICCs
  if (!mfrc522->PICC_IsNewCardPresent()) {                          // If a new PICC placed to RFID reader continue
    return P111_ERROR_NO_TAG;
  }
  addLog(LOG_LEVEL_INFO, F("MFRC522: New Card Detected"));

  if (!mfrc522->PICC_ReadCardSerial()) { // Since a PICC placed get Serial and continue
    return P111_ERROR_READ;
  }

  // There are Mifare PICCs which have 4 uint8_t or 7 uint8_t UID care if you use 7 uint8_t PICC
  // I think we should assume every PICC as they have 4 uint8_t UID
  // Until we support 7 uint8_t PICCs
  addLog(LOG_LEVEL_INFO, F("MFRC522: Scanned PICC's UID"));

  for (uint8_t i = 0; i < 4; i++) { //
    uid[i] = mfrc522->uid.uidByte[i];
  }
  *uidLength = 4;


  #ifndef BUILD_NO_DEBUG
  if (loglevelActiveFor(LOG_LEVEL_DEBUG))
  {
    PrintToString p2str;
    mfrc522->PICC_DumpToSerial(&(mfrc522->uid), p2str);
    if (p2str.length()) {
      addLog(LOG_LEVEL_DEBUG, concat(F("MFRC522: "), p2str.get()));
    }
  }
  #endif


  mfrc522->PICC_HaltA(); // Stop reading
  return P111_NO_ERROR;
}

void P111_data_struct::mfrc522_interrupt(P111_data_struct * self)
{
  self->_irq_pin_time_micros = getMicros64();
}

/*********************************************************************************************
 * Handle regular read and reset processing
 ********************************************************************************************/
bool P111_data_struct::plugin_ten_per_second(struct EventStruct *event) {
  return loop(event);
}

bool P111_data_struct::loop(struct EventStruct *event) {
  bool success = false;

  if (((initPhase == P111_initPhases::ResetDelay1) || // Whichever handler comes first
       (initPhase == P111_initPhases::ResetDelay2)) &&
      (timeToWait <= 0)) {
    timeToWait = 0;

    reset(_csPin, _rstPin); // Start next phase
    return success;
  }

  counter++;          // This variable replaces a static variable in the original implementation
  const bool ComIrqReg_bits = (mfrc522->PCD_ReadRegister(MFRC522::ComIrqReg) & (1<<5)) != 0;
  mfrc522->PCD_WriteRegister(MFRC522::ComIrqReg, 0x34);

  if (counter >= 10 || ComIrqReg_bits) { // Only every 3rd 0.1 second we do a read
    counter = 0;

    uint32_t key        = P111_NO_KEY;
    bool     removedTag = false;
    const uint8_t error = readCardStatus(&key, &removedTag);

    if (error == P111_NO_ERROR) {
      const uint32_t old_key = UserVar.getSensorTypeLong(event->TaskIndex);
      bool new_key           = false;

      # ifdef P111_USE_REMOVAL

      if (removedTag && (P111_TAG_AUTOREMOVAL == 2)) { // removal detected and enabled
        key = P111_REMOVALVALUE;
      }
      # endif // P111_USE_REMOVAL

      if ((old_key != key) && (key != P111_NO_KEY)) {
        UserVar.setSensorTypeLong(event->TaskIndex, key);
        new_key = true;
      }

      if (loglevelActiveFor(LOG_LEVEL_INFO) && (key != P111_NO_KEY)) {
        String log = F("MFRC522: ");

        if (new_key) {
          log += F("New Tag: ");
        } else {
          log += F("Old Tag: ");
        }
        log += formatToHex_decimal(key);

        if (!removedTag) {
          log += F(" card: ");
          log += getCardName();
        }
        addLogMove(LOG_LEVEL_INFO, log);
      }

      if (new_key && !removedTag) { // Removal event sent from PLUGIN_TIMER_IN, if any
        sendData(event);
      }
      Scheduler.setPluginTaskTimer(P111_REMOVALTIMEOUT, event->TaskIndex, event->Par1);
      success = true;
    }
  }
  return success;
}

/*********************************************************************************************
 * Handle timers instead of using delay()
 ********************************************************************************************/
bool P111_data_struct::plugin_fifty_per_second(struct EventStruct *event) {
  if ((initPhase == P111_initPhases::ResetDelay1) ||
      (initPhase == P111_initPhases::ResetDelay2)) {
    timeToWait -= 20; // milliseconds

    // String log = F("MFRC522: remaining wait: ");
    // log += timeToWait;
    // addLogMove(LOG_LEVEL_INFO, log);

    if (initPhase == P111_initPhases::ResetDelay1) { // Only handle ResetDelay1 here, as ResetDelay2 phase might be too much for 50/s
      if (timeToWait <= 0) {
        timeToWait = 0;

        reset(_csPin, _rstPin); // Start next phase
      }
    }
  }
  if (_irq_pin_time_micros > _last_served_irq_pin_time_micros) {
    _last_served_irq_pin_time_micros = _irq_pin_time_micros;
    //addLog(LOG_LEVEL_INFO, F("P111: acting on interrupt"));
    loop(event);
  }

  return true;
}


String P111_data_struct::PCD_getVersion(uint8_t& v) {
  v = 0xFF;
  if (mfrc522) {
    v = mfrc522->PCD_ReadRegister(MFRC522::VersionReg);
    if (v != 0xFF && v != 0) {
      // Human readable version.
      String res = concat(formatToHex(v, 2), F(" = "));
      switch(v) {
        case 0xb2:
          res += F("FM17522_1");
          break;
        case 0x88:
          res += F("FM17522");
          break;
        case 0x89:
          res += F("FM17522E");
          break;
        case 0x90:
          res += F("v0.0");
          break;
        case 0x91:
          res += F("v1.0");
          break;
        case 0x92:
          res += F("v2.0");
          break;
        case 0x12:
          res += F("counterfeit chip");
          break;
        default:
          res += F("(unknown)");
          break;
      }
      return res;
    }
  }
  return EMPTY_STRING;
}


#endif // ifdef USES_P111

#include "../PluginStructs/P064_data_struct.h"

#ifdef USES_P064

// Needed also here for PlatformIO's library finder as the .h file 
// is in a directory which is excluded in the src_filter
#include <SparkFun_APDS9960.h> // Lib is modified to work with ESP

P064_data_struct::P064_data_struct() {}


#endif // ifdef USES_P064

#include "../PluginStructs/P082_data_struct.h"

#ifdef USES_P082


// Needed also here for PlatformIO's library finder as the .h file
// is in a directory which is excluded in the src_filter
# include <TinyGPS++.h>
# include <ESPeasySerial.h>


const __FlashStringHelper* Plugin_082_valuename(P082_query value_nr, bool displayString) {
  switch (value_nr) {
    case P082_query::P082_QUERY_LONG:        return displayString ? F("Longitude")          : F("long");
    case P082_query::P082_QUERY_LAT:         return displayString ? F("Latitude")           : F("lat");
    case P082_query::P082_QUERY_ALT:         return displayString ? F("Altitude")           : F("alt");
    case P082_query::P082_QUERY_SPD:         return displayString ? F("Speed (m/s)")        : F("spd");
    case P082_query::P082_QUERY_SATVIS:      return displayString ? F("Satellites Visible") : F("sat_vis");
    case P082_query::P082_QUERY_SATUSE:      return displayString ? F("Satellites Tracked") : F("sat_tr");
    case P082_query::P082_QUERY_HDOP:        return displayString ? F("HDOP")               : F("hdop");
    case P082_query::P082_QUERY_FIXQ:        return displayString ? F("Fix Quality")        : F("fix_qual");
    case P082_query::P082_QUERY_DB_MAX:      return displayString ? F("Max SNR in dBHz")    : F("snr_max");
    case P082_query::P082_QUERY_CHKSUM_FAIL: return displayString ? F("Checksum Fail")      : F("chksum_fail");
    case P082_query::P082_QUERY_DISTANCE:    return displayString ? F("Distance (ODO)")     : F("dist");
    case P082_query::P082_QUERY_DIST_REF:    return displayString ? F("Distance from Reference Point") : F("dist_ref");
    case P082_query::P082_QUERY_COURSE:      return displayString ? F("Course (BeaRinG)")   : F("course");
    case P082_query::P082_NR_OUTPUT_OPTIONS: break;
  }
  return F("");
}

P082_query Plugin_082_from_valuename(const String& valuename)
{
  for (uint8_t query = 0; query < static_cast<uint8_t>(P082_query::P082_NR_OUTPUT_OPTIONS); ++query) {
    if (valuename.equalsIgnoreCase(Plugin_082_valuename(static_cast<P082_query>(query), false))) {
      return static_cast<P082_query>(query);
    }
  }
  return P082_query::P082_NR_OUTPUT_OPTIONS;
}

const __FlashStringHelper* toString(P082_PowerMode mode) {
  switch (mode) {
    case P082_PowerMode::Max_Performance: return F("Max Performance");
    case P082_PowerMode::Power_Save:      return F("Power Save");
    case P082_PowerMode::Eco:             return F("ECO");
  }
  return F("");
}

const __FlashStringHelper* toString(P082_DynamicModel model) {
  switch (model) {
    case P082_DynamicModel::Portable:    return F("Portable");
    case P082_DynamicModel::Stationary:  return F("Stationary");
    case P082_DynamicModel::Pedestrian:  return F("Pedestrian");
    case P082_DynamicModel::Automotive:  return F("Automotive");
    case P082_DynamicModel::Sea:         return F("Sea");
    case P082_DynamicModel::Airborne_1g: return F("Airborne_1g");
    case P082_DynamicModel::Airborne_2g: return F("Airborne_2g");
    case P082_DynamicModel::Airborne_4g: return F("Airborne_4g");
    case P082_DynamicModel::Wrist:       return F("Wrist");
    case P082_DynamicModel::Bike:        return F("Bike");
  }
  return F("");
}

P082_software_pps::P082_software_pps()
{
  for (size_t i = 0; i < NR_ELEMENTS(_second_frac_in_usec); ++i) {
    _second_frac_in_usec[i].setModulo(1000000ul);
  }
}

void P082_software_pps::addStartOfSentence(uint32_t bytesAvailableInSerialBuffer)
{
  if (_baudrate == 0) {
    return;
  }

  // Subtract the time (usec) taken to send the nr of bytes present in the serial buffer
  _cur_start_sentence_usec = getMicros64() - bytesToUsec(bytesAvailableInSerialBuffer);
}

void P082_software_pps::setSentenceType(
  TinyGPSPlus::GPS_Sentence_type sentenceType,
  uint32_t                       bytesAvailableInSerialBuffer)
{
  if ((sentenceType < TinyGPSPlus::GPS_SENTENCE_OTHER) && (_cur_start_sentence_usec != 0ull)) {
    const uint64_t endOfLine_received_usec = getMicros64() - bytesToUsec(bytesAvailableInSerialBuffer);
    const int64_t  sentence_duration       = timeDiff64(_cur_start_sentence_usec, endOfLine_received_usec);

//    if (usecPassedSince(_cur_start_sentence_usec) < 1000000ll) {

    // Assume a NMEA sentence cannot be over 80 bytes
    // Apply some tolerance, thus check for duration to receive 120 bytes
    if (sentence_duration < static_cast<int64_t>(bytesToUsec(120))) {
      // Make sure we're not committing a timestamp to the wrong sentence type
      // However we only set this when a complete sentence was processed,
      // so it is highly unlikely we missed the start of the sentence.
      // Only way this can happen is when we missed exactly a complete sentence (or multiple)
      _second_frac_in_usec[sentenceType].add(_cur_start_sentence_usec % 1000000ull);

      if (_second_frac_in_usec[sentenceType].getCount() >= 10) {
        // Filter out the outliers and seed with current average.
        _second_frac_in_usec[sentenceType].resetKeepLast();
      }
    }
  }
  _cur_start_sentence_usec = 0;
}

void P082_software_pps::setBaudrate(uint32_t baudrate) {
  if (baudrate != 0) {
    _baudrate = baudrate;
  }
}

bool P082_software_pps::getPPS(uint32_t& second_frac_in_usec) const
{
  /*
     // Check the timestamps of the sentences which are likely to only have occurred once per second
     constexpr TinyGPSPlus::GPS_Sentence_type sentenceTypes[] =
     {
     TinyGPSPlus::GPS_SENTENCE_GPRMC,
     TinyGPSPlus::GPS_SENTENCE_GPGGA,
     TinyGPSPlus::GPS_SENTENCE_GPGLL
     };
   */

  return _second_frac_in_usec[TinyGPSPlus::GPS_SENTENCE_GPRMC].peek(second_frac_in_usec);
}

uint64_t P082_software_pps::bytesToUsec(uint32_t bytes) const
{
  if (_baudrate == 0) {
    return 0ull;
  }

  // Assume 10 bits per byte. (8N1)
  uint64_t duration_usec = bytes;

  duration_usec *= 10000000ull;
  duration_usec /= _baudrate;
  return duration_usec;
}

#ifndef BUILD_NO_DEBUG
String P082_software_pps::getStats() const
{
  String res;
  constexpr uint32_t nrelements = NR_ELEMENTS(_second_frac_in_usec);
  for (size_t i = 0; i < nrelements; ++i) {
    uint32_t value{};
    _second_frac_in_usec[i].peek(value);
    {

      switch (i) {
        case TinyGPSPlus::GPS_SENTENCE_GPGGA: res += F("GGA"); break;
        case TinyGPSPlus::GPS_SENTENCE_GPRMC: res += F("RMC"); break;
        case TinyGPSPlus::GPS_SENTENCE_GPGSA: res += F("GSA"); break;
        case TinyGPSPlus::GPS_SENTENCE_GPGSV: res += F("GSV"); break;
        case TinyGPSPlus::GPS_SENTENCE_GPGLL: res += F("GLL"); break;
        case TinyGPSPlus::GPS_SENTENCE_GPTXT: res += F("TXT"); break;
        default:
        res += F("---");
        break;
      }
      res += strformat(F(": %06d (%d)<br>"), value, _second_frac_in_usec[i].getCount());
    }
  }
  return res;  
}
#endif



P082_data_struct::P082_data_struct() : gps(nullptr), easySerial(nullptr) {
  for (size_t i = 0; i < static_cast<uint8_t>(P082_query::P082_NR_OUTPUT_OPTIONS); ++i) {
    _cache[i] = 0.0f;
  }
}

P082_data_struct::~P082_data_struct() {
  if (validGpio(_ppsPin)) {
    detachInterrupt(digitalPinToInterrupt(_ppsPin));
  }

  if (gps != nullptr) {
    delete gps;
    gps = nullptr;
  }

  if (easySerial != nullptr) {
    delete easySerial;
    easySerial = nullptr;
  }
}

/*
   void P082_data_struct::reset() {
   if (gps != nullptr) {
    delete gps;
    gps = nullptr;
   }

   if (easySerial != nullptr) {
    delete easySerial;
    easySerial = nullptr;
   }
   }
 */
bool P082_data_struct::init(
  ESPEasySerialPort port,
  const int16_t     serial_rx,
  const int16_t     serial_tx,
  const int8_t      pps_pin) {
  if (serial_rx < 0) {
    return false;
  }

  if (gps != nullptr) {
    delete gps;
    gps = nullptr;
  }

  if (easySerial != nullptr) {
    delete easySerial;
    easySerial = nullptr;
  }

  # ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  # endif // ifdef USE_SECOND_HEAP


  gps        = new (std::nothrow) TinyGPSPlus();
  easySerial = new (std::nothrow) ESPeasySerial(port, serial_rx, serial_tx, false, 512);

  if (easySerial != nullptr) {
    easySerial->begin(9600);
    wakeUp();
  }

  _ppsPin = pps_pin;

  if (validGpio(_ppsPin)) {
    pinMode(_ppsPin, INPUT);

    attachInterruptArg(
      digitalPinToInterrupt(_ppsPin),
      reinterpret_cast<void (*)(void *)>(pps_interrupt),
      this, RISING);
  } else {
    _softwarePPS.setBaudrate(easySerial->getBaudRate());
  }

  return isInitialized();
}

bool P082_data_struct::loop() {
  if (!isInitialized()) {
    return false;
  }
  bool completeSentence = false;

  if (easySerial != nullptr) {
    int available           = easySerial->available();
    unsigned long startLoop = millis();

    while (available > 0 && timePassedSince(startLoop) < 10) {
      --available;
      int c = easySerial->read();

      if (c >= 0) {
# ifdef P082_SEND_GPS_TO_LOG

        if (_currentSentence.length() <= 80) {
          // No need to capture more than 80 bytes as a NMEA message is never that long.
          if (c != 0) {
            _currentSentence += static_cast<char>(c);
          }
        }
# endif // ifdef P082_SEND_GPS_TO_LOG

        if (c == 0x85) {
          // Found possible start of u-blox message
          unsigned long timeout   = millis() + 200;
          unsigned int  bytesRead = 0;
          bool done               = false;
          bool ack_nak_read       = false;

          while (!timeOutReached(timeout) && !done)
          {
            if (available == 0) {
              available = easySerial->available();
            } else {
              const int c = easySerial->read();

              if (c >= 0) {
                switch (bytesRead) {
                  case 0:

                    if (c != 0x62) {
                      done = true;
                    }
                    ++bytesRead;
                    break;
                  case 1:

                    if (c != 0x05) {
                      done = true;
                    }
                    ++bytesRead;
                    break;
                  case 2:

                    if (c == 0x01) {
                      ack_nak_read = true;
                      addLog(LOG_LEVEL_INFO, F("GPS  : ACK-ACK"));
                    } else if (c == 0x00) {
                      ack_nak_read = true;
                      addLog(LOG_LEVEL_ERROR, F("GPS  : ACK-NAK"));
                    }
                    done = true;
                    break;
                  default:
                    done = true;
                    break;
                }
              }
            }
          }

          if (!done) {
            addLog(LOG_LEVEL_ERROR, F("GPS  : Ack/Nack timeout"));
          } else if (!ack_nak_read) {
            addLog(LOG_LEVEL_ERROR, F("GPS  : Unexpected reply"));
          }
        }

        if (gps->encode(c)) {
          // Full sentence received
# ifdef P082_SEND_GPS_TO_LOG
          _lastSentence    = _currentSentence;
          free_string(_currentSentence);
# endif // ifdef P082_SEND_GPS_TO_LOG
          completeSentence = true;
          available        = easySerial->available();
          _softwarePPS.setSentenceType(gps->getCurrentSentenceType(), available);
        } else {
          if (c == '$') {
            available = easySerial->available();
            _softwarePPS.addStartOfSentence(available);
          }
          if (available == 0) {
            available = easySerial->available();
          }
        }
      }
    }
  }
  return completeSentence;
}

bool P082_data_struct::hasFix(unsigned int maxAge_msec) {
  if (!isInitialized()) {
    return false;
  }
  return gps->location.isValid() && gps->location.age() < maxAge_msec;
}

bool P082_data_struct::storeCurPos(unsigned int maxAge_msec) {
  if (!hasFix(maxAge_msec)) {
    return false;
  }

  _distance += distanceSinceLast(maxAge_msec);
  _last_lat  = gps->location.lat();
  _last_lng  = gps->location.lng();
  return true;
}

// Return the distance in meters compared to last stored position.
// @retval  -1 when no fix.
ESPEASY_RULES_FLOAT_TYPE P082_data_struct::distanceSinceLast(unsigned int maxAge_msec) {
  if (!hasFix(maxAge_msec)) {
    return -1.0;
  }

  if (((_last_lat < 0.0001) && (_last_lat > -0.0001)) || ((_last_lng < 0.0001) && (_last_lng > -0.0001))) {
    return -1.0;
  }
  return gps->distanceBetween(_last_lat, _last_lng, gps->location.lat(), gps->location.lng());
}

// Return the GPS time stamp, which is in UTC.
// @param age is the time in msec since the last update of the time +
// additional centiseconds given by the GPS.
bool P082_data_struct::getDateTime(
  struct tm& dateTime,
  uint8_t  & centiseconds,
  uint32_t & age,
  bool     & updated) {
  updated = false;

  if (!isInitialized()) {
    return false;
  }

  if (!gps->time.isUpdated() || !gps->date.isUpdated()) {
    return false;
  }

  age = gps->time.age();

  if (age > P082_TIMESTAMP_AGE) {
    return false;
  }

  if (gps->date.age() > P082_TIMESTAMP_AGE) {
    return false;
  }

  if (!gps->time.isValid()) {
    gps->time.value(); // Clear the 'updated' state
    return false;
  }

  if (!gps->date.isValid()) {
    gps->date.value(); // Clear the 'updated' state
    return false;
  }
  dateTime.tm_year = gps->date.year() - 1900;
  dateTime.tm_mon  = gps->date.month() - 1; // GPS month starts at 1, tm_mon at 0
  dateTime.tm_mday = gps->date.day();

  dateTime.tm_hour = gps->time.hour();
  dateTime.tm_min  = gps->time.minute();
  dateTime.tm_sec  = gps->time.second();

  const uint32_t reported_time = gps->time.value();
  const uint32_t reported_date = gps->date.value();

  // FIXME TD-er: Must the offset in centisecond be added when pps_sync active?
  if (!validGpio(_ppsPin)) {
    centiseconds = gps->time.centisecond();
  }

  updated    = reported_time != _last_time;
  _last_time = reported_time;
  _last_date = reported_date;

  return true;
}

bool P082_data_struct::getDateTime(struct tm& dateTime) const
{
  uint64_t value_usec{};

  if (_oversampling_gps_time_offset_usec.peek(value_usec)) {
    const double time = (getMicros64() + value_usec) / 1000000.0;
    breakTime(static_cast<uint32_t>(time), dateTime);
    return true;
  }
  return false;
}

bool P082_data_struct::tryUpdateSystemTime() {
  struct tm dateTime;
  uint8_t   centiseconds{};
  uint32_t  age{};
  bool updated{};

  if (getDateTime(dateTime, centiseconds, age, updated)) {
    if (updated) {
      const uint64_t cur_micros = getMicros64();

      uint32_t second_frac_in_usec{};

      if (validGpio(_ppsPin) && (_pps_time_micros > 0ll)) {
        // Rely on timestamp from PPS pin,
        // even when this might have been updated quite long ago
        second_frac_in_usec = _pps_time_micros % 1000000ll;
      } else if (_softwarePPS.getPPS(second_frac_in_usec)) {
        // we got some estimate based on the timestamp of receiving the first sentence.
      } else {
        // Determine the system micros at the time the GPS time was committed
        // This is the least accurate method.
        const uint64_t micros = cur_micros - (age * 1000ull);
        second_frac_in_usec = micros % 1000000ull;
      }

      // First round to seconds
      uint64_t sys_micros_at_start_second = cur_micros / 1000000ull;
      sys_micros_at_start_second *= 1000000ull;

      // Add fraction of seconds (phase offset in usec)
      sys_micros_at_start_second += second_frac_in_usec;

      if (sys_micros_at_start_second > cur_micros) {
        sys_micros_at_start_second -= 1000000ull;
      }
      const uint64_t unixTime_usec      = makeTime(dateTime) * 1000000ull + (10000ull * centiseconds);
      const uint64_t uptime_offset_usec =
        (unixTime_usec < sys_micros_at_start_second)
        ? unixTime_usec
        : (unixTime_usec - sys_micros_at_start_second);

      _oversampling_gps_time_offset_usec.add(uptime_offset_usec);

      // Compute average over offset between system micros and GPS reported timestamp.
      // Both extremes will be filtered out
      if (_oversampling_gps_time_offset_usec.getCount() >= 5) {
        uint64_t value_usec{};

        if (_oversampling_gps_time_offset_usec.get(value_usec)) {
          const double time = (getMicros64() + value_usec) / 1000000.0;

          // Seed oversampling with the current average value
          _oversampling_gps_time_offset_usec.add(value_usec);

          timeSource_t timeSource = timeSource_t::GPS_time_source_no_fix;

          if (hasFix(P082_TIMESTAMP_AGE)) {
            // Using PPS sync should be extremely stable without any significant time wander.
            // When we only can rely on keeping track of the timestamp at the start of a sentence, the fluctuation is significant.
            if (usecPassedSince(_pps_time_micros) < 1000000ll) {
              timeSource = timeSource_t::GPS_PPS_time_source;
            } else {
              timeSource = timeSource_t::GPS_time_source;
            }
          }

          if (node_time.setExternalTimeSource(
                time,
                timeSource)) {
            return true;
          }
        }
      }
    }
  }
  return false;
}

bool P082_data_struct::powerDown() {
  const uint8_t UBLOX_GPSStandby[] = { 0xB5, 0x62, 0x02, 0x41, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x4D, 0x3B };

  return writeToGPS(UBLOX_GPSStandby, sizeof(UBLOX_GPSStandby));
}

bool P082_data_struct::wakeUp() {
  if (isInitialized()) {
    if (easySerial->isTxEnabled()) {
      easySerial->println(); // Send some character to wake it up.
    }
  }
  return false;
}

# ifdef P082_USE_U_BLOX_SPECIFIC
bool P082_data_struct::setPowerMode(P082_PowerMode mode) {
  switch (mode) {
    case P082_PowerMode::Max_Performance:
    {
      const uint8_t UBLOX_command[] = { 0xB5, 0x62, 0x06, 0x11, 0x02, 0x00, 0x08, 0x00, 0x21, 0x91 };
      return writeToGPS(UBLOX_command, sizeof(UBLOX_command));
    }
    case P082_PowerMode::Power_Save:
    {
      const uint8_t UBLOX_command[] = { 0xB5, 0x62, 0x06, 0x11, 0x02, 0x00, 0x08, 0x01, 0x22, 0x92 };
      return writeToGPS(UBLOX_command, sizeof(UBLOX_command));
    }
    case P082_PowerMode::Eco:
    {
      const uint8_t UBLOX_command[] = { 0xB5, 0x62, 0x06, 0x11, 0x02, 0x00, 0x08, 0x04, 0x25, 0x95 };
      return writeToGPS(UBLOX_command, sizeof(UBLOX_command));
    }
  }
  return false;
}

bool P082_data_struct::setDynamicModel(P082_DynamicModel model) {
  const uint8_t dynModel = static_cast<uint8_t>(model);

  if ((dynModel == 1) || (dynModel > 10)) {
    return false;
  }

  uint8_t UBLOX_command[] = {
    0xB5, 0x62, // header
    0x06,       // class
    0x24,       // ID, UBX-CFG-NAV5
    0x24, 0x00, // length
    0x01, 0x00, // mask
    dynModel,   // dynModel
    0x03,       // fixMode auto 2D/3D
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,0x00, 0x00
  };

  setUbloxChecksum(UBLOX_command, sizeof(UBLOX_command));
  return writeToGPS(UBLOX_command, sizeof(UBLOX_command));
}

# endif // ifdef P082_USE_U_BLOX_SPECIFIC

# ifdef P082_USE_U_BLOX_SPECIFIC
void P082_data_struct::computeUbloxChecksum(const uint8_t *data, size_t size, uint8_t& CK_A, uint8_t& CK_B) {
  CK_A = 0;
  CK_B = 0;

  for (size_t i = 0; i < size; ++i) {
    CK_A = CK_A + data[i];
    CK_B = CK_B + CK_A;
  }
}

void P082_data_struct::setUbloxChecksum(uint8_t *data, size_t size) {
  uint8_t CK_A;
  uint8_t CK_B;

  computeUbloxChecksum(data + 2, size - 4, CK_A, CK_B);
  data[size - 2] = CK_A;
  data[size - 1] = CK_B;
}

# endif // ifdef P082_USE_U_BLOX_SPECIFIC

bool P082_data_struct::writeToGPS(const uint8_t *data, size_t size) {
  if (isInitialized()) {
    if (easySerial->isTxEnabled()) {
      if (size != easySerial->write(data, size)) {
        addLog(LOG_LEVEL_ERROR, F("GPS  : Written less bytes than expected"));
        return false;
      }
      return true;
    }
  }
  addLog(LOG_LEVEL_ERROR, F("GPS  : Cannot send to GPS"));
  return false;
}

void ICACHE_RAM_ATTR P082_data_struct::pps_interrupt(P082_data_struct *self)
{
  self->_pps_time_micros = getMicros64();
}

# if FEATURE_PLUGIN_STATS
bool P082_data_struct::webformLoad_show_stats(struct EventStruct *event, uint8_t var_index, P082_query query_type) const
{
  bool somethingAdded = false;

  const PluginStats *stats = getPluginStats(var_index);


  if (stats != nullptr) {
    if (stats->webformLoad_show_avg(event)) {
      somethingAdded = true;
    }

    bool show_custom = false;
    ESPEASY_RULES_FLOAT_TYPE dist_p2p{};
    ESPEASY_RULES_FLOAT_TYPE dist_stddev{};

    if (gps != nullptr) {
      switch (query_type) {
        case P082_query::P082_QUERY_LAT:
          show_custom = true;

          // Compute distance between min and max peak
          dist_p2p = gps->distanceBetween(
            stats->getPeakLow(),  _last_lng,
            stats->getPeakHigh(), _last_lng);
          dist_stddev = gps->distanceBetween(
            _last_lat,                            _last_lng,
            _last_lat + stats->getSampleStdDev(), _last_lng);
          break;
        case P082_query::P082_QUERY_LONG:
          show_custom = true;

          // Compute distance between min and max peak
          dist_p2p = gps->distanceBetween(
            _last_lat, stats->getPeakLow(),
            _last_lat, stats->getPeakHigh());

          // Compute distance for std.dev
          dist_stddev = gps->distanceBetween(
            _last_lat, _last_lng,
            _last_lat, _last_lng + stats->getSampleStdDev());
          break;
        default:
          break;
      }
    }

    // Only show standard deviation in meters, which is more useful than std. dev in degrees.
    if (somethingAdded) {
      if (show_custom) {
        stats->webformLoad_show_val(
          event,
          F(" std. dev"),
          dist_stddev,
          F("m"));
      } else {
        stats->webformLoad_show_stdev(event);
      }
    }

    if (stats->webformLoad_show_peaks(event, !show_custom)) {
      somethingAdded = true;

      if (show_custom) {
        stats->webformLoad_show_val(
          event,
          F(" Peak-to-peak coordinates"),
          stats->getPeakHigh() - stats->getPeakLow(),
          F("deg"));
        stats->webformLoad_show_val(
          event,
          F(" Peak-to-peak distance"),
          dist_p2p,
          F("m"));
      }
    }

    if (somethingAdded) {
      addFormSeparator(4);
    }
  }
  return somethingAdded;
}

#  if FEATURE_CHART_JS
void P082_data_struct::webformLoad_show_position_scatterplot(struct EventStruct *event)
{
  taskVarIndex_t stats_long = INVALID_TASKVAR_INDEX;
  taskVarIndex_t stats_lat  = INVALID_TASKVAR_INDEX;

  for (uint8_t var_index = 0; var_index < P082_NR_OUTPUT_VALUES; ++var_index) {
    const uint8_t pconfigIndex = var_index + P082_QUERY1_CONFIG_POS;
    const P082_query query     = static_cast<P082_query>(PCONFIG(pconfigIndex));

    switch (query) {
      case P082_query::P082_QUERY_LONG:
        stats_long = var_index;
        break;
      case P082_query::P082_QUERY_LAT:
        stats_lat = var_index;
        break;
      default:
        break;
    }
  }

  plot_ChartJS_scatter(
    stats_long,
    stats_lat,
    F("positionscatter"),
    { F("Position Scatter Plot") },
    { F("Coordinates"), F("rgb(255, 99, 132)") },
    500,
    500);
}

#  endif // if FEATURE_CHART_JS
# endif  // if FEATURE_PLUGIN_STATS

#ifndef BUILD_NO_DEBUG
String P082_data_struct::getPPSStats() const
{
  return _softwarePPS.getStats();
}
#endif


#endif   // ifdef USES_P082

#include "../PluginStructs/P085_data_struct.h"

#ifdef USES_P085

P085_data_struct::~P085_data_struct() {
  modbus.reset();
}

void P085_data_struct::reset() {
  modbus.reset();
}

bool P085_data_struct::init(ESPEasySerialPort port, const int16_t serial_rx, const int16_t serial_tx, int8_t dere_pin,
                            unsigned int baudrate, uint8_t modbusAddress) {
  return modbus.init(port, serial_rx, serial_tx, baudrate, modbusAddress, dere_pin);
}

const __FlashStringHelper* Plugin_085_valuename(uint8_t value_nr, bool displayString) {
  switch (value_nr) {
    case P085_QUERY_V:      return displayString ? F("Voltage (V)") : F("V");
    case P085_QUERY_A:      return displayString ? F("Current (A)") : F("A");
    case P085_QUERY_W:      return displayString ? F("Power (W)") : F("W");
    case P085_QUERY_Wh_imp: return displayString ? F("Import Energy (Wh)") : F("Wh_imp");
    case P085_QUERY_Wh_exp: return displayString ? F("Export Energy (Wh)") : F("Wh_exp");
    case P085_QUERY_Wh_tot: return displayString ? F("Total Energy (Wh)") : F("Wh_tot");
    case P085_QUERY_Wh_net: return displayString ? F("Net Energy (Wh)") : F("Wh_net");
    case P085_QUERY_h_tot:  return displayString ? F("Meter Running Time (h)") : F("h_tot");
    case P085_QUERY_h_load: return displayString ? F("Load Running Time (h)") : F("h_load");
  }
  return F("");
}

# if FEATURE_MQTT_DISCOVER
int Plugin_085_QueryVType(uint8_t value_nr) {
  Sensor_VType result = Sensor_VType::SENSOR_TYPE_NONE;

  switch (value_nr) {
    case P085_QUERY_V:      result = Sensor_VType::SENSOR_TYPE_VOLTAGE_ONLY; break;
    case P085_QUERY_A:      result = Sensor_VType::SENSOR_TYPE_CURRENT_ONLY; break;
    case P085_QUERY_W:      result = Sensor_VType::SENSOR_TYPE_POWER_USG_ONLY; break;
    case P085_QUERY_Wh_imp:
    case P085_QUERY_Wh_exp:
    case P085_QUERY_Wh_tot: // Fall through
    case P085_QUERY_Wh_net: result = Sensor_VType::SENSOR_TYPE_ENERGY; break;
    case P085_QUERY_h_tot:  // Fall through
    case P085_QUERY_h_load: result = Sensor_VType::SENSOR_TYPE_DURATION; break;
  }
  return static_cast<int>(result);
}

# endif // if FEATURE_MQTT_DISCOVER

int p085_storageValueToBaudrate(uint8_t baudrate_setting) {
  switch (baudrate_setting) {
    case 0:
      return 1200;
    case 1:
      return 2400;
    case 2:
      return 4800;
    case 3:
      return 9600;
    case 4:
      return 19200;
    case 5:
      return 38500;
  }
  return 19200;
}

float p085_readValue(uint8_t query, struct EventStruct *event) {
  P085_data_struct *P085_data =
    static_cast<P085_data_struct *>(getPluginTaskData(event->TaskIndex));

  if ((nullptr != P085_data) && P085_data->isInitialized()) {
    switch (query) {
      case P085_QUERY_V:
        return P085_data->modbus.read_float_HoldingRegister(0x200);
      case P085_QUERY_A:
        return P085_data->modbus.read_float_HoldingRegister(0x202);
      case P085_QUERY_W:
        return P085_data->modbus.read_float_HoldingRegister(0x204) * 1000.0f; // power (kW => W)
      case P085_QUERY_Wh_imp:
        return P085_data->modbus.read_32b_HoldingRegister(0x300) * 10.0f;     // 0.01 kWh => Wh
      case P085_QUERY_Wh_exp:
        return P085_data->modbus.read_32b_HoldingRegister(0x302) * 10.0f;     // 0.01 kWh => Wh
      case P085_QUERY_Wh_tot:
        return P085_data->modbus.read_32b_HoldingRegister(0x304) * 10.0f;     // 0.01 kWh => Wh
      case P085_QUERY_Wh_net:
      {
        int64_t intvalue = P085_data->modbus.read_32b_HoldingRegister(0x306);

        if (intvalue >= 2147483648ll) {
          intvalue = 4294967296ll - intvalue;
        }
        float value = static_cast<float>(intvalue);
        value *= 10.0f; // 0.01 kWh => Wh
        return value;
      }
      case P085_QUERY_h_tot:
        return P085_data->modbus.read_32b_HoldingRegister(0x280) / 100.0f;
      case P085_QUERY_h_load:
        return P085_data->modbus.read_32b_HoldingRegister(0x282) / 100.0f;
    }
  }
  return 0.0f;
}

void p085_showValueLoadPage(uint8_t query, struct EventStruct *event) {
  addRowLabel(Plugin_085_valuename(query, true));
  addHtml(String(p085_readValue(query, event)));
}

#endif // ifdef USES_P085

// ----------------------------------------------------------------------------
// P144 "Dust - PM1006(K) (Vindriktning)"
// Implementation of sensor abstraction
// 2022 By flashmark
// ----------------------------------------------------------------------------
#include "../PluginStructs/P144_data_struct.h"

#ifdef USES_P144
#include <ESPeasySerial.h>

// ----------------------------------------------------------------------------
// Function setSerial
// Connect the serial port used for the sensor
// The ESPeasySerial class is used to handle the connection
// ----------------------------------------------------------------------------
bool P144_data_struct::setSerial(ESPEasySerialPort portType, int8_t rxPin, int8_t txPin) 
{
  bool success = false;

  if (easySerial != nullptr)
  {
    delete easySerial;
  }
  // Try to open the assocaited serial port
  easySerial = new (std::nothrow) ESPeasySerial(portType, rxPin, txPin);
  if (easySerial != nullptr) 
  {
    easySerial->begin(9600);
    success = true;
  }

  #ifdef PLUGIN_144_DEBUG
  if (loglevelActiveFor(LOG_LEVEL_INFO))
  {
    String log = F("P144 : Init: ");
    if (success)
    {
      log += strformat(F("  ESP GPIO-pin RX:%d TX:%d"), rxPin, txPin);  
    }
    else
    {
      log += F("Failed opening serial port");
    }
    addLogMove(LOG_LEVEL_INFO, log);
  }
  #endif
  return success;
}

// ----------------------------------------------------------------------------
// Function exit
// Detaches the serial port
// ----------------------------------------------------------------------------
void P144_data_struct::disconnectSerial() 
{
  delete easySerial;
  easySerial = nullptr;
}

// ----------------------------------------------------------------------------
// Function processSensor
// This function must be called regularly to process the serial data received from the sensor
// It relies on processing serial RX characters fast enough to prever overrunning the buffer
// Function stops processing once the end of a message is reached, leaving remaining 
// characters to be processed next call.
// If the message contains sensor values these will be updated (pm25Value)
// ----------------------------------------------------------------------------
bool P144_data_struct::processSensor() {
  bool new_data = false;
  for (int charAvailable = easySerial->available(); ((charAvailable > 0) && !new_data); charAvailable--)
    {
      // Process received characters, return true when a complete message is received 
      // Message rate on Vindriktning is a few per minute
      new_data = processRx(easySerial->read());
      if (new_data) 
      {
        pm25Value = float((serialRxBuffer[3] << 8) + serialRxBuffer[4]);
        #ifdef PLUGIN_144_DEBUG
        if (loglevelActiveFor(LOG_LEVEL_INFO))
        {
          addLogMove(LOG_LEVEL_INFO, concat(F("P144 : New value received "), pm25Value));
        }
        #endif
      }
    }
    return true ;
}

// ----------------------------------------------------------------------------
// Function getValue
// Return the last received value of the sensor as stored in pm25Value
// ----------------------------------------------------------------------------
int P144_data_struct::getValue() 
{
  return(pm25Value);
}

// ----------------------------------------------------------------------------
// Function processRx
// Handles a single character received from the PM1006 in the Vindrikning
// It expects a response to the poll message sent by the Vindriknings own CPU
// 0x16 <len> <data> <chksum>
// <len>  = size of <data> in char [1 char]
// <data> = message payload, array of <len> chars
// <csum> = checksum, 256-(sum of all received characters) [1 char]
// ----------------------------------------------------------------------------
bool P144_data_struct::processRx(char c)
{
  switch (rxState) {
  case PM1006_HEADER:
    // Waiting for the expected message header (0x16)
    rxChecksum = c;
    if (c == 0x16) {
      rxState = PM1006_LENGTH;
    }
    break;

  case PM1006_LENGTH:
    // Waiting for the message length
    rxChecksum += c;
    if (c <= P144_bufferSize) {
      rxlen = c;
      rxIndex = 0;
      rxState = (rxlen > 0) ? PM1006_DATA : PM1006_CHECK;
    } 
    else 
    {
      rxState = PM1006_HEADER;
    }
    break;

  case PM1006_DATA:
    // Receiving the data part of the message
    rxChecksum += c;
    serialRxBuffer[rxIndex++] = c;
    if (rxIndex == rxlen) {
      rxState = PM1006_CHECK;
    }
    break;

  case PM1006_CHECK:
    // Waiting for the checksum of the message
    rxChecksum += c;
    rxState = PM1006_HEADER;
#ifdef PLUGIN_144_DEBUG
    dump();
#endif
    return ((rxChecksum & 0xFF) == 0);

  default:
    // Unexpected state, reset statemachine trashing pending data
    rxState = PM1006_HEADER;
    break;
  }
  return false;
}

#ifdef PLUGIN_144_DEBUG
// ----------------------------------------------------------------------------
// Function dump
// Dump the received buffer
// Note: Contents may be inconsistent unless alingned with the decoder
// ----------------------------------------------------------------------------
void P144_data_struct::dump()
{
  if (loglevelActiveFor(LOG_LEVEL_INFO))
  {
    String log = F("P144 : Dump message: ");
    log.reserve(100);
    for (int n = 0; n < rxlen; ++n)
    {
      log += formatToHex_no_prefix(serialRxBuffer[n]);
      log += ' ';
    }
    log += strformat(F(" size %d csum %d"), rxlen, rxChecksum & 0xFF);
    addLogMove(LOG_LEVEL_INFO, log);
  }
}
#endif // PLUGIN_144_DEBUG

#endif // USES_P144
#include "../PluginStructs/P095_data_struct.h"

#ifdef USES_P095

/****************************************************************************
 * ILI9xxx_type_toString: Display-value for the device selected
 ***************************************************************************/
const __FlashStringHelper* ILI9xxx_type_toString(const ILI9xxx_type_e& device) {
  switch (device) {
    case ILI9xxx_type_e::ILI9341_240x320: return F("ILI9341 240 x 320px");
    case ILI9xxx_type_e::ILI9342_240x320: return F("ILI9342 240 x 320px (M5Stack)");
    # if P095_ENABLE_ILI9342_2
    case ILI9xxx_type_e::ILI9342_CYD_AF_240x320: return F("ILI9342 240 x 320px (CYD-1)");
    case ILI9xxx_type_e::ILI9342_CYD_BD_240x320: return F("ILI9342 240 x 320px (CYD-2)");
    # endif // if P095_ENABLE_ILI9342_2
    case ILI9xxx_type_e::ILI9481_320x480: return F("ILI9481 320 x 480px");
    case ILI9xxx_type_e::ILI9481_CPT29_320x480: return F("ILI9481 320 x 480px (CPT29)");
    case ILI9xxx_type_e::ILI9481_PVI35_320x480: return F("ILI9481 320 x 480px (PVI35)");
    case ILI9xxx_type_e::ILI9481_AUO317_320x480: return F("ILI9481 320 x 480px (AUO317)");
    case ILI9xxx_type_e::ILI9481_CMO35_320x480: return F("ILI9481 320 x 480px (CMO35)");
    case ILI9xxx_type_e::ILI9481_RGB_320x480: return F("ILI9481 320 x 480px (RGB)");
    case ILI9xxx_type_e::ILI9481_CMI7_320x480: return F("ILI9481 320 x 480px (CMI7)");
    case ILI9xxx_type_e::ILI9481_CMI8_320x480: return F("ILI9481 320 x 480px (CMI8)");
    # if P095_ENABLE_ILI948X

    // case ILI9xxx_type_e::ILI9486_320x480: return F("ILI9486 320 x 480px");
    case ILI9xxx_type_e::ILI9488_320x480: return F("ILI9486/ILI9488 320 x 480px");
    # endif // if P095_ENABLE_ILI948X
  }
  # ifndef BUILD_NO_DEBUG
  return F("Unsupported type!");
  # else // ifndef BUILD_NO_DEBUG
  return F("");
  # endif // ifndef BUILD_NO_DEBUG
}

/****************************************************************************
 * ILI9xxx_type_toResolution: X and Y resolution for the selected type
 ***************************************************************************/
void ILI9xxx_type_toResolution(const ILI9xxx_type_e& device,
                               uint16_t            & x,
                               uint16_t            & y) {
  switch (device) {
    case ILI9xxx_type_e::ILI9341_240x320:
    case ILI9xxx_type_e::ILI9342_240x320:
    # if P095_ENABLE_ILI9342_2
    case ILI9xxx_type_e::ILI9342_CYD_AF_240x320:
    case ILI9xxx_type_e::ILI9342_CYD_BD_240x320:
    # endif // if P095_ENABLE_ILI9342_2
      x = 240;
      y = 320;
      break;
    case ILI9xxx_type_e::ILI9481_320x480:
    case ILI9xxx_type_e::ILI9481_CPT29_320x480:
    case ILI9xxx_type_e::ILI9481_PVI35_320x480:
    case ILI9xxx_type_e::ILI9481_AUO317_320x480:
    case ILI9xxx_type_e::ILI9481_CMO35_320x480:
    case ILI9xxx_type_e::ILI9481_RGB_320x480:
    case ILI9xxx_type_e::ILI9481_CMI7_320x480:
    case ILI9xxx_type_e::ILI9481_CMI8_320x480:
    # if P095_ENABLE_ILI948X

    // case ILI9xxx_type_e::ILI9486_320x480:
    case ILI9xxx_type_e::ILI9488_320x480:
    # endif // if P095_ENABLE_ILI948X
      x = 320;
      y = 480;
      break;
  }
}

/****************************************************************************
 * P095_CommandTrigger_toString: return the command string selected
 ***************************************************************************/
const __FlashStringHelper* P095_CommandTrigger_toString(const P095_CommandTrigger& cmd) {
  switch (cmd) {
    case P095_CommandTrigger::tft: return F("tft");
    case P095_CommandTrigger::ili9341: break;
    case P095_CommandTrigger::ili9342: return F("ili9342");
    case P095_CommandTrigger::ili9481: return F("ili9481");
    # if P095_ENABLE_ILI948X
    case P095_CommandTrigger::ili9486: return F("ili9486");
    case P095_CommandTrigger::ili9488: return F("ili9488");
    # endif // if P095_ENABLE_ILI948X
  }
  return F("ili9341"); // Default command trigger
}

/****************************************************************************
 * Constructor
 ***************************************************************************/
P095_data_struct::P095_data_struct(ILI9xxx_type_e      displayType,
                                   uint8_t             rotation,
                                   uint8_t             fontscaling,
                                   AdaGFXTextPrintMode textmode,
                                   int8_t              backlightPin,
                                   uint8_t             backlightPercentage,
                                   uint32_t            displayTimer,
                                   String              commandTrigger,
                                   uint16_t            fgcolor,
                                   uint16_t            bgcolor,
                                   bool                textBackFill
                                   # if                ADAGFX_FONTS_INCLUDED
                                   ,
                                   const uint8_t       defaultFontId
                                   # endif // if ADAGFX_FONTS_INCLUDED
                                   )
  : _displayType(displayType), _rotation(rotation), _fontscaling(fontscaling), _textmode(textmode),
  _backlightPin(backlightPin), _backlightPercentage(backlightPercentage), _displayTimer(displayTimer),
  _displayTimeout(displayTimer), _commandTrigger(commandTrigger), _fgcolor(fgcolor), _bgcolor(bgcolor),
  _textBackFill(textBackFill)
  # if ADAGFX_FONTS_INCLUDED
  , _defaultFontId(defaultFontId)
  # endif // if ADAGFX_FONTS_INCLUDED
{
  _commandTrigger.toLowerCase();
  _commandTriggerCmd  = _commandTrigger;
  _commandTriggerCmd += F("cmd");
}

/****************************************************************************
 * Destructor
 ***************************************************************************/
P095_data_struct::~P095_data_struct() {
  delete gfxHelper;
  delete tft;
  # if P095_ENABLE_ILI948X
  delete ili9488;
  # endif // if P095_ENABLE_ILI948X
}

void P095_data_struct::init() {
  _xpix = 240;
  _ypix = 320;
  ILI9xxx_type_toResolution(_displayType, _xpix, _ypix);

  updateFontMetrics();
}

/****************************************************************************
 * plugin_init: Initialize display
 ***************************************************************************/
bool P095_data_struct::plugin_init(struct EventStruct *event) {
  init();
  bool success = false;

  if (nullptr == tft
      # if P095_ENABLE_ILI948X
      && nullptr == ili9488
      # endif // if P095_ENABLE_ILI948X
      ) {
    # ifndef BUILD_NO_DEBUG
    addLog(LOG_LEVEL_INFO, F("ILI9341: Init start."));
    # endif // ifndef BUILD_NO_DEBUG

    # if P095_ENABLE_ILI948X

    if (ILI9xxx_type_e::ILI9488_320x480 == _displayType) {
      ili9488 = new (std::nothrow) ILI9488(PIN(0), PIN(1), PIN(2));

      if (nullptr != ili9488) {
        ili9488->begin();
        useILI9488 = true;
      }
    } else
    # endif // if P095_ENABLE_ILI948X
    {
      tft = new (std::nothrow) Adafruit_ILI9341(PIN(0), PIN(1), PIN(2), static_cast<uint8_t>(_displayType), _xpix, _ypix);

      if (nullptr != tft) {
        tft->begin();
      }
    }


    # ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      String log;
      log.reserve(65);
      log += strformat(F("ILI9341: Init done, address: 0x%x "),
                       #  if P095_ENABLE_ILI948X
                       useILI9488 ? reinterpret_cast<ulong>(ili9488) :
                       #  endif // if P095_ENABLE_ILI948X
                       reinterpret_cast<ulong>(tft));

      if (!isInitialized()) {
        log += F("in");
      }
      log += strformat(F("valid, display: %s, commands: %s/%s"),
                       FsP(ILI9xxx_type_toString(_displayType)),
                       _commandTrigger.c_str(),
                       _commandTriggerCmd.c_str());
      addLogMove(LOG_LEVEL_INFO, log);
    }
    # endif // ifndef BUILD_NO_DEBUG
  } else {
    addLog(LOG_LEVEL_INFO, F("ILI9341: No init?"));
  }

  if (isInitialized()) {
    # if P095_ENABLE_ILI948X

    if (useILI9488) {
      gfxHelper = new (std::nothrow) AdafruitGFX_helper(ili9488,
                                                        _commandTrigger,
                                                        _xpix,
                                                        _ypix,
                                                        AdaGFXColorDepth::FullColor,
                                                        _textmode,
                                                        _fontscaling,
                                                        _fgcolor,
                                                        _bgcolor,
                                                        true,
                                                        _textBackFill
                                                        #  if ADAGFX_FONTS_INCLUDED
                                                        , _defaultFontId
                                                        #  endif // if ADAGFX_FONTS_INCLUDED
                                                        );
    } else
    # endif // if P095_ENABLE_ILI948X
    {
      gfxHelper = new (std::nothrow) AdafruitGFX_helper(tft,
                                                        _commandTrigger,
                                                        _xpix,
                                                        _ypix,
                                                        AdaGFXColorDepth::FullColor,
                                                        _textmode,
                                                        _fontscaling,
                                                        _fgcolor,
                                                        _bgcolor,
                                                        true,
                                                        _textBackFill
                                                        # if ADAGFX_FONTS_INCLUDED
                                                        , _defaultFontId
                                                        # endif // if ADAGFX_FONTS_INCLUDED
                                                        );
    }

    if (nullptr != gfxHelper) {
      gfxHelper->initialize();
      gfxHelper->setRotation(_rotation);
      gfxHelper->setColumnRowMode(bitRead(P095_CONFIG_FLAGS, P095_CONFIG_FLAG_USE_COL_ROW));
      gfxHelper->setTxtfullCompensation(!bitRead(P095_CONFIG_FLAGS, P095_CONFIG_FLAG_COMPAT_P095) ? 0 : 1);
      gfxHelper->invertDisplay(P095_CONFIG_FLAG_GET_INVERTDISPLAY);
    }
    updateFontMetrics();
    # if P095_ENABLE_ILI948X

    if (useILI9488) {
      ili9488->fillScreen(_bgcolor);             // fill screen with background color
      ili9488->setTextColor(_fgcolor, _bgcolor); // set text color to white and configured background
      ili9488->setTextSize(_fontscaling);        // Handles 0 properly, text size, default 1 = very small
      ili9488->setCursor(0, 0);                  // move cursor to position (0, 0) pixel
    } else
    # endif // if P095_ENABLE_ILI948X
    {
      tft->fillScreen(_bgcolor);             // fill screen with background color
      tft->setTextColor(_fgcolor, _bgcolor); // set text color to white and configured background
      tft->setTextSize(_fontscaling);        // Handles 0 properly, text size, default 1 = very small
      tft->setCursor(0, 0);                  // move cursor to position (0, 0) pixel
    }
    displayOnOff(true);
    # ifdef P095_SHOW_SPLASH

    if (P095_CONFIG_FLAG_GET_SHOW_SPLASH) {
      uint16_t yPos = 0;
      gfxHelper->printText(String(F("ESPEasy")).c_str(),         0, yPos, 3, ADAGFX_WHITE, ADAGFX_BLUE);
      yPos += (3 * _fontheight);
      gfxHelper->printText(String(F("ILI934x/ILI948x")).c_str(), 0, yPos, 2, ADAGFX_BLUE,  ADAGFX_WHITE);
      _splashState   = true; // Splash
      _splashCounter = P095_SPLASH_DURATION;
      #  ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_INFO, F("P095 Splash start"));
      #  endif // ifndef BUILD_NO_DEBUG
    }
    # endif // ifdef P095_SHOW_SPLASH
    updateFontMetrics();


    if (validGpio(P095_CONFIG_BUTTON_PIN)) {
      pinMode(P095_CONFIG_BUTTON_PIN, INPUT_PULLUP);
    }

    if (!stringsLoaded) {
      LoadCustomTaskSettings(event->TaskIndex, strings, P095_Nlines, 0);
      stringsLoaded = true;

      for (uint8_t x = 0; x < P095_Nlines && !stringsHasContent; x++) {
        stringsHasContent = !strings[x].isEmpty();
      }
    }
    success = true;
  }
  return success;
}

/****************************************************************************
 * updateFontMetrics: recalculate x and y columns, based on font size and font scale
 ***************************************************************************/
void P095_data_struct::updateFontMetrics() {
  if (nullptr != gfxHelper) {
    gfxHelper->getTextMetrics(_textcols, _textrows, _fontwidth, _fontheight, _fontscaling, _heightOffset, _xpix, _ypix);
    gfxHelper->getColors(_fgcolor, _bgcolor);
  } else {
    _textcols = _xpix / (_fontwidth * _fontscaling);
    _textrows = _ypix / (_fontheight * _fontscaling);
  }
}

/****************************************************************************
 * plugin_exit: De-initialize before destruction
 ***************************************************************************/
bool P095_data_struct::plugin_exit(struct EventStruct *event) {
  # ifndef BUILD_NO_DEBUG
  addLog(LOG_LEVEL_INFO, F("ILI9341: Exit."));
  # endif // ifndef BUILD_NO_DEBUG

  if ((nullptr != tft) && bitRead(P095_CONFIG_FLAGS, P095_CONFIG_FLAG_CLEAR_ON_EXIT)) {
    tft->fillScreen(ADAGFX_BLACK); // fill screen with black color
  }
  # if P095_ENABLE_ILI948X

  if ((nullptr != ili9488) && bitRead(P095_CONFIG_FLAGS, P095_CONFIG_FLAG_CLEAR_ON_EXIT)) {
    ili9488->fillScreen(ADAGFX_BLACK); // fill screen with black color
  }
  # endif // if P095_ENABLE_ILI948X
  displayOnOff(false);

  delete gfxHelper;
  gfxHelper = nullptr;

  delete tft;
  tft = nullptr;
  # if P095_ENABLE_ILI948X
  delete ili9488;
  ili9488 = nullptr;
  # endif // if P095_ENABLE_ILI948X
  return true;
}

/****************************************************************************
 * plugin_read: Re-draw the default content
 ***************************************************************************/
bool P095_data_struct::plugin_read(struct EventStruct *event) {
  if (isInitialized() && !_splashState) {
    if (stringsHasContent) {
      gfxHelper->setColumnRowMode(false); // Turn off column mode

      int yPos = 0;

      for (uint8_t x = 0; x < P095_Nlines; x++) {
        String newString = AdaGFXparseTemplate(strings[x], _textcols, gfxHelper);

        # if ADAGFX_PARSE_SUBCOMMAND
        updateFontMetrics();
        # endif // if ADAGFX_PARSE_SUBCOMMAND

        if (yPos < _ypix) {
          gfxHelper->printText(newString.c_str(), 0, yPos, _fontscaling, _fgcolor, _bgcolor);
        }
        delay(0);
        yPos += (_fontheight * _fontscaling);
      }
      gfxHelper->setColumnRowMode(bitRead(P095_CONFIG_FLAGS, P095_CONFIG_FLAG_USE_COL_ROW)); // Restore column mode
      int16_t curX, curY;
      gfxHelper->getCursorXY(curX, curY);                                                    // Get current X and Y coordinates,
      UserVar.setFloat(event->TaskIndex, 0, curX);                                           // and put into Values
      UserVar.setFloat(event->TaskIndex, 1, curY);
    }
  }
  return false; // Always return false, so no attempt to send to
                // Controllers or generate events is started
}

/****************************************************************************
 * plugin_ten_per_second: check button, if any, that wakes up the display
 ***************************************************************************/
bool P095_data_struct::plugin_ten_per_second(struct EventStruct *event) {
  # ifdef P095_SHOW_SPLASH

  if (_splashState) { // Decrement splash counter
    _splashCounter--;
    _splashState = _splashCounter != 0;

    if (!_splashState) {
      #  ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_INFO, F("P095 Splash finished."));
      #  endif // ifndef BUILD_NO_DEBUG

      if (nullptr != tft) {
        tft->fillScreen(_bgcolor); // fill screen with background color
      }
      #  if P095_ENABLE_ILI948X

      if (nullptr != ili9488) {
        ili9488->fillScreen(_bgcolor); // fill screen with background color
      }
      #  endif // if P095_ENABLE_ILI948X

      // Schedule the surrogate initial PLUGIN_READ that has been suppressed by the splash
      Scheduler.schedule_task_device_timer(event->TaskIndex, millis() + 10);
    }
  }
  # endif // ifdef P095_SHOW_SPLASH

  if (validGpio(P095_CONFIG_BUTTON_PIN) && (getButtonState()) && isInitialized()) {
    displayOnOff(true);
    markButtonStateProcessed();
  }
  return true;
}

/****************************************************************************
 * plugin_once_a_second: Count down display timer, if any, and turn display off if countdown reached
 ***************************************************************************/
bool P095_data_struct::plugin_once_a_second(struct EventStruct *event) {
  if ((_displayTimer > 0) && !_splashState) {
    _displayTimer--;

    if (isInitialized() && (_displayTimer == 0)) {
      displayOnOff(false);
    }
  }
  return true;
}

/****************************************************************************
 * plugin_write: Handle commands
 ***************************************************************************/
bool P095_data_struct::plugin_write(struct EventStruct *event, const String& string) {
  bool   success = false;
  String cmd     = parseString(string, 1);

  if (isInitialized() && cmd.equals(_commandTriggerCmd) && !_splashState) {
    String arg1 = parseString(string, 2);
    success = true;

    if (equals(arg1, F("off"))) {
      displayOnOff(false);
    }
    else if (equals(arg1, F("on"))) {
      displayOnOff(true);
    }
    else if (equals(arg1, F("clear")))
    {
      String arg2 = parseString(string, 3);

      if (!arg2.isEmpty()) {
        # if P095_ENABLE_ILI948X

        if (useILI9488) {
          ili9488->fillScreen(AdaGFXparseColor(arg2));
        } else
        # endif // if P095_ENABLE_ILI948X
        {
          tft->fillScreen(AdaGFXparseColor(arg2));
        }
      } else {
        # if P095_ENABLE_ILI948X

        if (useILI9488) {
          ili9488->fillScreen(_bgcolor);
        } else
        # endif // if P095_ENABLE_ILI948X
        {
          tft->fillScreen(_bgcolor);
        }
      }
    }
    else if (equals(arg1, F("backlight"))) {
      if (validGpio(P095_CONFIG_BACKLIGHT_PIN) &&    // All is valid?
          (event->Par2 > 0) &&
          (event->Par2 <= 100)) {
        P095_CONFIG_BACKLIGHT_PERCENT = event->Par2; // Set but don't store
        _backlightPercentage          = event->Par2; // Also set to current
        displayOnOff(true);
      } else {
        success = false;
      }
    }
    else if (equals(arg1, F("inv"))) {
      if ((event->Par2 >= 0) &&
          (event->Par2 <= 1)) {
        # if P095_ENABLE_ILI948X

        if (useILI9488) {
          ili9488->invertDisplay(event->Par2);
        } else
        # endif // if P095_ENABLE_ILI948X
        {
          tft->invertDisplay(event->Par2);
        }
      } else {
        success = false;
      }
    }
    else if (equals(arg1, F("rot"))) {
      if ((event->Par2 >= 0)) {
        if (nullptr != gfxHelper) {
          gfxHelper->setRotation(event->Par2 % 4);
        } else {
          # if P095_ENABLE_ILI948X

          if (useILI9488) {
            ili9488->setRotation(event->Par2 % 4);
          } else
          # endif // if P095_ENABLE_ILI948X
          {
            tft->setRotation(event->Par2 % 4);
          }
        }
      } else {
        success = false;
      }
    } else {
      success = false;
    }
  }
  else if (isInitialized() && (cmd.equals(_commandTrigger) ||
                               (gfxHelper && gfxHelper->isAdaGFXTrigger(cmd))) && !_splashState) {
    success = true;

    if (!bitRead(P095_CONFIG_FLAGS, P095_CONFIG_FLAG_NO_WAKE)) { // Wake display?
      displayOnOff(true);
    }

    if (nullptr != gfxHelper) {
      String tmp = string;

      // Hand it over after replacing variables
      success = gfxHelper->processCommand(AdaGFXparseTemplate(tmp, _textcols, gfxHelper));

      updateFontMetrics(); // Font or color may have changed

      if (success) {
        int16_t curX, curY;
        gfxHelper->getCursorXY(curX, curY); // Get current X and Y coordinates, and put into Values
        UserVar.setFloat(event->TaskIndex, 0, curX);
        UserVar.setFloat(event->TaskIndex, 1, curY);
      }
    }
  }
  return success;
}

# if ADAGFX_ENABLE_GET_CONFIG_VALUE

/****************************************************************************
 * plugin_get_config_value: Retrieve values like [<taskname>#<valuename>]
 ***************************************************************************/
bool P095_data_struct::plugin_get_config_value(struct EventStruct *event,
                                               String            & string) {
  bool success = false;

  if (gfxHelper != nullptr) {
    success = gfxHelper->pluginGetConfigValue(string);
  }
  return success;
}

# endif // if ADAGFX_ENABLE_GET_CONFIG_VALUE

/****************************************************************************
 * displayOnOff: Turn display on or off
 ***************************************************************************/
void P095_data_struct::displayOnOff(bool state) {
  if (validGpio(_backlightPin)) {
    # if defined(ESP8266)
    analogWrite(_backlightPin, state ? ((1024 / 100) * _backlightPercentage) : 0);
    # endif // if defined(ESP8266)
    # if defined(ESP32)
    analogWriteESP32(_backlightPin, state ? ((1024 / 100) * _backlightPercentage) : 0, 0);
    # endif // if defined(ESP32)
  }

  # if P095_ENABLE_ILI948X

  if (useILI9488) {
    ili9488->writecommand(state ? ILI9341_DISPON : ILI9341_DISPOFF);
  } else
  # endif // if P095_ENABLE_ILI948X
  {
    tft->sendCommand(state ? ILI9341_DISPON : ILI9341_DISPOFF);
  }
  _displayTimer = (state ? _displayTimeout : 0);
}

/****************************************************************************
 * registerButtonState: the button has been pressed, apply some debouncing
 ***************************************************************************/
void P095_data_struct::registerButtonState(const uint8_t& newButtonState,
                                           const bool   & bPin3Invers) {
  if ((ButtonLastState == 0xFF) || (bPin3Invers != (!!newButtonState))) {
    ButtonLastState = newButtonState;
    DebounceCounter++;
  } else {
    ButtonLastState = 0xFF; // Reset
    DebounceCounter = 0;
    ButtonState     = false;
  }

  if ((ButtonLastState == newButtonState) &&
      (DebounceCounter >= P095_DebounceTreshold)) {
    ButtonState = true;
  }
}

/****************************************************************************
 * markButtonStateProcessed: reset the button state
 ***************************************************************************/
void P095_data_struct::markButtonStateProcessed() {
  ButtonState     = false;
  DebounceCounter = 0;
}

#endif // ifdef USES_P095

#include "../PluginStructs/P139_data_struct.h"

#ifdef USES_P139

# ifdef ESP32

#  include "../PluginStructs/P139_data_struct_formselectors.h"

// **************************************************************************/
// Constructors
// **************************************************************************/
P139_data_struct::P139_data_struct(struct EventStruct *event) {
  axp2101 = new (std::nothrow) AXP2101(); // Default address and I2C Wire object

  if (isInitialized()) {                  // Functions based on:
    axp2101->begin(&Wire, AXP2101_ADDR, static_cast<AXP2101_device_model_e>(P139_CURRENT_PREDEFINED));
    loadSettings(event);
    applySettings(event);
  } else {
    addLog(LOG_LEVEL_ERROR, F("AXP2101: Initialization failed"));
  }
}

// **************************************************************************/
// Destructor
// **************************************************************************/
P139_data_struct::~P139_data_struct() {
  delete axp2101;
}

// **************************************************************************/
// loadSettings: Load the (custom)settings from flash
// **************************************************************************/
String P139_data_struct::loadSettings(struct EventStruct *event) {
  String result;

  if (!_settingsLoaded) {
    result = LoadCustomTaskSettings(event->TaskIndex,
                                    reinterpret_cast<uint8_t *>(&_settings),
                                    sizeof(_settings));
    _settingsLoaded = true;
  }
  return result;
}

// **************************************************************************/
// applySettings: Write the current settings to AXP2101
// **************************************************************************/
void P139_data_struct::applySettings(struct EventStruct *event) {
  if (!isInitialized()) {
    return;
  }
  uint8_t count = 0;

  for (int s = 0; s < AXP2101_settings_count; ++s) {
    const AXP2101_registers_e reg = AXP2101_intToRegister(s);
    const AXP_pin_s pinState      = _settings.getState(reg);

    if (!AXP2101_isPinDefault(pinState)) {
      if (AXP_pin_s::On == pinState) {
        axp2101->setPortVoltage(_settings.getVoltage(reg), reg);
        axp2101->setPortState(true, reg);  // Turn on after setting the voltage
      } else {
        axp2101->setPortState(false, reg); // Turn off
      }
      ++count;

      #  ifndef BUILD_NO_DEBUG

      if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
        addLog(LOG_LEVEL_DEBUG,
               strformat(F("AXP2101: Port: %s, output: %dmV, pin state: %s"),
                         FsP(toString(reg)), _settings.getVoltage(reg), FsP(toString(pinState))));
      }
      #  endif // ifndef BUILD_NO_DEBUG
    }
  }

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, strformat(F("AXP2101: Set %d values to port(s)"), count));
  }
  axp2101->setChargeLed(_settings.getChargeLed());

  // Reg 61: Iprechg Charger Settings
  axp2101->setPreChargeCurrentLimit(_settings.getPreChargeCurrentLimit());

  // Reg 62: ICC Charger Settings
  axp2101->setConstChargeCurrentLimit(_settings.getConstChargeCurrentLimit());

  // Reg 63: Iterm Charger Settings and Control
  axp2101->setTerminationChargeCurrentLimit(_settings.getTerminationChargeCurrentLimit(), _settings.getTerminationChargeCurrentLimitEnable());

  // Reg 64: CV Charger Voltage Settings
  axp2101->setCV_chargeVoltage(_settings.getCV_chargeVoltage());

  // Reg 14: Minimum System Voltage Control
  axp2101->setLinear_Charger_Vsys_dpm(_settings.getLinear_Charger_Vsys_dpm());

  // Reg 15: Input Voltage Limit
  axp2101->setVin_DPM(_settings.getVin_DPM());

  // Reg 16: Input Current Limit
  axp2101->setInputCurrentLimit(_settings.getInputCurrentLimit());
  axp2101->setTS_disabled(_settings.getTS_disabled());


  //  axp2101->set_IRQ_enable_0(0b11110000); // Disable temperature checks
  axp2101->setConstChargeCurrentLimit(_settings.getConstChargeCurrentLimit());
}

// **************************************************************************/
// saveSettings: Save the settings to the custom settings area
// **************************************************************************/
String P139_data_struct::saveSettings(struct EventStruct *event) {
  const String result = SaveCustomTaskSettings(event->TaskIndex,
                                               reinterpret_cast<const uint8_t *>(&_settings),
                                               sizeof(_settings));

  _settingsLoaded = true; // When freshly saved == loaded :-)
  return result;
}

// **************************************************************************/
// applySettings: Update settings to defaults for selected device
// **************************************************************************/
bool P139_data_struct::applyDeviceModelTemplate(AXP2101_device_model_e device) {
  const int idx = static_cast<int>(AXP2101_device_model_e::UserDefined == device ?
                                   AXP2101_device_model_e::MAX :
                                   device);

  if ((idx > static_cast<int>(AXP2101_device_model_e::Unselected)) &&
      (idx <= static_cast<int>(AXP2101_device_model_e::MAX))) {
    _settings = AXP2101_deviceSettingsArray[idx];
    return true;
  }
  return false;
}

void P139_data_struct::webform_load(struct EventStruct *event) {
  const bool isPowerManagerTask = Settings.isPowerManagerTask(event->TaskIndex);

  {
    AXP2101_ChargeLED_FormSelector selector(_settings.getChargeLed());
  }
  {
    // Reg 61: Iprechg Charger Settings
    // 0 .. 200 mA in 25 mA steps
    AXP2101_PreChargeCurrentLimit_FormSelector selector(_settings.getPreChargeCurrentLimit());
  }
  {
    // Reg 62: ICC Charger Settings
    AXP2101_ConstChargeCurrentLimit_FormSelector selector(_settings.getConstChargeCurrentLimit());
  }
  {
    // Reg 63: Iterm Charger Settings and Control
    // 0 .. 200 mA in 25 mA steps  + enable checkbox
    AXP2101_TerminationChargeCurrentLimit_FormSelector selector(_settings.getTerminationChargeCurrentLimit());
    addFormCheckBox(F("Enable CV Charging"), F("iterm_en"), _settings.getTerminationChargeCurrentLimitEnable());
    addFormNote(F("When enabled, the last part of the charge cycle is done using constant voltage (CV)"));
  }

  {
    // Reg 64: CV Charger Voltage Settings
    AXP2101_CV_charger_voltage_FormSelector selector(_settings.getCV_chargeVoltage());
  }
  {
    // Reg 14: Minimum System Voltage Control
    AXP2101_Linear_Charger_Vsys_dpm_FormSelector selector(_settings.getLinear_Charger_Vsys_dpm());
  }

  {
    // Reg 15: Input Voltage Limit
    AXP2101_Vin_DPM_FormSelector selector(_settings.getVin_DPM());
  }

  {
    // Reg 16: Input Current Limit
    AXP2101_InputCurrentLimit_FormSelector selector(_settings.getInputCurrentLimit());
  }

  addFormCheckBox(F("Disable TS pin"), F("dis_TS"), _settings.getTS_disabled());
  addFormNote(F("Make sure to disable TS pin when no battery temperature sensor is used, or else the battery will not charge"));

  addFormCheckBox(F("Generate events"), F("events"), P139_GET_GENERATE_EVENTS);

  addFormSubHeader(F("Hardware outputs AXP2101"));

  {
    const __FlashStringHelper *predefinedNames[] = {
      toString(AXP2101_device_model_e::Unselected),
      toString(AXP2101_device_model_e::M5Stack_Core2_v1_1),
      toString(AXP2101_device_model_e::M5Stack_CoreS3),
      toString(AXP2101_device_model_e::LilyGO_TBeam_v1_2),
      toString(AXP2101_device_model_e::LilyGO_TBeamS3_v3),
      toString(AXP2101_device_model_e::LilyGO_TPCie_v1_2),
      toString(AXP2101_device_model_e::UserDefined) // keep last !!
    };
    const int predefinedValues[] = {
      static_cast<int>(AXP2101_device_model_e::Unselected),
      static_cast<int>(AXP2101_device_model_e::M5Stack_Core2_v1_1),
      static_cast<int>(AXP2101_device_model_e::M5Stack_CoreS3),
      static_cast<int>(AXP2101_device_model_e::LilyGO_TBeam_v1_2),
      static_cast<int>(AXP2101_device_model_e::LilyGO_TBeamS3_v3),
      static_cast<int>(AXP2101_device_model_e::LilyGO_TPCie_v1_2),
      static_cast<int>(AXP2101_device_model_e::UserDefined) }; // keep last !!
    constexpr uint8_t   valueCount = NR_ELEMENTS(predefinedValues);
    FormSelectorOptions selector(
      valueCount,
      predefinedNames, predefinedValues);
    selector.reloadonchange = !isPowerManagerTask;
    selector.addFormSelector(
      F("Predefined device configuration"),
      F("predef"),
      0);

    const AXP2101_device_model_e device = static_cast<AXP2101_device_model_e>(P139_CURRENT_PREDEFINED);

    if (AXP2101_device_model_e::Unselected != device) {
      addFormNote(concat(F("Last selected: "), toString(device)));

      if (AXP2101_device_model_e::UserDefined == device) {
        addHtml(F("<div class='note'><span style=\"color:red\">Warning: "
                  "Configuring invalid values can damage your device or render it useless!</span></div>"));
      }
    }
  }

  {
    const __FlashStringHelper *bootStates[] = {
      toString(AXP_pin_s::Off),
      toString(AXP_pin_s::On),
      toString(AXP_pin_s::Default),
    };
    const int bootStateValues[] = {
      static_cast<int>(AXP_pin_s::Off),
      static_cast<int>(AXP_pin_s::On),
      static_cast<int>(AXP_pin_s::Default),
    };

    // Don't include Disabled or Protected here, not user-selectable
    constexpr int bootStatesCount = NR_ELEMENTS(bootStateValues);

    const FormSelectorOptions selector(
          bootStatesCount,
          bootStates,
          bootStateValues);


    addRowLabel(F("Output ports"));

    html_table(EMPTY_STRING);
    html_table_header(F("Name"),         100);
    html_table_header(F("Voltage (mV)"), 270);
    html_table_header(F("Pin state"),    150);

    for (int s = 0; s < AXP2101_settings_count; ++s) {
      const AXP2101_registers_e reg = AXP2101_intToRegister(s);
      const AXP_pin_s pin           = _settings.getState(reg);
      html_TR_TD();
      addHtml(toString(reg));
      html_TD();
      addNumericBox(toString(reg, false),
                    _settings.getVoltage(reg, false),
                    -1,
                    AXP2101_maxVoltage(reg),
                    AXP2101_isPinDefault(pin));
      addUnit(strformat(F("range %d - %d"), AXP2101_minVoltage(reg), AXP2101_maxVoltage(reg)));
      html_TD();

      if (AXP2101_isPinProtected(pin)) {
        addUnit(toString(pin));
      } else {
        selector.addSelector(
          concat(F("ps"), toString(reg, false)),
          static_cast<int>(pin));
      }
    }

    html_end_table();

    addFormNote(F("Check your device documentation for what is connected to each output."));

    if (isInitialized()) {
      addFormSubHeader(F("Current State"));

      const AXP2101_registers_e registers[] = {
        AXP2101_registers_e::vbus,
        AXP2101_registers_e::vsys,
        AXP2101_registers_e::vbat,
        AXP2101_registers_e::batcharge,
        AXP2101_registers_e::battemp,
        AXP2101_registers_e::chiptemp
      };

      for (size_t i = 0; i < NR_ELEMENTS(registers); ++i) {
        addRowLabel(toString(registers[i], true));

        if ((registers[i] == AXP2101_registers_e::battemp) ||
            (registers[i] == AXP2101_registers_e::chiptemp))
        {
          addHtmlFloat(read_value(registers[i]));
          addUnit(F("°C"));
        } else {
          addHtmlInt(static_cast<int>(read_value(registers[i])));
          addUnit((registers[i] == AXP2101_registers_e::batcharge)
            ? F("%") : F("mV"));
        }
      }

      addRowLabel(F("Charging State"));

      const AXP2101_chargingState_e chargingState = axp2101->getChargingState();

      if (chargingState != AXP2101_chargingState_e::Charging) {
        addHtml(toString(chargingState));
      }
      else {
        addHtml(toString(axp2101->getChargingDetail()));
      }
    }
  }
}

void P139_data_struct::webform_save(struct EventStruct *event) {
  for (uint8_t i = 0; i < P139_NR_OUTPUT_VALUES; ++i) {
    sensorTypeHelper_saveOutputSelector(event, P139_CONFIG_BASE + i, i,
                                        toString(static_cast<AXP2101_registers_e>(PCONFIG(P139_CONFIG_BASE + i)), false));
  }

  const int predefined = getFormItemInt(F("predef"));
  P139_SET_GENERATE_EVENTS(isFormItemChecked(F("events")));

  if (predefined > 0) { // Apply new template to save it, even when task is disabled
    P139_CURRENT_PREDEFINED = predefined;
    applyDeviceModelTemplate(static_cast<AXP2101_device_model_e>(P139_CURRENT_PREDEFINED));
  } else {
    for (int s = 0; s < AXP2101_settings_count; ++s) {
      const AXP2101_registers_e reg = AXP2101_intToRegister(s);

      if (!AXP2101_isPinProtected(_settings.getState(reg))) {
        _settings.setVoltage(reg, getFormItemInt(toString(reg, false)));
        _settings.setState(reg, static_cast<AXP_pin_s>(getFormItemInt(concat(F("ps"), toString(reg, false)))));
      }
    }
  }

  _settings.setChargeLed(AXP2101_ChargeLED_FormSelector::get());

  // Reg 61: Iprechg Charger Settings
  _settings.setPreChargeCurrentLimit(AXP2101_PreChargeCurrentLimit_FormSelector::get());

  // Reg 62: ICC Charger Settings
  _settings.setConstChargeCurrentLimit(AXP2101_ConstChargeCurrentLimit_FormSelector::get());

  // Reg 63: Iterm Charger Settings and Control
  _settings.setTerminationChargeCurrentLimit(
    AXP2101_TerminationChargeCurrentLimit_FormSelector::get(),
    isFormItemChecked(F("iterm_en")));

  // Reg 64: CV Charger Voltage Settings
  _settings.setCV_chargeVoltage(AXP2101_CV_charger_voltage_FormSelector::get());

  // Reg 14: Minimum System Voltage Control
  _settings.setLinear_Charger_Vsys_dpm(AXP2101_Linear_Charger_Vsys_dpm_FormSelector::get());

  // Reg 15: Input Voltage Limit
  _settings.setVin_DPM(AXP2101_Vin_DPM_FormSelector::get());

  // Reg 16: Input Current Limit
  _settings.setInputCurrentLimit(AXP2101_InputCurrentLimit_FormSelector::get());

  _settings.setTS_disabled(isFormItemChecked(F("dis_TS")));

  saveSettings(event);
}

// **************************************************************************/
// plugin_read: Read the values and send to controller(s)
// **************************************************************************/
bool P139_data_struct::plugin_read(struct EventStruct *event) {
  bool success = true;

  const uint8_t valueCount = P139_NR_OUTPUT_VALUES;

  for (uint8_t i = 0; i < valueCount; ++i) {
    UserVar.setFloat(event->TaskIndex, i, read_value(static_cast<AXP2101_registers_e>(PCONFIG(P139_CONFIG_BASE + i))));
  }

  return success;
}

// **************************************************************************/
// read_value: Read the requested value
// **************************************************************************/
float P139_data_struct::read_value(AXP2101_registers_e value) {
  if (isInitialized()) {
    switch (value)
    {
      case AXP2101_registers_e::chargeled:
        return static_cast<float>(axp2101->getChargeLed());
      case AXP2101_registers_e::batcharge:
        return static_cast<float>(axp2101->getBatCharge());
      case AXP2101_registers_e::charging:
        return static_cast<float>(axp2101->getChargingState());
      case AXP2101_registers_e::batpresent:
        return static_cast<float>(axp2101->isBatteryDetected());
      case AXP2101_registers_e::chipid:
        return static_cast<float>(axp2101->getChipIDRaw());
      case AXP2101_registers_e::chargedet:
        return static_cast<float>(axp2101->getChargingDetail());

      case AXP2101_registers_e::vbat:
      case AXP2101_registers_e::vbus:
      case AXP2101_registers_e::vsys:
        return static_cast<float>(axp2101->getADCVoltage(value));

      case AXP2101_registers_e::battemp:
        return axp2101->TS_registerToTemp(axp2101->getADCVoltage(value));

      case AXP2101_registers_e::chiptemp:
        return 22.0f + (7274 - axp2101->getADCVoltage(value)) / 20.0f;

      default:
        return static_cast<float>(axp2101->getPortVoltage(value));
    }
  }
  return 0.0f;
}

// **************************************************************************/
// plugin_ten_per_second: Check state and generate events
// **************************************************************************/
// bool P139_data_struct::plugin_ten_per_second(struct EventStruct *event) {
//   // TODO ?
//   return false;
// }

// **************************************************************************/
// plugin_fifty_per_second: Check state and generate events
// **************************************************************************/
bool P139_data_struct::plugin_fifty_per_second(struct EventStruct *event) {
  if (isInitialized() && P139_GET_GENERATE_EVENTS) {
    const AXP2101_chargingState_e charging = axp2101->getChargingState();

    if (_chargingState != charging) {
      eventQueue.add(event->TaskIndex, F("ChargingState"), // Event: <taskname>#ChargingState=<new>,<old> (numeric)
                     strformat(F("%d,%d"), static_cast<int8_t>(charging), static_cast<int8_t>(_chargingState)));
      _chargingState = charging;
    }
    return true;
  }
  return false;
}

// **************************************************************************/
// plugin_write: Process commands
// **************************************************************************/
const char P139_subcommands[] PROGMEM = "readchip|voltage|off|on|percentage|range|chargeled";

enum class P139_subcommands_e : int8_t {
  invalid  = -1,
  readchip = 0,
  voltage,
  off,
  on,
  percentage,
  range,
  chargeled,
};

bool P139_data_struct::plugin_write(struct EventStruct *event,
                                    const String      & string) {
  bool   success = false;
  String cmd     = parseString(string, 1);

  if (isInitialized() && equals(cmd, F("axp"))) { // Command trigger
    cmd = parseString(string, 2);                 // sub command
    const int subcommand_i = GetCommandCode(cmd.c_str(), P139_subcommands);

    if (-1 == subcommand_i) { return success; } // Shortcut

    const P139_subcommands_e subcmd = static_cast<P139_subcommands_e>(subcommand_i);

    const String var3   = parseString(string, 3);
    const bool   empty3 = var3.isEmpty();
    const bool   empty4 = parseString(string, 4).isEmpty();

    switch (subcmd) {
      case P139_subcommands_e::invalid: break;
      case P139_subcommands_e::readchip:

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          addLog(LOG_LEVEL_INFO, F("AXP2101: Port voltages (mV), state and range:"));

          for (int s = 0; s < AXP2101_register_count; ++s) {
            const AXP2101_registers_e reg = AXP2101_intToRegister(s);

            int32_t value = -1;
            uint8_t state = 0u;
            String  data;

            if (AXP2101_registers_e::chargeled == reg) {
              data = strformat(F(", Led: %s"), FsP(toString(axp2101->getChargeLed())));
            } else
            if (AXP2101_registers_e::batcharge == reg) {
              data = strformat(F(", Battery: %d%%"), axp2101->getBatCharge());
            } else
            if (AXP2101_registers_e::charging == reg) {
              data = strformat(F(", Battery: %s"), FsP(toString(axp2101->getChargingState())));
            } else
            if (AXP2101_registers_e::batpresent == reg) {
              data = strformat(F(", Battery: %s"), FsP(boolToString(axp2101->isBatteryDetected())));
            } else
            if (AXP2101_registers_e::chipid == reg) {
              data = strformat(F(", ChipID: %s (0x%02x)"), FsP(toString(axp2101->getChipID())), axp2101->getChipIDRaw());
            } else
            if (AXP2101_registers_e::chargedet == reg) {
              data = strformat(F(", ChargingDetail: %s"), FsP(toString(axp2101->getChargingDetail())));
            } else {
              value = axp2101->getPortVoltage(reg);
              state = axp2101->getPortState(reg);
            }
            addLog(LOG_LEVEL_INFO, strformat(F("Port: %7s: %4dmV, state: %d, range: %d - %dmV%s"),
                                             FsP(toString(reg)),
                                             value,
                                             state,
                                             AXP2101_minVoltage(reg),
                                             AXP2101_maxVoltage(reg),
                                             data.c_str()));
          }
        } else {
          addLog(LOG_LEVEL_ERROR, F("AXP2101: 'readchip' needs logging level INFO"));
        }

        success = true;
        break;

      case P139_subcommands_e::voltage:

        if (!empty3 && !empty4) {
          for (int s = 0; s < AXP2101_settings_count; ++s) {
            const AXP2101_registers_e reg = AXP2101_intToRegister(s);

            if (equals(var3, toString(reg, false))) {
              const int min_ = AXP2101_minVoltage(reg);

              if (0 == event->Par3 /* < min_ */) {
                // Q: Turn off when A) 0 or B) below minimum voltage? Selected answer: A)
                axp2101->setPortState(false, reg);

                if (loglevelActiveFor(LOG_LEVEL_INFO)) {
                  addLog(LOG_LEVEL_INFO, strformat(F("AXP2101: Turn off port %s"), FsP(toString(reg))));
                }
                success = true;
              } else
              if ((event->Par3 >= min_) && (event->Par3 <= AXP2101_maxVoltage(reg))) {
                axp2101->setPortVoltage(event->Par3, reg);
                axp2101->setPortState(true, reg); // Turn on after setting the voltage

                if (loglevelActiveFor(LOG_LEVEL_INFO)) {
                  addLog(LOG_LEVEL_INFO, strformat(F("AXP2101: Set port %s to %dmV"), FsP(toString(reg)), event->Par3));
                }
                success = true;
              }
            }
          }
        }
        break;

      case P139_subcommands_e::off:
      case P139_subcommands_e::on:

        if (!empty3) {
          for (int s = 0; s < AXP2101_settings_count; ++s) {
            const AXP2101_registers_e reg = AXP2101_intToRegister(s);

            if (equals(var3, toString(reg, false))) {
              const bool stateOn       = P139_subcommands_e::on == subcmd;
              const AXP_pin_s pinState = _settings.getState(reg);

              if (AXP2101_isPinProtected(pinState)) {
                if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
                  addLog(LOG_LEVEL_ERROR, strformat(F("AXP2101: Port %s is %s"), FsP(toString(reg)), FsP(toString(pinState))));
                }
              } else {
                axp2101->setPortState(stateOn, reg);

                if (loglevelActiveFor(LOG_LEVEL_INFO)) {
                  addLog(LOG_LEVEL_INFO, strformat(F("AXP2101: Switch port %s: %s"), FsP(toString(reg)), FsP(stateOn ? F("On") : F("Off"))));
                }
                success = true;
              }
            }
          }
        }
        break;

      case P139_subcommands_e::percentage:

        if ((event->Par3 >= 0) && (event->Par3 <= P139_CONST_100_PERCENT) && !empty3 && empty4) {
          for (int s = 0; s < AXP2101_settings_count && !success; ++s) {
            const AXP2101_registers_e reg = AXP2101_intToRegister(s);

            if (equals(var3, toString(reg, false)) && !AXP2101_isPinProtected(_settings.getState(reg))) {
              if (event->Par3 > 0) {
                const uint16_t _value = map(event->Par3,
                                            P139_CONST_1_PERCENT, P139_CONST_100_PERCENT,
                                            _ranges[s][0], _ranges[s][1]);

                axp2101->setPortVoltage(_value, reg);
                axp2101->setPortState(true, reg);  // Turn on after setting the voltage
              } else {
                axp2101->setPortState(false, reg); // Turn off
              }
              success = true;
            }
          }
        }
        break;

      case P139_subcommands_e::range:

        if ((event->Par4 >= 0) && (event->Par4 <= P139_CONST_MAX_LDO) &&
            (event->Par3 < event->Par4) && !empty3 && !empty4) {
          for (int s = 0; s < AXP2101_settings_count && !success; ++s) {
            const AXP2101_registers_e reg = AXP2101_intToRegister(s);

            if (equals(var3, toString(reg, false)) &&
                (event->Par3 >= AXP2101_minVoltage(reg)) &&
                (event->Par4 <= AXP2101_maxVoltage(reg))) {
              _ranges[s][0] = event->Par3;
              _ranges[s][1] = event->Par4;
              success       = true;
            }
          }
        } else {
          if (loglevelActiveFor(LOG_LEVEL_INFO)) {
            for (int s = 0; s < AXP2101_settings_count; ++s) {
              const AXP2101_registers_e reg = AXP2101_intToRegister(s);
              addLog(LOG_LEVEL_INFO, strformat(F("AXP2101: %7s Percentage range: %d - %dmV (State: %s)"),
                                               FsP(toString(reg)),
                                               _ranges[s][0],
                                               _ranges[s][1],
                                               FsP(toString(_settings.getState(reg)))));
            }
          }
        }
        success = true;
        break;

      case P139_subcommands_e::chargeled:

        if (event->Par2 >= 0 and event->Par2 <= 3) { // Only allowed options
          AXP2101_chargeled_d led = static_cast<AXP2101_chargeled_d>(event->Par2);

          // axp2101->setChargeLed(led);
          _settings.setChargeLed(led); // Store in settings, but don't save yet
          success = true;
        }
        break;
    } // switch
  }

  return success;
}

/****************************************************************************
 * plugin_get_config_value: Retrieve values like [<taskname>#<valuename>]
 ***************************************************************************/
bool P139_data_struct::plugin_get_config_value(struct EventStruct *event,
                                               String            & string) {
  if (!isInitialized()) { return false; }
  bool success         = false;
  const String command = parseString(string, 1);

  for (int r = 0; r < AXP2101_register_count && !success; ++r) {
    const AXP2101_registers_e reg = AXP2101_intToRegister(r);

    if (equals(command, toString(reg, false))) { // Voltage (mV) / numeric state
      if ((reg == AXP2101_registers_e::battemp) ||
          (reg == AXP2101_registers_e::chiptemp))
      {
        string = floatToString(read_value(reg), 2);
      } else {
        string = read_value(reg);
      }
      success = true;
    } else
    if (command.equals(concat(toString(reg, false), F(".status")))) { // Status (name)
      if (r >= AXP2101_settings_count) {
        if (AXP2101_registers_e::chargeled == reg) {
          string  = toString(axp2101->getChargeLed());
          success = true;
        } else
        if (AXP2101_registers_e::charging == reg) {
          string  = toString(axp2101->getChargingState());
          success = true;
        } else
        if (AXP2101_registers_e::chipid == reg) {
          string  = toString(axp2101->getChipID());
          success = true;
        } else
        if (AXP2101_registers_e::chargedet == reg) {
          string  = toString(axp2101->getChargingDetail());
          success = true;
        }
      } else {
        string  = toString(axp2101->getPortState(reg));
        success = true;
      }
    } else
    if (command.equals(concat(toString(reg, false), F(".state")))) { // State (int)
      if (r >= AXP2101_settings_count) {
        if (AXP2101_registers_e::chargeled == reg) {
          string  = static_cast<uint8_t>(axp2101->getChargeLed());
          success = true;
        }
      } else {
        string  = static_cast<uint8_t>(axp2101->getPortState(reg));
        success = true;
      }
    }
  }

  return success;
}

# endif // ifdef ESP32
#endif  // ifdef USES_P139

#include "../PluginStructs/P180_data_struct.h"

#ifdef USES_P180
# include <GPIO_Direct_Access.h>
# include "../Globals/RulesCalculate.h"

P180_data_struct::P180_data_struct(struct EventStruct *event) {
  _enPin     = P180_ENABLE_PIN;
  _rstPin    = P180_RST_PIN;
  _showLog   = P180_LOG_DEBUG == 1;
  _taskIndex = event->TaskIndex;

  BusCmd_Handler_I2C*i2cHandler = new (std::nothrow) BusCmd_Handler_I2C(static_cast<uint8_t>(P180_I2C_ADDRESS));

  if (nullptr != i2cHandler) {
    busCmd_Helper = new (std::nothrow) BusCmd_Helper_struct(i2cHandler,
                                                            _taskIndex,
                                                            _enPin,
                                                            _rstPin,
                                                            P180_NR_OUTPUT_VALUES);

    if (nullptr != busCmd_Helper) {
      busCmd_Helper->setLog(_showLog);

      loadStrings(event);

      if (_stringsLoaded) {
        for (uint8_t i = 0; i < VARS_PER_TASK; ++i) {
          busCmd_Helper->setBuffer(i, _strings[P180_BUFFER_START_CACHE + i], _strings[P180_BUFFER_START_COMMANDS + i]);
          _strings[P180_BUFFER_START_CACHE + i].clear(); // Clear memory
          _strings[P180_BUFFER_START_COMMANDS + i].clear();
        }
      }
    }
  }
}

P180_data_struct::~P180_data_struct() {
  _strings->clear();
}

bool P180_data_struct::plugin_init(struct EventStruct *event) {
  if (validGpio(_enPin)) {
    pinMode(_enPin, OUTPUT);
    DIRECT_pinWrite(_enPin, HIGH);
  }

  if (validGpio(_rstPin)) {
    pinMode(_rstPin, OUTPUT);
    DIRECT_pinWrite(_rstPin, HIGH);
  }

  loadStrings(event);

  if (nullptr != busCmd_Helper) {
    _initialized = true;

    if (_stringsLoaded && !_strings[P180_BUFFER_ENTRY_INIT].isEmpty()) {
      _initialized = busCmd_Helper->parseAndExecute(BusCmd_CommandSource_e::PluginRead, // To allow logging
                                                    _strings[P180_BUFFER_ENTRY_INIT],
                                                    F("P180 : Device initialization with %d commands."));
    }
  }

  return _initialized;
}

void P180_data_struct::plugin_exit(struct EventStruct *event) {
  loadStrings(event);

  if (_stringsLoaded && !_strings[P180_BUFFER_ENTRY_EXIT].isEmpty() && (nullptr != busCmd_Helper)) {
    busCmd_Helper->parseAndExecute(BusCmd_CommandSource_e::PluginRead, // To allow logging
                                   _strings[P180_BUFFER_ENTRY_EXIT],
                                   F("P180 : Device shutdown/exit with %d commands."));
  }
}

void P180_data_struct::loadStrings(struct EventStruct*event) {
  if (!_stringsLoaded) {
    LoadCustomTaskSettings(event->TaskIndex, _strings, P180_CUSTOM_BUFFER_SIZE, 0);
    _stringsLoaded = true;
  }
}

bool P180_data_struct::plugin_once_a_second(struct EventStruct *event) {
  if (nullptr != busCmd_Helper) {
    return busCmd_Helper->plugin_once_a_second(event);
  }
  return false;
}

bool P180_data_struct::plugin_ten_per_second(struct EventStruct *event) {
  if (nullptr != busCmd_Helper) {
    return busCmd_Helper->plugin_ten_per_second(event);
  }
  return false;
}

bool P180_data_struct::plugin_fifty_per_second(struct EventStruct *event) {
  if (nullptr != busCmd_Helper) {
    return busCmd_Helper->plugin_fifty_per_second(event);
  }
  return false;
}

bool P180_data_struct::plugin_read(struct EventStruct *event) {
  if (nullptr != busCmd_Helper) {
    return busCmd_Helper->plugin_read(event);
  }
  return false;
}

/*********************************************************************************************
 * Handle command processing
 ********************************************************************************************/
bool P180_data_struct::plugin_write(struct EventStruct *event,
                                    String            & string) {
  bool success     = false;
  const String cmd = parseString(string, 1);

  if (equals(cmd, F("geni2c"))) {
    const String sub       = parseString(string, 2);
    const String par3      = parseStringKeepCaseNoTrim(string, 3);
    const String varName   = parseString(string, 4);
    const bool   hasPar3   = !par3.isEmpty();
    const bool   hasPar4   = !varName.isEmpty();
    const bool   hasPar5   = !parseString(string, 5).isEmpty();
    const bool   hasBusCmd = nullptr != busCmd_Helper;
    std::vector<BusCmd_Command_struct> cmds;
    taskVarIndex_t taskVar = INVALID_TASKVAR_INDEX;
    uint32_t val{};

    if (hasPar4) {
      if (validUIntFromString(varName, val) && (val > 0) && (val <= VARS_PER_TASK)) {
        taskVar = val - 1;
      } else {
        taskVar = findDeviceValueIndexByName(varName, event->TaskIndex);
      }
    }

    if (equals(sub, F("cmd")) && hasPar3 && hasBusCmd) { // genI2c,cmd,<I2Ccommands>[,<TaskValueIndex>[,<cache_name>]]
      String cacheName;

      if (hasPar5) {
        cacheName = parseString(string, 5);
      }

      BusCmd_CommandSource_e old = busCmd_Helper->getCommandSource();
      busCmd_Helper->setCommandSource(BusCmd_CommandSource_e::PluginWrite);
      cmds = busCmd_Helper->parseBusCmdCommands(cacheName, par3, !cacheName.isEmpty());
      busCmd_Helper->setCommandSource(old);
    } else if (equals(sub, F("exec")) && hasPar3 && hasBusCmd) {     // genI2c,exec,<cache_name>[,<TaskValueIndex>]
      cmds = busCmd_Helper->parseBusCmdCommands(par3, EMPTY_STRING); // Fetch commands from cache by name
    } else if (equals(sub, F("log")) && hasPar3) {                   // genI2c,log,<1|0>
      _showLog       = event->Par2 != 0;
      P180_LOG_DEBUG = _showLog ? 1 : 0;

      if (hasBusCmd) {
        busCmd_Helper->setLog(_showLog);
      }
      success = true;
    }

    if (!cmds.empty() && hasBusCmd && (BusCmd_CommandState_e::Idle == busCmd_Helper->getCommandState())) { // Execute commands when not busy
      busCmd_Helper->setCommands(cmds,
                                 taskVar,
                                 0,
                                 1, // Process single entry
                                 BusCmd_CommandState_e::Processing);
      Scheduler.schedule_task_device_timer(_taskIndex, millis() + 5);

      success = true;
    }
  }

  return success;
}

/*********************************************************************************************
 * Handle get config value retrieval processing
 ********************************************************************************************/
# ifndef LIMIT_BUILD_SIZE
bool P180_data_struct::plugin_get_config(struct EventStruct *event,
                                         String            & string) {
  bool success         = false;
  const bool hasBusCmd = nullptr != busCmd_Helper;

  if (hasBusCmd) {
    success = busCmd_Helper->plugin_get_config(event, string);
  }

  return success;
}

# endif // ifndef LIMIT_BUILD_SIZE

#endif // ifdef USES_P180

#include "../../_Plugin_Helper.h"
#ifdef USES_P128

# include "../PluginStructs/P128_data_struct.h"

// ***************************************************************/
// Constructor
// ***************************************************************/
P128_data_struct::P128_data_struct(int8_t   _gpioPin,
                                   uint16_t _pixelCount,
                                   uint8_t  _maxBright)
  : gpioPin(_gpioPin), pixelCount(_pixelCount), maxBright(_maxBright)
{
   # ifdef ESP8266
  Plugin_128_pixels = new (std::nothrow) NEOPIXEL_LIB<FEATURE, METHOD>(min(pixelCount, static_cast<uint16_t>(ARRAYSIZE)));
  # endif // ifdef ESP8266
  # ifdef ESP32
  Plugin_128_pixels = new (std::nothrow) NEOPIXEL_LIB<FEATURE, METHOD>(min(pixelCount, static_cast<uint16_t>(ARRAYSIZE)),
                                                                       _gpioPin);
  # endif // ifdef ESP32

  if (nullptr != Plugin_128_pixels) {
    Plugin_128_pixels->Begin(); // This initializes the NeoPixelBus library.
    Plugin_128_pixels->SetBrightness(maxBright);
  }
}

// ***************************************************************/
// Destructor
// ***************************************************************/
P128_data_struct::~P128_data_struct() {
  delete Plugin_128_pixels;
  Plugin_128_pixels = nullptr;
}

bool P128_data_struct::plugin_read(struct EventStruct *event) {
  // there is no need to read them, just use current values
  UserVar.setFloat(event->TaskIndex, 0, static_cast<int>(mode));
  UserVar.setFloat(event->TaskIndex, 1, static_cast<int>(savemode));
  UserVar.setFloat(event->TaskIndex, 2, fadetime);
  UserVar.setFloat(event->TaskIndex, 3, fadedelay);

  # ifndef LIMIT_BUILD_SIZE

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO, strformat(
      F("Lights: mode: %s lastmode: %s fadetime: %d fadedelay: %d"),
      String(P128_modeType_toString(mode)).c_str(),
      String(P128_modeType_toString(savemode)).c_str(),
      (int)UserVar[event->BaseVarIndex + 2],
      (int)UserVar[event->BaseVarIndex + 3]));
  }
  # endif // ifndef LIMIT_BUILD_SIZE
  return true;
}

const char neopixelfx_subcommands[] PROGMEM =
  "all"
  "|bgcolor"
  "|colorfade"
  "|comet"
  "|count"
  "|dim"
  "|dualscan"
  "|dualwipe"
  "|fade"
  "|fadedelay"
  "|fadetime"
# if P128_ENABLE_FAKETV
  "|faketv"
# endif // if P128_ENABLE_FAKETV
  "|fire"
  "|fireflicker"
  "|hsv"
  "|hsvline"
  "|hsvone"
  "|kitt"
  "|line"
  "|off"
  "|on"
  "|one"
  "|rainbow"
  "|rgb"
  "|scan"
  "|simpleclock"
  "|sparkle"
  "|speed"
  "|statusrequest"
  "|stop"
  "|theatre"
  "|tick"
  "|twinkle"
  "|twinklefade"
  "|wipe";


enum class neopixelfx_subcommands_e {
  all,
  bgcolor,
  colorfade,
  comet,
  count,
  dim,
  dualscan,
  dualwipe,
  fade,
  fadedelay,
  fadetime,
# if P128_ENABLE_FAKETV
  faketv,
# endif // if P128_ENABLE_FAKETV
  fire,
  fireflicker,
  hsv,
  hsvline,
  hsvone,
  kitt,
  line,
  off,
  on,
  one,
  rainbow,
  rgb,
  scan,
  simpleclock,
  sparkle,
  speed,
  statusrequest,
  stop,
  theatre,
  tick,
  twinkle,
  twinklefade,
  wipe
};


bool P128_data_struct::plugin_write(struct EventStruct *event,
                                    const String      & string) {
  bool success = false;

  const String command = parseString(string, 1);

  if ((equals(command, F("neopixelfx"))) || (equals(command, F("nfx")))) {
    const String subCommand = parseString(string, 2);

    const int subCommand_i = GetCommandCode(subCommand.c_str(), neopixelfx_subcommands);

    if (subCommand_i != -1) {
      const String  str3  = parseString(string, 3);
      const int32_t str3i = event->Par2;
      const String  str4  = parseString(string, 4);
      const int32_t str4i = event->Par3;
      const String  str5  = parseString(string, 5);
      const int32_t str5i = event->Par4;
      const String  str6  = parseString(string, 6);
      const int32_t str6i = event->Par5;
      const String  str7  = parseString(string, 7);
      const int32_t str7i = str7.toInt();


      const neopixelfx_subcommands_e subcommands_e = static_cast<neopixelfx_subcommands_e>(subCommand_i);

      success = true;

      switch (subcommands_e) {
        case neopixelfx_subcommands_e::fadetime:
        {
          fadetime = str3i;
          break;
        }

        case neopixelfx_subcommands_e::fadedelay:
        {
          fadedelay = str3i;
          break;
        }

        case neopixelfx_subcommands_e::speed:
        {
          defaultspeed = str3i;
          speed        = defaultspeed;
          break;
        }

        case neopixelfx_subcommands_e::bgcolor:
        {
          hex2rrggbb(str3);
          break;
        }

        case neopixelfx_subcommands_e::count:
        {
          count = str3i;
          break;
        }

        case neopixelfx_subcommands_e::on:
        case neopixelfx_subcommands_e::off:
        {
          fadetime = str3.isEmpty()
          ? 1000
          : str3i;
          fadedelay = str4.isEmpty()
          ? 0
          : str4i;

          for (int pixel = 0; pixel < pixelCount; pixel++) {
            r_pixel = (fadedelay < 0)
            ? pixelCount - pixel - 1
            : pixel;

            starttime[r_pixel] = counter20ms + (pixel * abs(fadedelay) / 20);

            if (subcommands_e == neopixelfx_subcommands_e::off) {
              rgb_old[pixel]    = Plugin_128_pixels->GetPixelColor(pixel);
              rgb_target[pixel] = RgbColor(0);
            } else if (mode == P128_modetype::Off) { // switch on
              rgb_target[pixel] = rgb_old[pixel];
              rgb_old[pixel]    = Plugin_128_pixels->GetPixelColor(pixel);
            }
          }

          if (subcommands_e == neopixelfx_subcommands_e::off) {
            savemode = mode;
            mode     = P128_modetype::Fade;
          } else if (mode == P128_modetype::Off) {
            // switch on
            mode = (savemode == P128_modetype::On) ? P128_modetype::Fade : savemode;
          }

          maxtime = starttime[r_pixel] + (fadetime / 20);
          break;
        }

        case neopixelfx_subcommands_e::dim:
        {
          if ((str3i >= 0) && (str3i <= maxBright)) { // Safety check
            Plugin_128_pixels->SetBrightness(str3i);
          } else { success = false; }
          break;
        }

        case neopixelfx_subcommands_e::line:
        {
          mode = P128_modetype::On;

          hex2rgb(str5);

          const int lastPixelIndex = (str4i - str3i + pixelCount) % pixelCount;

          for (int i = 0; i <= lastPixelIndex; ++i) {
            Plugin_128_pixels->SetPixelColor((i + str3i - 1) % pixelCount, rgb);
          }
          break;
        }

        case neopixelfx_subcommands_e::tick:
        {
          mode = P128_modetype::On;

          hex2rgb(str4);

          //          for (int i = 0; i < pixelCount ; i = i + (pixelCount / parseString(string, 3).toInt())) {
          for (int i = 0; i < str3i; i++) {
            Plugin_128_pixels->SetPixelColor(i * pixelCount / str3i, rgb);
          }
          break;
        }

        case neopixelfx_subcommands_e::one:
        {
          mode = P128_modetype::On;

          const uint16_t pixnum = str3i - 1;
          hex2rgb(str4);

          Plugin_128_pixels->SetPixelColor(pixnum, rgb);
          break;
        }

        case neopixelfx_subcommands_e::fade:
        case neopixelfx_subcommands_e::all:
        case neopixelfx_subcommands_e::rgb:
        {
          mode = P128_modetype::Fade;

          if ((subcommands_e == neopixelfx_subcommands_e::all) ||
              (subcommands_e == neopixelfx_subcommands_e::rgb)) {
            fadedelay = 0;
          }

          hex2rgb(str3);
          hex2rgb_pixel(str3);

          fadetime = str4.isEmpty()
          ? fadetime
          : str4i;
          fadedelay = str5.isEmpty()
          ? fadedelay
          : str5i;

          for (int pixel = 0; pixel < pixelCount; pixel++) {
            r_pixel = (fadedelay < 0)
            ? pixelCount - pixel - 1
            : pixel;

            starttime[r_pixel] = counter20ms + (pixel * abs(fadedelay) / 20);

            rgb_old[pixel] = Plugin_128_pixels->GetPixelColor(pixel);
          }
          maxtime = starttime[r_pixel] + (fadetime / 20);
          break;
        }

        case neopixelfx_subcommands_e::hsv:
        {
          mode      = P128_modetype::Fade;
          fadedelay = 0;
          rgb       =
            RgbColor(HsbColor(str3.toFloat() / 360.0f,
                              str4.toFloat() / 100.0f,
                              str5.toFloat() / 100.0f));

          rgb2colorStr();

          hex2rgb_pixel(colorStr);

          fadetime = str6.isEmpty()
          ? fadetime
          : str6i;
          fadedelay = str7.isEmpty()
          ? fadedelay
          : str7i;

          for (int pixel = 0; pixel < pixelCount; pixel++) {
            r_pixel = (fadedelay < 0)
            ? pixelCount - pixel - 1
            : pixel;

            starttime[r_pixel] = counter20ms + (pixel * abs(fadedelay) / 20);

            rgb_old[pixel] = Plugin_128_pixels->GetPixelColor(pixel);
          }
          maxtime = starttime[r_pixel] + (fadetime / 20);
          break;
        }

        case neopixelfx_subcommands_e::hsvone:
        {
          mode = P128_modetype::On;
          rgb  =
            RgbColor(HsbColor(str4.toFloat() / 360.0f,
                              str5.toFloat() / 100.0f,
                              str6.toFloat() / 100.0f));

          rgb2colorStr();

          hex2rgb(colorStr);
          const uint16_t pixnum = str3i - 1;
          Plugin_128_pixels->SetPixelColor(pixnum, rgb);
          break;
        }

        case neopixelfx_subcommands_e::hsvline:
        {
          mode = P128_modetype::On;

          rgb =
            RgbColor(HsbColor(str5.toFloat() / 360.0f,
                              str6.toFloat() / 100.0f,
                              str7.toFloat() / 100.0f));

          rgb2colorStr();

          hex2rgb(colorStr);

          const int lastPixelIndex = (str4i - str3i + pixelCount) % pixelCount;

          for (int i = 0; i <= lastPixelIndex; ++i) {
            Plugin_128_pixels->SetPixelColor((i + str3i - 1) % pixelCount, rgb);
          }
          break;
        }

        case neopixelfx_subcommands_e::rainbow:
        {
          fadeIn      = (mode == P128_modetype::Off) ? true : false;
          mode        = P128_modetype::Rainbow;
          starttimerb = counter20ms;

          rainbowspeed = str3.isEmpty()
          ? speed
          : str3i;

          fadetime = str4.isEmpty()
          ? fadetime
          : str4i;
          break;
        }

        case neopixelfx_subcommands_e::colorfade:
        {
          mode = P128_modetype::ColorFade;

          hex2rgb(str3);

          if (!str4.isEmpty()) { hex2rrggbb(str4); }

          startpixel = str5.isEmpty()
          ? 0
          : str5i - 1;
          endpixel = str6.isEmpty()
          ? pixelCount - 1
          : str6i - 1;
          break;
        }

        case neopixelfx_subcommands_e::kitt:
        {
          mode = P128_modetype::Kitt;

          _counter_mode_step = 0;

          hex2rgb(str3);

          speed = str4.isEmpty()
          ? defaultspeed
          : str4i;
          break;
        }

        case neopixelfx_subcommands_e::comet:
        {
          mode = P128_modetype::Comet;

          _counter_mode_step = 0;

          hex2rgb(str3);

          speed = str4.isEmpty()
          ? defaultspeed
          : str4i;
          break;
        }

        case neopixelfx_subcommands_e::theatre:
        {
          mode = P128_modetype::Theatre;

          hex2rgb(str3);

          if (!str4.isEmpty()) { hex2rrggbb(str4); }

          count = str5.isEmpty()
          ? count
          : str5i;

          speed = str6.isEmpty()
          ? defaultspeed
          : str6i;

          for (int i = 0; i < pixelCount; i++) {
            if ((i / count) % 2 == 0) {
              Plugin_128_pixels->SetPixelColor(i, rgb);
            } else {
              Plugin_128_pixels->SetPixelColor(i, rrggbb);
            }
          }
          break;
        }

        case neopixelfx_subcommands_e::scan:
        {
          mode = P128_modetype::Scan;

          _counter_mode_step = 0;

          hex2rrggbb(F("000000"));

          /*CLEAN ALL PIXELS */
          for (int i = 0; i < pixelCount; i++) {
            Plugin_128_pixels->SetPixelColor(i, rrggbb);
          }
          hex2rgb(str3);

          if (!str4.isEmpty()) { hex2rrggbb(str4); }

          speed = str5.isEmpty()
          ? defaultspeed
          : str5i;
          ledi = str6.isEmpty()
          ? 1
          : str6i;
          ledf = str7.isEmpty()
          ? pixelCount
          : str7i + 1;
          break;
        }

        case neopixelfx_subcommands_e::dualscan:
        {
          mode = P128_modetype::Dualscan;

          _counter_mode_step = 0;
          hex2rrggbb(F("000000"));

          /*CLEAN ALL PIXELS */
          for (int i = 0; i < pixelCount; i++) {
            Plugin_128_pixels->SetPixelColor(i, rrggbb);
          }
          hex2rgb(str3);

          if (!str4.isEmpty()) { hex2rrggbb(str4); }

          speed = str5.isEmpty()
          ? defaultspeed
          : str5i;
          ledi = str6.isEmpty()
          ? 1
          : str6i;
          ledf = str7.isEmpty()
          ? pixelCount
          : str7i + 1;
          break;
        }

        case neopixelfx_subcommands_e::twinkle:
        {
          // FIXME TD-er: code duplication in: Twinkle, twinklefade, sparkle
          mode = P128_modetype::Twinkle;

          _counter_mode_step = 0;

          hex2rgb(str3);

          if (!str4.isEmpty()) { hex2rrggbb(str4); }

          speed = str5.isEmpty()
          ? defaultspeed
          : str5i;
          break;
        }

        case neopixelfx_subcommands_e::twinklefade:
        {
          mode = P128_modetype::TwinkleFade;

          hex2rgb(str3);

          count = str4.isEmpty()
          ? count
          : str4i;

          speed = str5.isEmpty()
          ? defaultspeed
          : str5i;
          break;
        }

        case neopixelfx_subcommands_e::sparkle:
        {
          mode = P128_modetype::Sparkle;

          _counter_mode_step = 0;

          hex2rgb(str3);
          hex2rrggbb(str4);

          speed = str5.isEmpty()
          ? defaultspeed
          : str5i;
          break;
        }

        case neopixelfx_subcommands_e::wipe:
        case neopixelfx_subcommands_e::dualwipe:
        {
          mode =   (subcommands_e == neopixelfx_subcommands_e::wipe)
          ? P128_modetype::Wipe
          : P128_modetype::Dualwipe;

          _counter_mode_step = 0;

          hex2rgb(str3);

          if (!str4.isEmpty()) {
            hex2rrggbb(str4);
          } else {
            hex2rrggbb(F("000000"));
          }

          speed = str5.isEmpty()
          ? defaultspeed
          : str5i;
          break;
        }

    # if P128_ENABLE_FAKETV
        case neopixelfx_subcommands_e::faketv:
        {
          mode               = P128_modetype::FakeTV;
          _counter_mode_step = 0;

          randomSeed(analogRead(A0));
          pixelNum = HwRandom(NUMPixels); // Begin at random point

          startpixel = str3.isEmpty()
          ? 0
          : str3i - 1;
          endpixel = str4.isEmpty()
          ? pixelCount
          : str4i;
          break;
        }
    # endif // if P128_ENABLE_FAKETV

        case neopixelfx_subcommands_e::fire:
        {
          mode = P128_modetype::Fire;

          fps = str3.isEmpty()
          ? fps
          : str3i;

          fps = (fps == 0 || fps > 50) ? 50 : fps;

          brightness = str4.isEmpty()
          ? brightness
          : str4.toFloat();
          cooling = str5.isEmpty()
          ? cooling
          : str5.toFloat();
          sparking = str6.isEmpty()
          ? sparking
          : str6.toFloat();
          break;
        }

        case neopixelfx_subcommands_e::fireflicker:
        {
          mode = P128_modetype::FireFlicker;

          rev_intensity = str3.isEmpty()
          ? rev_intensity
          : str3i;

          speed = str4.isEmpty()
          ? defaultspeed
          : str4i;
          break;
        }

        case neopixelfx_subcommands_e::simpleclock:
        {
          mode = P128_modetype::SimpleClock;

      # if defined(RGBW) || defined(GRBW)

          if (!str3.isEmpty()) {
            rgb_tick_s = rgbStr2RgbWColor(str3);
          }

          if (!str4.isEmpty()) {
            rgb_tick_b = rgbStr2RgbWColor(str4);
          }

          if (!str5.isEmpty()) {
            rgb_h = rgbStr2RgbWColor(str5);
          }

          if (!str6.isEmpty()) {
            rgb_m = rgbStr2RgbWColor(str6);
          }

          if (!str7.isEmpty()) {
            if (equals(str7, F("off"))) {
              rgb_s_off = true;
            } else {
              rgb_s     = rgbStr2RgbWColor(str7);
              rgb_s_off = false;
            }
          }

      # else // if defined(RGBW) || defined(GRBW)

          if (!str3.isEmpty()) {
            rgb_tick_s = rgbStr2RgbColor(str3);
          }

          if (!str4.isEmpty()) {
            rgb_tick_b = rgbStr2RgbColor(str4);
          }

          if (!str5.isEmpty()) {
            rgb_h = rgbStr2RgbColor(str5);
          }

          if (!str6.isEmpty()) {
            rgb_m = rgbStr2RgbColor(str6);
          }

          if (!str7.isEmpty()) {
            if (equals(str7, F("off"))) {
              rgb_s_off = true;
            } else {
              rgb_s     = rgbStr2RgbColor(str7);
              rgb_s_off = false;
            }
          }

      # endif // if defined(RGBW) || defined(GRBW)

          if (!parseString(string, 8).isEmpty()) {
            hex2rrggbb(parseString(string, 8));
          }
          break;
        }

        case neopixelfx_subcommands_e::stop:
        {
          mode = P128_modetype::On;
          break;
        }

        case neopixelfx_subcommands_e::statusrequest:
        {
          break;
        }
      }
    }

    if (!success) {
      success = true; // Fake the command to be successful, to get this custom error message out

      String error(concat(F("NeoPixelBus: unknown subcommand: "), subCommand));

      printToWebJSON = true;

      // event->Source=EventValueSource::Enum::VALUE_SOURCE_HTTP;
      SendStatus(
        event,
        strformat(
          F("{\n\"plugin\":128,\n\"log\":\"%s\"\n}\n"),
          error.c_str())
        ); // send http response to controller (JSON format)
      printToWeb = false;

      addLogMove(LOG_LEVEL_INFO, error);
    }
    NeoPixelSendStatus(event);

    if (speed == 0) {
      mode = P128_modetype::On; // speed = 0 = stop mode
    }

    // avoid invalid values
    if ((speed > SPEED_MAX) || (speed < -SPEED_MAX)) {
      speed = defaultspeed;
    }

    if (fadetime <= 0) {
      fadetime = 20;
    }
  } // command neopixel

  return success;
}

void P128_data_struct::rgb2colorStr() {
  colorStr = formatToHex_no_prefix(
    (rgb.R << 16) | (rgb.G << 8) | rgb.B, 
    6);
}

bool P128_data_struct::plugin_fifty_per_second(struct EventStruct *event) {
  counter20ms++;
  lastmode = mode;

  switch (mode) {
    case P128_modetype::Fade:
      fade();
      break;

    case P128_modetype::ColorFade:
      colorfade();
      break;

    case P128_modetype::Rainbow:
      rainbow();
      break;

    case P128_modetype::Kitt:
      kitt();
      break;

    case P128_modetype::Comet:
      comet();
      break;

    case P128_modetype::Theatre:
      theatre();
      break;

    case P128_modetype::Scan:
      scan();
      break;

    case P128_modetype::Dualscan:
      dualscan();
      break;

    case P128_modetype::Twinkle:
      twinkle();
      break;

    case P128_modetype::TwinkleFade:
      twinklefade();
      break;

    case P128_modetype::Sparkle:
      sparkle();
      break;

    case P128_modetype::Fire:
      fire();
      break;

    case P128_modetype::FireFlicker:
      fire_flicker();
      break;

    case P128_modetype::Wipe:
      wipe();
      break;

    case P128_modetype::Dualwipe:
      dualwipe();
      break;

    # if P128_ENABLE_FAKETV
    case P128_modetype::FakeTV:
      faketv();
      break;
    # endif // if P128_ENABLE_FAKETV

    case P128_modetype::SimpleClock:
      Plugin_128_simpleclock();
      break;

    default:
      break;
  } // switch mode

  Plugin_128_pixels->Show();

  if (mode != lastmode) {
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, concat(
        F("NeoPixelBus: Mode Change: "), 
        P128_modeType_toString(mode)));
    }
    NeoPixelSendStatus(event);
  }
  return true;
}

void P128_data_struct::fade(void) {
  for (int pixel = 0; pixel < pixelCount; pixel++) {
    long  counter  = 20 * (counter20ms - starttime[pixel]);
    float progress = (float)counter / (float)fadetime;
    progress = constrain(progress, 0.0f, 1.0f);

    # if defined(RGBW) || defined(GRBW)
    RgbwColor updatedColor = RgbwColor::LinearBlend(
      rgb_old[pixel], rgb_target[pixel],
      progress);
    # else // if defined(RGBW) || defined(GRBW)
    RgbColor updatedColor = RgbColor::LinearBlend(
      rgb_old[pixel], rgb_target[pixel],
      progress);
    # endif // if defined(RGBW) || defined(GRBW)

    if ((counter20ms > maxtime) && (Plugin_128_pixels->GetPixelColor(pixel).CalculateBrightness() == 0)) {
      mode = P128_modetype::Off;
    } else if (counter20ms > maxtime) {
      mode = P128_modetype::On;
    }

    Plugin_128_pixels->SetPixelColor(pixel, updatedColor);
  }
}

void P128_data_struct::colorfade(void) {
  float progress = 0;

  difference = (endpixel - startpixel + pixelCount) % pixelCount;

  for (uint16_t i = 0; i <= difference; i++)
  {
    progress = (float)i / (difference - 1);
    progress = constrain(progress, 0.0f, 1.0f);

    # if defined(RGBW) || defined(GRBW)
    RgbwColor updatedColor = RgbwColor::LinearBlend(
      rgb, rrggbb,
      progress);
    # else // if defined(RGBW) || defined(GRBW)
    RgbColor updatedColor = RgbColor::LinearBlend(
      rgb, rrggbb,
      progress);
    # endif // if defined(RGBW) || defined(GRBW)

    Plugin_128_pixels->SetPixelColor((i + startpixel) % pixelCount, updatedColor);
  }
  mode = P128_modetype::On;
}

void P128_data_struct::wipe(void) {
  if (counter20ms % (unsigned long)(SPEED_MAX / abs(speed)) == 0) {
    if (speed > 0) {
      Plugin_128_pixels->SetPixelColor(_counter_mode_step, rrggbb);

      if (_counter_mode_step > 0) { Plugin_128_pixels->SetPixelColor(_counter_mode_step - 1, rgb); }
    } else {
      Plugin_128_pixels->SetPixelColor(pixelCount - _counter_mode_step - 1, rrggbb);

      if (_counter_mode_step > 0) { Plugin_128_pixels->SetPixelColor(pixelCount - _counter_mode_step, rgb); }
    }

    if (_counter_mode_step == pixelCount) { mode = P128_modetype::On; }
    _counter_mode_step++;
  }
}

void P128_data_struct::dualwipe(void) {
  if (counter20ms % (unsigned long)(SPEED_MAX / abs(speed)) == 0) {
    if (speed > 0) {
      const int i = abs(static_cast<int>(_counter_mode_step - pixelCount));
      Plugin_128_pixels->SetPixelColor(_counter_mode_step, rrggbb);
      Plugin_128_pixels->SetPixelColor(i,                  rgb);

      if (_counter_mode_step > 0) {
        Plugin_128_pixels->SetPixelColor(_counter_mode_step - 1, rgb);
        Plugin_128_pixels->SetPixelColor(i - 1,                  rrggbb);
      }
    } else {
      const int i = abs(static_cast<int>((pixelCount / 2) - _counter_mode_step));
      Plugin_128_pixels->SetPixelColor(_counter_mode_step + (pixelCount / 2), rrggbb);
      Plugin_128_pixels->SetPixelColor(i,                                     rgb);

      if (_counter_mode_step > 0) {
        Plugin_128_pixels->SetPixelColor(_counter_mode_step + (pixelCount / 2) - 1, rgb);
        Plugin_128_pixels->SetPixelColor(i - 1,                                     rrggbb);
      }
    }

    if (_counter_mode_step >= pixelCount / 2) {
      mode = P128_modetype::On;
      Plugin_128_pixels->SetPixelColor(_counter_mode_step - 1, rgb);
    }
    _counter_mode_step++;
  }
}

# if P128_ENABLE_FAKETV
void P128_data_struct::faketv(void) {
  if (counter20ms >= ftv_holdTime) {
    difference = abs(endpixel - startpixel);

    if (ftv_elapsed >= ftv_fadeTime) {
      // Read next 16-bit (5/6/5) color
      ftv_hi = pgm_read_byte(&ftv_colors[pixelNum * 2]);
      ftv_lo = pgm_read_byte(&ftv_colors[pixelNum * 2 + 1]);

      if (++pixelNum >= NUMPixels) { pixelNum = 0; }

      // Expand to 24-bit (8/8/8)
      ftv_r8 = (ftv_hi & 0xF8) | (ftv_hi >> 5);
      ftv_g8 = (ftv_hi << 5) | ((ftv_lo & 0xE0) >> 3) | ((ftv_hi & 0x06) >> 1);
      ftv_b8 = (ftv_lo << 3) | ((ftv_lo & 0x1F) >> 2);

      // Apply gamma correction, further expand to 16/16/16
      ftv_nr = (uint8_t)pgm_read_byte(&ftv_gamma8[ftv_r8]) * 257; // New R/G/B
      ftv_ng = (uint8_t)pgm_read_byte(&ftv_gamma8[ftv_g8]) * 257;
      ftv_nb = (uint8_t)pgm_read_byte(&ftv_gamma8[ftv_b8]) * 257;

      ftv_totalTime = HwRandom(12, 125);                          // Semi-random pixel-to-pixel time
      ftv_fadeTime  = HwRandom(0, ftv_totalTime);                 // Pixel-to-pixel transition time

      if (HwRandom(10) < 3) { ftv_fadeTime = 0; }                 // Force scene cut 30% of time
      ftv_holdTime  = counter20ms + ftv_totalTime - ftv_fadeTime; // Non-transition time
      ftv_startTime = counter20ms;
    }

    ftv_elapsed = counter20ms - ftv_startTime;

    if (ftv_fadeTime) {
      ftv_r = map(ftv_elapsed, 0, ftv_fadeTime, ftv_pr, ftv_nr); // 16-bit interp
      ftv_g = map(ftv_elapsed, 0, ftv_fadeTime, ftv_pg, ftv_ng);
      ftv_b = map(ftv_elapsed, 0, ftv_fadeTime, ftv_pb, ftv_nb);
    } else {                                                     // Avoid divide-by-ftv_fraczero in map()
      ftv_r = ftv_nr;
      ftv_g = ftv_ng;
      ftv_b = ftv_nb;
    }

    for (ftv_i = 0; ftv_i < difference; ftv_i++) {
      ftv_r8   = ftv_r >> 8;                 // Quantize to 8-bit
      ftv_g8   = ftv_g >> 8;
      ftv_b8   = ftv_b >> 8;
      ftv_frac = (ftv_i << 16) / difference; // LED index scaled to 0-65535 (16Bit)

      if ((ftv_r8 < 255) && ((ftv_r & 0xFF) >= ftv_frac)) { ftv_r8++; } // Boost some fraction

      if ((ftv_g8 < 255) && ((ftv_g & 0xFF) >= ftv_frac)) { ftv_g8++; } // of LEDs to handle

      if ((ftv_b8 < 255) && ((ftv_b & 0xFF) >= ftv_frac)) { ftv_b8++; } // interp > 8bit

      Plugin_128_pixels->SetPixelColor(ftv_i + startpixel, RgbColor(ftv_r8, ftv_g8, ftv_b8));
    }

    ftv_pr = ftv_nr; // Prev RGB = new RGB
    ftv_pg = ftv_ng;
    ftv_pb = ftv_nb;
  }
}

# endif // if P128_ENABLE_FAKETV

/*
 * Cycles a rainbow over the entire string of LEDs.
 */
void P128_data_struct::rainbow(void) {
  const long  counter  = 20 * (counter20ms - starttimerb);
  const float progress = (float)counter / (float)fadetime;

  if (fadeIn == true) {
    Plugin_128_pixels->SetBrightness(progress * maxBright); // Safety check
    fadeIn = (progress == 1) ? false : true;
  }

  for (int i = 0; i < pixelCount; i++) {
    const uint32_t color = Wheel(((i * 256 / pixelCount) + counter20ms * rainbowspeed / 10) & 255);
    Plugin_128_pixels->SetPixelColor(i, 
      RgbColor(
        (color >> 16), // r
        (color >> 8),  // g
        (color)));     // b
  }
  mode = (rainbowspeed == 0) ? P128_modetype::On : P128_modetype::Rainbow;
}

/*
 * Put a value 0 to 255 in to get a color value.
 * The colours are a transition r -> g -> b -> back to r
 * Inspired by the Adafruit examples.
 */
uint32_t P128_data_struct::Wheel(uint8_t pos) {
  pos = 255 - pos;

  if (pos < 85) {
    return ((uint32_t)(255 - pos * 3) << 16) | ((uint32_t)(0) << 8) | (pos * 3);
  } else if (pos < 170) {
    pos -= 85;
    return ((uint32_t)(0) << 16) | ((uint32_t)(pos * 3) << 8) | (255 - pos * 3);
  } else {
    pos -= 170;
    return ((uint32_t)(pos * 3) << 16) | ((uint32_t)(255 - pos * 3) << 8) | (0);
  }
}

// Larson Scanner K.I.T.T.
void P128_data_struct::kitt(void) {
  if (counter20ms % (unsigned long)(SPEED_MAX / abs(speed)) == 0) {
    for (uint16_t i = 0; i < pixelCount; i++) {
      # if defined(RGBW) || defined(GRBW)
      RgbwColor px_rgb = Plugin_128_pixels->GetPixelColor(i);

      // fade out (divide by 2)
      px_rgb.R >>= 1;
      px_rgb.G >>= 1;
      px_rgb.B >>= 1;
      px_rgb.W >>= 1;

      # else // if defined(RGBW) || defined(GRBW)

      RgbColor px_rgb = Plugin_128_pixels->GetPixelColor(i);

      // fade out (divide by 2)
      px_rgb.R >>= 1;
      px_rgb.G >>= 1;
      px_rgb.B >>= 1;
      # endif // if defined(RGBW) || defined(GRBW)

      Plugin_128_pixels->SetPixelColor(i, px_rgb);
    }

    uint16_t pos = 0;

    if (_counter_mode_step < pixelCount) {
      pos = _counter_mode_step;
    } else {
      pos = (pixelCount * 2) - _counter_mode_step - 2;
    }

    Plugin_128_pixels->SetPixelColor(pos, rgb);

    _counter_mode_step = (_counter_mode_step + 1) % ((pixelCount * 2) - 2);
  }
}

// Firing comets from one end.
void P128_data_struct::comet(void) {
  if (counter20ms % (unsigned long)(SPEED_MAX / abs(speed)) == 0) {
    for (uint16_t i = 0; i < pixelCount; i++) {
      const uint16_t pixelIndex = (speed > 0) ? i : pixelCount - i - 1;
      # if defined(RGBW) || defined(GRBW)
      RgbwColor px_rgb = Plugin_128_pixels->GetPixelColor(pixelIndex);

      // fade out (divide by 2)
      px_rgb.R >>= 1;
      px_rgb.G >>= 1;
      px_rgb.B >>= 1;
      px_rgb.W >>= 1;

      # else // if defined(RGBW) || defined(GRBW)

      RgbColor px_rgb = Plugin_128_pixels->GetPixelColor(pixelIndex);

      // fade out (divide by 2)
      px_rgb.R >>= 1;
      px_rgb.G >>= 1;
      px_rgb.B >>= 1;
      # endif // if defined(RGBW) || defined(GRBW)

      Plugin_128_pixels->SetPixelColor(pixelIndex, px_rgb);
    }

    {
      const uint16_t pixelIndex = (speed > 0) ? _counter_mode_step : pixelCount - _counter_mode_step - 1;
      Plugin_128_pixels->SetPixelColor(pixelIndex, rgb);
    }

    _counter_mode_step = (_counter_mode_step + 1) % pixelCount;
  }
}

// Theatre lights
void P128_data_struct::theatre(void) {
  if ((counter20ms % (unsigned long)(SPEED_MAX / abs(speed)) == 0) && (speed != 0)) {
    if (speed > 0) {
      Plugin_128_pixels->RotateLeft(1, 0, (pixelCount / count) * count - 1);
    } else {
      Plugin_128_pixels->RotateRight(1, 0, (pixelCount / count) * count - 1);
    }
  }
}

/*
 * Runs a single pixel back and forth.
 */
void P128_data_struct::scan(void) {
  if ((counter20ms % (unsigned long)(SPEED_MAX / abs(speed)) == 0) && (speed != 0)) {
    if (_counter_mode_step >= uint16_t(((ledf - ledi) * 2) - 2)) {
      _counter_mode_step = 0;
    }
    _counter_mode_step++;

    int i = _counter_mode_step - ((ledf - ledi) - 1);
    i = abs(i);

    // Plugin_128_pixels->ClearTo(rrggbb);
    for (int i = ledi - 1; i < ledf - 1; i++) {
      Plugin_128_pixels->SetPixelColor(i, rrggbb);
    }
    Plugin_128_pixels->SetPixelColor(abs(i + (ledi - 1)), rgb);
  }
}

/*
 * Runs two pixel back and forth in opposite directions.
 */
void P128_data_struct::dualscan(void) {
  if ((counter20ms % (unsigned long)(SPEED_MAX / abs(speed)) == 0) && (speed != 0)) {
    if (_counter_mode_step >= uint16_t(((ledf - ledi) * 2) - 2)) {
      _counter_mode_step = 0;
    }
    _counter_mode_step++;

    int i = _counter_mode_step - ((ledf - ledi) - 1);
    i = abs(i);

    for (int i = ledi - 1; i < ledf - 1; i++) {
      Plugin_128_pixels->SetPixelColor(i, rrggbb);
    }
    Plugin_128_pixels->SetPixelColor(abs(i + (ledi - 1)),                  rgb);
    Plugin_128_pixels->SetPixelColor(((ledf - ledi) - (i + 1)) + ledi - 1, rgb);
  }
}

/*
 * Blink several LEDs on, reset, repeat.
 * Inspired by www.tweaking4all.com/hardware/arduino/arduino-led-strip-effects/
 */
void P128_data_struct::twinkle(void) {
  if ((counter20ms % (unsigned long)(SPEED_MAX / abs(speed)) == 0) && (speed != 0)) {
    if (_counter_mode_step == 0) {
      // Plugin_128_pixels->ClearTo(rrggbb);
      for (int i = 0; i < pixelCount; i++) {
        Plugin_128_pixels->SetPixelColor(i, rrggbb);
      }
      uint16_t min_leds = _max(1, pixelCount / 5); // make sure, at least one LED is on
      uint16_t max_leds = _max(1, pixelCount / 2); // make sure, at least one LED is on
      _counter_mode_step = HwRandom(min_leds, max_leds);
    }

    Plugin_128_pixels->SetPixelColor(HwRandom(pixelCount), rgb);

    _counter_mode_step--;
  }
}

/*
 * Blink several LEDs on, fading out.
 */
void P128_data_struct::twinklefade(void) {
  if ((counter20ms % (unsigned long)(SPEED_MAX / abs(speed)) == 0) && (speed != 0)) {
    for (uint16_t i = 0; i < pixelCount; i++) {
      # if defined(RGBW) || defined(GRBW)
      RgbwColor px_rgb = Plugin_128_pixels->GetPixelColor(pixelCount - i - 1);

      // fade out (divide by 2)
      px_rgb.R = px_rgb.R >> 1;
      px_rgb.G = px_rgb.G >> 1;
      px_rgb.B = px_rgb.B >> 1;
      px_rgb.W = px_rgb.W >> 1;

      # else // if defined(RGBW) || defined(GRBW)

      RgbColor px_rgb = Plugin_128_pixels->GetPixelColor(pixelCount - i - 1);

      // fade out (divide by 2)
      px_rgb.R = px_rgb.R >> 1;
      px_rgb.G = px_rgb.G >> 1;
      px_rgb.B = px_rgb.B >> 1;
      # endif // if defined(RGBW) || defined(GRBW)

      Plugin_128_pixels->SetPixelColor(i, px_rgb);
    }

    if (HwRandom(count) < 50) {
      Plugin_128_pixels->SetPixelColor(HwRandom(pixelCount), rgb);
    }
  }
}

/*
 * Blinks one LED at a time.
 * Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/
 */
void P128_data_struct::sparkle(void) {
  if ((counter20ms % (unsigned long)(SPEED_MAX / abs(speed)) == 0) && (speed != 0)) {
    // Plugin_128_pixels->ClearTo(rrggbb);
    for (int i = 0; i < pixelCount; i++) {
      Plugin_128_pixels->SetPixelColor(i, rrggbb);
    }
    Plugin_128_pixels->SetPixelColor(HwRandom(pixelCount), rgb);
  }
}

void P128_data_struct::fire(void) {
  if (counter20ms > fireTimer + 50 / fps) {
    fireTimer = counter20ms;
    Fire2012();
    RgbColor pixel;

    for (int i = 0; i < pixelCount; i++) {
      pixel = leds[i];
      pixel = RgbColor::LinearBlend(pixel, RgbColor(0, 0, 0), (255 - brightness) / 255.0f);
      Plugin_128_pixels->SetPixelColor(i, pixel);
    }
  }
}

/// Generate an 8-bit random number
uint8_t P128_data_struct::random8() {
  rand16seed = (rand16seed * ((uint16_t)(2053))) + ((uint16_t)(13849));

  // return the sum of the high and low bytes, for better
  //  mixing and non-sequential correlation
  return (uint8_t)(((uint8_t)(rand16seed & 0xFF)) +
                   ((uint8_t)(rand16seed >> 8)));
}

/// Generate an 8-bit random number between 0 and lim
/// @param lim the upper bound for the result
uint8_t P128_data_struct::random8(uint8_t lim) {
  uint8_t r = random8();

  r = (r * lim) >> 8;
  return r;
}

/// Generate an 8-bit random number in the given range
/// @param min the lower bound for the random number
/// @param lim the upper bound for the random number
uint8_t P128_data_struct::random8(uint8_t min, uint8_t lim) {
  uint8_t delta = lim - min;
  uint8_t r     = random8(delta) + min;

  return r;
}

/// subtract one byte from another, saturating at 0x00
/// @returns i - j with a floor of 0
uint8_t P128_data_struct::qsub8(uint8_t i, uint8_t j) {
  int t = i - j;

  if (t < 0) { t = 0; }
  return t;
}

/// add one byte to another, saturating at 0xFF
/// @param i - first byte to add
/// @param j - second byte to add
/// @returns the sum of i & j, capped at 0xFF
uint8_t P128_data_struct::qadd8(uint8_t i, uint8_t j) {
  unsigned int t = i + j;

  if (t > 255) { t = 255; }
  return t;
}

///  The "video" version of scale8 guarantees that the output will
///  be only be zero if one or both of the inputs are zero.  If both
///  inputs are non-zero, the output is guaranteed to be non-zero.
///  This makes for better 'video'/LED dimming, at the cost of
///  several additional cycles.
uint8_t P128_data_struct::scale8_video(uint8_t i, uint8_t scale) {
  uint8_t j = (((int)i * (int)scale) >> 8) + ((i && scale) ? 1 : 0);

  // uint8_t nonzeroscale = (scale != 0) ? 1 : 0;
  // uint8_t j = (i == 0) ? 0 : (((int)i * (int)(scale) ) >> 8) + nonzeroscale;
  return j;
}

void P128_data_struct::Fire2012(void) {
  // Step 1.  Cool down every cell a little
  for (int i = 0; i < pixelCount; i++) {
    heat[i] = qsub8(heat[i],  random8(0, ((cooling * 10) / pixelCount) + 2));
  }

  // Step 2.  Heat from each cell drifts 'up' and diffuses a little
  for (int k = pixelCount - 1; k >= 2; k--) {
    heat[k] = (heat[k - 1] + heat[k - 2] + heat[k - 2]) / 3;
  }

  // Step 3.  Randomly ignite new 'sparks' of heat near the bottom
  if (random8() < sparking) {
    int y = random8(7);
    heat[y] = qadd8(heat[y], random8(160, 255));
  }

  // Step 4.  Map from heat cells to LED colors
  for (int j = 0; j < pixelCount; j++) {
    RgbColor heatcolor;

    // Scale 'heat' down from 0-255 to 0-191,
    // which can then be easily divided into three
    // equal 'thirds' of 64 units each.
    uint8_t t192 = scale8_video(heat[j], 191);

    // calculate a value that ramps up from
    // zero to 255 in each 'third' of the scale.
    uint8_t heatramp = t192 & 0x3F; // 0..63
    heatramp <<= 2;                 // scale up to 0..252

    // now figure out which third of the spectrum we're in:
    if (t192 & 0x80) {
      // we're in the hottest third
      heatcolor.R = 255;      // full red
      heatcolor.G = 255;      // full green
      heatcolor.B = heatramp; // ramp up blue
    } else if (t192 & 0x40) {
      // we're in the middle third
      heatcolor.R = 255;      // full red
      heatcolor.G = heatramp; // ramp up green
      heatcolor.B = 0;        // no blue
    } else {
      // we're in the coolest third
      heatcolor.R = heatramp; // ramp up red
      heatcolor.G = 0;        // no green
      heatcolor.B = 0;        // no blue
    }

    int pixelnumber;

    if (gReverseDirection) {
      pixelnumber = (pixelCount - 1) - j;
    } else {
      pixelnumber = j;
    }
    leds[pixelnumber] = heatcolor;
  }
}

/*
 * Fire flicker function
 */
void P128_data_struct::fire_flicker() {
  if ((counter20ms % (unsigned long)(SPEED_MAX / abs(speed)) == 0) && (speed != 0)) {
    byte w   = 0;   // (SEGMENT.colors[0] >> 24) & 0xFF;
    byte r   = 255; // (SEGMENT.colors[0] >> 16) & 0xFF;
    byte g   = 96;  // (SEGMENT.colors[0] >>  8) & 0xFF;
    byte b   = 12;  // (SEGMENT.colors[0]        & 0xFF);
    byte lum = max(w, max(r, max(g, b))) / rev_intensity;

    for (uint16_t i = 0; i <= NUMPixels - 1; i++) {
      int flicker = random8(lum);

      # if defined(RGBW) || defined(GRBW)
      Plugin_128_pixels->SetPixelColor(i, RgbwColor(max(r - flicker, 0), max(g - flicker, 0), max(b - flicker, 0), max(w - flicker, 0)));
      # else // if defined(RGBW) || defined(GRBW)
      Plugin_128_pixels->SetPixelColor(i, RgbColor(max(r - flicker, 0), max(g - flicker, 0), max(b - flicker, 0)));
      # endif  // if defined(RGBW) || defined(GRBW)
    }
  }
}

void P128_data_struct::Plugin_128_simpleclock() {
  byte Hours      = node_time.hour() % 12;
  byte Minutes    = node_time.minute();
  byte Seconds    = node_time.second();
  byte big_tick   = 15;
  byte small_tick = 5;

  // hack for sub-second calculations.... reset when first time new second begins..
  if (cooling != Seconds) { maxtime = counter20ms; }
  cooling = Seconds;
  Plugin_128_pixels->ClearTo(rrggbb);

  for (int i = 0; i < (60 / small_tick); i++) {
    const bool use_big_tick = i % (big_tick / small_tick) == 0;
    Plugin_128_pixels->SetPixelColor((i * pixelCount * small_tick / 60) % pixelCount, use_big_tick ? rgb_tick_b : rgb_tick_s);
  }


  for (int i = 0; i < pixelCount; i++) {
    if (lround((((float)Seconds + ((float)counter20ms - (float)maxtime) / 50.0f) * (float)pixelCount) / 60.0f) == i) {
      if (rgb_s_off  == false) {
        Plugin_128_pixels->SetPixelColor(i, rgb_s);
      }
    }
    else if (lround((((float)Minutes * 60.0f) + (float)Seconds) / 60.0f * (float)pixelCount / 60.0f) == i) {
      Plugin_128_pixels->SetPixelColor(i, rgb_m);
    }
    else if (lround(((float)Hours + (float)Minutes / 60) * (float)pixelCount / 12.0f)  == i) {
      Plugin_128_pixels->SetPixelColor(i,                                 rgb_h);
      Plugin_128_pixels->SetPixelColor((i + 1) % pixelCount,              rgb_h);
      Plugin_128_pixels->SetPixelColor((i - 1 + pixelCount) % pixelCount, rgb_h);
    }
  }
}

uint32_t P128_data_struct::rgbStr2Num(const String& rgbStr) {
  return static_cast<uint32_t>(strtoul(rgbStr.c_str(), NULL, 16));
}

RgbColor P128_data_struct::rgbStr2RgbColor(const String& str)
{
  const uint32_t hcolorui = rgbStr2Num(str);

  return RgbColor(hcolorui >> 16, hcolorui >> 8, hcolorui);
}

RgbwColor P128_data_struct::rgbStr2RgbWColor(const String& str)
{
  const uint32_t hcolorui = rgbStr2Num(str);

  if (str.length() <= 6) {
    // w = 0
    return RgbwColor(hcolorui >> 16,
                     hcolorui >> 8,
                     hcolorui);
  }

  return RgbwColor(hcolorui >> 24,
                   hcolorui >> 16,
                   hcolorui >> 8,
                   hcolorui);
}

void P128_data_struct::hex2rgb(const String& hexcolor) {
  colorStr = hexcolor;
  const uint32_t hcolorui = rgbStr2Num(hexcolor);

  # if defined(RGBW) || defined(GRBW)
  hexcolor.length() <= 6
    ? rgb = RgbColor(hcolorui >> 16, hcolorui >> 8, hcolorui)
    : rgb = RgbwColor(hcolorui >> 24, hcolorui >> 16, hcolorui >> 8, hcolorui);
  # else // if defined(RGBW) || defined(GRBW)
  rgb = RgbColor(hcolorui >> 16, hcolorui >> 8, hcolorui);
  # endif // if defined(RGBW) || defined(GRBW)
}

void P128_data_struct::hex2rrggbb(const String& hexcolor) {
  backgroundcolorStr = hexcolor;
  const uint32_t hcolorui = rgbStr2Num(hexcolor);

  # if defined(RGBW) || defined(GRBW)
  hexcolor.length() <= 6
    ? rrggbb = RgbColor(hcolorui >> 16, hcolorui >> 8, hcolorui)
    : rrggbb = RgbwColor(hcolorui >> 24, hcolorui >> 16, hcolorui >> 8, hcolorui);
  # else // if defined(RGBW) || defined(GRBW)
  rrggbb = RgbColor(hcolorui >> 16, hcolorui >> 8, hcolorui);
  # endif // if defined(RGBW) || defined(GRBW)
}

void P128_data_struct::hex2rgb_pixel(const String& hexcolor) {
  colorStr = hexcolor;
  const uint32_t hcolorui = rgbStr2Num(hexcolor);

  for (int i = 0; i < pixelCount; i++) {
    # if defined(RGBW) || defined(GRBW)
    hexcolor.length() <= 6
      ? rgb_target[i] = RgbColor(hcolorui >> 16, hcolorui >> 8, hcolorui)
      : rgb_target[i] = RgbwColor(hcolorui >> 24, hcolorui >> 16, hcolorui >> 8, hcolorui);
    # else // if defined(RGBW) || defined(GRBW)
    rgb_target[i] = RgbColor(hcolorui >> 16, hcolorui >> 8, hcolorui);
    # endif // if defined(RGBW) || defined(GRBW)
  }
}

// ---------------------------------------------------------------------------------
// ------------------------------ JsonResponse -------------------------------------
// ---------------------------------------------------------------------------------
void P128_data_struct::NeoPixelSendStatus(struct EventStruct *eventSource) {
  addLogMove(LOG_LEVEL_INFO, strformat(
               F("NeoPixelBusFX: Set %u/%u/%u"),
               rgb.R,
               rgb.G,
               rgb.B));

  printToWebJSON = true;

  HsbColor hsbColor = HsbColor(RgbColor(rgb.R, rgb.G, rgb.B)); // Calculate only once

  SendStatus(
    eventSource,
    strformat(
      F("{\n%s" // "plugin"
        ",\n%s" // "mode"
        ",\n%s" // "lastmode"
        ",\n%s" // "fadetime"
        ",\n%s" // "fadedelay"
        ",\n%s" // "dim"
        ",\n%s" // "rgb"
        ",\n%s" // "hue"
        ",\n%s" // "saturation"
        ",\n%s" // "brightness"
        ",\n%s" // "bgcolor"
        ",\n%s" // "count"
        ",\n%s" // "speed"
        ",\n%s" // "pixelcount"
        "\n}\n"),
      to_json_object_value(F("plugin"),     128).c_str(),
      to_json_object_value(F("mode"),       P128_modeType_toString(mode)).c_str(),
      to_json_object_value(F("lastmode"),   P128_modeType_toString(savemode)).c_str(),
      to_json_object_value(F("fadetime"),   static_cast<int>(fadetime)).c_str(),
      to_json_object_value(F("fadedelay"),  static_cast<int>(fadedelay)).c_str(),
      to_json_object_value(F("dim"),        static_cast<int>(Plugin_128_pixels->GetBrightness())).c_str(),
      to_json_object_value(F("rgb"),        colorStr, true).c_str(),
      to_json_object_value(F("hue"),        static_cast<int>(hsbColor.H * 360.0f)).c_str(),
      to_json_object_value(F("saturation"), static_cast<int>(hsbColor.S * 100.0f)).c_str(),
      to_json_object_value(F("brightness"), static_cast<int>(hsbColor.B * 100.0f)).c_str(),
      to_json_object_value(F("bgcolor"),    backgroundcolorStr, true).c_str(),
      to_json_object_value(F("count"),      static_cast<int>(count)).c_str(),
      to_json_object_value(F("speed"),      static_cast<int>(speed)).c_str(),
      to_json_object_value(F("pixelcount"), static_cast<int>(pixelCount)).c_str()
      ));
  printToWeb = false;
}

const __FlashStringHelper * P128_data_struct::P128_modeType_toString(P128_modetype modeType) {
  switch (modeType) {
    case P128_modetype::Off: return F("off");
    case P128_modetype::On: return F("on");
    case P128_modetype::Fade: return F("fade");
    case P128_modetype::ColorFade: return F("colorfade");
    case P128_modetype::Rainbow: return F("rainbow");
    case P128_modetype::Kitt: return F("kitt");
    case P128_modetype::Comet: return F("comet");
    case P128_modetype::Theatre: return F("theatre");
    case P128_modetype::Scan: return F("scan");
    case P128_modetype::Dualscan: return F("dualscan");
    case P128_modetype::Twinkle: return F("twinkle");
    case P128_modetype::TwinkleFade: return F("twinklefade");
    case P128_modetype::Sparkle: return F("sparkle");
    case P128_modetype::Fire: return F("fire");
    case P128_modetype::FireFlicker: return F("fireflicker");
    case P128_modetype::Wipe: return F("wipe");
    case P128_modetype::Dualwipe: return F("dualwipe");
    # if P128_ENABLE_FAKETV
    case P128_modetype::FakeTV: return F("faketv");
    # endif // if P128_ENABLE_FAKETV
    case P128_modetype::SimpleClock: return F("simpleclock");
  }
  return F("*unknown*");
}

#endif // ifdef USES_P128

#include "../PluginStructs/P001_data_struct.h"

#ifdef USES_P001


# include "../Helpers/_Plugin_Helper_webform.h"

# include "../ESPEasyCore/ESPEasyGPIO.h"

// TD-er: Needed to fix a mistake in earlier fixes.
uint8_t P001_data_struct::P001_getSwitchType(struct EventStruct *event)
{
  const uint8_t choice = P001_SWITCH_OR_DIMMER;

  if ((choice == 2) || // Old implementation for Dimmer
      (choice == PLUGIN_001_TYPE_DIMMER))
  {
    return PLUGIN_001_TYPE_DIMMER;
  }
  return PLUGIN_001_TYPE_SWITCH;
}

P001_data_struct::P001_data_struct(struct EventStruct *event) :
  _data(
    PLUGIN_GPIO,
    CONFIG_PIN1,
    P001_DEBOUNCE,
    P001_DC_MAX_INT,
    P001_LP_MIN_INT,
    P001_DOUBLECLICK,
    P001_LONGPRESS,
    P001_SAFE_BTN != 0,
    P001_BOOTSTATE,
    (P001_getSwitchType(event) == PLUGIN_001_TYPE_DIMMER) ? SWITCH_TYPE_DIMMER : P001_BUTTON_TYPE,
    P001_DIMMER_VALUE)
{
  uint8_t pinModeValue = PIN_MODE_INPUT;

  // setPinState(PLUGIN_ID_001, _data._pin, PIN_MODE_INPUT, switchstate[event->TaskIndex]);
  //  if it is in the device list we assume it's an input pin
  if (Settings.TaskDevicePin1PullUp[event->TaskIndex])
  {
    setInternalGPIOPullupMode(_data._pin);
    pinModeValue = PIN_MODE_INPUT_PULLUP;
  }
  else
  {
    pinMode(_data._pin, INPUT);
    pinModeValue = PIN_MODE_INPUT;
  }


  // read and store current state to prevent switching at boot time
  // "state" could be -1, 0 or 1
  const int8_t pinState = GPIO_Read_Switch_State(event);

  _data.init(
    event,
    pinState,
    pinModeValue);
}

void P001_data_struct::tenPerSecond(struct EventStruct *event)
{
  if (_data._pin == -1) { return; }

  portStatusStruct currentStatus = globalMapPortStatus[_data._portStatus_key];

  const int8_t pinState = GPIO_Read_Switch_State(_data._pin, currentStatus.mode);

  _data.tenPerSecond(event, F("GPIO"), pinState);
}

#endif // ifdef USES_P001

#include "../PluginStructs/P165_data_struct.h"

#ifdef USES_P165

P165_data_struct::P165_data_struct(struct EventStruct *event) {
  if (!validGpio(CONFIG_PIN1)) {
    addLog(LOG_LEVEL_ERROR, F("NeoPixel7Segment: GPIO pin invalid."));
    return;
  }

  _stripType     = P165_CONFIG_STRIP_TYPE;
  _output        = P165_CONFIG_OUTPUTTYPE;
  _pixelGroups   = P165_CONFIG_GROUPCOUNT;
  _defBrightness = P165_CONFIG_DEF_BRIGHT;
  _maxBrightness = P165_CONFIG_MAX_BRIGHT;
  _fgColor       = P165_CONFIG_FG_COLOR;
  _bgColor       = P165_CONFIG_BG_COLOR;
  # if P165_FEATURE_P073 && P165_EXTRA_FONTS
  _fontset = P165_CONFIG_FONTSET;
  # endif // if P165_FEATURE_P073 && P165_EXTRA_FONTS
  _scrollSpeed      = P165_CONFIG_SCROLLSPEED;
  _suppressLeading0 = P165_GET_FLAG_SUPP0;
  _clearOnExit      = P165_GET_FLAG_CLEAR_EXIT;
  _blinkDot         = P165_GET_FLAG_BLINK_DOT;
  _txtScrolling     = P165_GET_FLAG_SCROLL_TEXT;
  _scrollFull       = P165_GET_FLAG_SCROLL_FULL;
  _stdOffset        = P165_GET_FLAG_STD_OFFSET;

  _periods = false; // If we don't have any digits with a decimal dot, disable using that for displaying dots...

  for (uint8_t grp = 0; grp < _pixelGroups; ++grp) {
    memcpy(&_pixelGroupCfg[grp], &P165_GROUP_CFG(grp), sizeof(P165_GROUP_CFG(grp)));
    _pixelGroupCfg[grp].aoffs = 0;
    _pixelGroupCfg[grp].boffs = 0;
    _totalDigits             += _pixelGroupCfg[grp].dgts;
    _periods                 |= _pixelGroupCfg[grp].dotp > 0; // Check decimal dot pixelsS
  }

  const uint16_t pxlCount = calculateDisplayPixels();         // Needs the _pixelGroupCfg filled

  # if P165_DEBUG_INFO
  addLog(LOG_LEVEL_INFO, strformat(F("NeoPixel7Segment: Start stripe for %d pixels, %d digits."), pxlCount, _totalDigits));
  # endif // if P165_DEBUG_INFO
  strip = new (std::nothrow) NeoPixelBus_wrapper(pxlCount, CONFIG_PIN1, P165_STRIP_TYPE_RGBW == _stripType
                                                                          ? NEO_GRBW + NEO_KHZ800
                                                                          : NEO_GRB + NEO_KHZ800);
  _initialized = (nullptr != strip);
  uint16_t pxlOffset = 0;

  if (_initialized) {
    strip->begin(); // Start the strip
    strip->setBrightness(std::min(_maxBrightness, _defBrightness));

    int8_t fromGrp = 0;
    int8_t toGrp   = _pixelGroups;
    int8_t incGrp  = 1;
    bool   allRTL  = true;

    for (uint8_t grp = 0; grp < _pixelGroups; ++grp) {
      allRTL &= _pixelGroupCfg[grp].rtld;
    }

    if (allRTL) { // Revert group order if all groups have RTL set
      fromGrp = _pixelGroups - 1;
      toGrp   = -1;
      incGrp  = -1;
    }
    uint8_t dgtMap = 0; // starting digit

    for (int8_t grp = fromGrp; grp != toGrp; grp += incGrp) {
      fillSegmentBitmap(grp, _pixelGroupCfg[grp]);

      // Set up digit mapping
      uint8_t gOffs = 0;

      for (uint8_t g = 0; g < grp; ++g) { // Determine digit offset
        gOffs += _pixelGroupCfg[g].dgts;
      }

      for (uint8_t d = 0; d < _pixelGroupCfg[grp].dgts; ++d) {
        showmap[dgtMap] = gOffs + d;
        # if P165_DEBUG_DEBUG
        addLog(LOG_LEVEL_INFO, strformat(F("P165 : showmap digit: %d, to group %d digit: %d"), dgtMap + 1, grp + 1, gOffs + d + 1));
        # endif // if P165_DEBUG_DEBUG
        dgtMap++;
      }

      if (_pixelGroupCfg[grp].offs > 0) {
        _pixelGroupCfg[grp].boffs = pxlOffset;
        # if P165_DEBUG_DEBUG
        addLog(LOG_LEVEL_INFO, strformat(F("P165 : group: %d before offset: %d, length: %d"),
                                         grp + 1, _pixelGroupCfg[grp].boffs, _pixelGroupCfg[grp].offs));
        # endif // if P165_DEBUG_DEBUG
      }
      pxlOffset += _pixelGroupCfg[grp].offs;
      const uint8_t pxlDigit = min(static_cast<uint16_t>(63),
                                   calculateGroupPixels(1, // Count pixels for 1 digit only
                                                        _pixelGroupCfg[grp].wpix,
                                                        _pixelGroupCfg[grp].hpix,
                                                        _pixelGroupCfg[grp].crnr,
                                                        _pixelGroupCfg[grp].dotp,
                                                        0)); // Extra pixels passed separately
      # if P165_DEBUG_DEBUG
      addLog(LOG_LEVEL_INFO, strformat(F("P165 : group: %d -------- start-pixel: %d"), grp + 1, pxlOffset + 1));

      for (uint8_t seg = 0; seg < 8; ++seg) {
        addLog(LOG_LEVEL_INFO, strformat(F("P165 : segment: %c, bits: 0b%s"), 'a' + seg,
                                         ull2String(pxlDigit < 63 ? bitSetULL(_segments[grp][seg], pxlDigit + 1)
                                                                  : _segments[grp][seg], 2).substring(1).c_str()));

        if (pxlDigit < 63) { bitClearULL(_segments[grp][seg], pxlDigit + 1); // Reset additional bit
        }
      }
      # endif // if P165_DEBUG_DEBUG

      display[grp] = new (std::nothrow) Noiasca_NeopixelDisplay(*strip,
                                                                _segments[grp],
                                                                _pixelGroupCfg[grp].dgts,
                                                                pxlDigit,
                                                                pxlOffset,
                                                                _pixelGroupCfg[grp].addn,
                                                                offsetLogic_callback);
      pxlOffset += (pxlDigit * _pixelGroupCfg[grp].dgts);

      if (_pixelGroupCfg[grp].addn > 0) {
        _pixelGroupCfg[grp].aoffs = pxlOffset;
        # if P165_DEBUG_DEBUG
        addLog(LOG_LEVEL_INFO, strformat(F("P165 : group: %d add-on offset: %d, length: %d"),
                                         grp + 1, _pixelGroupCfg[grp].aoffs, _pixelGroupCfg[grp].addn));
        # endif // if P165_DEBUG_DEBUG
      }
      pxlOffset += _pixelGroupCfg[grp].addn;

      _initialized = (nullptr != display[grp]);

      if (_initialized) {
        display[grp]->setColorFont(AdaGFXrgb565ToRgb888(_fgColor));
        display[grp]->setColorBack(AdaGFXrgb565ToRgb888(_bgColor));
      }
    }
  }

  if (!_initialized) { // Clean-up on fail
    for (uint8_t grp = 0; grp < PLUGIN_CONFIGLONGVAR_MAX; ++grp) {
      delete display[grp];
    }
    delete strip;
    addLog(LOG_LEVEL_ERROR, F("NeoPixel7Segment: Initialization failed."));
  }
}

int P165_data_struct::offsetLogic_callback(uint16_t position) {
  int offset = 0;

  // if (position > 1 ) offset = addPixels;
  return offset;
}

P165_data_struct::~P165_data_struct() {
  if (_clearOnExit && (nullptr != strip)) {
    const uint16_t pxlCount = calculateDisplayPixels();

    for (uint16_t pxl = 0; pxl < pxlCount; ++pxl) {
      strip->setPixelColor(pxl, 0);
    }
    strip->show();
  }

  for (uint8_t grp = 0; grp < PLUGIN_CONFIGLONGVAR_MAX; ++grp) {
    delete display[grp];
  }
  delete strip;
}

/********************************************************************
* Initialize a single digit group
********************************************************************/
void P165_data_struct::initDigitGroup(struct EventStruct *event,
                                      uint8_t             grp) {
  P165_SET_CONFIG_WPIXELS(grp, 2); // Width 2 pixels
  P165_SET_CONFIG_HPIXELS(grp, 2); // Height 2 pixels
  P165_SET_CONFIG_CORNER(grp, false);
  P165_SET_CONFIG_DOT(grp, 1);     // 1 pixel per decimal point
  P165_SET_CONFIG_EXTRA(grp, 0);
  P165_SET_CONFIG_OFFSET(grp, 0);
  P165_SET_CONFIG_DIGITS(grp, 1);  // 1 digit per group
  P165_SET_CONFIG_START(grp, false);
  P165_SET_CONFIG_DEND(grp, false);
  P165_SET_CONFIG_RTLD(grp, false);
  P165_SET_CONFIG_SPLTG(grp, false);
}

/********************************************************************
* Show the configuration Web UI
********************************************************************/
bool P165_data_struct::plugin_webform_load(struct EventStruct *event) {
  addJavascript();

  const uint8_t grpCount = P165_CONFIG_GROUPCOUNT;

  {
    const __FlashStringHelper *stripOptions[] = { F("GRB"), F("GRBW") };
    const int stripOptionValues[]             = { P165_STRIP_TYPE_RGB, P165_STRIP_TYPE_RGBW };
    const FormSelectorOptions selector(NR_ELEMENTS(stripOptionValues), stripOptions, stripOptionValues);
    selector.addFormSelector(F("Strip Type"), F("stripe"),  P165_CONFIG_STRIP_TYPE);
  }

  if ((0 == P165_CONFIG_DEF_BRIGHT) && (0 == P165_CONFIG_MAX_BRIGHT)) {
    P165_CONFIG_DEF_BRIGHT = 40;
    P165_CONFIG_MAX_BRIGHT = 255;
  }

  addFormNumericBox(F("Initial brightness"), F("brght"), P165_CONFIG_DEF_BRIGHT, 0, 255);
  addUnit(F("0..255"));
  addFormNumericBox(F("Maximum allowed brightness"), F("maxbrght"), P165_CONFIG_MAX_BRIGHT, 1, 255);
  addUnit(F("1..255"));

  {
    # if P165_FEATURE_P073
    P073_display_output_selector(F("dspout"), P165_CONFIG_OUTPUTTYPE);
    # else // if P165_FEATURE_P073
    const __FlashStringHelper *displout[] = {
      F("Manual"),
      F("Clock 24h - Blink"),
      F("Clock 24h - No Blink"),
      F("Clock 12h - Blink"),
      F("Clock 12h - No Blink"),
      F("Date"),
    };
    const int disploutOptions[] = {
      P165_DISP_MANUAL,
      P165_DISP_CLOCK24BLNK,
      P165_DISP_CLOCK24,
      P165_DISP_CLOCK12BLNK,
      P165_DISP_CLOCK12,
      P165_DISP_DATE,
    };
    const FormSelectorOptions selector(NR_ELEMENTS(disploutOptions), displout, disploutOptions);
    selector.addFormSelector(F("Display Output"), F("dspout"), P165_CONFIG_OUTPUTTYPE);
    # endif // if P165_FEATURE_P073

    int dgtCount = 0;

    for (uint8_t grp = 0; grp < grpCount; ++grp) {
      dgtCount += P165_GET_CONFIG_DIGITS(grp);
    }
    const int maxOffset = max(1, dgtCount + 1); // Minimum width for standard content is 4 digits
    addFormNumericBox(F("Start at digit"), F("stdoff"), P165_GET_FLAG_STD_OFFSET + 1, 1, max(1, maxOffset - 4),
                      # if FEATURE_TOOLTIPS
                      EMPTY_STRING,
                      # endif // if FEATURE_TOOLTIPS
                      maxOffset <= 5);
    addFormNote(F("Min. 4 digits used for Time/Date Display Output"));
  }

  # if P165_FEATURE_P073 && P165_EXTRA_FONTS
  P073_font_selector(F("fontset"), P165_CONFIG_FONTSET);
  # endif // if P165_FEATURE_P073 && P165_EXTRA_FONTS

  {
    addFormSubHeader(F("Options"));

    addFormCheckBox(F("Suppress leading 0 on day/hour"), F("supp0"),   P165_GET_FLAG_SUPP0);
    addFormCheckBox(F("Use decimal dot for blink"),      F("bldot"),   P165_GET_FLAG_BLINK_DOT);

    addFormCheckBox(F("Scroll text &gt; display width"), F("scrltxt"), P165_GET_FLAG_SCROLL_TEXT);
    addFormCheckBox(F("Scroll text in from right"),      F("scrlfll"), P165_GET_FLAG_SCROLL_FULL);

    if (P165_CONFIG_SCROLLSPEED == 0) { P165_CONFIG_SCROLLSPEED = 10; }
    addFormNumericBox(F("Scroll speed (0.1 sec/step)"), F("scrlspd"), P165_CONFIG_SCROLLSPEED, 1, 600);
    addUnit(F("1..600 = 0.1..60 sec/step"));
    addFormCheckBox(F("Clear display on exit"), F("clrexit"), P165_GET_FLAG_CLEAR_EXIT);
  }

  addFormSubHeader(F("Display"));

  addFormNote(F("Fields with *: When changed will save and reload the page."));

  const __FlashStringHelper *digitOptions[]      = { F("1"), F("2"), F("3"), F("4") };
  const int digitOptionValues[]                  = { 1, 2, 3, 4 };
  const __FlashStringHelper *startPixelOptions[] = { F("a (top)"), F("b/f (right/left)"), F("g (center)") };

  const String fgColor = ADAGFX_WHITE == P165_CONFIG_FG_COLOR || ADAGFX_BLACK == P165_CONFIG_FG_COLOR
                          ? EMPTY_STRING
                          : AdaGFXrgb565ToWebColor(P165_CONFIG_FG_COLOR);

  FormSelectorOptions selector(
    NR_ELEMENTS(digitOptionValues),
    digitOptions,
    digitOptionValues);
  selector.reloadonchange = true;
  selector.addFormSelector(F("Number of Groups *"), F("grps"), grpCount);

  AdaGFXFormForeAndBackColors(F("fgcolor"),
                              P165_CONFIG_FG_COLOR,
                              F("bgcolor"),
                              P165_CONFIG_BG_COLOR);

  const bool numberPlan = P165_GET_FLAG_NUMBERPLAN > 0;
  addFormSelector_YesNo(F("Show Pixel number-plan *"), F("nbrpln"), P165_GET_FLAG_NUMBERPLAN, true);
  addFormNote(F("When set to Yes will block digit-changes."));

  int totalPixels = 0;
  int totalDigits = 0;
  int8_t fromGrp  = 0;
  int8_t toGrp    = grpCount;
  int8_t incGrp   = 1;
  bool   allRTL   = true;

  for (uint8_t grp = 0; grp < grpCount; ++grp) {
    allRTL &= P165_GET_CONFIG_RTLD(grp);
  }

  addFormSubHeader(F("Groups and Digits"));

  if (allRTL) { // Revert group order if all groups have RTL set
    fromGrp = grpCount - 1;
    toGrp   = -1;
    incGrp  = -1;
    addFormNote(F("Attention, Groups are shown in reverse order!"));
  }

  int16_t dgtOffset = 0;

  for (int8_t grp = fromGrp; grp != toGrp; grp += incGrp) {
    const uint8_t grp10 = grp * 10;

    if (0 == P165_GET_CONFIG_WPIXELS(grp)) { // Check for invalid settings
      initDigitGroup(event, grp);
    }

    const uint8_t grpDgts  = P165_GET_CONFIG_DIGITS(grp);
    const uint8_t grpWPxls = P165_GET_CONFIG_WPIXELS(grp);
    const uint8_t grpHPxls = P165_GET_CONFIG_HPIXELS(grp);
    const uint8_t grpCrnr  = P165_GET_CONFIG_CORNER(grp);
    const uint8_t grpDotP  = P165_GET_CONFIG_DOT(grp);
    const uint8_t grpAddN  = P165_GET_CONFIG_EXTRA(grp);
    const uint8_t grpOffs  = P165_GET_CONFIG_OFFSET(grp);
    const uint8_t grpStart = P165_GET_CONFIG_START(grp);
    const uint8_t grpRtld  = P165_GET_CONFIG_RTLD(grp);
    const uint8_t grpGstrt = P165_GET_CONFIG_GSTRT(grp);
    # if P165_FEATURE_C_CLOCKWISE
    const uint8_t grpCclkw = P165_GET_CONFIG_CCLKW(grp);
    # else // if P165_FEATURE_C_CLOCKWISE
    const uint8_t grpCclkw = false;
    # endif // if P165_FEATURE_C_CLOCKWISE

    if (grp != fromGrp) {
      addFormSeparator(2);
    }

    addRowLabel(concat(F("Group "), grp + 1));

    {
      html_table(F(""));

      addRowLabel(F("Number of Digits *"));
      FormSelectorOptions selector(
        NR_ELEMENTS(digitOptionValues),
        digitOptions,
        digitOptionValues);
      selector.reloadonchange = true;
      selector.enabled = !numberPlan;  // 1st and 2nd column
      selector.addSelector(
        concat(F("dgts"), grp10),
        grpDgts);
      {
        // 3rd column = "Digit <nr>" / "(Extra)"
        for (uint8_t dgt = 0; dgt < grpDgts; ++dgt) {
          if ((0 == dgt) && grpRtld) { html_TD(); addHtml(F("(Extra)")); }
          html_TD(); addHtml(F("Digit "));
          addHtmlInt(static_cast<int8_t>(dgt + 1));
        }

        if (!grpRtld) { html_TD(); addHtml(F("(Extra)")); }
      }
    }

    addFormNumericBox(F("Segment Width pixels"), concat(F("wdth"), grp10),
                      grpWPxls, 1, P165_SEGMENT_WIDTH_PIXELS,
                      # if FEATURE_TOOLTIPS
                      EMPTY_STRING,
                      # endif // if FEATURE_TOOLTIPS
                      numberPlan);

    dgtOffset += grpOffs;

    const int16_t dgtPxls = calculateGroupPixels(1, // Count single digit without additional pixels
                                                 grpWPxls,
                                                 grpHPxls,
                                                 grpCrnr,
                                                 grpDotP,
                                                 0);

    {
      if (grpRtld) { // Take care of Right To Left configured groups
        dgtOffset += (dgtPxls * (grpDgts - 1));
      }

      for (uint8_t dgt = 0; dgt < grpDgts; ++dgt) {
        drawSevenSegment(dgt, grp10, // 3rd column = subtable with digit
                         grpWPxls,
                         grpHPxls,
                         grpCrnr,
                         grpDotP,
                         grpAddN,
                         grpDgts - 1,
                         dgtOffset,
                         grpStart,
                         P165_GET_CONFIG_DEND(grp),
                         fgColor,
                         numberPlan,
                         grpOffs,
                         P165_GET_CONFIG_SPLTG(grp),
                         grpRtld,
                         grpCclkw,
                         grpGstrt);
        dgtOffset += (dgtPxls * (grpRtld ? -1 : 1));
      }

      if (grpRtld) {
        dgtOffset += (dgtPxls * (grpDgts + 1)); // Add 'm all back
      }
      dgtOffset += grpAddN;
    }

    {
      addFormNumericBox(F("Segment Height pixels"), concat(F("hght"), grp10),
                        grpHPxls, 1, P165_SEGMENT_HEIGHT_PIXELS,
                        # if FEATURE_TOOLTIPS
                        EMPTY_STRING,
                        # endif // if FEATURE_TOOLTIPS
                        numberPlan);

      addFormCheckBox(F("Segment Corners overlap"), concat(F("crnr"), grp10),
                      grpCrnr, numberPlan);

      addFormNumericBox(F("Decimal dot pixels"), concat(F("decp"), grp10),
                        grpDotP, 0, P165_SEGMENT_DOT_PIXELS,
                        # if FEATURE_TOOLTIPS
                        EMPTY_STRING,
                        # endif // if FEATURE_TOOLTIPS
                        numberPlan);
    }
    {
      addFormNumericBox(F("Extra pixels after"), concat(F("addn"), grp10),
                        grpAddN, 0, P165_SEGMENT_ADDON_PIXELS,
                        # if FEATURE_TOOLTIPS
                        EMPTY_STRING,
                        # endif // if FEATURE_TOOLTIPS
                        numberPlan);

      const int grpPixels = calculateGroupPixels(grpDgts,
                                                 grpWPxls,
                                                 grpHPxls,
                                                 grpCrnr,
                                                 grpDotP,
                                                 grpAddN);
      totalPixels += grpPixels;
      totalDigits += grpDgts;

      addFormNumericBox(F("Pixels in group"), concat(F("totp"), grp10),
                        grpPixels,
                        0, INT32_MAX,
                        # if FEATURE_TOOLTIPS
                        EMPTY_STRING,
                        # endif // if FEATURE_TOOLTIPS
                        true);
    }
    addHtml(F("<TR><TD style='height:2px' colspan='2'><hr>")); // Reduce height of separator

    {
      addFormNumericBox(F("Pixel-offset before"), concat(F("offs"), grp10),
                        grpOffs, 0, P165_SEGMENT_EXTRA_PIXELS,
                        # if FEATURE_TOOLTIPS
                        EMPTY_STRING,
                        # endif // if FEATURE_TOOLTIPS
                        numberPlan);
      totalPixels += grpOffs;

      const uint8_t strt = grpStart + (grpGstrt << 1);
      addRowLabel(F("Starting segment"));
      FormSelectorOptions selector(
        NR_ELEMENTS(startPixelOptions),
        startPixelOptions);
      selector.enabled = !numberPlan;
      selector.addSelector(
        concat(F("strt"), grp10),
        strt);

      addFormCheckBox(F("Split g-segment pixels"), concat(F("spltg"), grp10),
                      P165_GET_CONFIG_SPLTG(grp), numberPlan || grpGstrt);

      addFormCheckBox(F("Decimal dot last segment"), concat(F("dend"), grp10),
                      P165_GET_CONFIG_DEND(grp), numberPlan || grpGstrt);

      # if P165_FEATURE_C_CLOCKWISE
      addFormCheckBox(F("Numbering counter-clockwise"), concat(F("cclkw"), grp10),
                      grpCclkw, numberPlan);
      # endif // if P165_FEATURE_C_CLOCKWISE

      addFormCheckBox(F("Right to Left digits"), concat(F("rtld"), grp10),
                      grpRtld, numberPlan);

      # if P165_DIGIT_TABLE_H_INT > 17
      int rws = 17; // Above should be fixed number of rows, matching with ~80% digit table size

      for (; rws < P165_DIGIT_TABLE_H_INT; ++rws) {
        html_TR_TD();
        addHtml(F("&nbsp;")); // We need P165_digit_table_h_int rows for the digit table to work as intended
      }
      # endif // if P165_DIGIT_TABLE_H_INT > 17
    }

    html_end_table();

    {
      // Bind handlers on input fields to update the 7 segment simulation and digit counts
      addHtml(F("\n<script type='text/javascript'>"));
      const __FlashStringHelper *_fmt = F("elId('%s%d').onchange=function(){%s(this.%s,%d,%d,%d,'%s'),"
                                          "dgts(%d,['wdth','hght','decp','addn','offs','dgts'])};");

      //                          fieldname   index        function    fieldattribute   function arguments
      addHtml(strformat(_fmt, FsP(F("wdth")), grp10, FsP(F("chWdth")), FsP(F("value")), grp10, grpDgts, false, "",
                        grpCount));
      addHtml(strformat(_fmt, FsP(F("hght")), grp10, FsP(F("chHght")), FsP(F("value")), grp10, grpDgts, false, "",
                        grpCount));
      addHtml(strformat(_fmt, FsP(F("crnr")), grp10, FsP(F("chCrnr")), FsP(F("checked")), grp10, grpDgts, false, fgColor.c_str(),
                        grpCount));
      addHtml(strformat(_fmt, FsP(F("decp")), grp10, FsP(F("chDecp")), FsP(F("value")), grp10, grpDgts, false, "",
                        grpCount));
      addHtml(strformat(_fmt, FsP(F("addn")), grp10, FsP(F("chAddn")), FsP(F("value")), grp10, grpRtld ? 0 : (grpDgts - 1), true, "",
                        grpCount));
      addHtml(strformat(F("elId('offs%d').onchange=function(){dgts(%d,['wdth','hght','decp','addn','offs','dgts'])};"),
                        grp10, grpCount));
      addHtml(strformat(F("elId('strt%d').onchange=function(){stSeg(this,%d)};"),
                        grp10, grp10));
      addHtml(F("</script>"));
    }
  }

  addFormSeparator(2);

  addFormNumericBox(F("Total digits"), F("totdgt"),
                    totalDigits,
                    0, INT32_MAX,
                    # if FEATURE_TOOLTIPS
                    EMPTY_STRING,
                    # endif // if FEATURE_TOOLTIPS
                    true);
  addFormNumericBox(F("Total pixels to connect"), F("totpx"),
                    totalPixels,
                    0, INT32_MAX,
                    # if FEATURE_TOOLTIPS
                    EMPTY_STRING,
                    # endif // if FEATURE_TOOLTIPS
                    true);

  return true;
}

/********************************************************************
* Save the updated settings
********************************************************************/
bool P165_data_struct::plugin_webform_save(struct EventStruct *event) {
  const int16_t grps = getFormItemInt(F("grps"));

  P165_CONFIG_GROUPCOUNT = grps;
  P165_CONFIG_STRIP_TYPE = getFormItemInt(F("stripe"));
  P165_CONFIG_OUTPUTTYPE = getFormItemInt(F("dspout"));
  P165_CONFIG_DEF_BRIGHT = getFormItemInt(F("brght"));
  P165_CONFIG_MAX_BRIGHT = getFormItemInt(F("maxbrght"));
  # if P165_FEATURE_P073 && P165_EXTRA_FONTS
  P165_CONFIG_FONTSET = getFormItemInt(F("fontset"));
  # endif // if P165_FEATURE_P073 && P165_EXTRA_FONTS
  P165_CONFIG_SCROLLSPEED = getFormItemInt(F("scrlspd"));

  P165_SET_FLAG_SUPP0(isFormItemChecked(F("supp0")) ? 1 : 0);
  P165_SET_FLAG_SCROLL_TEXT(isFormItemChecked(F("scrltxt")) ? 1 : 0);
  P165_SET_FLAG_SCROLL_FULL(isFormItemChecked(F("scrlfll")) ? 1 : 0);
  P165_SET_FLAG_CLEAR_EXIT(isFormItemChecked(F("clrexit")) ? 1 : 0);
  P165_SET_FLAG_BLINK_DOT(isFormItemChecked(F("bldot")) ? 1 : 0);
  int stdoff = 1;

  if (update_whenset_FormItemInt(F("stdoff"), stdoff)) {
    P165_SET_FLAG_STD_OFFSET(stdoff - 1); // Show 1..16, used as offset 0..15
  }

  const bool prevNumberPlan = P165_GET_FLAG_NUMBERPLAN > 0;

  P165_SET_FLAG_NUMBERPLAN(getFormItemInt(F("nbrpln")));

  String color = webArg(F("fgcolor"));

  P165_CONFIG_FG_COLOR = ADAGFX_RED;                // Default to red when empty

  if (!color.isEmpty()) {
    P165_CONFIG_FG_COLOR = AdaGFXparseColor(color); // Reduce to rgb565
  }
  color                = webArg(F("bgcolor"));
  P165_CONFIG_BG_COLOR = AdaGFXparseColor(color);   // Empty = black

  if (!prevNumberPlan) {                            // Don't save now as the disabled inputs will return empty values
    for (int grp = 0; grp < grps; ++grp) {
      const uint8_t grp10   = grp * 10;
      const uint8_t grpStrt = getFormItemInt(concat(F("strt"), grp10));
      P165_SET_CONFIG_WPIXELS(grp, getFormItemInt(concat(F("wdth"), grp10)));
      P165_SET_CONFIG_HPIXELS(grp, getFormItemInt(concat(F("hght"), grp10)));
      P165_SET_CONFIG_CORNER(grp, isFormItemChecked(concat(F("crnr"), grp10)));
      P165_SET_CONFIG_DOT(grp, getFormItemInt(concat(F("decp"), grp10)));
      P165_SET_CONFIG_EXTRA(grp, getFormItemInt(concat(F("addn"), grp10)));
      P165_SET_CONFIG_OFFSET(grp, getFormItemInt(concat(F("offs"), grp10)));
      P165_SET_CONFIG_DIGITS(grp, getFormItemInt(concat(F("dgts"), grp10)));
      P165_SET_CONFIG_START(grp, (grpStrt & 0x01) == 0x01); // split into 2 settings
      P165_SET_CONFIG_GSTRT(grp, (grpStrt & 0x02) == 0x02);
      P165_SET_CONFIG_RTLD(grp, isFormItemChecked(concat(F("rtld"), grp10)));

      if ((grpStrt & 0x02) != 0x02) { // Can't read disabled inputs
        P165_SET_CONFIG_DEND(grp, isFormItemChecked(concat(F("dend"), grp10)));
        P165_SET_CONFIG_SPLTG(grp, isFormItemChecked(concat(F("spltg"), grp10)));
      }
      # if P165_FEATURE_C_CLOCKWISE
      P165_SET_CONFIG_CCLKW(grp, isFormItemChecked(concat(F("cclkw"), grp10)));
      # endif // if P165_FEATURE_C_CLOCKWISE
      # if P165_SEGMENT_WIDTH_PIXELS > 7 || P165_SEGMENT_HEIGHT_PIXELS > 7
      const uint8_t grpPixels = calculateGroupPixels(1,
                                                     P165_GET_CONFIG_WPIXELS(grp),
                                                     P165_GET_CONFIG_HPIXELS(grp),
                                                     P165_GET_CONFIG_CORNER(grp),
                                                     P165_GET_CONFIG_DOT(grp),
                                                     0);

      if (grpPixels > 64) {
        addHtmlError(strformat(F("Error: Group %d &gt; 64 pixels configured (%d)"), grp + 1, grpPixels));
      }
      # endif // if P165_SEGMENT_WIDTH_PIXELS > 7 || P165_SEGMENT_HEIGHT_PIXELS > 7
    }
  }
  return true;
}

/********************************************************************
* Calculate the number of pixels in a group
********************************************************************/
uint16_t P165_data_struct::calculateGroupPixels(const uint8_t count,
                                                const uint8_t wpixels,
                                                const uint8_t hpixels,
                                                const bool    overlap,
                                                const uint8_t decPt,
                                                const uint8_t addN) {
  return count * (3 * wpixels + 4 * hpixels + (overlap ? 6 : 0) + decPt) + addN;
}

/********************************************************************
* Calculate the total number of pixels for the entire display
********************************************************************/
uint16_t P165_data_struct::calculateDisplayPixels() {
  uint16_t result = 0;

  for (uint8_t grp = 0; grp < _pixelGroups; ++grp) {
    result += _pixelGroupCfg[grp].offs;
    result += calculateGroupPixels(_pixelGroupCfg[grp].dgts,
                                   _pixelGroupCfg[grp].wpix,
                                   _pixelGroupCfg[grp].hpix,
                                   _pixelGroupCfg[grp].crnr,
                                   _pixelGroupCfg[grp].dotp,
                                   _pixelGroupCfg[grp].addn);
  }
  return result;
}

/********************************************************************
* Calculate the total number of digits configured
********************************************************************/
uint16_t P165_data_struct::calculateDisplayDigits() {
  uint16_t result = 0;

  for (uint8_t grp = 0; grp < _pixelGroups; ++grp) {
    result += _pixelGroupCfg[grp].dgts;
  }
  return result;
}

// Draw a 7segment digit with optional decimal point and extra pixels, and max 5-wide segments and 5-high segments
constexpr uint16_t P165_digitMask[P165_DIGIT_TABLE_H_INT] = { // Regular digit layout, max size
  0b0111111100,
  0b1000000010,
  0b1000000010,
  0b1000000010,
  0b1000000010,
  0b1000000010,
  0b1000000010,
  0b1000000010,
  0b0111111100,
  0b1000000010,
  0b1000000010,
  0b1000000010,
  0b1000000010,
  0b1000000010,
  0b1000000010,
  0b1000000010,
  0b0111111101,                                              // Decimal point pixels
};

constexpr uint8_t P165_extraMask[P165_DIGIT_TABLE_H_INT] = { // Extra pixel table mask
  0b0,
  0b0,
  0b0,
  0b0,
  0b0,
  0b0,
  0b0,
  0b0,
  0b1, // Extra pixels after
  0b0,
  0b0,
  0b0,
  0b0,
  0b0,
  0b0,
  0b0,
  0b0,
};

constexpr uint16_t P165_digitOverlap[P165_DIGIT_TABLE_H_INT] = { // Overlap enabled, overlay these 6 pixels
  0b1000000010,
  0b0000000000,
  0b0000000000,
  0b0000000000,
  0b0000000000,
  0b0000000000,
  0b0000000000,
  0b0000000000,
  0b1000000010,
  0b0000000000,
  0b0000000000,
  0b0000000000,
  0b0000000000,
  0b0000000000,
  0b0000000000,
  0b0000000000,
  0b1000000010,
};

/********************************************************************
* Draw a 7-segment digit-group by creating a table
* with some rows and columns hidden when not max. size
********************************************************************/
void P165_data_struct::drawSevenSegment(const uint8_t  digit,   // Digit
                                        const uint8_t  grp10,   // Group * 10
                                        const uint8_t  wpixels, // width pixels
                                        const uint8_t  hpixels, // heoght pixels
                                        const bool     overlap, // corner overlap
                                        const uint8_t  decPt,   // decimal point pixels
                                        const uint8_t  addN,    // additional pixels
                                        const uint8_t  max,     // max already has 1 subtracted
                                        const uint16_t offset,  // pre-offset
                                        const bool     strt,    // start a (top) or b/f (right/left)
                                        const bool     dend,    // decimal point at end
                                        const String & fgColor, // foreground color
                                        const bool     dspPlan, // show number plan
                                        const int16_t  aOffs,   // addon offset
                                        const bool     splitG,  // split segment G in 2 halves
                                        const bool     rtld,    // direction ltr or rtl
                                        const bool     cclkw,   // counter-clockwise
                                        const bool     gstrt) { // g-segment-start
  constexpr uint8_t hrMax  = NR_ELEMENTS(P165_digitMask) - 1;   // Height row max
  constexpr uint8_t hcMask = hrMax / 2;                         // Height center
  const uint8_t     wrMask = 7;                                 // Width right
  const uint8_t     wdMask = 8;                                 // Width dot pos
  const uint8_t     wcBits = 10;                                // Width column bits

  int8_t tblFrom = 0;
  int8_t tblTo   = 1;
  int8_t tblInc  = 1;

  if ((aOffs >= 0) && (digit == (rtld ? 0 : max))) {
    if (rtld) {
      tblFrom = 1;
      tblTo   = -1;
      tblInc  = -1;
    } else {
      tblTo = 2;
    }
  }

  for (int8_t tbl = tblFrom; tbl != tblTo; tbl += tblInc) {
    const bool drawDigit   = 0 == tbl;
    const uint8_t maskBits = drawDigit ? wcBits : 1;

    addHtml(F("<TD rowspan='" P165_DIGIT_TABLE_HEIGHT "' style='padding:0;'>"));
    addHtml(strformat(F("<table id='d%ctbl%d'>"), drawDigit ? 'g' : 'x', digit + grp10)); // Group is factor 10

    uint8_t hor = 0;
    int8_t  ver = 0;
    uint8_t seg = 0;

    for (uint8_t h = 0; h < NR_ELEMENTS(P165_digitMask); ++h) {
      const bool showRow = !(((h < hcMask) && (hcMask - h >= hpixels) && (h > 1)) ||
                             ((h > hcMask) && (h - hcMask > hpixels) && (h < hrMax)));
      addHtml(F("<TR style='height:" P165_TD_SIZE ";display:"));

      if (!showRow) {
        addHtml(F("none'>"));  // Hide row
      } else {
        addHtml(F("block'>")); // Show row

        if ((h == 0) || (h == hcMask)) {
          ver = 0;             // Restart vertical counter
        } else {
          ver++;
        }
      }

      hor = 0; // Restart horizontal counter

      for (uint8_t w = 0; w < maskBits; ++w) {
        String pIndex;

        const bool showCol = !(w > 0 && w >= wpixels && w < wrMask);

        html_TD(showCol ? (dspPlan && (w == maskBits - 1)
                                        ? F("min-width:" P165_TD_SIZE ";max-width:4rem;font-size:80%;display:inline-block")
                                        : F("width:" P165_TD_SIZE ";font-size:80%;display:inline-block"))
                        : F("width:" P165_TD_SIZE ";display:none"));

        const bool showBit = drawDigit
                            ? (bitRead(P165_digitMask[h], (maskBits - 1) - w) ||
                               (overlap && bitRead(P165_digitOverlap[h], (maskBits - 1) - w)))
                            : bitRead(P165_extraMask[h], (maskBits - 1) - w);

        if (showBit) {
          if (dspPlan && showRow && showCol) {         // Determine segment for pixel-indexes:
            if ((h > 0) && (h < hcMask)) {             // b/f
              if (w == 0) {
                seg = 5;                               // f
              } else {
                seg = 1;                               // b
              }
            } else if ((h > hcMask) && (h < hrMax)) {  // c/e
              if (w == 0) {
                seg = 4;                               // e
              } else {
                seg = 2;                               // c
              }
            } else if ((w > wdMask) && (h == hrMax)) { // Decimal point
              seg = 7;
            } else if (h == hcMask) {                  // g / Additional pixels
              if ((w > wdMask) || !drawDigit) {
                seg = 8;                               // virtual 9th segment
              } else {
                seg = 6;                               // g
              }
            } else if ((w <= wdMask) && (h == hrMax)) {
              seg = 3;                                 // d
            } else {
              seg = 0;                                 // a
            }

            pIndex = calculatePixelIndex(hor,
                                         ver - 1,
                                         seg,
                                         offset,
                                         wpixels,
                                         hpixels,
                                         overlap,
                                         strt,
                                         dend,
                                         decPt,
                                         addN,
                                         splitG,
                                         cclkw,
                                         gstrt);

            // pIndex = strformat(F("%d/%d/%d"), hor, ver - 1, seg); // For debugging only
            hor++;
          }

          if ((w <= wdMask) && drawDigit) {
            if (dspPlan) {
              addHtml(pIndex);
            } else {
              if (!fgColor.isEmpty()) {           // Colored pixel
                addHtml(strformat(F("<span style='color:%s;'>" P165_PIXEL_CHARACTER "</span>"), fgColor.c_str()));
              } else {
                addHtml(F(P165_PIXEL_CHARACTER)); // Pixel
              }
            }
          } else if (h == hcMask) {               // Extra pixels after last digit
            if ((addN > 0) && ((digit == max) || !drawDigit)) {
              if (dspPlan) {
                addHtml(pIndex);
              } else {
                addHtmlInt(addN);   // Show number of pixels
              }
            } else {
              addHtml(F("&nbsp;")); // None
            }
          } else if (h == hrMax) {  // Decimal point
            if (decPt > 0) {
              if (dspPlan) {
                addHtml(pIndex);
              } else {
                addHtmlInt(decPt);  // Show number of pixels
              }
            } else {
              addHtml(F("&nbsp;")); // None
            }
          } else {
            addHtml('?');           // this shouldn't ever show up... ;-)
          }
        } else {
          addHtml(F("&nbsp;"));     // No pixel
        }
      }
    }
    html_end_table();
  }
}

/************************************************************************
 * Segment order maps, depending on configuration settings:
 * - top-left or top-right (0/2)
 * - dot-after-c or dot-at-end (0/1)
 * - g-segment as a single 'block' or split in 2 halves (0/4)
 * - counter-clockwise numbering (0/8)
 ***********************************************************************/

/* *INDENT-OFF* */
const uint8_t P165_segmentMap[][9] PROGMEM = { // 81 or 162 bytes, values: 0..7 segments, 16/26 => 6, 255 = ignore
  { 0, 1,  2,  7,  3,  4,  6,  5,  255 },      // top-left, dot after c, order: a, b, c, h, d, e, g, f, -
  { 0, 1,  2,  3,  4,  6,  5,  7,  255 },      // top-left, dot as last, order: a, b, c, d, e, g, f, h, -
  { 1, 2,  7,  3,  4,  6,  5,  0,  255 },      // top-right, dot after c, order: b, c, h, d, e, g, f, a, -
  { 1, 2,  3,  4,  6,  5,  0,  7,  255 },      // top-right, dot as last, order: b, c, d, e, g, f, a, h, -
  { 0, 1,  16, 2,  7,  3,  4,  26, 5   },      // top-left, dot after c, split g, order: a, b, g1, c, h, d, e, g2, f
  { 0, 1,  16, 2,  3,  4,  26, 5,  7   },      // top-left, dot as last, split g, order: a, b, g1, c, d, e, g2, f, h
  { 1, 16, 2,  7,  3,  4,  26, 5,  0   },      // top-right, dot after c, split g, order: b, g1, c, h, d, e, g2, f, a
  { 1, 16, 2,  3,  4,  26, 5,  0,  7   },      // top-right, dot as last, split g, order: b, g1, c, d, e, g2, f, a, h
  { 6, 2,  7,  3,  4,  5,  0,  1,  255 },      // mid-left, dot after c, order: g, c, h, d, e, f, a, b -
  # if P165_FEATURE_C_CLOCKWISE
  { 5, 6,  4,  3,  7,  2,  1,  0,  255 },      // cclkw, top-left, dot after c, order: f, g, e, d, h, c, b, a, -
  { 5, 6,  4,  3,  2,  1,  0,  7,  255 },      // cclkw, top-left, dot as last, order: f, g, e, d, c, b, a, h, -
  { 0, 5,  6,  4,  3,  7,  2,  1,  255 },      // cclkw, top-right, dot after c, order: a, f, g, e, d, h, c, b, -
  { 0, 5,  6,  4,  3,  2,  1,  7,  255 },      // cclkw, top-right, dot as last, order: a, f, g, e, d, c, b, h, -
  { 5, 26, 4,  3,  7,  2,  16, 1,  0   },      // cclkw, top-left, dot after c, split g, order: f, g2, e, d, h, c, g1, b, a
  { 5, 26, 4,  3,  2,  16, 1,  0,  7   },      // cclkw, top-left, dot as last, split g, order: f, g2, e, d, c, g1, b, a, h
  { 0, 5,  26, 4,  3,  7,  2,  16, 1   },      // cclkw, top-right, dot after c, split g, order: a, f, g2, e, d, h, c, g1, b
  { 0, 5,  26, 4,  3,  2,  16, 1,  7   },      // cclkw, top-right, dot as last, split g, order: a, f, g2, e, d, c, g1, b, h
  { 6, 4,  3,  7,  2,  1,  0,  5,  255 },      // cclkw, mid-left, dot after c, order: g, e, d, h, c, b, a, f, -
  # endif // if P165_FEATURE_C_CLOCKWISE
};
/* *INDENT-ON* */
constexpr uint8_t P165_segmentCnt = NR_ELEMENTS(P165_segmentMap[0]);

/*************************************************************************
 * Returns the pixel-index as a string to show in the UI for a pixel
 ************************************************************************/
String P165_data_struct::calculatePixelIndex(const uint8_t  hor,     // horizontal 'pixel'
                                             const int8_t   ver,     // vertical 'pixel'
                                             const uint8_t  seg,     // segment
                                             const uint16_t offset,  // pre-offset
                                             const uint8_t  wpixels, // width pixels
                                             const uint8_t  hpixels, // height pixels
                                             const bool     overlap, // corner overlap
                                             const bool     strt,    // start left-top or right-top
                                             const bool     dend,    // decimal point at end
                                             const uint8_t  decPt,   // decimal point pixels
                                             const uint8_t  addN,    // additional pixels
                                             const bool     splitG,  // split G segment in 2 halves
                                             const bool     cclkw,   // counter-clockwise
                                             const bool     gstrt) { // g-segment-start
  int16_t result(offset);
  const uint8_t hpx = wpixels + (overlap ? 2 : 0);                   // Overlapping pixels checked on horizontal segments
  const uint8_t vpx = hpixels;                                       // Vertical pixels
  // smap: Determine the segment(part) order, g-segment start inhibits top-left/right, g-split and dot-at-end!
  const uint8_t smap = (gstrt ? 8 : ((strt ? 2 : 0) + (dend ? 1 : 0) + (splitG ? 4 : 0))) + (cclkw ? 9 : 0);
  const uint8_t rh   = hpx / 2;                                      // Horizontal half part, right
  const uint8_t lh   = hpx - rh;                                     // Horizontal half part, left
  int8_t fh          = 0;                                            // from-h
  int8_t th          = hpx;                                          // to-h
  int8_t ih          = 1;                                            // inc-h
  int8_t vh          = hpx - 1;
  int8_t mh          = -1;                                           // minmax-h

  # if P165_FEATURE_C_CLOCKWISE

  if (cclkw) {
    fh = hpx - 1;
    th = -1;
    ih = -1;
    vh = 0;
    mh = hpx;
  }
  # endif // if P165_FEATURE_C_CLOCKWISE

  result++;                                                      // 1-based index

  for (uint8_t spoint = 0; spoint < P165_segmentCnt; ++spoint) { // loop over all segments a..i
    const uint8_t segment = pgm_read_byte(&(P165_segmentMap[smap][spoint]));

    if ((0 == segment) || (6 == segment)) {                      // horizontal segments a/g
      for (int8_t h = fh; h != th; h += ih) {
        if ((seg == segment) && (hor == h)) {
          return String(result);
        }
        result++;
      }
    } else if (16 == segment) { // horizontal segment g, right half
      for (int8_t h = rh; h < hpx; ++h) {
        if ((6 == seg) && (hor == h)) {
          return String(result);
        }
        result++;
      }
    } else if (26 == segment) { // horizontal segment g, left half
      for (int8_t h = 0; h < hpx - lh; ++h) {
        if ((6 == seg) && (hor == h)) {
          return String(result);
        }
        result++;
      }
    } else if (3 == segment) { // horizontal segment d reversed
      for (int8_t h = vh; h != mh; h -= ih) {
        if ((seg == segment) && (hor == h)) {
          return String(result);
        }
        result++;
      }
    } else if (7 == segment) { // Decimal point segment
      if (seg == segment) {
        String res(result);
        res.reserve(decPt * 4);

        for (uint8_t dp = 1; dp < decPt; ++dp) {
          result++;
          res = strformat(F("%s %d"), res.c_str(), result);
        }
        return res;
      } else {
        result += decPt;
      }
    } else if ((!cclkw && ((4 == segment) || (5 == segment)))   // Vertical segments e/f
               # if P165_FEATURE_C_CLOCKWISE
               || (cclkw && ((1 == segment) || (2 == segment))) // Vertical segments b/c, counter-clockwise
               # endif // if P165_FEATURE_C_CLOCKWISE
               ) {
      for (int8_t v = vpx - 1; v >= 0; --v) {
        if ((seg == segment) && (ver == v)) {
          return String(result);
        }
        result++;
      }
    } else if ((!cclkw && ((1 == segment) || (2 == segment)))   // Vertical segments b/c
               # if P165_FEATURE_C_CLOCKWISE
               || (cclkw && ((4 == segment) || (5 == segment))) // Vertical segments e/f, counter-clockwise
               # endif // if P165_FEATURE_C_CLOCKWISE
               ) {
      // } else if (255 != segment) { // Vertical segments b/c
      for (uint8_t v = 0; v < vpx; ++v) {
        if ((seg == segment) && (ver == v)) {
          return String(result);
        }
        result++;
      }
    }
  }

  if ((seg == 8) && (addN > 0)) {
    String res(result);
    res.reserve(addN * 4);

    for (uint8_t a = 1; a < addN; ++a) {
      result++;
      res = strformat(F("%s %d"), res.c_str(), result);
    }
    return res;
  }
  return F("x"); // We shouldn't get here...
}

/**************************************************************************
 * Setup the pixels to be lit per segment for a digit
 *************************************************************************/
void P165_data_struct::fillSegmentBitmap(const uint8_t       grp,
                                         const PixelGroupCfg pixCfg) {
  if (grp > _pixelGroups) { return; }

  segsize_t pbit    = 0;
  const uint8_t hpx = pixCfg.wpix + (pixCfg.crnr ? 2 : 0); // Overlapping pixels checked on horizontal segments
  const uint8_t vpx = pixCfg.hpix;                         // Vertical pixels = height pixels
  // smap: Determine the segment(part) order, g-segment start inhibits top-left/right, g-split and dot-at-end!
  const uint8_t smap = (pixCfg.gstrt ? 8 : ((pixCfg.strt ? 2 : 0) + (pixCfg.dend ? 1 : 0) + (pixCfg.splt ? 4 : 0))) + (pixCfg.cclkw ? 9 : 0);
  const uint8_t rh   = hpx / 2;                            // Horizontal half part, right
  const uint8_t lh   = hpx - rh;                           // Horizontal half part, left

  // loop over all segments a..h but don't exceed 64 bits
  for (uint8_t spoint = 0; spoint < P165_segmentCnt && pbit < 64; ++spoint) {
    const uint8_t segment = pgm_read_byte(&(P165_segmentMap[smap][spoint]));

    if ((0 == segment) || (3 == segment) || (6 == segment)) { // horizontal segments a/d/g
      if (pixCfg.crnr) {
        for (uint8_t h = 0; h < hpx && pbit < 64; ++h) {
          bitSetULL(_segments[grp][segment], pbit);

          if ((0 == segment) && (h == 0)) {       // left pixel (a,f)
            bitSetULL(_segments[grp][5], pbit);
          } else
          if ((0 == segment) && (h == hpx - 1)) { // right pixel (a,b)
            bitSetULL(_segments[grp][1], pbit);
          } else
          if ((3 == segment) && (h == 0)) {       // right pixel (d,c), (other direction)
            bitSetULL(_segments[grp][2], pbit);
          } else
          if ((3 == segment) && (h == hpx - 1)) { // left pixel (d,e), (other direction)
            bitSetULL(_segments[grp][4], pbit);
          } else
          if ((6 == segment) && (h == 0)) {       // left pixel (g,e,f)
            bitSetULL(_segments[grp][4], pbit);
            bitSetULL(_segments[grp][5], pbit);
          } else
          if ((6 == segment) && (h == hpx - 1)) { // right pixel (g,b,c)
            bitSetULL(_segments[grp][1], pbit);
            bitSetULL(_segments[grp][2], pbit);
          }
          pbit++; // next pixel
        }
      } else {
        for (uint8_t h = 0; h < hpx && pbit < 64; ++h) {
          bitSetULL(_segments[grp][segment], pbit);
          pbit++;
        }
      }
    } else if (16 == segment) { // Horizontal segment g, right half
      for (uint8_t h = rh; h < hpx && pbit < 64; ++h) {
        bitSetULL(_segments[grp][6], pbit);

        if (pixCfg.crnr && (h == hpx - 1)) { // right pixel (g,b,c)
          bitSetULL(_segments[grp][1], pbit);
          bitSetULL(_segments[grp][2], pbit);
        }
        pbit++;
      }
    } else if (26 == segment) { // Horizontal segment g, left half
      for (uint8_t h = 0; h < hpx - lh && pbit < 64; ++h) {
        bitSetULL(_segments[grp][6], pbit);

        if (pixCfg.crnr && (h == 0)) { // left pixel (g,e,f)
          bitSetULL(_segments[grp][4], pbit);
          bitSetULL(_segments[grp][5], pbit);
        }
        pbit++;
      }
    } else if (7 == segment) { // Decimal point segment
      for (uint8_t dp = 0; dp < pixCfg.dotp && pbit < 64; ++dp) {
        bitSetULL(_segments[grp][segment], pbit);
        pbit++;
      }
    } else if (255 != segment) { // Vertical segments b/c/e/f
      for (uint8_t v = 0; v < vpx && pbit < 64; ++v) {
        bitSetULL(_segments[grp][segment], pbit);
        pbit++;
      }
    }
  }
}

/****************************************************************************************
 * Fetch from local file system, flash or CDN
 ***************************************************************************************/
void P165_data_struct::addJavascript() {
  serve_JS(JSfiles_e::P165_digit); // Source in static/p165_digit.js, minified script source in src/src/Static/WebStaticData.h
}

/***************************************************************************
 * Update display with standard content once a second
 **************************************************************************/
bool P165_data_struct::plugin_once_a_second(struct EventStruct *event) {
  if (_output == P165_DISP_MANUAL) {
    return false;
  }

  if ((_output == P165_DISP_CLOCK24BLNK) ||
      (_output == P165_DISP_CLOCK12BLNK)) {
    _timesep = !_timesep;
  } else {
    _timesep = true; // On
  }

  if (_output == P165_DISP_DATE) {
    fillBufferWithDate(true, 0, 0, 0,
                       _suppressLeading0,
                       _stdOffset);
  } else {
    fillBufferWithTime(true, 0, 0, 0,
                       !((_output == P165_DISP_CLOCK24BLNK) ||
                         (_output == P165_DISP_CLOCK24)),
                       _suppressLeading0,
                       _stdOffset);
  }

  if (_blinkDot &&
      ((_output == P165_DISP_CLOCK24BLNK) ||
       (_output == P165_DISP_CLOCK12BLNK))) {
    showperiods[showmap[1 + _stdOffset]] = _timesep; // Blink dot on second digit
  }

  writeBufferToDisplay();

  // FIXME Determine what group(s) are used and set the extra pixels for those groups
  if (!_blinkDot) {
    uint8_t dgts             = 0;
    const uint8_t dgtsNeeded = std::min(_totalDigits, static_cast<uint8_t>(P165_DISP_DATE == _output ? 8 : 6));

    for (uint8_t grp = 0; grp < _pixelGroups; ++grp) {
      dgts += _pixelGroupCfg[grp].dgts;
      addLog(LOG_LEVEL_INFO, strformat(F("P165 : seconds marker %d Group %d, digits %d offset %d"), _timesep, grp, dgts, _stdOffset));

      if ((_stdOffset <= dgts) && (dgtsNeeded - _stdOffset > dgts)) {
        extraPixelsState(grp + 1, _timesep ? 1 : 0, AdaGFXrgb565ToRgb888(_timesep ? _fgColor : _bgColor));
      }
    }
  }

  return true;
}

/***************************************************************************
 * Scroll text in 0.1 second steps
 **************************************************************************/
bool P165_data_struct::plugin_ten_per_second(struct EventStruct *event) {
  if ((_output != P165_DISP_MANUAL) || !isScrollEnabled()) {
    return false;
  }

  if (nextScroll()) {
    writeBufferToDisplay();
  }
  return true;
}

/*************************************************************************
 * Handle commands, quite compatible with P073
 ************************************************************************/
const char p165_commands[] PROGMEM =
  "7dtext|"
  # if P165_FEATURE_P073
  "7dfont|"
  # endif // if P165_FEATURE_P073
  "7dbin|"
  "7dextra|"
  "7dbefore|"
  "7don|"
  "7doff|"
  "7db|"
  "7output|"
  "7dgroup|"
  "7color|"
  "7digit|"
  # if P165_FEATURE_DIGITCOLOR
  "7digitcolor|"
  # endif // if P165_FEATURE_DIGITCOLOR
  # if P165_FEATURE_GROUPCOLOR
  "7groupcolor|"
  # endif // if P165_FEATURE_GROUPCOLOR
;
enum class p165_commands_e : int8_t {
  invalid = -1,
  c7dtext,
  # if P165_FEATURE_P073
  c7dfont,
  # endif // if P165_FEATURE_P073
  c7dbin,
  c7dextra,
  c7dbefore,
  c7don,
  c7doff,
  c7db,
  c7output,
  c7group,
  c7color,
  c7digit,
  # if P165_FEATURE_DIGITCOLOR
  c7digitcolor,
  # endif // if P165_FEATURE_DIGITCOLOR
  # if P165_FEATURE_GROUPCOLOR
  c7groupcolor,
  # endif // if P165_FEATURE_GROUPCOLOR
};

bool P165_data_struct::plugin_write(struct EventStruct *event,
                                    const String      & string) {
  const String cmd_s = parseString(string, 1);

  const int cmd_i = GetCommandCode(cmd_s.c_str(), p165_commands);

  if (cmd_i < 0) { return false; } // Fail fast

  const bool currentScroll = isScrollEnabled(); // Save current state
  bool newScroll           = currentScroll;     // keep scroll if command changes

  const p165_commands_e cmd = static_cast<p165_commands_e>(cmd_i);

  const String text = parseStringToEndKeepCase(string, 2);
  bool success      = false;
  bool displayon    = false;

  switch (cmd) {
    case p165_commands_e::c7dtext:          // Set text to show
      setScrollEnabled(0 == _currentGroup); // Scrolling allowed for 7dtext command on entire display only
      return plugin_write_7dtext(text);
      break;
    # if P165_FEATURE_P073
    case p165_commands_e::c7dfont: // Select font
      return plugin_write_7dfont(event, text);
      break;
    # endif // if P165_FEATURE_P073
    case p165_commands_e::c7dbin:           // Send binary data
      setScrollEnabled(0 == _currentGroup); // Scrolling allowed for 7dbin command on entire display only
      return plugin_write_7dbin(text);
      break;
    case p165_commands_e::c7don:            // Display on
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_INFO, F("Neo7Seg : Display ON"));
      # endif // ifndef BUILD_NO_DEBUG
      displayon = true;
      success   = true;
      break;
    case p165_commands_e::c7doff: // Display off
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_INFO, F("Neo7Seg : Display OFF"));
      # endif // ifndef BUILD_NO_DEBUG
      displayon = false;
      success   = true;
      break;
    case p165_commands_e::c7db: // Set brightness

      if ((event->Par1 >= 0) && (event->Par1 <= _maxBrightness)) {
        # ifndef BUILD_NO_DEBUG

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          addLog(LOG_LEVEL_INFO, concat(F("Neo7Seg : Brightness="), event->Par1));
        }
        # endif // ifndef BUILD_NO_DEBUG
        _defBrightness         = event->Par1;
        P165_CONFIG_DEF_BRIGHT = event->Par1;
        displayon              = true;
        success                = true;
      }
      break;
    case p165_commands_e::c7output:                  // 7output,<output> : Select display output

      if ((event->Par1 >= 0) && (event->Par1 < 6)) { // 0:"Manual",1:"Clock 24h - Blink",2:"Clock 24h - No Blink",
                                                     // 3:"Clock 12h - Blink",4:"Clock 12h - No Blink",5:"Date"
        # if P165_DEBUG_INFO

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          addLog(LOG_LEVEL_INFO, concat(F("Neo7Seg : Display output="), event->Par1));
        }
        # endif // if P165_DEBUG_INFO
        _output                = event->Par1;
        P165_CONFIG_OUTPUTTYPE = event->Par1;
        displayon              = true;
        success                = true;

        if (event->Par1 != 0) { newScroll = false; } // Scrolling only for Manual
      }
      break;
    case p165_commands_e::c7group: // 7group,<grp> : 0 = global, 1..configured groups

      if ((event->Par1 >= 0) && (event->Par1 <= _pixelGroups)) {
        _currentGroup = event->Par1;
        success       = true;
      }
      break;
    case p165_commands_e::c7color: // 7color,fg_r,fg_g,fg_b[,fg_w],bg_r,bg_g,bg_b[,bg_w]
    {
      const bool rgbW    = P165_STRIP_TYPE_RGBW == _stripType;
      uint32_t   fgColor = 0;
      uint32_t   bgColor = 0;
      bool fgSet         = false;
      bool bgSet         = false;

      if (parseRGBWColors(parseStringToEnd(string, 2), rgbW, fgColor, bgColor, fgSet, bgSet)) {
        uint8_t from = 0;
        uint8_t to   = _pixelGroups;

        if (_currentGroup > 0) {
          from = _currentGroup - 1;
          to   = _currentGroup;
        } else {
          if (fgSet) { _fgColor = AdaGFXrgb888ToRgb565(fgColor); }

          if (bgSet) { _bgColor = AdaGFXrgb888ToRgb565(bgColor); }

          if (loglevelActiveFor(LOG_LEVEL_INFO)) {
            addLog(LOG_LEVEL_INFO, strformat(F("Neo7Dgt: Colors: FG: 0x%08x BG: 0x%08x"), fgColor, bgColor));
          }
        }

        for (uint8_t i = from; i < to; ++i) {
          if (fgSet) {
            display[i]->setColorFont(fgColor);
            success = true;
          }

          if (bgSet) {
            display[i]->setColorBack(bgColor);
            success = true;
          }

          if (success && (_currentGroup > 0) && loglevelActiveFor(LOG_LEVEL_INFO)) {
            addLog(LOG_LEVEL_INFO, strformat(F("Neo7Dgt: Group: %d colors: FG: 0x%08x BG: 0x%08x"), i + 1, fgColor, bgColor));
          }
        }
      }
      break;
    }
    case p165_commands_e::c7digit: // 7digit,<grp>,<dgt>,<char>[.] : write a single character to a digit, with optional period
      // Scrolling not allowed for 7digit command
      newScroll = false;
      success   = plugin_write_7digit(text);
      break;
    case p165_commands_e::c7dextra:  // 7dextra,<grp>,<0|1|2|3|4|5>[,<r>,<g>,<b>[,<w>]] : Set the 'Extra pixels after' on/off
    // with optional color, group 0 sets the color to all groups additional pixels
    case p165_commands_e::c7dbefore: // 7dbefore,<grp>,<0|1|2|3|4|5>[,<r>,<g>,<b>[,<w>]] : Set the 'Pixel-offset before' on/off
    {
      const uint8_t par1     = event->Par1;
      const bool    pxlExtra = p165_commands_e::c7dextra == cmd;

      if (((0 == par1) || ((par1 > 0) && (par1 <= _pixelGroups) &&
                           pxlExtra ? (_pixelGroupCfg[par1 - 1].addn > 0)
                                    : (_pixelGroupCfg[par1 - 1].offs > 0)
                           )) &&                        // single group have extra pixels after?
          !parseString(string, 3).isEmpty()) {          // on/off is given
        const bool rgbW    = P165_STRIP_TYPE_RGBW == _stripType;
        const bool onState = 1 == (event->Par2 & 0x01); // Check for On (1/3/5) or Off (0/2/4)
        uint32_t   fgColor = 0;
        uint32_t   bgColor = 0;                         // Ignored!
        bool fgSet         = false;
        bool bgSet         = false;                     // Ignored!

        parseRGBWColors(parseStringToEnd(string, 4), rgbW, fgColor, bgColor, fgSet, bgSet);
        const uint32_t newColor = onState
                                    ? (fgSet ? fgColor : AdaGFXrgb565ToRgb888(_fgColor))
                                    : (fgSet ? fgColor : AdaGFXrgb565ToRgb888(_bgColor));

        extraPixelsState(par1, event->Par2, newColor, pxlExtra);
        success = true;
      }
      break;
    }
    # if P165_FEATURE_DIGITCOLOR
    case p165_commands_e::c7digitcolor: // 7digitcolor,grp,dgt,[-|fg_r,fg_g,fg_b[,fg_w][,bg_r,bg_g,bg_b[,bg_w]]]
    # endif // if P165_FEATURE_DIGITCOLOR
    # if P165_FEATURE_GROUPCOLOR
    case p165_commands_e::c7groupcolor: // 7groupcolor,grp,[-|fg_r,fg_g,fg_b[,fg_w][,bg_r,bg_g,bg_b[,bg_w]]]
    # endif // if P165_FEATURE_GROUPCOLOR
    # if P165_FEATURE_DIGITCOLOR || P165_FEATURE_GROUPCOLOR
    {
      const bool rgbW     = P165_STRIP_TYPE_RGBW == _stripType;
      uint8_t    grp      = 0;
      uint8_t    dgt      = 0;
      uint32_t   fgColor  = 0;
      uint32_t   bgColor  = 0;
      const bool grpColor =
        #  if P165_FEATURE_GROUPCOLOR
        p165_commands_e::c7groupcolor == cmd;
        #  else // if P165_FEATURE_GROUPCOLOR
        false;
        #  endif // if P165_FEATURE_GROUPCOLOR
      bool fgSet = false;
      bool bgSet = false;

      // First 2 arguments: group and digit
      if ((event->Par1 > 0) && (event->Par1 <= _pixelGroups)) {
        grp = event->Par1;
      }

      if ((grp > 0) && !grpColor && ((event->Par2 > 0) && (event->Par2 <= _pixelGroupCfg[grp - 1].dgts))) {
        dgt = event->Par2;
      }

      if (((0 == dgt) && !grpColor) || (grpColor && (0 == grp))) {
        break; // Invalid grp or dgt arguments
      }

      const uint16_t grp100dgt = 0x100 + (grp << 4) + (dgt - 1);
      const uint16_t grp200dgt = 0x200 + (grp << 4) + (dgt - 1);
      const uint16_t grp300    = 0x300 + (grp << 4);
      const uint16_t grp400    = 0x400 + (grp << 4);
      const uint8_t  parIdx    = grpColor ? 3 : 4;
      const String   par4      = parseString(string, parIdx);

      // Next argument: delete digit/groupcolor?
      if (equals(par4, F("-"))) {                                  // use dash to remove a digit/group color setting
        auto it = digitColors.find(grpColor ? grp300 : grp100dgt); // fg color

        if (it != digitColors.end()) {
          digitColors.erase(it);
        }
        it = digitColors.find(grpColor ? grp400 : grp200dgt); // bg color

        if (it != digitColors.end()) {
          digitColors.erase(it);
        }
        success = true;
      } else
      if (parseRGBWColors(parseStringToEnd(string, parIdx), rgbW, fgColor, bgColor, fgSet, bgSet)) {
        if (fgSet) {
          auto it = digitColors.find(grpColor ? grp300 : grp100dgt);     // fg color

          if (it != digitColors.end()) {                                 // Update fg color
            it->second = fgColor;
          } else {
            digitColors.emplace(grpColor ? grp300 : grp100dgt, fgColor); // New fg color
          }
          success = true;
        }

        if (bgSet) {
          auto it = digitColors.find(grpColor ? grp400 : grp200dgt);     // bg color

          if (it != digitColors.end()) {                                 // Update bg color
            it->second = bgColor;
          } else {
            digitColors.emplace(grpColor ? grp400 : grp200dgt, bgColor); // New bg color
          }
          success = true;
        }
      }
      #  if P165_DEBUG_DEBUG

      for (auto const& it : digitColors) {
        addLog(LOG_LEVEL_INFO, strformat(F("P165 : 7digit/groupcolor key: 0x%03x color: 0x%08x"), it.first, it.second));
      }
      #  endif // if P165_DEBUG_DEBUG
      break;
    }
    # endif // if P165_FEATURE_DIGITCOLOR || P165_FEATURE_GROUPCOLOR
    case p165_commands_e::invalid:
      break;
  }

  if (success) {
    if (displayon) {
      // TODO (On)
    } else {
      // TODO (Off)
    }
    setScrollEnabled(newScroll);

    strip->setBrightness(std::min(_maxBrightness, _defBrightness));
  }
  return success;
}

/**************************************************************************************
 * set the state (color) of the extra befoer/after pixels of 1 or all(0) groups,
 * state: 0/1: all off/on, 2/3: first half, 4/5: second half
 *************************************************************************************/
bool P165_data_struct::extraPixelsState(uint8_t group, uint8_t state, uint32_t color, bool pxlExtra) {
  const uint8_t pxPart  = (state >> 1);               // Check what part to switch, cut off lowest bit
  const uint8_t fromGrp = 0 == group ? 0 : group - 1; // All or 1 group
  const uint8_t toGrp   = 0 == group ? _pixelGroups : group;

  for (uint8_t grp = fromGrp; grp < toGrp; ++grp) {
    const uint8_t xPixels = pxlExtra ? _pixelGroupCfg[grp].addn : _pixelGroupCfg[grp].offs;

    if (xPixels > 0) {
      uint16_t pxFrom = 0;
      uint16_t pxTo   = xPixels;

      if ((xPixels > 1) && (pxPart > 0)) { // 0 = full block
        if (1 == pxPart) {                 // 2/3 first half off/on
          pxTo = xPixels / 2;
        } else {                           // 4/5 second half off/on
          pxFrom = xPixels / 2;
        }
      }
      const uint16_t xOffset = pxlExtra ? _pixelGroupCfg[grp].aoffs : _pixelGroupCfg[grp].boffs;

      for (uint16_t i = pxFrom; i < pxTo; ++i) {
        display[grp]->setPixelColor(i + xOffset, color);
      }

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLog(LOG_LEVEL_INFO,
               strformat(F("Neo7Dgt: Set group %d extra %c pixels %d..%d to color 0x%08x"),
                         grp + 1,
                         pxlExtra ? 'a' : 'b',
                         xOffset + 1 + pxFrom,
                         xOffset + pxTo,
                         color));
      }
    }
  }

  strip->show(); // Show the output
  return true;
}

/**********************************************************************************************
 * Parse a string to foreground and background RGB or RGBW values, each requires 3 or 4 values
 *********************************************************************************************/
bool P165_data_struct::parseRGBWColors(const String& string,
                                       bool          rgbW,
                                       uint32_t    & fgColor,
                                       uint32_t    & bgColor,
                                       bool        & fgSet,
                                       bool        & bgSet) {
  uint32_t clr_r = 0;
  uint32_t clr_g = 0;
  uint32_t clr_b = 0;
  uint32_t clr_w = 0;
  uint8_t  rgbI  = 0;

  String par_r = parseString(string, 1);
  String par_g = parseString(string, 2);
  String par_b = parseString(string, 3);
  String par_w;

  if (rgbW) { par_w = parseString(string, 4); }

  // Foreground color check
  if (!par_r.isEmpty() && !par_g.isEmpty() && !par_b.isEmpty() && (!rgbW || !par_w.isEmpty())) {
    validUIntFromString(par_r, clr_r); // No value check as value is &-limited
    validUIntFromString(par_g, clr_g);
    validUIntFromString(par_b, clr_b);

    if (rgbW) { validUIntFromString(par_w, clr_w); }

    fgColor = ((clr_r & 0xFF) << 16) + ((clr_g & 0xFF) << 8) + (clr_b & 0xFF) + ((clr_w & 0xFF) << 24);
    fgSet   = true;
  }

  // Background color check
  if (rgbW) { rgbI++; } // Extra offset needed
  par_r = parseString(string, 4 + rgbI);
  par_g = parseString(string, 5 + rgbI);
  par_b = parseString(string, 6 + rgbI);

  if (rgbW) { par_w = parseString(string, 7 + rgbI); }

  if (!par_r.isEmpty() && !par_g.isEmpty() && !par_b.isEmpty() && (!rgbW || !par_w.isEmpty())) {
    validUIntFromString(par_r, clr_r); // No value check as value is &-limited
    validUIntFromString(par_g, clr_g);
    validUIntFromString(par_b, clr_b);

    if (rgbW) {
      clr_w = 0;
      validUIntFromString(par_w, clr_w);
    }

    bgColor = ((clr_r & 0xFF) << 16) + ((clr_g & 0xFF) << 8) + (clr_b & 0xFF) + ((clr_w & 0xFF) << 24);
    bgSet   = true;
  }
  return fgSet || bgSet; // Skipping all arguments is not a valid outcome
}

/******************************************************************************************
 * Write some text to the buffer to display. Scroll if that's enabled and text doesn't fit
 *****************************************************************************************/
bool P165_data_struct::plugin_write_7dtext(const String& text) {
  if (_output != P165_DISP_MANUAL) {
    return false;
  }
  # ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO, strformat(F("Neo7Seg : Show Text=%s, group %d"), text.c_str(), _currentGroup));
  }
  # endif // ifndef BUILD_NO_DEBUG
  setTextToScroll(EMPTY_STRING);
  const uint8_t bufLen = calculateDisplayDigits();

  if (isScrollEnabled() && (getEffectiveTextLength(text) > bufLen)) {
    setTextToScroll(text);
  } else {
    fillBufferWithString(text);

    writeBufferToDisplay(_currentGroup);
  }
  return true;
}

# if P165_FEATURE_P073

/***********************************************************************
 * Select a font by name or number
 **********************************************************************/
bool P165_data_struct::plugin_write_7dfont(struct EventStruct *event,
                                           const String      & text) {
  if (!text.isEmpty()) {
    const int32_t fontNr = P073_parse_7dfont(event, text);
    #  if P165_DEBUG_INFO

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO, strformat(F("P165 7dfont,%s -> %d"), parseString(text, 1).c_str(), fontNr));
    }
    #  endif // if P165_DEBUG_INFO

    if ((fontNr >= 0) && (fontNr <= 3)) {
      _fontset            = fontNr;
      P165_CONFIG_FONTSET = fontNr;
      return true;
    }
  }
  return false;
}

# endif // if P165_FEATURE_P073

/**********************************************************************************************
 * Write binary data into the buffer to display, provide bytes with data matching the segments
 *********************************************************************************************/
bool P165_data_struct::plugin_write_7dbin(const String& text) {
  if (!text.isEmpty()) {
    String  data;
    int32_t byteValue{};
    int     arg      = 1;
    String  argValue = parseString(text, arg);

    while (!argValue.isEmpty()) {
      if (validIntFromString(argValue, byteValue) && (byteValue < 256) && (byteValue > -1)) {
        data += static_cast<char>(byteValue);
      }
      arg++;
      argValue = parseString(text, arg);
    }
    # if P165_DEBUG_INFO
    addLog(LOG_LEVEL_INFO, strformat(F("7dbin: text: %s, data len: %d"), text.c_str(), data.length()));
    # endif // if P165_DEBUG_INFO
    const uint8_t bufLen = calculateDisplayDigits();

    if (!data.isEmpty()) {
      setTextToScroll(EMPTY_STRING); // Clear any scrolling text

      if (isScrollEnabled() && (data.length() > bufLen)) {
        setBinaryData(data);
      } else {
        fillBufferWithString(data, true);

        writeBufferToDisplay(_currentGroup);
      }
      return true;
    }
  }
  return false;
}

/****************************************************************************
 * Write a character to an explicit digit in a group
 ***************************************************************************/
bool P165_data_struct::plugin_write_7digit(const String& text) {
  if (!text.isEmpty()) {
    uint32_t grp      = 0;
    uint32_t dgt      = 0;
    const String data = parseStringKeepCase(text, 3);

    if (validUIntFromString(parseString(text, 1), grp) && (grp > 0) && (grp <= _pixelGroups) &&
        validUIntFromString(parseString(text, 2), dgt) && (dgt > 0) && (dgt <= _pixelGroupCfg[grp - 1].dgts) &&
        !data.isEmpty()) {
      grp--;
      dgt--;
      # if P165_FEATURE_P073
      const uint8_t cdata = P073_mapCharToFontPosition(data.charAt(0), _fontset);
      # else // if P165_FEATURE_P073
      const uint8_t cdata = data.charAt(0);
      # endif // if P165_FEATURE_P073
      writeCharacterToDisplay(grp, dgt, cdata, data.length() > 1 && data.charAt(1) == '.');
      strip->show();
      return true;
    }
  }
  return false;
}

/**********************************************************************************
 * Put a string into the buffer, periods are smashed into the previous digit
 *********************************************************************************/
void P165_data_struct::fillBufferWithString(const String& textToShow,
                                            bool          useBinaryData) {
  _binaryData = useBinaryData;
  clearBuffer();
  const int txtlength = textToShow.length();
  uint8_t   bufLen    = calculateDisplayDigits();

  int p = 0;

  if (_currentGroup > 0) {
    bufLen = _pixelGroupCfg[_currentGroup].dgts;            // Adjust length

    for (uint8_t grp = 0; grp < _currentGroup - 1; ++grp) { // Find current group start digit
      p += _pixelGroupCfg[grp].dgts;                        // Add offset
    }
  }

  uint8_t mp = 0;

  for (int i = 0; i < txtlength && p <= bufLen; ++i) { // p <= bufLen to allow a period after last digit
    mp = showmap[p];                                   // digit mapping

    if (_periods
        && (textToShow.charAt(i) == '.')
        && !_binaryData
        ) {         // If setting periods true
      if (p == 0) { // Text starts with a period, becomes a space with a dot
        showperiods[mp] = true;
        showbuffer[mp]  =
          # if P165_FEATURE_P073
          10  // space in 7dgt fonts
          # else // if P165_FEATURE_P073
          ' ' // space
          # endif // if P165_FEATURE_P073
        ;
        p++;
        mp = showmap[p]; // update
      } else {
        // if (p > 0) {
        mp              = showmap[p - 1];
        showperiods[mp] = true;                           // The period displays as a dot on the previous digit!
      }

      if ((i > 0) && (textToShow.charAt(i - 1) == '.')) { // Handle consecutive periods
        p++;

        if ((p - 1) < bufLen) {
          mp              = showmap[p - 1];
          showperiods[mp] = true; // The period displays as a dot on the previous digit!
        }
      }
    } else if (p < bufLen) {
      # if P165_FEATURE_P073
      showbuffer[mp] = useBinaryData
                        ? textToShow.charAt(i)
                        : P073_mapCharToFontPosition(textToShow.charAt(i), _fontset);
      # else // if P165_FEATURE_P073
      showbuffer[mp] = textToShow.charAt(i);
      # endif // if P165_FEATURE_P073
      p++;
    }
  }
  # if P165_DEBUG_INFO
  logBufferContent(F("7dtext"));
  # endif // if P165_DEBUG_INFO
}

void P165_data_struct::setBinaryData(const String& data) {
  _binaryData = true;
  setTextToScroll(data);
  _binaryData  = true; // is reset in setTextToScroll
  _scrollCount = _scrollSpeed;
  _scrollPos   = 0;
}

void P165_data_struct::fillBufferWithTime(const bool    sevendgt_now,
                                          uint8_t       sevendgt_hours,
                                          uint8_t       sevendgt_minutes,
                                          uint8_t       sevendgt_seconds,
                                          const bool    flag12h,
                                          const bool    suppressLeading0,
                                          const uint8_t offset) {
  clearBuffer();

  if (sevendgt_now) {
    sevendgt_hours   = node_time.hour();
    sevendgt_minutes = node_time.minute();
    sevendgt_seconds = node_time.second();
  }

  if (flag12h && (sevendgt_hours > 12)) {
    sevendgt_hours -= 12; // if flag 12h is TRUE and h>12 adjust subtracting 12
  }

  if (flag12h && (sevendgt_hours == 0)) {
    sevendgt_hours = 12; // if flag 12h is TRUE and h=0  adjust to h=12
  }
  put4NumbersInBuffer(sevendgt_hours, sevendgt_minutes, sevendgt_seconds, -1, suppressLeading0, offset);
}

void P165_data_struct::fillBufferWithDate(const bool    sevendgt_now,
                                          uint8_t       sevendgt_day,
                                          uint8_t       sevendgt_month,
                                          const int     sevendgt_year,
                                          const bool    suppressLeading0,
                                          const uint8_t offset) {
  clearBuffer();
  int sevendgt_year0 = sevendgt_year;

  if (sevendgt_now) {
    sevendgt_day   = node_time.day();
    sevendgt_month = node_time.month();
    sevendgt_year0 = node_time.year();
  } else if (sevendgt_year0 < 100) {
    sevendgt_year0 += 2000;
  }
  const uint8_t sevendgt_year1 = static_cast<uint8_t>(sevendgt_year0 / 100);
  const uint8_t sevendgt_year2 = static_cast<uint8_t>(sevendgt_year0 % 100);

  put4NumbersInBuffer(sevendgt_day, sevendgt_month, sevendgt_year1, sevendgt_year2, suppressLeading0, offset);
}

void P165_data_struct::put4NumbersInBuffer(const uint8_t nr1,
                                           const uint8_t nr2,
                                           const uint8_t nr3,
                                           const int8_t  nr4,
                                           const bool    suppressLeading0,
                                           const uint8_t offset) {
  uint8_t cOffs = 0;

  # if !P165_FEATURE_P073
  cOffs = '0'; // Fallback to ASCII if P073 not available
  # endif // if !P165_FEATURE_P073

  # if P165_DEBUG_DEBUG
  addLog(LOG_LEVEL_INFO,
         strformat(F("P165 : put4NumbersInBuffer: a:%d b:%d c:%d d:%d cOffs:%d offset:%d"),
                   nr1, nr2, nr3, nr4, cOffs, offset));
  # endif // if P165_DEBUG_DEBUG

  // offset shouldn't ever be > buffer size - 4
  showbuffer[showmap[0 + offset]] = static_cast<uint8_t>((nr1 / 10) + cOffs);
  showbuffer[showmap[1 + offset]] = (nr1 % 10) + cOffs;
  showbuffer[showmap[2 + offset]] = static_cast<uint8_t>((nr2 / 10) + cOffs);
  showbuffer[showmap[3 + offset]] = (nr2 % 10) + cOffs;

  if ((nr3 > -1) && ((5 + offset) < _totalDigits)) {
    showbuffer[showmap[4 + offset]] = static_cast<uint8_t>((nr3 / 10) + cOffs);
    showbuffer[showmap[5 + offset]] = (nr3 % 10) + cOffs;
  }

  if ((nr4 > -1) && ((7 + offset) < _totalDigits)) {
    showbuffer[showmap[6 + offset]] = static_cast<uint8_t>((nr4 / 10) + cOffs);
    showbuffer[showmap[7 + offset]] = (nr4 % 10) + cOffs;
  }

  if (suppressLeading0 && (showbuffer[showmap[0 + offset]] == cOffs)) {
    showbuffer[showmap[0 + offset]] =
      # if P165_FEATURE_P073
      10
      # else // if P165_FEATURE_P073
      ' '
      # endif // if P165_FEATURE_P073
    ; // set to space
  }
}

/***********************************************
 * Nothing in the buffer
 **********************************************/
void P165_data_struct::clearBuffer() {
  memset(showbuffer,
         _binaryData ? 0 :
         # if P165_FEATURE_P073
         10,
         # else // if P165_FEATURE_P073
         ' ', // space
         # endif // if P165_FEATURE_P073
         sizeof(showbuffer));

  for (uint8_t i = 0; i < P165_SHOW_BUFFER_SIZE; ++i) {
    showperiods[i] = false;
  }
}

/********************************************************************************************
 * Write a single character-mask to a digit
 *******************************************************************************************/
void P165_data_struct::writeCharacterToDisplay(uint8_t group, uint8_t digit, uint8_t character, bool period) {
  uint8_t data =
    # if P165_FEATURE_P073
    _binaryData ?
    # endif // if P165_FEATURE_P073
    character
    # if P165_FEATURE_P073 // Use P073 7-segment fonts and buffer content when available
    : P073_revert7bits(P073_getFontChar(character, _fontset))
    # endif // if P165_FEATURE_P073
  ;

  if (!_binaryData && _periods && period) {
    data |= 0x80;                   // Decimal point On
  }

  display[group]->setCursor(digit); // Have to move the cursor ourselves for writeLowLevel and at pos. 0
  # if P165_FEATURE_P073
  display[group]->writeLowLevel(digit, digit2SegmentMap(group, data));
  # else // if P165_FEATURE_P073

  if (_binaryData) {
    display[group]->writeLowLevel(digit, digit2SegmentMap(group, data));
  } else {
    display[group]->write(data); // Fallback to ASCII in buffer and built-in font if P073 not available
  }
  # endif // if P165_FEATURE_P073
}

/*************************************************************************************
 * Send current buffer content to the display
 ************************************************************************************/
void P165_data_struct::writeBufferToDisplay(uint8_t group) {
  uint8_t i          = 0;
  const uint8_t from = (group == 0 ? 0 : group - 1);
  const uint8_t to   = (group == 0 ? _pixelGroups : std::min(group, _pixelGroups));

  # if P165_DEBUG_DEBUG
  String log;
  # endif // if P165_DEBUG_DEBUG

  for (uint8_t grp = from; grp < to; ++grp) {
    uint32_t fgColor = AdaGFXrgb565ToRgb888(_fgColor);
    uint32_t bgColor = AdaGFXrgb565ToRgb888(_bgColor);
    # if P165_FEATURE_GROUPCOLOR
    bool fgColorGrp       = false;
    bool bgColorGrp       = false;
    const uint16_t grp300 = 0x300 + ((grp + 1) << 4);
    const uint16_t grp400 = 0x400 + ((grp + 1) << 4);
    auto itgrp            = digitColors.find(grp300); // fg color

    if (itgrp != digitColors.end()) {
      fgColor = itgrp->second;
      display[grp]->setColorFont(fgColor);
      fgColorGrp = true;
    }
    itgrp = digitColors.find(grp400); // bg color

    if (itgrp != digitColors.end()) {
      bgColor = itgrp->second;
      display[grp]->setColorBack(bgColor);
      bgColorGrp = true;
    }
    # endif // if P165_FEATURE_GROUPCOLOR

    for (uint8_t dgt = 0; dgt < _pixelGroupCfg[grp].dgts; ++dgt) {
      # if P165_DEBUG_DEBUG

      if ((grp == from) && (dgt == 0)) {
        log += F("P165 : BufToDisp: ");
      }
      # endif // if P165_DEBUG_DEBUG
      # if P165_FEATURE_DIGITCOLOR

      // apply per-digit colormap
      bool fgColorSet          = false;
      bool bgColorSet          = false;
      const uint16_t grp100dgt = 0x100 + ((grp + 1) << 4) + dgt;
      const uint16_t grp200dgt = 0x200 + ((grp + 1) << 4) + dgt;
      auto it                  = digitColors.find(grp100dgt); // fg color

      if (it != digitColors.end()) {
        display[grp]->setColorFont(it->second);
        fgColorSet = true;
      }
      it = digitColors.find(grp200dgt); // bg color

      if (it != digitColors.end()) {
        display[grp]->setColorBack(it->second);
        bgColorSet = true;
      }
      # endif // if P165_FEATURE_DIGITCOLOR

      writeCharacterToDisplay(grp, dgt, showbuffer[i], showperiods[i]);

      # if P165_FEATURE_DIGITCOLOR

      // Restore global colors
      if (fgColorSet) {
        display[grp]->setColorFont(fgColor);
      }

      if (bgColorSet) {
        display[grp]->setColorBack(bgColor);
      }
      # endif // if P165_FEATURE_DIGITCOLOR

      # if P165_DEBUG_DEBUG
      log += strformat(F(" g:%d d:%d 0x%02x"), grp, dgt, showbuffer[i]);
      # endif // if P165_DEBUG_DEBUG
      ++i;
    }
    # if P165_FEATURE_GROUPCOLOR

    // Restore global colors
    if (fgColorGrp) {
      display[grp]->setColorFont(AdaGFXrgb565ToRgb888(_fgColor));
    }

    if (bgColorGrp) {
      display[grp]->setColorBack(AdaGFXrgb565ToRgb888(_bgColor));
    }
    # endif // if P165_FEATURE_GROUPCOLOR
  }
  # if P165_DEBUG_DEBUG
  addLogMove(LOG_LEVEL_INFO, log);
  # endif // if P165_DEBUG_DEBUG
  strip->show(); // Show all content at once
}

/*************************************************************************************
 * fetch the pixels to turn on for a digit
 ************************************************************************************/
segsize_t P165_data_struct::digit2SegmentMap(uint8_t grp, uint8_t digit) {
  segsize_t currentBitmap = 0u; // clear the current bitmap

  for (byte i = 0; i < 8; i++) {
    if (digit & (1UL << i)) {   // UL not necessary, but uses less Flash than if you leave it away
      currentBitmap |= _segments[grp][i];
    }
  }

  # if P165_DEBUG_DEBUG

  const uint8_t pxlDigit = min(static_cast<uint16_t>(63),
                               calculateGroupPixels(1, // Count pixels for 1 digit only
                                                    _pixelGroupCfg[grp].wpix,
                                                    _pixelGroupCfg[grp].hpix,
                                                    _pixelGroupCfg[grp].crnr,
                                                    _pixelGroupCfg[grp].dotp,
                                                    0));

  // Used for debugging
  addLog(LOG_LEVEL_INFO, strformat(F("digit2SegmentMap: digit: 0x%02x, bits: 0b%s"), digit,
                                   ull2String(pxlDigit < 63 ? bitSetULL(currentBitmap, pxlDigit + 1)
                                                            : currentBitmap, 2).substring(1).c_str()));

  if (pxlDigit < 63) { bitClearULL(currentBitmap, pxlDigit + 1); }
  # endif // if P165_DEBUG_DEBUG
  return currentBitmap;
}

/********************************************************************
 * Skip periods as they are shown on previous digit
 *******************************************************************/
int P165_data_struct::getEffectiveTextLength(const String& text) {
  const int textLength = text.length();
  int p                = 0;

  for (int i = 0; i < textLength; ++i) {
    if (_periods && (text.charAt(i) == '.')) { // If setting periods true
      if (p == 0) {                            // Text starts with a period, becomes a space with a dot
        p++;
      }

      if ((i > 0) && (text.charAt(i - 1) == '.')) { // Handle consecutive periods
        p++;
      }
    } else {
      p++;
    }
  }
  return p;
}

/**************************************************************************************
 * Scroll current buffer with text/binary data to display (copied/adjusted from P073)
 *************************************************************************************/
bool P165_data_struct::nextScroll() {
  bool result = false;

  if (isScrollEnabled() && (!_textToScroll.isEmpty())) {
    if ((_scrollCount > 0) && (_scrollCount < 0xFFFF)) { _scrollCount--; }

    if (_scrollCount == 0) {
      _scrollCount = 0xFFFF; // Max value to avoid interference when scrolling long texts
      result       = true;
      const uint8_t  bufToFill = calculateDisplayDigits();
      const uint16_t txtlength = _textToScroll.length();
      clearBuffer();

      uint8_t p = 0;
      uint8_t mp;

      for (int i = _scrollPos; i < txtlength && p <= bufToFill; ++i) { // p <= bufToFill to allow a period after last digit
        mp = showmap[p];

        if (_periods
            && (_textToScroll.charAt(i) == '.')
            && !_binaryData
            ) {         // If setting periods true
          if (p == 0) { // Text starts with a period, becomes a space with a dot
            showperiods[mp] = true;
            p++;
          } else {
            mp              = showmap[p - 1];
            showperiods[mp] = true;                                       // The period displays as a dot on the previous digit!
          }

          if ((i > _scrollPos) && (_textToScroll.charAt(i - 1) == '.')) { // Handle consecutive periods
            mp              = showmap[p - 1];
            showperiods[mp] = true;                                       // The period displays as a dot on the previous digit!
            p++;
          }
        } else if (p < bufToFill) {
          showbuffer[mp] =
            # if P165_FEATURE_P073
            _binaryData ?
            # endif // if P165_FEATURE_P073
            _textToScroll.charAt(i)
            # if P165_FEATURE_P073
            : P073_mapCharToFontPosition(_textToScroll.charAt(i), _fontset)
            # endif // if P165_FEATURE_P073
          ;
          p++;
        }
      }
      _scrollPos++;

      if (_scrollPos > _textToScroll.length() - bufToFill) {
        _scrollPos = 0;            // Restart when all text displayed
      }
      _scrollCount = _scrollSpeed; // Restart countdown
      # if P165_DEBUG_DEBUG
      logBufferContent(F("nextScroll"));
      # endif // if P165_DEBUG_DEBUG
    }
  }
  return result;
}

/***********************************************************************************
 * Set up the string to scroll across the display, with optional prefixed spaces
 **********************************************************************************/
void P165_data_struct::setTextToScroll(const String& text) {
  free_string(_textToScroll);

  if (!text.isEmpty()) {
    const int bufToFill = calculateDisplayDigits();
    _textToScroll.reserve(text.length() + bufToFill + (_scrollFull ? bufToFill : 0));

    for (int i = 0; _scrollFull && i < bufToFill; ++i) { // Scroll text in from the right, so start with all blancs
      _textToScroll += _binaryData ? (char)0x00 : ' ';
    }
    _textToScroll += text;

    for (int i = 0; i < bufToFill; ++i) { // Scroll text off completely before restarting
      _textToScroll += _binaryData ? (char)0x00 : ' ';
    }
  }
  _scrollCount = _scrollSpeed;
  _scrollPos   = 0;
  _binaryData  = false;
}

void P165_data_struct::setScrollSpeed(uint8_t speed) {
  _scrollSpeed = speed;
  _scrollCount = _scrollSpeed;
  _scrollPos   = 0;
}

bool P165_data_struct::isScrollEnabled() {
  return _txtScrolling && _scrollAllowed;
}

void P165_data_struct::setScrollEnabled(bool scroll) {
  _scrollAllowed = scroll;
}

# if P165_DEBUG_INFO || P165_DEBUG_DEBUG

/**********************************************************************
 * Log the current buffer content with a prefix
 *********************************************************************/
void P165_data_struct::logBufferContent(String prefix) {
  String log;

  if (loglevelActiveFor(LOG_LEVEL_INFO) &&
      log.reserve(26 + 4 * _totalDigits)) {
    log = strformat(F("%s buffer: periods: %c"), prefix.c_str(), _periods ? 't' : 'f');

    for (uint8_t i = 0; i < _totalDigits; i++) {
      log += strformat(F("%c0x%X,%c%d(%d)"), i > 0 ? ',' : ' ', showbuffer[i], showperiods[i] ? '.' : ' ', i, showmap[i]);
    }
    addLogMove(LOG_LEVEL_INFO, log);
  }
}

# endif // if P165_DEBUG_INFO || P165_DEBUG_DEBUG

#endif // ifdef USES_P165

#include "../PluginStructs/P087_data_struct.h"

#ifdef USES_P087


// Needed also here for PlatformIO's library finder as the .h file
// is in a directory which is excluded in the src_filter
# include <ESPeasySerial.h>
# include <Regexp.h>


# include <vector>


P087_data_struct::~P087_data_struct() {
  delete easySerial;
  easySerial = nullptr;
}

void P087_data_struct::reset() {
  delete easySerial;
  easySerial = nullptr;
}

bool P087_data_struct::init(ESPEasySerialPort port, const int16_t serial_rx, const int16_t serial_tx, unsigned long baudrate,
                            uint8_t config) {
  if ((serial_rx < 0) && (serial_tx < 0)) {
    return false;
  }
  reset();
  easySerial = new (std::nothrow) ESPeasySerial(port, serial_rx, serial_tx);

  if (isInitialized()) {
    # if defined(ESP8266)
    easySerial->begin(baudrate, (SerialConfig)config);
    # elif defined(ESP32)
    easySerial->begin(baudrate, config);
    # endif // if defined(ESP8266)
    return true;
  }
  return false;
}

void P087_data_struct::post_init() {
  for (uint8_t i = 0; i < P87_MAX_CAPTURE_INDEX; ++i) {
    capture_index_used[i] = false;
  }
  regex_empty = _lines[P087_REGEX_POS].isEmpty();
  # ifndef BUILD_NO_DEBUG
  String log = F("P087_post_init:");
  # endif // ifndef BUILD_NO_DEBUG

  for (uint8_t i = 0; i < P087_NR_FILTERS; ++i) {
    // Create some quick lookup table to see if we have a filter for the specific index
    capture_index_must_not_match[i] = _lines[i * 3 + P087_FIRST_FILTER_POS + 1].toInt() == P087_Filter_Comp::NotEqual;
    int index = _lines[i * 3 + P087_FIRST_FILTER_POS].toInt();

    // Index is negative when not used.
    if ((index >= 0) && (index < P87_MAX_CAPTURE_INDEX) && (_lines[i * 3 + P087_FIRST_FILTER_POS + 2].length() > 0)) {
      # ifndef BUILD_NO_DEBUG
      log += strformat(F(" %d:%d"), i, index);
      # endif // ifndef BUILD_NO_DEBUG
      capture_index[i]          = index;
      capture_index_used[index] = true;
    }
  }
  # ifndef BUILD_NO_DEBUG
  addLogMove(LOG_LEVEL_DEBUG, log);
  # endif // ifndef BUILD_NO_DEBUG
}

bool P087_data_struct::isInitialized() const {
  return easySerial != nullptr;
}

void P087_data_struct::sendString(const String& data) {
  if (isInitialized() && (!data.isEmpty())) {
    setDisableFilterWindowTimer();
    easySerial->write(data.c_str());

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, concat(F("Proxy: Sending: "), data));
    }
  }
}

void P087_data_struct::sendData(uint8_t *data, size_t size) {
  if (isInitialized() && size) {
    setDisableFilterWindowTimer();
    easySerial->write(data, size);

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, strformat(F("Proxy: Sending %d bytes."), size));
    }
  }
}

bool P087_data_struct::loop() {
  if (!isInitialized()) {
    return false;
  }
  bool fullSentenceReceived = false;

  if (easySerial != nullptr) {
    int available = easySerial->available();

    while (available > 0 && !fullSentenceReceived) {
      // Look for end marker
      uint8_t c = easySerial->read();
      --available;

      if (available == 0) {
        available = easySerial->available();
        delay(0);
      }
      const size_t length = sentence_part.length();
      # ifdef LIMIT_BUILD_SIZE
      const bool addCharacter = (10u != c);
      const bool finished     = (13u == c);
      # else // ifdef LIMIT_BUILD_SIZE
      const bool addCharacter = (10u != c) || handle_binary;  // Skip LF in ascii-mode
      bool finished           = (13u == c) && !handle_binary; // Done on CR in ascii-mode

      if (0 != fixed_length) {
        finished = (length == fixed_length);

        if (finished) {
          available = 0; // Forced exit
        }
      }
      # endif // ifdef LIMIT_BUILD_SIZE

      if (finished) {
        bool valid = length > 0;

        for (size_t i = 0; i < length && valid
             # ifndef LIMIT_BUILD_SIZE
             && !handle_binary // Skip valid-ascii check
             # endif // ifndef LIMIT_BUILD_SIZE
             ; ++i) {
          if ((sentence_part[i] > 127) || (sentence_part[i] < 32)) {
            sentence_part = EMPTY_STRING;
            ++sentences_received_error;
            valid = false;
          }
        }

        if (valid) {
          fullSentenceReceived = true;
          last_sentence        = sentence_part;
          sentence_part        = EMPTY_STRING;
        }
      }
      else if (addCharacter) {
        sentence_part += static_cast<char>(c);
      }

      if (max_length_reached()) { fullSentenceReceived = true; }
    }
  }

  if (fullSentenceReceived) {
    ++sentences_received;
    length_last_received = last_sentence.length();
  }
  # ifndef LIMIT_BUILD_SIZE
  else if (handle_binary && last_sentence.isEmpty() && !sentence_part.isEmpty()) { // Receive binary data (no end-marker)
    fullSentenceReceived = true;
    last_sentence        = sentence_part;
    sentence_part        = EMPTY_STRING;
    ++sentences_received;
    length_last_received = last_sentence.length();
  }
  # endif // ifndef LIMIT_BUILD_SIZE

  return fullSentenceReceived;
}

bool P087_data_struct::getSentence(String& string) {
  string = last_sentence;

  if (string.isEmpty()) {
    return false;
  }
  last_sentence = EMPTY_STRING;
  return true;
}

void P087_data_struct::getSentencesReceived(uint32_t& succes, uint32_t& error, uint32_t& length_last) const {
  succes      = sentences_received;
  error       = sentences_received_error;
  length_last = length_last_received;
}

void P087_data_struct::setMaxLength(uint16_t maxlenght) {
  max_length = maxlenght;
}

void P087_data_struct::setLine(uint8_t varNr, const String& line) {
  if (varNr < P87_Nlines) {
    _lines[varNr] = line;
  }
}

String P087_data_struct::getRegEx() const {
  return _lines[P087_REGEX_POS];
}

uint16_t P087_data_struct::getRegExpMatchLength() const {
  return _lines[P087_NR_CHAR_USE_POS].toInt();
}

uint32_t P087_data_struct::getFilterOffWindowTime() const {
  return _lines[P087_FILTER_OFF_WINDOW_POS].toInt();
}

P087_Match_Type P087_data_struct::getMatchType() const {
  return static_cast<P087_Match_Type>(_lines[P087_MATCH_TYPE_POS].toInt());
}

bool P087_data_struct::invertMatch() const {
  switch (getMatchType()) {
    case Regular_Match:          // fallthrough
    case Global_Match:
      break;
    case Regular_Match_inverted: // fallthrough
    case Global_Match_inverted:
      return true;
    case Filter_Disabled:
      break;
  }
  return false;
}

bool P087_data_struct::globalMatch() const {
  switch (getMatchType()) {
    case Regular_Match: // fallthrough
    case Regular_Match_inverted:
      break;
    case Global_Match:  // fallthrough
    case Global_Match_inverted:
      return true;
    case Filter_Disabled:
      break;
  }
  return false;
}

String P087_data_struct::getFilter(uint8_t lineNr, uint8_t& capture, P087_Filter_Comp& comparator) const
{
  uint8_t varNr = lineNr * 3 + P087_FIRST_FILTER_POS;

  if ((varNr + 3) > P87_Nlines) { return EMPTY_STRING; }

  capture    = _lines[varNr++].toInt();
  comparator = equals(_lines[varNr++], '1') ? P087_Filter_Comp::NotEqual : P087_Filter_Comp::Equal;
  return _lines[varNr];
}

void P087_data_struct::setDisableFilterWindowTimer() {
  if (getFilterOffWindowTime() == 0) {
    disable_filter_window = 0;
  }
  else {
    disable_filter_window = millis() + getFilterOffWindowTime();
  }
}

bool P087_data_struct::disableFilterWindowActive() const {
  if (disable_filter_window != 0) {
    if (!timeOutReached(disable_filter_window)) {
      // We're still in the window where filtering is disabled
      return true;
    }
  }
  return false;
}

typedef std::pair<uint8_t, String> capture_tuple;
static std::vector<capture_tuple> capture_vector;


// called for each match
void P087_data_struct::match_callback(const char *match, const unsigned int length, const MatchState& ms)
{
  for (uint8_t i = 0; i < ms.level; i++)
  {
    capture_tuple tuple;
    tuple.first  = i;
    tuple.second = ms.GetCapture(i);
    capture_vector.push_back(tuple);
  } // end of for each capture
}

bool P087_data_struct::matchRegexp(String& received) const {
  size_t strlength = received.length();

  if (strlength == 0) {
    return false;
  }

  if (regex_empty || (getMatchType() == Filter_Disabled)) {
    return true;
  }


  const uint32_t regexp_match_length = getRegExpMatchLength();

  if ((regexp_match_length > 0) && (strlength > regexp_match_length)) {
    strlength = regexp_match_length;
  }

  // We need to do a const_cast here, but this only is valid as long as we
  // don't call a replace function from regexp.
  MatchState ms(const_cast<char *>(received.c_str()), strlength);

  bool match_result = false;

  capture_vector.clear();
  ms.GlobalMatch(getRegEx().c_str(), match_callback); // To allow the matched values be retrieved also when not using Global Match option

  if (globalMatch()) {
    const uint8_t vectorlength = capture_vector.size();

    for (uint8_t i = 0; i < vectorlength; ++i) {
      if ((capture_vector[i].first < P87_MAX_CAPTURE_INDEX) && capture_index_used[capture_vector[i].first]) {
        for (uint8_t n = 0; n < P087_NR_FILTERS; ++n) {
          unsigned int lines_index = n * 3 + P087_FIRST_FILTER_POS + 2;

          if ((capture_index[n] == capture_vector[i].first) && !(_lines[lines_index].isEmpty())) {
            String log;
            log.reserve(32);
            log = strformat(F("P087: Index: %d Found %s"), capture_vector[i].first, capture_vector[i].second.c_str());

            // Found a Capture Filter with this capture index.
            if (capture_vector[i].second.equals(_lines[lines_index])) {
              log += F(" Matches");

              // Found a match. Now check if it is supposed to be one or not.
              if (capture_index_must_not_match[n]) {
                log += F(" (!=)");
                addLogMove(LOG_LEVEL_INFO, log);
                return false;
              } else {
                match_result = true;
                log         += F(" (==)");
              }
            } else {
              log += F(" No Match");

              if (capture_index_must_not_match[n]) {
                log += F(" (!=)");
              } else {
                log += F(" (==)");
              }
              log += ' ';
              log += _lines[lines_index];
            }
            addLogMove(LOG_LEVEL_INFO, log);
          }
        }
      }
    }

    // capture_vector.clear(); // KEEP so we can use plugin_get_config_value to retrieve the values
  } else {
    char result = ms.Match(getRegEx().c_str());

    if (result == REGEXP_MATCHED) {
      # ifndef BUILD_NO_DEBUG

      if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
        addLogMove(LOG_LEVEL_DEBUG, strformat(F("Match at: %d Match Length: %d"), ms.MatchStart, ms.MatchLength));
      }
      # endif // ifndef BUILD_NO_DEBUG
      match_result = true;
    }
  }
  return match_result;
}

const __FlashStringHelper * P087_data_struct::MatchType_toString(P087_Match_Type matchType) {
  switch (matchType)
  {
    case P087_Match_Type::Regular_Match:          return F("Regular Match");
    case P087_Match_Type::Regular_Match_inverted: return F("Regular Match inverted");
    case P087_Match_Type::Global_Match:           return F("Global Match");
    case P087_Match_Type::Global_Match_inverted:  return F("Global Match inverted");
    case P087_Match_Type::Filter_Disabled:        return F("Filter Disabled");
  }
  return F("");
}

bool P087_data_struct::max_length_reached() const {
  if (max_length == 0) { return false; }
  return sentence_part.length() >= max_length;
}

void P087_data_struct::setLastSentence(String string) {
  last_sentence = string;
}

bool P087_data_struct::plugin_get_config_value(struct EventStruct *event,
                                               String            & string) {
  bool success               = false;
  const uint8_t vectorlength = capture_vector.size();
  char sep                   = '.';

  if ((-1 == string.indexOf(sep)) && (string.indexOf(',') >= 0)) {
    sep = ',';
  }
  const String cmd = parseString(string, 1, sep);

  # ifndef BUILD_NO_DEBUG
  addLog(LOG_LEVEL_DEBUG, concat(F("P087: Before GetConfig: "), string));
  # endif // ifndef BUILD_NO_DEBUG

  if (equals(cmd, F("group"))) {
    int32_t par2;

    if (validIntFromString(parseString(string, 2, sep), par2) &&
        (par2 >= 0)) {
      for (uint8_t i = 0; i < vectorlength && !success; ++i) { // Stop when we find the requested group
        # ifndef BUILD_NO_DEBUG
        addLog(LOG_LEVEL_DEBUG, strformat(F("P087: get group: %d = %s"),
                                          capture_vector[i].first,
                                          capture_vector[i].second.c_str()));
        # endif // ifndef BUILD_NO_DEBUG

        if (par2 == capture_vector[i].first) {
          string  = capture_vector[i].second;
          success = true;
        }
      }
    }
  }  else
  if (equals(cmd, F("next"))) {                                    // Get next group value after matching name
    const String name_ = parseString(string, 2, sep);

    for (uint8_t i = 0; i < (vectorlength - 1) && !success; ++i) { // Stop when we find the requested name
                                                                   // Loops until 1 BEFORE the end of the vector!
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_DEBUG, strformat(F("P087: get next: %d = %s => %s"),
                                        capture_vector[i].first,
                                        capture_vector[i].second.c_str(),
                                        capture_vector[i + 1].second.c_str()));
      # endif // ifndef BUILD_NO_DEBUG

      if (name_.equalsIgnoreCase(capture_vector[i].second)) {
        string  = capture_vector[i + 1].second; // Take NEXT value
        success = true;
      }
    }
  } // else...
  # ifndef BUILD_NO_DEBUG
  addLog(LOG_LEVEL_DEBUG, concat(F("P087: After GetConfig: "), string));
  # endif // ifndef BUILD_NO_DEBUG

  return success;
}

#endif // USES_P087

#include "../PluginStructs/P098_data_struct.h"

#ifdef USES_P098

# include "../ESPEasyCore/ESPEasyGPIO.h"

# include "../Commands/GPIO.h" // FIXME TD-er: Only needed till we can call GPIO commands from the ESPEasy core.


const __FlashStringHelper * P098_config_struct::toString(P098_config_struct::PWM_mode_type PWM_mode) {
  switch (PWM_mode) {
    case P098_config_struct::PWM_mode_type::NoPWM:  return F("No PWM");
    case P098_config_struct::PWM_mode_type::PWM:    return F("PWM");

    case P098_config_struct::PWM_mode_type::MAX_TYPE: break;
  }

  return F("");
}

P098_data_struct::P098_data_struct(const P098_config_struct& config) : _config(config) {}

P098_data_struct::~P098_data_struct() {
  if (initialized) {
    detachInterrupt(digitalPinToInterrupt(_config.limitA.gpio));
    detachInterrupt(digitalPinToInterrupt(_config.limitB.gpio));
    detachInterrupt(digitalPinToInterrupt(_config.encoder.gpio));
  }
}

bool P098_data_struct::begin(int pos, int limitApos, int limitBpos)
{
  if (!initialized) {
    initialized = true;

    const bool switchPosSet = pos != 0 && limitApos != 0;

    limitA.switchposSet = switchPosSet;
    limitA.switchpos    = switchPosSet ? limitApos : 0;
    limitB.switchpos    = limitBpos;
    position            = pos + limitA.switchpos;
    stop();
    state = P098_data_struct::State::Idle;

    if (setPinMode(_config.limitA)) {
      attachInterruptArg(
        digitalPinToInterrupt(_config.limitA.gpio),
        reinterpret_cast<void (*)(void *)>(ISRlimitA),
        this, CHANGE);
    }

    if (setPinMode(_config.limitB)) {
      attachInterruptArg(
        digitalPinToInterrupt(_config.limitB.gpio),
        reinterpret_cast<void (*)(void *)>(ISRlimitB),
        this, CHANGE);
    }

    if (setPinMode(_config.encoder)) {
      attachInterruptArg(
        digitalPinToInterrupt(_config.encoder.gpio),
        reinterpret_cast<void (*)(void *)>(ISRencoder),
        this, CHANGE); // Act on 'CHANGE', not on rising/falling
    }
  }

  return true;
}

bool P098_data_struct::loop()
{
  const State old_state(state);

  check_limit_switch(_config.limitA, limitA);
  check_limit_switch(_config.limitB, limitB);

  if (check_encoder_timeout(_config.encoder)) {
    stop();
    state = P098_data_struct::State::StopEncoderTimeout;  
    return old_state == state;
  }

  switch (state) {
    case P098_data_struct::State::Idle:
      return true;
    case P098_data_struct::State::RunFwd:
    {
      updatePosition();
      checkLimit(limitB);
      break;
    }
    case P098_data_struct::State::RunRev:
    {
      updatePosition();
      checkLimit(limitA);
      break;
    }
    case P098_data_struct::State::StopEncoderTimeout:
    case P098_data_struct::State::StopLimitSw:
    case P098_data_struct::State::StopPosReached:
      // Still in a state that needs inspection
      return false;
  }

  // Must check when state has changed from running to some other state
  return old_state == state;
}

bool P098_data_struct::homePosSet() const
{
  return limitA.switchposSet;
}

bool P098_data_struct::canRun()
{
  if (checkValidPortRange(PLUGIN_GPIO, _config.limitA.gpio) && !homePosSet()) { return false; }

  switch (state) {
    case P098_data_struct::State::Idle:
    case P098_data_struct::State::RunFwd:
    case P098_data_struct::State::RunRev:
      return true;
    case P098_data_struct::State::StopLimitSw:
    case P098_data_struct::State::StopPosReached:
    case P098_data_struct::State::StopEncoderTimeout:
      // Still in a state that needs inspection
      return false;
  }
  return false;
}

void P098_data_struct::findHome()
{
  pos_dest            = INT_MIN;
  limitA.switchposSet = false;
  startMoving();
}

void P098_data_struct::moveForward(int steps)
{
  if (steps <= 0) {
    pos_dest            = INT_MAX;
    limitB.switchposSet = false;
  } else {
    pos_dest = position + steps;
  }
  startMoving();
}

void P098_data_struct::moveReverse(int steps)
{
  if (steps > 0) {
    pos_dest = position - steps;
    startMoving();
  }
}

bool P098_data_struct::moveToPos(int pos)
{
  if (!canRun()) { return false; }
  const int offset = pos - getPosition();

  pos_dest = position + offset;
  startMoving();
  return true;
}

void P098_data_struct::stop()
{
  setPinState(_config.motorFwd, 0);
  setPinState(_config.motorRev, 0);

  lastVirtualSpeedApplied_us = 0;
}

int P098_data_struct::getPosition() const
{
  if (limitA.switchposSet) {
    return position - limitA.switchpos;
  }
  return position;
}

void P098_data_struct::getLimitSwitchStates(bool& limitA_triggered, bool& limitB_triggered) const
{
  limitA_triggered = limitA.state == P098_limit_switch_state::State::High ? 1 : 0;
  limitB_triggered = limitB.state == P098_limit_switch_state::State::High ? 1 : 0;

  /*
     limitA_triggered = _config.limitA.readState();
     limitB_triggered = _config.limitB.readState();
   */
}

void P098_data_struct::getLimitSwitchPositions(int& limitApos, int& limitBpos) const
{
  limitApos = limitA.switchposSet ? limitA.switchpos : 0;
  limitBpos = limitB.switchposSet ? limitB.switchpos : 0;
}

void P098_data_struct::startMoving()
{
  if(pos_dest == 0) {
    pos_overshoot = -_config.pos0supplement;
  }

  // Stop first, to make sure both outputs will not be set high
  stop();

  if (pos_dest > position) {
    state = P098_data_struct::State::RunFwd;
    setPinState(_config.motorFwd, 1);
  } else {
    state = P098_data_struct::State::RunRev;
    setPinState(_config.motorRev, 1);
  }
  // Touch the timer, so it will not immediately timeout.
  enc_lastChanged_us = getMicros64();

  if(_config.virtualSpeed > 0) {
    lastVirtualSpeedApplied_us = enc_lastChanged_us;
  }
}

void P098_data_struct::updatePosition()
{
  if(lastVirtualSpeedApplied_us == 0) {
    return; 
  }

  int direction = 0;
  switch (state) {
    case P098_data_struct::State::RunFwd:
    {
      direction = 1;
      break;
    }
    case P098_data_struct::State::RunRev:
    {
      direction = -1;
      break;
    }
    default:
      return;
  }

  uint64_t time = getMicros64();

  uint64_t steps = ((time-lastVirtualSpeedApplied_us)*((uint64_t)_config.virtualSpeed))/1000ul;
  if(_config.PWM_mode == P098_config_struct::PWM_mode_type::NoPWM) {
    position += direction*steps;
  } else {
    position += direction*steps*_config.pwm_duty_cycle/1023; // pwm range is 1023
  }
  
  lastVirtualSpeedApplied_us = time;
}

void P098_data_struct::checkLimit(volatile P098_limit_switch_state& switch_state)
{
  if (switch_state.state == P098_limit_switch_state::State::High) {
    stop();
    state = P098_data_struct::State::StopLimitSw;
    return;
  }
  checkPosition();
}

void P098_data_struct::checkPosition()
{
  bool mustStop = false;

  switch (state) {
    case P098_data_struct::State::RunFwd:

      mustStop = ((position + pos_overshoot) >= pos_dest);
      break;
    case P098_data_struct::State::RunRev:

      mustStop = ((position - pos_overshoot) <= pos_dest);
      break;
    default:
      return;
  }

  if (mustStop) {
    stop();
    position -= pos_overshoot;
    pos_overshoot = 0;
    state = P098_data_struct::State::StopPosReached;

    /*
       // Correct for position error
       if (std::abs(position - pos_dest) > 10) {
       startMoving();
       }
     */
  }
}

void P098_data_struct::setPinState(const P098_GPIO_config& gpio_config, int8_t state)
{
  // FIXME TD-er: Must move this code to the ESPEasy core code.
  uint8_t mode = PIN_MODE_OUTPUT;

  state = state == 0 ? gpio_config.low() : gpio_config.high();
  uint32_t key = createKey(PLUGIN_GPIO, gpio_config.gpio);

  if (globalMapPortStatus[key].mode != PIN_MODE_OFFLINE)
  {
    int8_t currentState;
    GPIO_Read(PLUGIN_GPIO, gpio_config.gpio, currentState);

    if (currentState == -1) {
      mode  = PIN_MODE_OFFLINE;
      state = -1;
    }

    switch (_config.PWM_mode) {
      case P098_config_struct::PWM_mode_type::NoPWM:
        if (mode == PIN_MODE_OUTPUT)  {
          createAndSetPortStatus_Mode_State(key, mode, state);
          GPIO_Write(
            PLUGIN_GPIO,
            gpio_config.gpio,
            state,
            mode);
        }
        break;
      case P098_config_struct::PWM_mode_type::PWM:
      {
        const uint32_t dutycycle = state == 0 ? 0 : _config.pwm_duty_cycle;
        const uint32_t fade_duration = _config.pwm_soft_startstop ? 
              100 /* (_config.encoder.timer_us / 1000) */
              : 0;
        uint32_t frequency = _config.pwm_freq;
        set_Gpio_PWM(
          gpio_config.gpio,
          dutycycle,
          fade_duration,
          frequency,
          key);
        if (state == 0) {
          // Turn off PWM mode too
          createAndSetPortStatus_Mode_State(key, PIN_MODE_OUTPUT, state);
          GPIO_Write(
            PLUGIN_GPIO,
            gpio_config.gpio,
            state,
            PIN_MODE_OUTPUT);
        }
      }
      break;
      case P098_config_struct::PWM_mode_type::MAX_TYPE:
        break;
    }
  }
}

bool P098_data_struct::setPinMode(const P098_GPIO_config& gpio_config)
{
  if (checkValidPortRange(PLUGIN_GPIO, gpio_config.gpio)) {
    pinMode(gpio_config.gpio, gpio_config.pullUp ? INPUT_PULLUP : INPUT);
    return true;
  }
  return false;
}

void P098_data_struct::check_limit_switch(
  const P098_GPIO_config          & gpio_config,
  volatile P098_limit_switch_state& switch_state)
{
  if (gpio_config.gpio == -1) {
    return;
  }
  // State is changed first in ISR, but compared after values are copied.
  const int triggerpos          = switch_state.triggerpos;
  const uint64_t lastChanged_us = switch_state.lastChanged_us;

  if (switch_state.state == P098_limit_switch_state::State::TriggerWaitBounce) {
    if (lastChanged_us != 0) {
      const uint64_t timeSinceLastTrigger = getMicros64() - lastChanged_us;

      if (timeSinceLastTrigger > gpio_config.timer_us) {
        mark_limit_switch_state(triggerpos, switch_state);
      }
    }
  }
}

void P098_data_struct::mark_limit_switch_state(
    int triggerpos, 
    volatile P098_limit_switch_state& switch_state)
{
  if (!switch_state.switchposSet) {
    switch_state.switchpos    = triggerpos;
    switch_state.switchposSet = true;
  }

  // Perform an extra check here on the state as it may have changed in the ISR call
  if (switch_state.state == P098_limit_switch_state::State::TriggerWaitBounce) {
    switch_state.state = P098_limit_switch_state::State::High;
  }
}

bool P098_data_struct::check_encoder_timeout(const P098_GPIO_config & gpio_config)
{
  if (gpio_config.gpio == -1) {
    return false;
  }
  if (enc_lastChanged_us == 0) {
    return false;
  }
  const bool expired = usecPassedSince(enc_lastChanged_us) > static_cast<int64_t>(_config.encoder.timer_us);
  if (!expired) {
    return false;
  }
  switch (state) {
    case P098_data_struct::State::RunFwd:
    {
      mark_limit_switch_state(position, limitB);
      break;
    }
    case P098_data_struct::State::RunRev:
    {
      mark_limit_switch_state(position, limitA);
      break;
    }
    default:
      return false;
  }
  return true;
}

void ICACHE_RAM_ATTR P098_data_struct::process_limit_switch(
  const P098_GPIO_config          & gpio_config,
  volatile P098_limit_switch_state& switch_state,
  int                               position)
{
  ISR_noInterrupts();
  {
    // Don't call gpio_config.readState() here
    const bool pinState        = 
      (DIRECT_pinRead_ISR(gpio_config.gpio) != 0) ^ gpio_config.inverted;
    const uint64_t currentTime = getMicros64();


    switch (switch_state.state) {
      case P098_limit_switch_state::State::Low:

        if (pinState) {
          switch_state.state          = P098_limit_switch_state::State::TriggerWaitBounce;
          switch_state.lastChanged_us = currentTime;
          switch_state.triggerpos     = position;
        }
        break;
      case P098_limit_switch_state::State::TriggerWaitBounce:
      {
        // Do not evaluate the debounce time here, evaluate in the loop
        if (pinState) {
          // Only situation we can get here is when we missed a low state interrupt.
          switch_state.lastChanged_us = currentTime;
          switch_state.triggerpos     = position;
        } else {
          switch_state.state          = P098_limit_switch_state::State::Low;
          switch_state.lastChanged_us = 0;
          switch_state.triggerpos     = 0;
        }
        break;
      }
      case P098_limit_switch_state::State::High:

        if (!pinState) {
          switch_state.state          = P098_limit_switch_state::State::Low;
          switch_state.lastChanged_us = 0;
          switch_state.triggerpos     = 0;
        }
        break;
    }
  }
  ISR_interrupts(); // enable interrupts again.
}

void ICACHE_RAM_ATTR P098_data_struct::ISRlimitA(P098_data_struct *self)
{
  process_limit_switch(self->_config.limitA, self->limitA, self->position);
}

void ICACHE_RAM_ATTR P098_data_struct::ISRlimitB(P098_data_struct *self)
{
  process_limit_switch(self->_config.limitB, self->limitB, self->position);
}

void ICACHE_RAM_ATTR P098_data_struct::ISRencoder(P098_data_struct *self)
{
  ISR_noInterrupts();

  switch (self->state) {
    case P098_data_struct::State::RunFwd:
      ++(self->position);
      break;
    case P098_data_struct::State::RunRev:
      --(self->position);
      break;
    default:
      ISR_interrupts(); // enable interrupts again.
      return;
  }
  self->enc_lastChanged_us = getMicros64();
  ISR_interrupts(); // enable interrupts again.
}

#endif // ifdef USES_P098

#include "../PluginStructs/P075_data_struct.h"

#ifdef USES_P075


P075_data_struct::P075_data_struct(ESPEasySerialPort port, int rx, int tx, uint32_t baud) : rxPin(rx), txPin(tx), baudrate(baud) {
  if ((baudrate < 9600) || (baudrate > 115200)) {
    baudrate = 9600;
  }
  easySerial = new (std::nothrow) ESPeasySerial(port, rx, tx, false, RXBUFFSZ);

  if (easySerial != nullptr) {
    easySerial->begin(baudrate);
    easySerial->flush();
  }
}

P075_data_struct::~P075_data_struct() {
  if (easySerial != nullptr) {
    easySerial->flush();
    delete easySerial;
    easySerial = nullptr;
  }
}

void P075_data_struct::loadDisplayLines(taskIndex_t taskIndex) {
  LoadCustomTaskSettings(taskIndex, displayLines, P75_Nlines, P75_Nchars);
}

String P075_data_struct::getLogString() const {
  String result;

  if (easySerial != nullptr) {
    result = easySerial->getLogString();
  }
  return result;
}

#endif // ifdef USES_P075

#include "../PluginStructs/P015_data_struct.h"
#ifdef USES_P015

# include "../Helpers/Misc.h"


# define TSL2561_CMD           0x80
# define TSL2561_REG_CONTROL   0x00
# define TSL2561_REG_TIMING    0x01
# define TSL2561_REG_DATA_0    0x0C
# define TSL2561_REG_DATA_1    0x0E


P015_data_struct::P015_data_struct(uint8_t i2caddr, unsigned int gain, uint8_t integration) :
  _gain(gain),
  _i2cAddr(i2caddr),
  _integration(integration)
{
  // If gain = false (0), device is set to low gain (1X)
  // If gain = high (1), device is set to high gain (16X)

  _gain16xActive = gain == 1;

  if (!useAutoGain()) {
    _gain16xActive = gain == 1;
  }
}

bool P015_data_struct::performRead(float& luxVal,
                                   float& infraredVal,
                                   float& broadbandVal,
                                   float& ir_broadband_ratio)
{
  bool success = false;
  int  attempt = useAutoGain() ? 2 : 1;

  while (!success && attempt > 0) {
    --attempt;

    float ms; // Integration ("shutter") time in milliseconds

    // If time = 0, integration will be 13.7ms
    // If time = 1, integration will be 101ms
    // If time = 2, integration will be 402ms
    unsigned char time = _integration;

    plugin_015_setTiming(_gain16xActive, time, ms);
    setPowerUp();
    delayBackground(ms); // FIXME TD-er: Do not use delayBackground but collect data later.
    unsigned int data0, data1;

    if (getData(data0, data1))
    {
      float lux;       // Resulting lux value
      float infrared;  // Resulting infrared value
      float broadband; // Resulting broadband value


      // Perform lux calculation:
      success = !ADC_saturated(time,  data0) && !ADC_saturated(time, data1);
      getLux(_gain16xActive, ms, data0, data1, lux, infrared, broadband);

      if (useAutoGain()) {
        if (_gain16xActive) {
          // Last reading was using 16x gain
          // Check using some margin to see if gain is still needed
          if (ADC_saturated(time,  data0 * 16)) {
            _gain16xActive = false;
          }
        } else {
          // Check using some margin to see if gain will improve reading resolution
          if (lux < 40) {
            _gain16xActive = true;
          }
        }
      }

      if (success) {
        if (broadband > 0.0f) {
          // Store the ratio in an unused user var. (should we make it available?)
          // Only store/update it when not close to the limits of both ADC ranges.
          // When using this value to compute extended ranges, it must not be using a ratio taken from a
          // heated sensor, since then the IR part may be off quite a bit resulting in very unrealistic values.
          if (!ADC_saturated(time,  data0 * 2) && !ADC_saturated(time, data1 * 2)) {
            ir_broadband_ratio = infrared / broadband;
          }
        }
      } else {
        // Use last known ratio to reconstruct the broadband value
        // If IR is saturated, output the max value based on the last known ratio.
        if ((ir_broadband_ratio > 0.0f) && (_gain == P015_EXT_AUTO_GAIN)) {
          data0 = static_cast<float>(data1) / ir_broadband_ratio;
          getLux(_gain16xActive, ms, data0, data1, lux, infrared, broadband);
          success = true;
        }
      }
      luxVal       = lux;
      infraredVal  = infrared;
      broadbandVal = broadband;
    }
    else
    {
      // getData() returned false because of an I2C error, inform the user.
      addLog(LOG_LEVEL_ERROR, F("TSL2561: i2c error"));
      success = false;
      attempt = 0;
    }
  }
  return success;
}

bool P015_data_struct::begin()
{
  // Wire.begin();   called in ESPEasy framework
  return true;
}

bool P015_data_struct::readByte(unsigned char address, unsigned char& value)

// Reads a byte from a TSL2561 address
// Address: TSL2561 address (0 to 15)
// Value will be set to stored byte
// Returns true (1) if successful, false (0) if there was an I2C error
{
  // Set up command byte for read
  bool is_ok = false;
  const uint8_t val = I2C_read8_reg(_i2cAddr, (address & 0x0F) | TSL2561_CMD, &is_ok);

  if (is_ok) {
    value = val;
  }
  return is_ok;
}

bool P015_data_struct::writeByte(unsigned char address, unsigned char value)

// Write a byte to a TSL2561 address
// Address: TSL2561 address (0 to 15)
// Value: byte to write to address
// Returns true (1) if successful, false (0) if there was an I2C error
// (Also see getError() above)
{
  return I2C_write8_reg(_i2cAddr, (address & 0x0F) | TSL2561_CMD, value);
}

bool P015_data_struct::readUInt(unsigned char address, unsigned int& value)

// Reads an unsigned integer (16 bits) from a TSL2561 address (low byte first)
// Address: TSL2561 address (0 to 15), low byte first
// Value will be set to stored unsigned integer
// Returns true (1) if successful, false (0) if there was an I2C error
// (Also see getError() above)
{
  bool success = false;
  const uint16_t val = I2C_read16_LE_reg(_i2cAddr, (address & 0x0F) | TSL2561_CMD, &success);
  if (success) {
    value = val;
  }
  return success;
}

bool P015_data_struct::writeUInt(unsigned char address, unsigned int value)

// Write an unsigned integer (16 bits) to a TSL2561 address (low byte first)
// Address: TSL2561 address (0 to 15), low byte first
// Value: unsigned int to write to address
// Returns true (1) if successful, false (0) if there was an I2C error
// (Also see getError() above)
{
  // Split int into lower and upper bytes, write each byte
  if (writeByte(address, lowByte(value))
      && writeByte(address + 1, highByte(value))) {
    return true;
  }

  return false;
}

bool P015_data_struct::plugin_015_setTiming(bool gain, unsigned char time)

// If gain = false (0), device is set to low gain (1X)
// If gain = high (1), device is set to high gain (16X)
// If time = 0, integration will be 13.7ms
// If time = 1, integration will be 101ms
// If time = 2, integration will be 402ms
// If time = 3, use manual start / stop
// Returns true (1) if successful, false (0) if there was an I2C error
// (Also see getError() below)
{
  unsigned char timing;

  // Get timing byte
  if (readByte(TSL2561_REG_TIMING, timing))
  {
    // Set gain (0 or 1)
    if (gain) {
      timing |= 0x10;
    }
    else {
      timing &= ~0x10;
    }

    // Set integration time (0 to 3)
    timing &= ~0x03;
    timing |= (time & 0x03);

    // Write modified timing byte back to device
    if (writeByte(TSL2561_REG_TIMING, timing)) {
      return true;
    }
  }
  return false;
}

bool P015_data_struct::plugin_015_setTiming(bool gain, unsigned char time, float& ms)

// If gain = false (0), device is set to low gain (1X)
// If gain = high (1), device is set to high gain (16X)
// If time = 0, integration will be 13.7ms
// If time = 1, integration will be 101ms
// If time = 2, integration will be 402ms
// If time = 3, use manual start / stop (ms = 0)
// ms will be set to integration time
// Returns true (1) if successful, false (0) if there was an I2C error
// (Also see getError() below)
{
  // Calculate ms for user
  switch (time)
  {
    case 0: ms  = 13.7f; break;
    case 1: ms  = 101; break;
    case 2: ms  = 402; break;
    default: ms = 402; // used in a division, so do not use 0
  }

  // Set integration using base function
  return plugin_015_setTiming(gain, time);
}

// Determine if either sensor saturated (max depends on clock freq. and integration time)
// If so, abandon ship (calculation will not be accurate)
bool P015_data_struct::ADC_saturated(unsigned char time, unsigned int value) {
  unsigned int max_ADC_count = 65535;

  switch (time)
  {
    case 0: max_ADC_count = 5047; break;
    case 1: max_ADC_count = 37177; break;
    case 2:
    default: break;
  }
  return value >= max_ADC_count;
}

bool P015_data_struct::setPowerUp(void)

// Turn on TSL2561, begin integrations
// Returns true (1) if successful, false (0) if there was an I2C error
// (Also see getError() below)
{
  // Write 0x03 to command byte (power on)
  return writeByte(TSL2561_REG_CONTROL, 0x03);
}

bool P015_data_struct::setPowerDown(void)

// Turn off TSL2561
// Returns true (1) if successful, false (0) if there was an I2C error
// (Also see getError() below)
{
  // Clear command byte (power off)
  return writeByte(TSL2561_REG_CONTROL, 0x00);
}

bool P015_data_struct::getData(unsigned int& data0, unsigned int& data1)

// Retrieve raw integration results
// data0 and data1 will be set to integration results
// Returns true (1) if successful, false (0) if there was an I2C error
// (Also see getError() below)
{
  // Get data0 and data1 out of result registers
  if (readUInt(TSL2561_REG_DATA_0, data0) && readUInt(TSL2561_REG_DATA_1, data1)) {
    return true;
  }

  return false;
}

void P015_data_struct::getLux(unsigned char gain,
                              float         ms,
                              unsigned int  CH0,
                              unsigned int  CH1,
                              float       & lux,
                              float       & infrared,
                              float       & broadband)

// Convert raw data to lux
// gain: 0 (1X) or 1 (16X), see setTiming()
// ms: integration time in ms, from setTiming() or from manual integration
// CH0, CH1: results from getData()
// lux will be set to resulting lux calculation
// returns true (1) if calculation was successful
// RETURNS false (0) AND lux = 0.0 IF EITHER SENSOR WAS SATURATED (0XFFFF)
{
  float ratio, d0, d1;

  // Convert from unsigned integer to floating point
  d0 = CH0; d1 = CH1;

  // We will need the ratio for subsequent calculations
  ratio = d1 / d0;

  // save original values
  infrared  = d1;
  broadband = d0;

  // Normalize for integration time
  d0 *= (402.0f / ms);
  d1 *= (402.0f / ms);

  // Normalize for gain
  if (!gain)
  {
    d0 *= 16;
    d1 *= 16;
  }

  // Determine lux per datasheet equations:
  if (ratio < 0.5f)
  {
    lux = 0.0304f * d0 - 0.062f * d0 * powf(ratio, 1.4f);
  } else if (ratio < 0.61f)
  {
    lux = 0.0224f * d0 - 0.031f * d1;
  } else if (ratio < 0.80f)
  {
    lux = 0.0128f * d0 - 0.0153f * d1;
  } else if (ratio < 1.30f)
  {
    lux = 0.00146f * d0 - 0.00112f * d1;
  } else {
    // ratio >= 1.30
    lux = 0.0f;
  }
}

#endif // ifdef USES_P015


#include "../PluginStructs/P056_data_struct.h"

#ifdef USES_P056

P056_data_struct::P056_data_struct(EventStruct *event) {
  delete SDS;

  const ESPEasySerialPort port = static_cast<ESPEasySerialPort>(CONFIG_PORT);
  SDS = new (std::nothrow) CjkSDS011(port, CONFIG_PIN1, CONFIG_PIN2);

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, strformat(F("SDS  : Init OK  ESP GPIO-pin RX:%d TX:%d"), CONFIG_PIN1, CONFIG_PIN2));
  }
}

P056_data_struct::~P056_data_struct() {
  delete SDS;
  SDS = nullptr;
}

void P056_data_struct::Process() {
  if (isInitialized()) {
    this->SDS->Process();
  }
}

bool P056_data_struct::available() {
  if (isInitialized()) {
    return this->SDS->available();
  } else {
    return false;
  }
}

float P056_data_struct::GetPM2_5() {
  if (isInitialized()) {
    return this->SDS->GetPM2_5();
  } else {
    return 0.0f;
  }
}

float P056_data_struct::GetPM10_() {
  if (isInitialized()) {
    return this->SDS->GetPM10_();
  } else {
    return 0.0f;
  }
}

bool P056_data_struct::ReadAverage(float pm25, float pm10) {
  if (isInitialized()) {
    return this->SDS->ReadAverage(pm25, pm10);
  } else {
    return false;
  }
}

void P056_data_struct::SetWorkingPeriod(int minutes) {
  if (isInitialized()) {
    this->SDS->SetWorkingPeriod(minutes);
  }
}

#endif

#include "../PluginStructs/P008_data_struct.h"

#ifdef USES_P008

/**
 * Convert/cast a hexadecimal input to a decimal representation, so 0x1234 (= 4660) comes out as 1234.
 *
 * //FIXME Move to a more global place to also be used elsewhere?
 */
uint64_t P008_data_struct::castHexAsDec(uint64_t hexValue) {
  uint64_t result = 0;
  uint8_t  digit;

  uint64_t factor = 1;

  for (int i = 0; i < 8; ++i) {
    digit = (hexValue & 0x0000000F);

    if (digit > 10) {
      digit = 0; // Cast by dropping any non-decimal input
    }

    if (digit > 0) {
      result += (digit * factor);
    }
    hexValue >>= 4;

    if (hexValue == 0) {
      break; // Stop when no more to process
    }
    factor *= 10;
  }
  return result;
}

/**************************************************************************
* Constructor
**************************************************************************/
P008_data_struct::P008_data_struct(struct EventStruct *event) {
  _pin1 = CONFIG_PIN1;
  _pin2 = CONFIG_PIN2;
}

/*****************************************************
* Destructor
*****************************************************/
P008_data_struct::~P008_data_struct() {
  if (initialised) {
    detachInterrupt(digitalPinToInterrupt(_pin1));
    detachInterrupt(digitalPinToInterrupt(_pin2));
  }
}

/**************************************************************************
* plugin_init Initialize interrupt handling
**************************************************************************/
bool P008_data_struct::plugin_init(struct EventStruct *event) {
  if (validGpio(_pin1) && validGpio(_pin2)) {
    pinMode(_pin1, INPUT_PULLUP);
    pinMode(_pin2, INPUT_PULLUP);

    // Keep 'old' setting for backward compatibility
    uint8_t _p1 = _pin1;
    uint8_t _p2 = _pin2;

    if (P008_COMPATIBILITY != 0) {
      _p1 = _pin2; // Original code had the pins swapped, swap 'm back to be Wiegand compatible
      _p2 = _pin1;
    }
    attachInterruptArg(digitalPinToInterrupt(_p1),
                       reinterpret_cast<void (*)(void *)>(Plugin_008_interrupt1),
                       this,
                       FALLING);
    attachInterruptArg(digitalPinToInterrupt(_p2),
                       reinterpret_cast<void (*)(void *)>(Plugin_008_interrupt2),
                       this,
                       FALLING);
    initialised = true;
  }
  return initialised;
}

/*****************************************************
* plugin_once_a_second
*****************************************************/
bool P008_data_struct::plugin_once_a_second(struct EventStruct *event) {
  bool success = false;

  if (initialised) {
    if (bitCount > 0) {
      success = true; // Let's assume read succeeded
      uint64_t keyMask = 0ull;

      if ((bitCount % 4 == 0) && ((keyBuffer & 0xF) == 11)) {
        // a number of keys were pressed and finished by #
        keyBuffer   = keyBuffer >> 4; // Strip #
        bufferValid = true;
        bufferBits  = bitCount;
      } else if (bitCount == P008_DATA_BITS) {
        // read a tag
        keyBuffer = keyBuffer >> 1;                 // Strip leading and trailing parity bits from the keyBuffer

        keyMask = (0x1ull << (P008_DATA_BITS - 2)); // Shift in 1 just past the number of remaining bits
        keyMask--;                                  // Decrement by 1 to get 0xFFFFFFFFFFFF...
        keyBuffer  &= keyMask;
        bufferValid = true;
        bufferBits  = bitCount;
      } else {
        // not enough bits, maybe next time
        timeoutCount++;
        bufferValid = false;
        bufferBits  = 0u;

        if (timeoutCount > P008_TIMEOUT_LIMIT) {
          if (loglevelActiveFor(LOG_LEVEL_INFO)) {
            addLogMove(LOG_LEVEL_INFO, concat(F("RFID : reset bits: "), static_cast<int>(bitCount)));
          }

          // reset after ~5 sec
          keyBuffer    = 0ull;
          bitCount     = 0u;
          timeoutCount = 0u;
        }
        success = false;
      }

      if (success) {
        uint64_t old_key = UserVar.getSensorTypeLong(event->TaskIndex);
        bool     new_key = false;

        if (P008_HEX_AS_DEC == 1) {
          keyBuffer = castHexAsDec(keyBuffer);
        }

        if (old_key != keyBuffer) {
          UserVar.setSensorTypeLong(event->TaskIndex, keyBuffer);
          new_key = true;
        }

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          // write log
          String log = F("RFID : ");

          if (new_key) {
            log += F("New Tag: ");
          } else {
            log += F("Old Tag: ");
          }
          log += strformat(F("%s, 0x%s, mask: 0x%s Bits: %d"),
                           ull2String(keyBuffer).c_str(),
                           ull2String(keyBuffer, 16).c_str(),
                           ull2String(keyMask,   16).c_str(),
                           static_cast<uint8_t>(bitCount));
          addLogMove(LOG_LEVEL_INFO, log);
        }

        // reset everything
        keyBuffer    = 0ull;
        bitCount     = 0u;
        timeoutCount = 0u;

        if (new_key) { sendData(event); }
        uint32_t resetTimer = P008_REMOVE_TIMEOUT;

        if (resetTimer < 250) { resetTimer = 250; }
        Scheduler.setPluginTaskTimer(resetTimer, event->TaskIndex, event->Par1);

        // Used during debugging
        // String   info;
        // uint64_t invalue  = 0x1234;
        // uint64_t outvalue = castHexAsDec(invalue);
        // info.reserve(40);
        // info += F("Test castHexAsDec(");
        // info += (ESPEASY_RULES_FLOAT_TYPE)invalue;
        // info += F(") => ");
        // info += (ESPEASY_RULES_FLOAT_TYPE)outvalue;
        // addLog(LOG_LEVEL_INFO, info);
      }
    }
  }
  return success;
}

/*****************************************************
* plugin_timer_in
*****************************************************/
bool P008_data_struct::plugin_timer_in(struct EventStruct *event) {
  if (initialised && (P008_AUTO_REMOVE == 0)) { // P008_AUTO_REMOVE check uses inversed logic!
    // Reset card id on timeout
    UserVar.setSensorTypeLong(event->TaskIndex, P008_REMOVE_VALUE);
    bufferValid = true;
    bufferBits  = P008_DATA_BITS;
    addLog(LOG_LEVEL_INFO, F("RFID : Removed Tag"));

    if (P008_REMOVE_EVENT == 1) {
      sendData(event);
    }
    return true;
  }
  return false;
}

/***********************************************************************
 * shift_bit_in_buffer
 **********************************************************************/
void IRAM_ATTR P008_data_struct::Plugin_008_shift_bit_in_buffer(P008_data_struct *self,
                                                                uint8_t           bit) {
  self->keyBuffer = self->keyBuffer << 1; // Left shift the number (effectively multiplying by 2)

  if (bit) { self->keyBuffer |= 1ull; }   // Add the 1 (not necessary for the zeroes)
  self->bitCount++;                       // Increment the bit count
}

/*********************************************************************
* Interrupt 1 : Handle 1 bits
*********************************************************************/
void IRAM_ATTR P008_data_struct::Plugin_008_interrupt1(P008_data_struct *self) {
  // We've received a 1 bit. (bit 0 = high, bit 1 = low)
  Plugin_008_shift_bit_in_buffer(self, 1); // Shift in a 1
}

/*********************************************************************
* Interrupt 2 : Handle 0 bits
*********************************************************************/
void IRAM_ATTR P008_data_struct::Plugin_008_interrupt2(P008_data_struct *self) {
  // We've received a 0 bit. (bit 0 = low, bit 1 = high)
  Plugin_008_shift_bit_in_buffer(self, 0); // Shift in a 0
}

/********************************************************************
* plugin_get_config
********************************************************************/
bool P008_data_struct::plugin_get_config(struct EventStruct *event,
                                         String            & string) {
  bool success = false;

  if (initialised) {
    String sub = parseString(string, 1);

    if (sub.equals(F("tagstr"))) { // Format tag as hex/dec
      uint64_t tag  = UserVar.getSensorTypeLong(event->TaskIndex);
      uint8_t  bits = bufferBits == 0 ? (P008_DATA_BITS - (P008_DATA_BITS % 4)) / 4 : bufferBits;
      string  = formatToHex_no_prefix(tag, bits);
      success = true;
    } else
    if (sub.equals(F("tagsize")) && bufferValid) { // Last tagsize in characters
      string  = (bufferBits - (bufferBits % 4)) / 4;
      success = true;
    } else
    if (sub.equals(F("tagbits")) && bufferValid) { // Last tagsize in bits
      string  = bufferBits;
      success = true;
    } else
    if (sub.equals(F("tagvalid"))) { // Buffer valid
      string  = bufferValid;
      success = true;
    }
  }

  return success;
}

#endif // ifdef USES_P008

#include "../PluginStructs/P150_data_struct.h"

#ifdef USES_P150

P150_data_struct::P150_data_struct(struct EventStruct *event) {
  _deviceAddress     = P150_I2C_ADDRESS;
  _config            = P150_CONFIG;
  _temperatureOffset = P150_TEMPERATURE_OFFSET / 10.0f; // Per 0.1 degree
  _rawEnabled        = P150_GET_OPT_ENABLE_RAW;
  _logEnabled        = P150_GET_OPT_ENABLE_LOG;
  # if P150_USE_EXTRA_LOG
  _extraLog = P150_GET_OPT_EXTRA_LOG;
  # endif // if P150_USE_EXTRA_LOG
}

/**
 * init(): Initialize sensor
 * - Set config
 * - Set temperature offset if <> 0
 * Will fail if no sensor is found, or the device id doesn't match (0x0117)
 */
bool P150_data_struct::init() {
  // make sure the TMP will acknowledge over I2C
  if ((_deviceAddress < 0) || (I2C_wakeup(_deviceAddress) != 0)) {
    return false;
  }

  uint16_t deviceID = I2C_read16_reg(_deviceAddress, TMP117_DEVICE_ID); // reads registers into rawData

  // make sure the device ID reported by the TMP is correct
  // should always be 0x0117
  if (deviceID != TMP117_DEVICE_ID_VALUE) {
    # ifndef BUILD_NO_DEBUG
    addLog(LOG_LEVEL_ERROR, concat(F("TMP117: Device ID mismatch: "), formatToHex(deviceID, 4)));
    # endif // ifndef BUILD_NO_DEBUG
    return false;
  }

  setConfig();

  if (!essentiallyZero(_temperatureOffset)) {
    setTemperatureOffset(_temperatureOffset); // set offset
  }

  return true;                                // returns true if all the checks pass
}

/**
 * Set configuration, also used to start a one-shot conversion
 */
void P150_data_struct::setConfig() {
  uint16_t config = I2C_read16_reg(_deviceAddress, TMP117_CONFIGURATION); // Read current configuration

  config &= P150_CONFIG_RESET_MASK;                                       // Reset the bits we want to update
  config |= _config;                                                      // Apply (new) config settings
  I2C_write16_reg(_deviceAddress, TMP117_CONFIGURATION, config);          // Update config
}

/**
 * plugin_read(): Fetch last read values
 */
bool P150_data_struct::plugin_read(struct EventStruct *event) {
  if (_readValid && definitelyGreaterThan(_finalTempC, -256.0f)) {
    UserVar.setFloat(event->TaskIndex, 0, _finalTempC);
    UserVar.setFloat(event->TaskIndex, 1, _digitalTempC);

    if (_logEnabled && loglevelActiveFor(LOG_LEVEL_INFO)) {
      String log = strformat(F("TMP117: Temperature: %sC"), formatUserVarNoCheck(event, 0).c_str());

      if (_rawEnabled) {
        log += concat(F(", Raw: "), formatUserVarNoCheck(event, 1));
      }
      addLogMove(LOG_LEVEL_INFO, log);
    }

    if (P150_GET_CONF_CONVERSION_MODE == P150_CONVERSION_ONE_SHOT) {
      setConfig(); // Start the next one-shot measurement
    }
    return true;
  }
  return false;
}

/**
 * plugin_once_a_second(): Read sensor if data is available
 * Will return true if data is actually available and valid
 */
bool P150_data_struct::plugin_once_a_second(struct EventStruct *event) {
  if (dataReady()) {
    readTemp();
    _readValid = true;

    # if P150_USE_EXTRA_LOG

    if (_extraLog && loglevelActiveFor(LOG_LEVEL_INFO)) {
      String log = strformat(F("TMP117: Read temp: %.2fC, raw: %d"),
                             _finalTempC,
                             _digitalTempC);

      if (P150_GET_CONF_CONVERSION_MODE == P150_CONVERSION_ONE_SHOT) {
        log += F(" (one-shot)");
      }
      addLog(LOG_LEVEL_INFO, log);
    }
    # endif // if P150_USE_EXTRA_LOG
  }
  return false;
}

/**
 * Private: readTemp()
 * Read raw temperature value and calculate final temperature
 */
float P150_data_struct::readTemp() {
  _digitalTempC = I2C_read16_reg(_deviceAddress, TMP117_TEMP_RESULT);

  _finalTempC = _digitalTempC * TMP117_RESOLUTION; // Multiplied by the resolution for digital to final temp

  return _finalTempC;
}

void P150_data_struct::setTemperatureOffset(float offset) {
  const int16_t resolutionOffset = offset / TMP117_RESOLUTION;           // Divide by resolution to send to the sensor

  I2C_write16_reg(_deviceAddress, TMP117_TEMP_OFFSET, resolutionOffset); // Write to the offset temperature register
}

/**
 * Private: dataReady()
 * Check if sensor os ready processing the measurement(s)
 */
bool P150_data_struct::dataReady() {
  const uint16_t response = I2C_read16_reg(_deviceAddress, TMP117_CONFIGURATION);

  // If statement to see if the 13th bit of the register is 1 or not
  return bitRead(response, 13);
}

#endif // ifdef USES_P150

#include "../PluginStructs/P129_data_struct.h"

#ifdef USES_P129
# include <GPIO_Direct_Access.h>

// **************************************************************************/
// Constructor
// **************************************************************************/
P129_data_struct::P129_data_struct(int8_t  dataPin,
                                   int8_t  clockPin,
                                   int8_t  enablePin,
                                   int8_t  loadPin,
                                   uint8_t chipCount)
  : _dataPin(dataPin), _clockPin(clockPin), _enablePin(enablePin), _loadPin(loadPin), _chipCount(chipCount) {}


bool P129_data_struct::plugin_init(struct EventStruct *event) {
  if (isInitialized()) {
    for (uint8_t i = 0; i < P129_MAX_CHIP_COUNT; ++i) { // Clear entire buffer
      readBuffer[i] = 0;
    }

    // Prepare all used GPIO pins
    if (validGpio(_enablePin)) { pinMode(_enablePin, OUTPUT); }
    pinMode(_loadPin,  OUTPUT);
    pinMode(_clockPin, OUTPUT);
    pinMode(_dataPin,  INPUT);
    DIRECT_pinWrite(_loadPin, HIGH);

    if (validGpio(_enablePin)) { DIRECT_pinWrite(_enablePin, HIGH); }

    return true;
  }
  return false;
}

uint32_t P129_data_struct::getChannelState(uint8_t offset,
                                           uint8_t size) const {
  uint32_t result    = 0u;
  uint16_t sft       = 0u;
  const uint8_t last = offset + size;

  for (uint8_t ofs = offset; ofs < last; ++ofs, ++sft) {
    result += (readBuffer[ofs] << (8 * sft));
  }

  # ifdef P129_DEBUG_LOG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLogMove(LOG_LEVEL_DEBUG,
               strformat(F("SHIFTIN: getChannelState offset: %d, size: %d, result: %d/0x%x"),
                         offset, size, result, result));
  }
  # endif // ifdef P129_DEBUG_LOG

  return result;
}

bool P129_data_struct::plugin_read(struct EventStruct *event) {
  const uint16_t last = P129_CONFIG_SHOW_OFFSET + (VARS_PER_TASK * 4);
  uint8_t varNr       = 0;

  for (uint16_t index = P129_CONFIG_SHOW_OFFSET; index < _chipCount && index < last && varNr < VARS_PER_TASK; index += 4, ++varNr) {
    uint32_t result = getChannelState(index, min(VARS_PER_TASK, _chipCount - index));
    UserVar.setUint32(event->TaskIndex, varNr, result);
  }
  return true;
}

const char p129_subcommands[] PROGMEM = "pinevent|chipevent|setchipcount|samplefrequency|eventperpin";
enum class p129_subcommands_e {
  pinevent,
  chipevent,
  setchipcount,
  samplefrequency,
  eventperpin
};

bool P129_data_struct::plugin_write(struct EventStruct *event,
                                    const String      & string) {
  bool success = false;

  const String command = parseString(string, 1);

  if (equals(command, F("shiftin"))) {
    const String subcommand = parseString(string, 2);
    const int    command_i  = GetCommandCode(subcommand.c_str(), p129_subcommands);

    if (command_i == -1) {
      // No matching subcommand found
      return false;
    }

    switch (static_cast<p129_subcommands_e>(command_i)) {
      case p129_subcommands_e::pinevent:
      { // ShiftIn,pinevent,<pin>,<0|1>
        const uint8_t pin   = event->Par2 - 1;
        const uint8_t value = event->Par3;

        if (validChannel(pin + 1) && ((value == 0) || (value == 1))) {
          const uint8_t ulong = pin / 32;
          const uint8_t bit   = pin % 32;
          uint32_t lSettings  = PCONFIG_ULONG(ulong);

          bitWrite(lSettings, bit, value);
          PCONFIG_ULONG(ulong) = lSettings;
          success              = true;
        # ifdef P129_DEBUG_LOG

          if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
            addLogMove(LOG_LEVEL_DEBUG,
                       strformat(F("%s, pin: %d, value: %d, config: %u, bit: %u"),
                                 command.c_str(), event->Par2, value, ulong, bit));
          }
        # endif // ifdef P129_DEBUG_LOG
        }
        break;
      }
      case p129_subcommands_e::chipevent:
      { // ShiftIn,chipevent,<chip>,<0|1>
        const int8_t  chip  = event->Par2 - 1;
        const uint8_t value = event->Par3;

        if ((chip >= 0) && (chip < P129_CONFIG_CHIP_COUNT) && ((value == 0) || (value == 1))) {
          const uint8_t ulong = chip / 4;
          const uint8_t bit   = (chip % 4) * 8;
          uint32_t lSettings  = PCONFIG_ULONG(ulong);

          set8BitToUL(lSettings, bit, value == 1 ? 0xFF : 0x00);
          PCONFIG_ULONG(ulong) = lSettings;
          success              = true;
        # ifdef P129_DEBUG_LOG

          if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
            addLogMove(LOG_LEVEL_DEBUG,
                       strformat(F("%s, chip: %d, value: %d, config: %u, bit: %u"),
                                 command.c_str(), event->Par2, value, ulong, bit));
          }
        # endif // ifdef P129_DEBUG_LOG
        }
        break;
      }
      case p129_subcommands_e::setchipcount:
      { // ShiftIn,setchipcount,<count>
        if ((event->Par2 >= 1) && (event->Par2 <= P129_MAX_CHIP_COUNT)) {
          P129_CONFIG_CHIP_COUNT = event->Par2;
          _chipCount             = event->Par2;
          success                = true;
        }
        break;
      }
      case p129_subcommands_e::samplefrequency:
      { // ShiftIn,samplefrequency,<0|1>
        if ((event->Par2 == 0) || (event->Par2 == 1)) {
          uint32_t lSettings = P129_CONFIG_FLAGS;
          bitWrite(lSettings, P129_FLAGS_READ_FREQUENCY, event->Par2 == 1);
          P129_CONFIG_FLAGS = lSettings;
          success           = true;
        }
        break;
      }
      case p129_subcommands_e::eventperpin:
      { // ShiftIn,eventperpin,<0|1>
        if ((event->Par2 == 0) || (event->Par2 == 1)) {
          uint32_t lSettings = P129_CONFIG_FLAGS;
          bitWrite(lSettings, P129_FLAGS_SEPARATE_EVENTS, event->Par2 == 1);
          P129_CONFIG_FLAGS = lSettings;
          success           = true;
        }
        break;
      }
    }
    # ifdef P129_DEBUG_LOG

    if (success) {
      addLog(LOG_LEVEL_DEBUG, string);
    }
    # endif // ifdef P129_DEBUG_LOG
  }
  return success;
}

bool P129_data_struct::plugin_readData(struct EventStruct *event) {
  if (isInitialized()) {
    DIRECT_pinWrite(_loadPin, LOW);
    delayMicroseconds(5);
    DIRECT_pinWrite(_loadPin, HIGH);
    delayMicroseconds(5);

    if (validGpio(_enablePin)) { DIRECT_pinWrite(_enablePin, LOW); }

    for (uint8_t i = 0; i < P129_CONFIG_CHIP_COUNT; ++i) {
      prevBuffer[i] = readBuffer[i];
      readBuffer[i] = shiftIn(static_cast<uint8_t>(_dataPin), static_cast<uint8_t>(_clockPin), MSBFIRST);
    }

    if (validGpio(_enablePin)) { DIRECT_pinWrite(_enablePin, HIGH); }
    delay(0);

    checkDiff(event);

    return true;
  }
  return false;
}

void P129_data_struct::checkDiff(struct EventStruct *event) {
  for (uint8_t i = 0; i < P129_CONFIG_CHIP_COUNT; i += 4) {
    if (PCONFIG_ULONG(i / 4) != 0) { // Any input event enabled?
      const uint32_t read = readBuffer[i + 3] << 24 | readBuffer[i + 2] << 16 | readBuffer[i + 1] << 8 | readBuffer[i + 0];
      const uint32_t prev = prevBuffer[i + 3] << 24 | prevBuffer[i + 2] << 16 | prevBuffer[i + 1] << 8 | prevBuffer[i + 0];

      for (uint8_t j = 0; j < 32; ++j) {                                                  // Check all 32 bits
        if (bitRead(PCONFIG_ULONG(i / 4), j) && (bitRead(read, j) != bitRead(prev, j))) { // Event enabled and bit changed?
          sendInputEvent(event, i, j, bitRead(read, j));                                  // Send out new state
        }
      }
    }
    delay(0);
  }
}

void P129_data_struct::sendInputEvent(struct EventStruct *event,
                                      uint8_t             group,
                                      uint8_t             bit,
                                      uint8_t             state) {
  String send;
  const uint8_t pin  = (group * 8) + bit + 1; // 1..128
  const uint8_t chip = group + (bit / 8) + 1; // 1..16
  const uint8_t port = (bit % 8);             // 0..7

  send.reserve(40);
  send += getTaskDeviceName(event->TaskIndex);

  if (P129_CONFIG_FLAGS_GET_SEPARATE_EVENTS) {
    send += '#';
    send += pin;
  }
  send += strformat(F("=%u,%u,%u,%u"), state, chip, port, pin);
  eventQueue.addMove(std::move(send));
}

#endif // ifdef USES_P129

#include "../PluginStructs/P173_data_struct.h"

#ifdef USES_P173

# include "../Helpers/CRC_functions.h"

/**************************************************************************
* Constructor
**************************************************************************/
P173_data_struct::P173_data_struct(float tempOffset, bool lowPower) :
  _tempOffset(tempOffset), _lowPower(lowPower)
{}

bool P173_data_struct::init() {
  wakeup();
  softwareReset();
  delay(1);

  // Read sensor device id
  if (checkDeviceID()) {
    sleep();
    initialized = true;
  } else {
    addLog(LOG_LEVEL_ERROR, F("SHTC3: Sensor not recognized."));
  }

  return isInitialized();
}

/*****************************************************
* plugin_read
*****************************************************/
bool P173_data_struct::plugin_read(struct EventStruct *event)           {
  bool success = false;

  if (isInitialized()) {
    // Wake up the sensor
    wakeup();

    // Read the sensor data
    uint8_t data[6]{};
    const uint16_t command = _lowPower ? P173_SHTC3_READ_RH_T_LP : P173_SHTC3_READ_RH_T;

    if (!readValue(command, 6, data)) {
      addLog(LOG_LEVEL_ERROR, strformat(F("%s: Error reading sensor"), getTaskDeviceName(event->TaskIndex).c_str()));
    } else

    // Read succesful, data valid?
    if (calc_CRC8(data[0], data[1], data[2]) && calc_CRC8(data[3], data[4], data[5])) {
      const float temp = static_cast<float>(((uint16_t)data[0] << 8) | (uint16_t)data[1]);
      const float hum  = static_cast<float>(((uint16_t)data[3] << 8) | (uint16_t)data[4]);
      temperature = -45.0f + 175.0f * temp / 65535.0f;
      humidity    = 100.0f * hum / 65535.0f;

      if (definitelyLessThan(humidity, 0.0f)) { humidity = 0.0f; }

      if (definitelyGreaterThan(humidity, 100.0f)) { humidity = 100.0f; }

      UserVar.setFloat(event->TaskIndex, 0, temperature + _tempOffset); // Apply offset
      UserVar.setFloat(event->TaskIndex, 1, humidity);

      success    = true;
      errorCount = 0;

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLog(LOG_LEVEL_INFO, strformat(F("%s: Temperature: %s, Humidity: %s"),
                                         getTaskDeviceName(event->TaskIndex).c_str(),
                                         formatUserVarNoCheck(event, 0).c_str(),
                                         formatUserVarNoCheck(event, 1).c_str()));
      }
    } else {
      addLog(LOG_LEVEL_ERROR, concat(F("SHTC3: READ CRC Error, data: 0x"), formatToHex_array(data, 6)));
      errorCount++;

      if (errorCount > P173_MAX_ERRORCOUNT) {
        softwareReset();
        delay(1);
        addLog(LOG_LEVEL_ERROR, F("SHTC3: READ Error count reached, sensor reset."));
      }
    }

    if (!success) {
      UserVar.setFloat(event->TaskIndex, 0, NAN); // Read error or I/O error
      UserVar.setFloat(event->TaskIndex, 1, NAN);
    }

    // Send sensor to sleep mode for least power consumption
    sleep();
  }

  return success;
}

/********************************
 * Low level functions
 *******************************/
void P173_data_struct::softwareReset() {
  writeCommand(P173_SHTC3_RESET);
  delayMicroseconds(173);
}

void P173_data_struct::wakeup() {
  writeCommand(P173_SHTC3_WAKEUP);
  delayMicroseconds(500);
}

void P173_data_struct::sleep() {
  writeCommand(P173_SHTC3_SLEEP);
  delayMicroseconds(230);
}

bool P173_data_struct::checkDeviceID() {
  uint8_t idArray[3];

  readValue(P173_SHTC3_READ_DEVICEID, 3, idArray);

  if (calc_CRC8(idArray[0], idArray[1], idArray[2])) {
    const uint16_t id = (idArray[0] << 8) | idArray[1];

    if ((id & 0x807) == 0x807) {
      return true;
    }
  }
  return false;
}

void P173_data_struct::writeCommand(uint16_t command) {
  I2C_write16(_address, command);
}

bool P173_data_struct::readValue(uint16_t command, uint8_t readnum, uint8_t *readArray) {
  I2C_write16(_address, command);
  delayMicroseconds(12000);

  if (Wire.requestFrom(_address, readnum) == readnum) {
    for (uint8_t i = 0; i < readnum; ++i) {
      readArray[i] = Wire.read();
    }
    return true;
  }
  return false;
}

#endif // ifdef USES_P173

#include "../PluginStructs/P025_data_struct.h"

#ifdef USES_P025

# include "../WebServer/DevicesPage.h" // Needed for format_I2C_port_description


# define P025_CONVERSION_REGISTER  0x00
# define P025_CONFIG_REGISTER      0x01


P025_VARIOUS_BITS_t::P025_VARIOUS_BITS_t(int16_t value) : _regValue(value) {}


const __FlashStringHelper* Plugin_025_valuename(uint8_t value_nr, bool displayString) {
  const __FlashStringHelper *strings[] {
    F("AIN0 - AIN1 (Differential)"),     F("AIN0_1"),
    F("AIN0 - AIN3 (Differential)"),     F("AIN0_3"),
    F("AIN1 - AIN3 (Differential)"),     F("AIN1_3"),
    F("AIN2 - AIN3 (Differential)"),     F("AIN2_3"),
    F("AIN0 - GND (Single-Ended)"),      F("AIN0"),
    F("AIN1 - GND (Single-Ended)"),      F("AIN1"),
    F("AIN2 - GND (Single-Ended)"),      F("AIN2"),
    F("AIN3 - GND (Single-Ended)"),      F("AIN3")
  };
  const size_t index         = (2 * value_nr) + (displayString ? 0 : 1);
  constexpr size_t nrStrings = NR_ELEMENTS(strings);

  if (index < nrStrings) {
    return strings[index];
  }
  return F("");
}

const __FlashStringHelper* toString(P025_sensorType sensorType)
{
  if (P025_sensorType::None == sensorType) { return F("None"); }
  return (P025_sensorType::ADS1015 == sensorType) ?
         F("ADS1015") : F("ADS1115");
}

struct P025_config_register {
  union {
    struct {
      uint16_t comp_que        : 2;
      uint16_t comp_lat        : 1;
      uint16_t comp_pol        : 1;
      uint16_t compMode        : 1;
      uint16_t datarate        : 3;
      uint16_t mode            : 1;
      uint16_t PGA             : 3;
      uint16_t MUX             : 3;
      uint16_t operatingStatus : 1;
    };
    uint16_t _regValue{};
  };

  P025_config_register(uint16_t regval) : _regValue(regval) {}

  void setRegval(uint16_t regval) {
    _regValue = regval;
  }

  uint16_t getRegval() const {
    return _regValue;
  }

  String toString() const {
    return strformat(F("reg: %X OS: %d MUX: %d PGA: %d mode: %d DR: %d"),
                     getRegval(), operatingStatus, MUX, PGA, mode, datarate
                     );
  }
};


P025_data_struct::P025_data_struct(struct EventStruct *event) {
  _i2cAddress = P025_I2C_ADDR;

  const P025_VARIOUS_BITS_t p025_variousBits(P025_VARIOUS_BITS);

  constexpr uint16_t defaultValue =
    (0x0003)    | // Disable the comparator (default val)
    (0x0000)    | // Non-latching (default val)
    (0x0000)    | // Alert/Rdy active low   (default val)
    (0x0000)    | // Traditional comparator (default val)
    (0x0100)    | // Single-shot mode (default)
    (0x8000);     // Start a single conversion

  P025_config_register reg(defaultValue);

  reg.datarate         = p025_variousBits.getSampleRate();
  reg.PGA              = P025_GAIN;
  _configRegisterValue = reg.getRegval();

  _fullScaleFactor = 1.0f;

  if (p025_variousBits.outputVolt) {
    if (reg.PGA == 0) {
      _fullScaleFactor = 6144;
    } else {
      const uint8_t shift = 13 - reg.PGA;
      _fullScaleFactor = (1 << shift);
    }
    _fullScaleFactor /= 32768000.0f;
  }

  for (taskVarIndex_t i = 0; i < VARS_PER_TASK; ++i) {
    _mux[i] = P025_MUX(i);
  }
}

bool P025_data_struct::read(float& value, taskVarIndex_t index) const {
  if (!validTaskVarIndex(index)) {
    return false;
  }
  P025_config_register reg(_configRegisterValue);

  reg.MUX = _mux[index];

  if (!startMeasurement(_i2cAddress, reg.getRegval())) {
    return false;
  }

  if (!I2C_write16_reg(_i2cAddress, P025_CONFIG_REGISTER, reg.getRegval())) {
# ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      addLog(LOG_LEVEL_DEBUG,
             concat(F("ADS1x15: Start measurement failed"),
                    reg.toString()));
    }
# endif // ifndef BUILD_NO_DEBUG

    return false;
  }

  const long sds = waitReady025(_i2cAddress);

  if (sds == 0) {
# ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      addLog(LOG_LEVEL_DEBUG,
             concat(F("ADS1x15: Not Ready after start measurement"),
                    reg.toString()));
    }
# endif // ifndef BUILD_NO_DEBUG

    return false;
  }

# ifndef BUILD_NO_DEBUG
  else {
    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      addLog(LOG_LEVEL_DEBUG,
             concat(F("ADS1x15: Detected SPS: "), sds));
    }
  }
# endif // ifndef BUILD_NO_DEBUG


  int16_t raw = 0;

  if (!readConversionRegister025(raw)) {
# ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      addLog(LOG_LEVEL_DEBUG,
             concat(F("ADS1x15: Cannot read from conversion register"),
                    reg.toString()));
    }
# endif // ifndef BUILD_NO_DEBUG
    return false;
  }

  value = _fullScaleFactor * raw;

  addLog(LOG_LEVEL_INFO, strformat(F("ADS1x15: RAW value: %d, output value: %f"), raw, value));
  return true;
}

bool P025_data_struct::readConversionRegister025(int16_t& value) const {
  bool is_ok = false;

  // Conversion register represents 2-complement format.
  value = (int16_t)I2C_read16_reg(_i2cAddress, P025_CONVERSION_REGISTER, &is_ok);
  return is_ok;
}

P025_sensorType P025_data_struct::detectType(uint8_t i2cAddress)
{
  P025_sensorType sensorType = P025_sensorType::None;

  constexpr uint16_t defaultValue =
    (0x0003)    | // Disable the comparator (default val)
    (0x0000)    | // Non-latching (default val)
    (0x0000)    | // Alert/Rdy active low   (default val)
    (0x0000)    | // Traditional comparator (default val)
    (0x00A0)    | // 250 (ADS1115) or 2400 (ADS1015) samples per second
    (0x0100)    | // Single-shot mode (default)
    (0x0000)    | // PGA to max FSR of 6.144V
    (0x0000)    | // MUX: differential AIN0/AIN1 (default val)
    (0x8000);     // Start a single conversion

  if (startMeasurement(i2cAddress, defaultValue)) {
    const long sps = waitReady025(i2cAddress);

    if (sps > 0) {
      // Sample rate is either 250 (ADS1115) or 2400 (ADS1015)
      sensorType = (sps < 500) ? P025_sensorType::ADS1115 : P025_sensorType::ADS1015;
    }
  }
  return sensorType;
}

bool P025_data_struct::startMeasurement(uint8_t i2cAddress, uint16_t configRegisterValue)
{
  if (I2C_write16_reg(i2cAddress, P025_CONFIG_REGISTER, configRegisterValue)) {
    return true;
  }
# ifndef BUILD_NO_DEBUG

  const P025_config_register reg(configRegisterValue);

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLog(LOG_LEVEL_DEBUG,
           concat(F("ADS1x15: Start measurement failed"),
                  reg.toString()));
  }
# endif // ifndef BUILD_NO_DEBUG
  return false;
}

long P025_data_struct::waitReady025(uint8_t i2cAddress)
{
  const uint32_t start   = micros();
  unsigned long  timeout = millis();

  bool is_ok = false;

  // Only need to set the Address Pointer Register once
  P025_config_register reg(I2C_read16_reg(i2cAddress, P025_CONFIG_REGISTER, &is_ok));

  if (!is_ok) {
    return 0;
  }

  // Compute expected timeout
  // Rough estimate of SPS = (1 << (DR + 3))
  // Add margin of roughly 50%
  // Add 1 msec as minimum, due to rounding errors at highest frame rate
  // See https://github.com/letscontrolit/ESPEasy/issues/3159#issuecomment-660546091
  timeout += 1500 / (1 << (reg.datarate + 3)) + 1;

  while (!timeOutReached(timeout)) {
    // bit15=0 performing a conversion   =1 not performing a conversion
    const bool ready = reg.operatingStatus == 1;

    if (ready && is_ok) {
      const long res = usecPassedSince_fast(start);
      const long sps = (res > 0) ? 1000000 / res : 0;
# ifndef BUILD_NO_DEBUG

      if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
        addLog(LOG_LEVEL_DEBUG,
               concat(F("ADS1x15: waitReady OK, Config_reg: "),
                      reg.toString()));
      }
# endif // ifndef BUILD_NO_DEBUG

      return sps;
    }
    delay(0);

    // Address Pointer Register is the same, so only need to read bytes again
    reg.setRegval(I2C_read16(i2cAddress, &is_ok));
  }

# ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLog(LOG_LEVEL_DEBUG,
           concat(F("ADS1x15: waitReady timeout, Config_reg: "),
                  reg.toString()));
  }
# endif // ifndef BUILD_NO_DEBUG
  return 0;
}

bool P025_data_struct::webformLoad(struct EventStruct *event)
{
  const uint8_t port = CONFIG_PORT;

  if (port > 0) // map old port logic to new gain and mode settings
  {
    P025_GAIN                       = PCONFIG(0) / 2;
    P025_I2C_ADDR                   = 0x48 + ((port - 1) / 4);
    P025_MUX(0)                     = ((port - 1) & 3) | 4;
    CONFIG_PORT                     = 0;
    PCONFIG(P025_SENSOR_TYPE_INDEX) = static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_SINGLE);
  }

  const P025_sensorType detectedType = detectType(P025_I2C_ADDR);

  addRowLabel(F("Detected Sensor Type"));
  addHtml(toString(detectedType));

  const P025_VARIOUS_BITS_t p025_variousBits(P025_VARIOUS_BITS);

  {
    const __FlashStringHelper *pgaOptions[] = {
      F("2/3x gain (FS=6.144V)"),
      F("1x gain (FS=4.096V)"),
      F("2x gain (FS=2.048V)"),
      F("4x gain (FS=1.024V)"),
      F("8x gain (FS=0.512V)"),
      F("16x gain (FS=0.256V)")
    };

    constexpr size_t ADS1115_PGA_OPTIONS = NR_ELEMENTS(pgaOptions);
    const FormSelectorOptions selector(ADS1115_PGA_OPTIONS, pgaOptions);
    selector.addFormSelector(F("Gain"), F("gain"), P025_GAIN);
    addFormNote(F("Do not apply more than VDD + 0.3 V to the analog inputs of the device."));
  }
  {
    const __FlashStringHelper *P025_SPSOptions[] = {
      F("8 / 128"),
      F("16 / 250"),
      F("32 / 490"),
      F("64 / 920"),
      F("128 / 1600"),
      F("250 / 2400"),
      F("475 / 3300"),
      F("860 / 3300"),
    };
    constexpr size_t NR_OPTIONS = NR_ELEMENTS(P025_SPSOptions);
    const FormSelectorOptions selector(NR_OPTIONS, P025_SPSOptions);
    selector.addFormSelector(F("Sample Rate"), F("sps"), p025_variousBits.getSampleRate());
    addUnit(F("SPS"));
    addFormNote(F("Lower values for ADS1115, higher values for ADS1015"));

    if (P025_sensorType::ADS1015 != detectedType) {
      addFormNote(F("Sample rates < 64 may cause issues"));
    }
  }

  addFormCheckBox(F("Convert to Volt"), F("volt"), p025_variousBits.outputVolt);


  addFormSubHeader(F("Two Point Calibration"));

  addFormCheckBox(F("Calibration Enabled"), F("cal"), p025_variousBits.cal);

  addFormNumericBox(F("Point 1"), F("adc1"), P025_CAL_ADC1, -32768, 32767);
  html_add_estimate_symbol();
  addTextBox(F("out1"), toString(P025_CAL_OUT1, 3), 10);

  addFormNumericBox(F("Point 2"), F("adc2"), P025_CAL_ADC2, -32768, 32767);
  html_add_estimate_symbol();
  addTextBox(F("out2"), toString(P025_CAL_OUT2, 3), 10);
  return true;
}

bool P025_data_struct::webformSave(struct EventStruct *event)
{
  for (uint8_t i = 0; i < P025_NR_OUTPUT_VALUES; ++i) {
    const uint8_t pconfigIndex = P025_PCONFIG_INDEX(i);
    const uint8_t choice       = PCONFIG(pconfigIndex);
    sensorTypeHelper_saveOutputSelector(event, pconfigIndex, i,
                                        Plugin_025_valuename(choice, false));
  }

  P025_I2C_ADDR = getFormItemInt(F("i2c_addr"));

  P025_GAIN = getFormItemInt(F("gain"));

  P025_VARIOUS_BITS_t p025_variousBits(P025_VARIOUS_BITS);

  p025_variousBits.setSampleRate(getFormItemInt(F("sps")));
  p025_variousBits.outputVolt = isFormItemChecked(F("volt"));
  p025_variousBits.cal        = isFormItemChecked(F("cal"));
  P025_VARIOUS_BITS           = p025_variousBits.pconfigvalue();

  P025_CAL_ADC1 = getFormItemInt(F("adc1"));
  P025_CAL_OUT1 = getFormItemFloat(F("out1"));

  P025_CAL_ADC2 = getFormItemInt(F("adc2"));
  P025_CAL_OUT2 = getFormItemFloat(F("out2"));

  return true;
}

bool P025_data_struct::webform_showConfig(struct EventStruct *event)
{
  format_I2C_port_description(event->TaskIndex);

  for (uint8_t i = 0; i < P025_NR_OUTPUT_VALUES; ++i) {
    const uint8_t choice = PCONFIG(P025_PCONFIG_INDEX(i));

    if ((choice >= 0) && (choice < 8)) {
      addHtml(F("<br>"));
      addHtml(Plugin_025_valuename(choice, false));
    }
  }

  return true;
}

#endif // ifdef USES_P025

#include "../PluginStructs/P134_data_struct.h"

#ifdef USES_P134

# ifndef LIMIT_BUILD_SIZE
const __FlashStringHelper* toString(A02YYUW_status_e status) {
  switch (status) {
    case A02YYUW_status_e::STATUS_OK: return F("OK");
    case A02YYUW_status_e::STATUS_ERROR_CHECK_SUM: return F("Checksum error");
    case A02YYUW_status_e::STATUS_ERROR_MAX_LIMIT: return F("Distance over max. limit");
    case A02YYUW_status_e::STATUS_ERROR_MIN_LIMIT: return F("Distance under min. limit");
    case A02YYUW_status_e::STATUS_ERROR_SERIAL: return F("Serial communication error");
  }
  return F("Unknown");
}

# endif // ifndef LIMIT_BUILD_SIZE

/**************************************************************************
* Constructor
**************************************************************************/
P134_data_struct::P134_data_struct(uint8_t config_port,
                                   int8_t  config_pin1,
                                   int8_t  config_pin2)
  : _config_port(config_port), _config_pin1(config_pin1), _config_pin2(config_pin2)
{
  const ESPEasySerialPort port = static_cast<ESPEasySerialPort>(_config_port);

  P134_Serial = new (std::nothrow) ESPeasySerial(port, _config_pin1, _config_pin2);

  if (P134_Serial != nullptr) {
    P134_Serial->begin(P134_SERIAL_BAUD_RATE);
    P134_Serial->flush();
    addLog(LOG_LEVEL_INFO, F("A02YYUW: Initialization OK"));
  } else {
    addLog(LOG_LEVEL_ERROR, F("A02YYUW: Initialization FAILED"));
  }
}

/*****************************************************
* Destructor
*****************************************************/
P134_data_struct::~P134_data_struct() {
  delete P134_Serial;
  P134_Serial = nullptr;
}

/*****************************************************
* plugin_read
*****************************************************/
bool P134_data_struct::plugin_read(struct EventStruct *event)           {
  bool success = false;

  if (isInitialized()) {
    uint8_t  data[P134_DISTANCE_DATA_SIZE] = { 0 };
    int16_t  i                             = 0;
    uint16_t measuredDistance              = P134_MIN_DISTANCE;
    A02YYUW_status_e measurementStatus;
    P134_Serial->flush();

    while (!P134_Serial->available() && i < P134_SERIAL_AVAILABLE_CHECK_CYCLES) {
      i++;
      // FIXME TD-er: Why making such a lousy check for data holding up everything running on the ESP node.
      // Repeat after me: "Thou shall not use delay()!"
      delay(P134_SERIAL_AVAILABLE_CHECK_DELAY);
    }
    i = 0;

    while (P134_Serial->available() && i < P134_DISTANCE_DATA_SIZE) {
      // FIXME TD-er: This does not have a timeout => Will crash the node when only data is received != P134_SERIAL_HEAD_DATA
      // Reading bytes should be done from the PLUGIN_TEN_PER_SECOND or something similar.
      data[i] = P134_Serial->read();
      i++;

      if (data[0] != P134_SERIAL_HEAD_DATA) {
        i = 0;
      }
    }

    if (i != P134_DISTANCE_DATA_SIZE) {
      measurementStatus = A02YYUW_status_e::STATUS_ERROR_SERIAL;
    } else {
      if (!(((data[0] + data[1] + data[2]) & 0xFF) == data[3])) {
        measurementStatus = A02YYUW_status_e::STATUS_ERROR_CHECK_SUM;
      } else {
        measuredDistance = ((data[1] << 8) + data[2]);

        if (measuredDistance < P134_MIN_DISTANCE) {
          measurementStatus = A02YYUW_status_e::STATUS_ERROR_MIN_LIMIT;
        }
        else if (measuredDistance > P134_MAX_DISTANCE) {
          measurementStatus = A02YYUW_status_e::STATUS_ERROR_MAX_LIMIT;
        } else {
          measurementStatus = A02YYUW_status_e::STATUS_OK;
        }
      }
    }

    if (measurementStatus == A02YYUW_status_e::STATUS_OK) {
      UserVar.setFloat(event->TaskIndex, 0, static_cast<float>(measuredDistance));

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLogMove(LOG_LEVEL_INFO, concat(F("A02YYUW: Distance value = "), static_cast<int>(measuredDistance)));
      }
      success = true;
    } else {
      if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
        String log;
        log += concat(F("A02YYUW: Error status = "), static_cast<int>(measurementStatus));
        # ifndef LIMIT_BUILD_SIZE
        log += concat(F(", "), toString(measurementStatus));
        # endif // ifndef LIMIT_BUILD_SIZE
        addLogMove(LOG_LEVEL_ERROR, log);
      }
    }
  }
  return success;
}

#endif // ifdef USES_P134

#include "../PluginStructs/P142_data_struct.h"

#ifdef USES_P142

const __FlashStringHelper* Plugin_142_valuename(uint8_t value_nr,
                                                bool    displayString) {
  const __FlashStringHelper *strings[] { /*** ATTENTION: Don't change order of values as these are stored as user-selected!!! ***/
    F("Angle"), F("angle"),              /*** And the order has to match for this list, P142_output_options and getOutputValue() ***/
    F("Magnitude"), F("magnitude"),
    F("Rpm"), F("rpm"),
    F("ReadAngle"), F("readangle"),
    F("RawAngle"), F("rawangle"),
    F("Automatic Gain Control"), F("agc"),
    F("Has magnet"), F("hasmagnet"),
  };
  const size_t index         = (2 * value_nr) + (displayString ? 0 : 1);
  constexpr size_t nrStrings = NR_ELEMENTS(strings);

  if (index < nrStrings) {
    return strings[index];
  }
  return F("");
}

/**************************************************************************
* Constructor
**************************************************************************/
P142_data_struct::P142_data_struct(struct EventStruct *event) {
  _powerMode     = P142_GET_POWER_MODE;
  _hysteresis    = P142_GET_HYSTERESIS;
  _slowFilter    = P142_GET_SLOW_FILTER;
  _fastFilter    = P142_GET_FAST_FILTER;
  _angleOffset   = P142_PRESET_ANGLE;
  _startPosition = P142_START_POSITION;
  _maxPosition   = P142_MAX_POSITION;
}

P142_data_struct::~P142_data_struct() {
  delete as5600;
}

bool P142_data_struct::init(struct EventStruct *event) {
  //
  as5600 = new (std::nothrow) AS5600();

  if ((nullptr != as5600) && as5600->begin()) {
    initialized = true;

    as5600->setPowerMode(_powerMode);
    as5600->setHysteresis(_hysteresis);
    as5600->setSlowFilter(_slowFilter);
    as5600->setFastFilter(_fastFilter);
    as5600->setZPosition(_startPosition);
    as5600->setMPosition(_maxPosition);
    as5600->setOffset(_angleOffset);
  }

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, concat(F("AS5600: Initialization "), isInitialized() ? F("succeeded") : F("failed")));
  }
  return isInitialized();
}

/*****************************************************
* plugin_read
*****************************************************/
bool P142_data_struct::plugin_read(struct EventStruct *event) {
  if (isInitialized() && _dataAvailable && (_hasMagnet || !P142_GET_CHECK_MAGNET)) {
    if (setOutputValues(event)) {
      _dataAvailable = false;
      return true;
    }
  }
  return false;
}

bool P142_data_struct::setOutputValues(struct EventStruct *event) {
  bool result             = false;
  const int8_t valueCount = P142_NR_OUTPUT_VALUES;

  result = !P142_GET_UPDATE_DIFF_ONLY;

  for (int8_t i = 0; i < valueCount; ++i) {
    const uint8_t pconfigIndex = i + P142_QUERY1_CONFIG_POS;
    bool isChanged             = false;
    UserVar.setFloat(event->TaskIndex, i, getOutputValue(event, PCONFIG(pconfigIndex), UserVar[event->BaseVarIndex + i], isChanged));

    result |= isChanged;
  }
  return result;
}

float P142_data_struct::getOutputValue(struct EventStruct *event, int idx, float currentValue, bool& changed) {
  float result = NAN;

  if (isInitialized()) {
    if (speedCount > 0) { --speedCount; }

    switch (idx) {
      case P142_OUTPUT_ANGLE:
        result = _angle;
        break;
      case P142_OUTPUT_MAGNET:
        result = _magnitude;
        break;
      case P142_OUTPUT_RPM:
        result = speedCount == 0 ? _speed : currentValue;
        break;
      case P142_OUTPUT_RAW_ANGLE:
        result = _rawAngle;
        break;
      case P142_OUTPUT_READ_ANGLE:
        result = _readAngle;
        break;
      case P142_OUTPUT_AGC:
        result = _agc;
        break;
      case P142_OUTPUT_HAS_MAGNET:
        result = _hasMagnet;
        break;
    }
    changed = result != NAN && !essentiallyEqual(currentValue, result);
    return result;
  }
  return result;
}

bool P142_data_struct::plugin_ten_per_second(struct EventStruct *event) {
  if (isInitialized()) {
    bool  send  = false;
    float speed = as5600->getAngularSpeed(AS5600_MODE_RPM);

    if (speedCount > 0) { --speedCount; }

    if ((0 == speedCount) && !essentiallyEqual(_speed, speed)) {
      _speed         = speed;
      _dataAvailable = true;
    }
    const uint16_t rawAngle  = as5600->rawAngle();
    const uint16_t readAngle = as5600->readAngle();
    const uint8_t  agc       = as5600->readAGC();
    const float    angle     = (AS5600_MODE_DEGREES == P142_GET_OUTPUT_MODE ? AS5600_RAW_TO_DEGREES : AS5600_RAW_TO_RADIANS) * _readAngle;
    const uint16_t magnitude = as5600->readMagnitude();
    const bool     hasMagnet = as5600->detectMagnet();

    if (_rawAngle != rawAngle) {
      _rawAngle      = rawAngle;
      _dataAvailable = true;
    }

    if (_readAngle != readAngle) {
      _readAngle     = readAngle;
      _dataAvailable = true;
    }

    if (!essentiallyEqual(_angle, angle)) {
      _angle         = angle;
      _dataAvailable = true;
    }

    if (_magnitude != magnitude) {
      _magnitude     = magnitude;
      _dataAvailable = true;
    }

    if (hasMagnet != _hasMagnet) {
      _hasMagnet     = hasMagnet;
      _dataAvailable = true;
    }

    if (agc != _agc) {
      _agc           = agc;
      _dataAvailable = true;
    }

    send = _dataAvailable &&
           (_hasMagnet || !P142_GET_CHECK_MAGNET) &&
           setOutputValues(event);

    if (send) {
      sendData(event);
      _dataAvailable = false;
    }

    if (P142_GET_ENABLE_LOG) {
      addLog(LOG_LEVEL_INFO, strformat(F("AS5600: Read: %d, Angle: %.2f, magnitude: %d, speed: %.2f, hasMagnet: %d"),
                                       _readAngle, _angle, _magnitude, _speed, _hasMagnet));
    }
    return true;
  }
  return false;
}

const char P142_subcommands[] PROGMEM = "dir|";

enum class P142_subcmd_e : int8_t {
  invalid = -1,
  dir     = 0,
};

/*****************************************************
* plugin_write
*****************************************************/
bool P142_data_struct::plugin_write(struct EventStruct *event,
                                    String            & string) {
  bool success = false;

  const String command = parseString(string, 1);

  if (isInitialized() && equals(command, F("as5600"))) {
    const String subcommand   = parseString(string, 2);
    const int    subcommand_i = GetCommandCode(subcommand.c_str(), P142_subcommands);

    if (subcommand_i < 0) { return false; } // Fail fast

    const P142_subcmd_e subcmd = static_cast<P142_subcmd_e>(subcommand_i);

    switch (subcmd) {
      case P142_subcmd_e::invalid:
        break;
      case P142_subcmd_e::dir:

        if ((event->Par2 >= 0) && (event->Par2 <= 1)) {
          as5600->setDirection(event->Par2);
          success = true;
        }
        break;
    }
  }
  return success;
}

/*****************************************************
* plugin_get_config_value
*****************************************************/
bool P142_data_struct::plugin_get_config_value(struct EventStruct *event,
                                               String            & string) {
  bool success = false;
  bool changed;

  const String var = parseString(string, 1);

  for (uint8_t v = 0; v < P142_NR_OUTPUT_OPTIONS && !success; ++v) {
    if (var.equals(Plugin_142_valuename(v, false))) {
      # ifndef BUILD_NO_DEBUG

      if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
        addLog(LOG_LEVEL_DEBUG, strformat(F("AS5600: Get Config Value: %s: %.2f"),
                                          string.c_str(), getOutputValue(event, v, 0.0f, changed)));
      }
      # endif // ifndef BUILD_NO_DEBUG
      string  = toString(getOutputValue(event, v, 0.0f, changed));
      success = true;
      break;
    }
  }

  return success;
}

#endif // ifdef USES_P142

#include "../PluginStructs/P131_data_struct.h"

#ifdef USES_P131
# include "../Helpers/AdafruitGFX_helper.h" // Use Adafruit graphics helper object

/****************************************************************************
 * P131_CommandTrigger_toString: return the command string selected
 ***************************************************************************/
const __FlashStringHelper* P131_CommandTrigger_toString(P131_CommandTrigger cmd) {
  switch (cmd) {
    case P131_CommandTrigger::neomatrix: return F("neomatrix");
    case P131_CommandTrigger::neo: return F("neo");
  }
  return F("None");
}

// **************************************************************************/
// Constructor
// **************************************************************************/
P131_data_struct::P131_data_struct(uint8_t             matrixWidth,
                                   uint8_t             matrixHeight,
                                   uint8_t             tileWidth,
                                   uint8_t             tileHeight,
                                   int8_t              pin,
                                   uint8_t             matrixType,
                                   neoPixelType        ledType,
                                   uint8_t             rotation,
                                   uint8_t             fontscaling,
                                   AdaGFXTextPrintMode textmode,
                                   String              commandTrigger,
                                   uint8_t             brightness,
                                   uint8_t             maxbright,
                                   uint16_t            fgcolor,
                                   uint16_t            bgcolor,
                                   const uint8_t       defaultFontId)
  :  _matrixWidth(matrixWidth),  _matrixHeight(matrixHeight),  _tileWidth(tileWidth),  _tileHeight(tileHeight),
  _pin(pin),  _matrixType(matrixType),  _ledType(ledType), _rotation(rotation), _fontscaling(fontscaling), _textmode(textmode),
  _commandTrigger(commandTrigger), _brightness(brightness), _maxbright(maxbright), _fgcolor(fgcolor), _bgcolor(bgcolor),
  _defaultFontId(defaultFontId) {
  _commandTrigger.toLowerCase();
  _commandTriggerCmd  = _commandTrigger;
  _commandTriggerCmd += F("cmd");
}

// **************************************************************************/
// Destructor
// **************************************************************************/
P131_data_struct::~P131_data_struct() {
  if (isInitialized()) {
    delete matrix; // Doesn't have a virtual destructor (yet)
    matrix = nullptr;
  }
}

/****************************************************************************
 * plugin_init: Initialize display
 ***************************************************************************/
bool P131_data_struct::plugin_init(struct EventStruct *event) {
  updateFontMetrics();

  bool success = false;

  if (!isInitialized()) {
    # ifndef BUILD_NO_DEBUG
    addLog(LOG_LEVEL_INFO, F("NEOMATRIX: Init start."));
    # endif // ifndef BUILD_NO_DEBUG
    matrix = new (std::nothrow) Adafruit_NeoMatrix(_matrixWidth,
                                                   _matrixHeight,
                                                   _tileWidth,
                                                   _tileHeight,
                                                   _pin,
                                                   _matrixType,
                                                   _ledType);

    if (isInitialized()) {
      _xpix = matrix->width();
      _ypix = matrix->height();
    }
    # ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      String log;
      log.reserve(90);
      log += F("NEOMATRIX: Init done, address: 0x");
      log += String(reinterpret_cast<ulong>(matrix), HEX);
      log += ' ';

      if (!isInitialized()) {
        log += F("in");
      }
      log += F("valid, commands: ");
      log += _commandTrigger;
      log += '/';
      log += _commandTriggerCmd;
      log += F(", size: w:");
      log += _xpix;
      log += F(", h:");
      log += _ypix;
      addLogMove(LOG_LEVEL_INFO, log);
    }
  } else {
    addLog(LOG_LEVEL_INFO, F("NEOMATRIX: Init failed."));
    # endif // ifndef BUILD_NO_DEBUG
  }

  if (isInitialized()) {
    gfxHelper = new (std::nothrow) AdafruitGFX_helper(matrix,
                                                      _commandTrigger,
                                                      _xpix,
                                                      _ypix,
                                                      AdaGFXColorDepth::FullColor,
                                                      _textmode,
                                                      _fontscaling,
                                                      _fgcolor,
                                                      _bgcolor,
                                                      true,
                                                      _textBackFill,
                                                      _defaultFontId);

    success = (nullptr != gfxHelper);

    if (success) {
      gfxHelper->initialize();
      gfxHelper->setRotation(_rotation);
      matrix->begin();
      matrix->setBrightness(std::min(_maxbright, _brightness)); // Set brightness, so we don't get blinded by the light
      matrix->fillScreen(_bgcolor);                             // fill screen with black color
      matrix->show();                                           // Update the display

      # ifdef P131_SHOW_SPLASH

      if (P131_CONFIG_FLAG_GET_SHOW_SPLASH) {
        uint16_t yPos = 0;
        gfxHelper->printText(String(F("ESPEasy")).c_str(), 0, yPos, 1, ADAGFX_WHITE, ADAGFX_BLACK);
        matrix->show();
        _splashState   = true; // Splash
        _splashCounter = P131_SPLASH_DURATION;
        #  ifndef BUILD_NO_DEBUG
        addLog(LOG_LEVEL_INFO, F("P131 Splash start."));
        #  endif // ifndef BUILD_NO_DEBUG
      }
      # endif // ifdef P131_SHOW_SPLASH

      matrix->setTextColor(_fgcolor, _bgcolor); // set text color to white and black background
      gfxHelper->setColumnRowMode(false);       // Pixel-mode
      matrix->setTextSize(_fontscaling);        // Handles 0 properly, text size, default 1
      matrix->setCursor(0, 0);                  // move cursor to position (0, 0) pixel
      updateFontMetrics();

      // Load
      loadContent(event);

      for (uint8_t x = 0; x < P131_Nlines && !stringsHasContent; x++) {
        stringsHasContent = !strings[x].isEmpty();
      }

      // Setup initial scroll position
      for (uint8_t x = 0; x < P131_CONFIG_TILE_HEIGHT; x++) {
        content[x].pixelPos = 0;

        if (content[x].active) {
          String   tmpString = parseStringKeepCaseNoTrim(strings[x], 1);
          String   newString = AdaGFXparseTemplate(tmpString, _textcols, gfxHelper);
          uint16_t h;
          content[x].length = gfxHelper->getTextSize(newString, h);

          if (content[x].startBlank && (content[x].length > _xpix)) {
            if (content[x].rightScroll) {
              content[x].pixelPos = -1 * content[x].length;
            } else {
              content[x].pixelPos = _xpix;
            }
          }
        }
      }
    }
  }
  return success;
}

/****************************************************************************
 * plugin_exit: De-initialize before destruction
 ***************************************************************************/
bool P131_data_struct::plugin_exit(struct EventStruct *event) {
  # ifndef BUILD_NO_DEBUG
  addLog(LOG_LEVEL_INFO, F("NEOMATRIX: Exit."));
  # endif // ifndef BUILD_NO_DEBUG

  if ((nullptr != matrix) && bitRead(P131_CONFIG_FLAGS, P131_CONFIG_FLAG_CLEAR_ON_EXIT)) {
    matrix->fillScreen(ADAGFX_BLACK); // fill screen with black color
    matrix->show();
  }
  cleanup();
  return true;
}

/****************************************************************************
 * cleanup: De-initialize pointers
 ***************************************************************************/
void P131_data_struct::cleanup() {
  delete gfxHelper;
  gfxHelper = nullptr;

  delete matrix; // Doesn't have a virtual destructor (yet)
  matrix = nullptr;
}

/****************************************************************************
 * loadContent: load the default content if not yet loaded
 ***************************************************************************/
void P131_data_struct::loadContent(struct EventStruct *event) {
  if (!stringsInitialized) {
    LoadCustomTaskSettings(event->TaskIndex, strings, P131_Nlines, 0);
    stringsInitialized = true;
  }

  if (!contentInitialized && stringsInitialized) {
    content.clear();
    content.resize(P131_CONFIG_TILE_HEIGHT);

    for (uint8_t x = 0; x < P131_CONFIG_TILE_HEIGHT; x++) {
      content[x] = P131_content_struct();
      initialize_content(event, x);
    }
    contentInitialized = true;
  }
}

/****************************************************************************
 * initialize_content: set the content[x] flags from arguments provided
 ***************************************************************************/
void P131_data_struct::initialize_content(struct EventStruct *event,
                                          uint8_t             x) {
  String   opts    = parseString(strings[x], 2);
  uint32_t optBits = 0;

  validUIntFromString(opts, optBits);
  content[x].active      = bitRead(optBits, P131_OPTBITS_SCROLL);
  content[x].rightScroll = bitRead(optBits, P131_OPTBITS_RIGHTSCROLL);
  content[x].startBlank  = bitRead(optBits, P131_OPTBITS_STARTBLANK) == 0;      // Inverted
  content[x].stepWidth   = get4BitFromUL(optBits, P131_OPTBITS_SCROLLSTEP) + 1; // Add offset once
  opts                   = parseString(strings[x], 3);
  int32_t speed = 0;

  validIntFromString(opts, speed);
  content[x].speed = speed;
}

/****************************************************************************
 * plugin_read: Re-draw the default content
 ***************************************************************************/
bool P131_data_struct::plugin_read(struct EventStruct *event) {
  if (isInitialized() && !_splashState) {
    loadContent(event);

    if (stringsHasContent) {
      display_content(event);
    }
  }
  return false; // Always return false, so no attempt to send to
                // Controllers or generate events is started
}

/****************************************************************************
 * display_content: Re-display the text, and apply any scrolling offset
 ***************************************************************************/
void P131_data_struct::display_content(struct EventStruct *event,
                                       bool                scrollOnly,
                                       uint8_t             line) {
  if (isInitialized() && (nullptr != gfxHelper)) {
    int16_t yPos      = 0;
    const bool useVal = gfxHelper->getValidation();
    gfxHelper->setValidation(false); // Ignore validation to enable scrolling

    uint8_t x     = 0;
    uint8_t x_end = P131_CONFIG_TILE_HEIGHT;

    if (line != 255) {
      x     = line;
      x_end = line + 1;
    }

    for (; x < x_end; x++) {
      if (!scrollOnly ||
          (scrollOnly && content[x].active)) {
        String   tmpString = parseStringKeepCaseNoTrim(strings[x], 1);
        String   newString = AdaGFXparseTemplate(tmpString, _textcols, gfxHelper);
        uint16_t h;
        content[x].length = gfxHelper->getTextSize(newString, h);

        # if ADAGFX_PARSE_SUBCOMMAND
        updateFontMetrics();
        # endif // if ADAGFX_PARSE_SUBCOMMAND

        if (yPos < _ypix) {
          gfxHelper->printText(newString.c_str(),
                               content[x].pixelPos,
                               yPos,
                               _fontscaling,
                               _fgcolor,
                               _bgcolor);

          if (scrollOnly && content[x].active && (content[x].length > _xpix))  {
            if (content[x].rightScroll && (content[x].pixelPos > 0)) {
              // Clear left from text
              matrix->fillRect(content[x].pixelPos - content[x].stepWidth,
                               yPos,
                               content[x].stepWidth,
                               h,
                               _bgcolor);
            }

            if (!content[x].rightScroll && (content[x].pixelPos + content[x].length < _xpix) && (content[x].stepWidth >= 1)) {
              // Clear right from text
              matrix->fillRect(content[x].pixelPos + content[x].length,
                               yPos,
                               content[x].stepWidth,
                               h,
                               _bgcolor);
            }
          }
        }

        if (scrollOnly && content[x].active) {
          if (content[x].rightScroll) {
            // Fully scrolled? then reset, starting left of the screen or with right side aligned right if not startBlank
            if (content[x].pixelPos > (content[x].startBlank ? _xpix : 0 - content[x].stepWidth)) {
              if (content[x].startBlank) {
                content[x].pixelPos = -1 * content[x].length;
              } else {
                content[x].pixelPos = (-1 * content[x].length) + _xpix - content[x].stepWidth;
              }
            }
          } else {
            // Fully scrolled? then reset, starting at right of the screen or left if not startBlank
            if (content[x].pixelPos + content[x].length < (content[x].startBlank ? 0 : _xpix + content[x].stepWidth)) {
              if (content[x].startBlank) {
                content[x].pixelPos = _xpix;
              } else {
                content[x].pixelPos = content[x].stepWidth;
              }
            }
          }

          // Logging used only during development
          // String log = F("display_content: x=");
          // log += x;
          // log += F(", pxPos=");
          // log += content[x].pixelPos;
          // log += F(", len=");
          // log += content[x].length;
          // log += F(", stp=");
          // log += content[x].stepWidth;
          // log += F(", xpix=");
          // log += _xpix;
          // addLogMove(LOG_LEVEL_INFO, log);
        }
      }
      delay(0);
      yPos += P131_CONFIG_MATRIX_HEIGHT;
    }
    gfxHelper->setValidation(useVal);
    matrix->show();
  }
}

/****************************************************************************
 * plugin_write: Handle commands
 ***************************************************************************/
bool P131_data_struct::plugin_write(struct EventStruct *event, const String& string) {
  bool success = false;

  if (isInitialized()  && !_splashState) {
    const String cmd = parseString(string, 1);

    if ((nullptr != matrix) && cmd.equals(_commandTriggerCmd)) {
      String  sub = parseString(string, 2);
      int16_t x   = event->Par2 - 1;
      success = true;

      if (equals(sub, F("clear"))) {
        matrix->fillScreen(_bgcolor);
      } else if (sub.startsWith(F("bright")) && (event->Par2 >= 0) && (event->Par2 <= 255)) {
        if (parseString(string, 3).isEmpty()) {                     // No argument, then
          matrix->setBrightness(std::min(_maxbright, _brightness)); // use initial brightness
        } else {
          matrix->setBrightness(std::min(_maxbright, static_cast<uint8_t>(event->Par2)));
        }
      } else if (equals(sub, F("settext"))
                 && ((event->Par2 > 0) && (event->Par2 <= P131_CONFIG_TILE_HEIGHT))) { // line
        String tmpString = parseStringToEnd(strings[x], 2);                            // settings to be transferred
        strings[x]  = wrapWithQuotesIfContainsParameterSeparatorChar(parseStringToEndKeepCase(string, 4));
        strings[x] += ',';
        strings[x] += tmpString;
      } else if ((equals(sub, F("setscroll")) ||                                    // neomatrixcmd,setscroll,<line>,0|1
                  equals(sub, F("setempty")) ||                                     // neomatrixcmd,setempty,<line>,0|1
                  equals(sub, F("setright"))                                        // neomatrixcmd,setright,<line>,0|1
                  )
                 && ((event->Par2 > 0) && (event->Par2 <= P131_CONFIG_TILE_HEIGHT)) // line
                 && ((event->Par3 >= 0) && (event->Par3 <= 1))) {                   // on/off
        String   tmpString1 = parseStringKeepCase(strings[x], 1);                   // settings to be transferred
        String   tmpString3 = parseString(strings[x], 3);                           // settings to be transferred
        String   opts       = parseString(strings[x], 2);
        uint32_t optBits    = 0;

        validUIntFromString(opts, optBits);

        if (sub[3] == 's') {                                            // setscroll
          bitWrite(optBits, P131_OPTBITS_SCROLL, event->Par3 == 1);
        } else if (sub[3] == 'e') {                                     // setempty
          bitWrite(optBits, P131_OPTBITS_STARTBLANK, event->Par3 == 0); // Inverted value
        } else if (sub[3] == 'r') {                                     // setright
          bitWrite(optBits, P131_OPTBITS_RIGHTSCROLL, event->Par3 == 1);
        }
        strings[x]  = wrapWithQuotesIfContainsParameterSeparatorChar(tmpString1);
        strings[x] += ',';
        strings[x] += optBits;
        strings[x] += ',';
        strings[x] += tmpString3;
      } else if (equals(sub, F("setstep"))                                           // neomatrixcmd,setstep,<line>,1..16
                 && ((event->Par2 > 0) && (event->Par2 <= P131_CONFIG_TILE_HEIGHT))  // line
                 && ((event->Par3 > 0) && (event->Par3 <= P131_MAX_SCROLL_STEPS))) { // 1..16
        String   tmpString1 = parseStringKeepCase(strings[x], 1);                    // settings to be transferred
        String   tmpString3 = parseString(strings[x], 3);                            // settings to be transferred
        String   opts       = parseString(strings[x], 2);
        uint32_t optBits    = 0;

        validUIntFromString(opts, optBits);
        set4BitToUL(optBits, P131_OPTBITS_SCROLLSTEP, event->Par3 - 1); // Use offset of -1
        strings[x]  = wrapWithQuotesIfContainsParameterSeparatorChar(tmpString1);
        strings[x] += ',';
        strings[x] += optBits;
        strings[x] += ',';
        strings[x] += tmpString3;
      } else if (equals(sub, F("setspeed"))                                          // neomatrixcmd,setspeed,<line>,1..600
                 && ((event->Par2 > 0) && (event->Par2 <= P131_CONFIG_TILE_HEIGHT))  // line
                 && ((event->Par3 > 0) && (event->Par3 <= P131_MAX_SCROLL_SPEED))) { // 1..600
        String tmpString1 = parseStringKeepCase(strings[x], 1);                      // settings to be transferred
        String tmpString2 = parseString(strings[x], 2);                              // settings to be transferred
        strings[x]  = wrapWithQuotesIfContainsParameterSeparatorChar(tmpString1);
        strings[x] += ',';
        strings[x] += tmpString2;
        strings[x] += ',';
        strings[x] += event->Par3;
      } else {
        success = false;
      }

      if (success && sub.startsWith(F("set"))) {
        initialize_content(event, x);     // Set up line parameters
        content[x].loop = -1;             // Restart loop
        display_content(event, false, x); // (re-)display line
        # ifndef BUILD_NO_DEBUG

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          String log = F("NEOMATRIX: set line ");
          log += event->Par2;
          log += F(": ");
          log += strings[x];
          addLogMove(LOG_LEVEL_INFO, log);
        }
        # endif // ifndef BUILD_NO_DEBUG
      }
    }
    else if (matrix && (cmd.equals(_commandTrigger) ||
                        (gfxHelper && gfxHelper->isAdaGFXTrigger(cmd)))) {
      success = true;

      if (nullptr != gfxHelper) {
        String tmp(string);

        // Hand it over after replacing variables
        success = gfxHelper->processCommand(AdaGFXparseTemplate(tmp, _textcols, gfxHelper));

        updateFontMetrics(); // Font or color may have changed
      }
    }

    if (success && matrix) {
      matrix->show();
    }
  }
  return success;
}

# if ADAGFX_ENABLE_GET_CONFIG_VALUE

/****************************************************************************
 * plugin_get_config_value: Retrieve values like [<taskname>#<valuename>]
 ***************************************************************************/
bool P131_data_struct::plugin_get_config_value(struct EventStruct *event,
                                               String            & string) {
  bool success = false;

  if (gfxHelper != nullptr) {
    success = gfxHelper->pluginGetConfigValue(string);
  }
  return success;
}

# endif // if ADAGFX_ENABLE_GET_CONFIG_VALUE

/****************************************************************************
 * plugin_ten_per_second: Re-draw the default content that should be scrolled
 ***************************************************************************/
bool P131_data_struct::plugin_ten_per_second(struct EventStruct *event) {
  bool success = false;

  # ifdef P131_SHOW_SPLASH

  if (_splashState) { // Decrement splash counter
    _splashCounter--;
    _splashState = _splashCounter != 0;

    if (!_splashState) {
      #  ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_INFO, F("P131 Splash finished."));
      #  endif // ifndef BUILD_NO_DEBUG

      if (nullptr != matrix) {
        matrix->fillScreen(_bgcolor); // fill screen with black color
      }

      // Schedule the surrogate initial PLUGIN_READ that has been suppressed by the splash
      Scheduler.schedule_task_device_timer(event->TaskIndex, millis() + 10);
    }
  }
  # endif // ifdef P131_SHOW_SPLASH

  if (isInitialized() && !_splashState) {
    loadContent(event);
    success = true;

    for (uint8_t x = 0; x < P131_CONFIG_TILE_HEIGHT; x++) {
      if (content[x].active && (content[x].length > _xpix)) {
        if (content[x].loop == -1) { content[x].loop = content[x].speed; } // Initialize

        if (!content[x].loop--) {
          content[x].pixelPos += (content[x].rightScroll ? 1 : -1) * content[x].stepWidth;

          display_content(event, true);
        }
      }
    }
  }
  return success;
}

/****************************************************************************
 * updateFontMetrics: recalculate x and y columns, based on font size and font scale
 ***************************************************************************/
void P131_data_struct::updateFontMetrics() {
  if (_fontscaling == 0) { _fontscaling = 1; } // Sanity checks

  if (_fontwidth == 0) { _fontwidth = 6; }

  if (_fontheight == 0) { _fontheight = 10; }

  if (nullptr != gfxHelper) {
    gfxHelper->getTextMetrics(_textcols, _textrows, _fontwidth, _fontheight, _fontscaling, _heightOffset, _xpix, _ypix);
    gfxHelper->getColors(_fgcolor, _bgcolor);
  } else {
    _textcols = _xpix / (_fontwidth * _fontscaling);
    _textrows = _ypix / (_fontheight * _fontscaling);
  }
  # ifdef P131_DEBUG_LOG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    String log = F("updateFontMetrics: size: ");
    log += _fontscaling;
    log += F(", fg: ");
    log += String(_fgcolor, HEX); // AdaGFXcolorToString(_fgcolor);
    log += F(", bg: ");
    log += String(_bgcolor, HEX); // AdaGFXcolorToString(_bgcolor);

    if (nullptr != matrix) {
      log += F(", xp: ");
      log += matrix->getCursorX();
      log += F(", yp: ");
      log += matrix->getCursorY();
    }
    addLogMove(LOG_LEVEL_DEBUG, log);
  }
  # endif // ifdef P131_DEBUG_LOG
}

#endif // ifdef USES_P131

#include "../PluginStructs/P036_data_struct.h"

#ifdef USES_P036

# include "../ESPEasyCore/ESPEasyNetwork.h"
# include "../Globals/RTC.h"
# include "../Helpers/ESPEasy_Storage.h"
# include "../Helpers/Memory.h"
# include "../Helpers/Misc.h"
# include "../Helpers/Scheduler.h"
# include "../Helpers/StringConverter.h"
# include "../Helpers/StringParser.h"
# include "../Helpers/SystemVariables.h"

# ifndef P036_LIMIT_BUILD_SIZE
#  include <Dialog_Plain_12_font.h>
#  include <Dialog_Plain_18_font.h>
# endif // ifndef P036_LIMIT_BUILD_SIZE
# include <OLED_SSD1306_SH1106_images.h>

void P036_LineContent::loadDisplayLines(taskIndex_t taskIndex, uint8_t LoadVersion) {
  # ifdef USE_SECOND_HEAP
  HeapSelectIram ephemeral;
  # endif // ifdef USE_SECOND_HEAP

  if (LoadVersion == 0) {
    // read data of version 0 (up to 22.11.2019)
    String DisplayLinesV0[P36_Nlines];                                           // used to load the CustomTaskSettings for V0
    LoadCustomTaskSettings(taskIndex, DisplayLinesV0, P36_Nlines, P36_NcharsV0); // max. length 1024 Byte  (DAT_TASKS_CUSTOM_SIZE)

    for (int i = 0; i < P36_Nlines; ++i) {
      DisplayLinesV1[i].Content      = std::move(DisplayLinesV0[i]);
      DisplayLinesV1[i].FontType     = 0xff;
      DisplayLinesV1[i].ModifyLayout = 0xff;
      DisplayLinesV1[i].FontSpace    = 0xff;
      DisplayLinesV1[i].reserved     = 0xff;
    }
  } else {
    // read data of version 1 (beginning from 22.11.2019)
    for (int i = 0; i < P36_Nlines; ++i) {
      tDisplayLines_storage tmp;
      LoadCustomTaskSettings(
        taskIndex,
        reinterpret_cast<uint8_t *>(&tmp),
        sizeof(tDisplayLines_storage),
        i * sizeof(tDisplayLines_storage));
      DisplayLinesV1[i] = tmp.get();
    }
  }
}

String P036_LineContent::saveDisplayLines(taskIndex_t taskIndex) {
  String error;

  if (FreeMem() > 8000) {
    // Write in one single chunk.
    tDisplayLines_storage_full *tmp = nullptr;

    // Try to allocate in PSRAM or 2nd heap if possible
    constexpr unsigned size = sizeof(tDisplayLines_storage_full);
    void *ptr               = special_calloc(1, size);

    if (ptr) {
      tmp = new (ptr) tDisplayLines_storage_full;
    }

    if (tmp != nullptr) {
      for (int i = 0; i < P36_Nlines; ++i) {
        safe_strncpy(tmp->lines[i].Content, DisplayLinesV1[i].Content, P36_NcharsV1);
        tmp->lines[i].FontType     = DisplayLinesV1[i].FontType;
        tmp->lines[i].ModifyLayout = DisplayLinesV1[i].ModifyLayout;
        tmp->lines[i].FontSpace    = DisplayLinesV1[i].FontSpace;
        tmp->lines[i].reserved     = DisplayLinesV1[i].reserved;
      }
      error = SaveCustomTaskSettings(
        taskIndex,
        reinterpret_cast<uint8_t *>(tmp),
        sizeof(tDisplayLines_storage_full));
      delete tmp;
      return error;
    }
  }

  // Since we're making several calls to save, make sure to consider this as a single save call.
  const uint8_t flashCounter = RTC.flashDayCounter;

  for (int i = 0; i < P36_Nlines && error.isEmpty(); ++i) {
    tDisplayLines_storage tmp(DisplayLinesV1[i]);
    RTC.flashDayCounter = flashCounter;
    error               = SaveCustomTaskSettings(
      taskIndex,
      reinterpret_cast<uint8_t *>(&tmp),
      sizeof(tDisplayLines_storage),
      i * sizeof(tDisplayLines_storage));
  }
  return error;
}

P036_data_struct::~P036_data_struct() {
  if (display != nullptr) {
    display->displayOff();
    display->end();
    delete display;
    display = nullptr;
  }

  if (LineContent != nullptr) {
    delete LineContent;
    LineContent = nullptr;
  }
}

void P036_data_struct::reset() {
  if (display != nullptr) {
    display->displayOff();
    display->end();
    delete display;
    display = nullptr;
  }

  if (LineContent != nullptr) {
    delete LineContent;
    LineContent = nullptr;
  }
}

# ifdef P036_FONT_CALC_LOG
const __FlashStringHelper * tFontSettings::FontName() const {
  switch (fontIdx) {
    case 0: return F("Arial_24"); break;
    #  ifndef P036_LIMIT_BUILD_SIZE
    case 1: return F("Dialog_18"); break;
    case 2: return F("Arial_16"); break;
    case 3: return F("Dialog_12"); break;
    case 4: return F("Arial_10"); break;
    #  else // ifndef P036_LIMIT_BUILD_SIZE
    case 1: return F("Arial_16"); break;
    case 2: return F("Arial_10"); break;
    #  endif // ifndef P036_LIMIT_BUILD_SIZE
    default: return F("Unknown font");
  }
}

# endif // ifdef P036_FONT_CALC_LOG

// FIXME TD-er: with using functions to get the font, this object is stored in .dram0.data
// The same as when using the DRAM_ATTR attribute used for interrupt code.
// This is very precious memory, so we must find something other way to define this.

/* *INDENT-OFF* */
const tFontSizes FontSizes[] = {
  { getArialMT_Plain_24(), 24,  28                         }, // 9643
  # ifndef P036_LIMIT_BUILD_SIZE
  { getDialog_plain_18(),  19,  22                         }, // 7399
  # endif // ifndef P036_LIMIT_BUILD_SIZE
  { getArialMT_Plain_16(), 16,  19                         }, // 5049
  # ifndef P036_LIMIT_BUILD_SIZE
  { getDialog_plain_12(),  13,  15                         }, // 3707
  # endif // ifndef P036_LIMIT_BUILD_SIZE
  { getArialMT_Plain_10(), 10,  13                         }, // 2731
};
/* *INDENT-ON* */

constexpr tSizeSettings SizeSettings[] = {
  { P36_MaxDisplayWidth, P36_MaxDisplayHeight, 0,  // 128x64
    4,                                             // max. line count
    113, 15                                        // WiFi indicator
  },
  { P36_MaxDisplayWidth, 32,                   0,  // 128x32
    2,                                             // max. line count
    113, 15                                        // WiFi indicator
  },
  { 64,                  48,                   32, // 64x48
    3,                                             // max. line count
    32,  10                                        // WiFi indicator
  }
};


const tSizeSettings& P036_data_struct::getDisplaySizeSettings(p036_resolution disp_resolution) {
  int index = static_cast<int>(disp_resolution);

  if ((index < 0) || (index >= static_cast<int>(NR_ELEMENTS(SizeSettings)))) { index = 0; }

  return SizeSettings[index];
}

bool P036_data_struct::init(taskIndex_t      taskIndex,
                            uint8_t          LoadVersion,
                            uint8_t          Type,
                            uint8_t          Address,
                            uint8_t          Sda,
                            uint8_t          Scl,
                            p036_resolution  Disp_resolution,
                            bool             Rotated,
                            uint8_t          Contrast,
                            uint16_t         DisplayTimer,
                            ePageScrollSpeed ScrollSpeed,
                            uint8_t          NrLines
                            # if             P036_FLAG_HIDE_LOGO
                            ,
                            bool             HideLogo
                            # endif // if P036_FLAG_HIDE_LOGO
                            ) {
  reset();

  lastWiFiState       = P36_WIFI_STATE_UNSET;
  disp_resolution     = Disp_resolution;
  bAlternativHeader   = false; // start with first header content
  HeaderCount         = 0;
  bPageScrollDisabled = true;  // first page after INIT without scrolling
  TopLineOffset       = 0;     // Offset for top line, used for rotated image while using displays < P36_MaxDisplayHeight lines

  HeaderContent            = eHeaderContent::eSysName;
  HeaderContentAlternative = eHeaderContent::eSysName;
  MaxFramesToDisplay       = 0xFF;
  currentFrameToDisplay    = 0;
  nextFrameToDisplay       = 0xFF; // next frame because content changed in PLUGIN_WRITE

  ButtonState     = false;         // button not touched
  ButtonLastState = 0xFF;          // Last state checked (debouncing in progress)
  DebounceCounter = 0;             // debounce counter
  RepeatCounter   = 0;             // Repeat delay counter when holding button pressed

  displayTimer          = DisplayTimer;
  frameCounter          = 0;       // need to keep track of framecounter from call to call
  disableFrameChangeCnt = 0;       // counter to disable frame change after JumpToPage in case PLUGIN_READ already scheduled

  switch (Type) {
    case 1:
      display = new (std::nothrow) SSD1306Wire(Address, Sda, Scl);
      break;
    case 2:
      display = new (std::nothrow) SH1106Wire(Address, Sda, Scl);
      break;
    default:
      return false;
  }

  {
    // Try to allocate in PSRAM or 2nd heap if possible
    constexpr unsigned size = sizeof(P036_LineContent);
    void *ptr               = special_calloc(1, size);

    if (ptr) {
      LineContent = new (ptr) P036_LineContent();
    }
  }

  if (isInitialized()) {
    display->init(); // call to local override of init function

    // disp_resolution = Disp_resolution;
    bHideFooter |= !(getDisplaySizeSettings(disp_resolution).Height == P36_MaxDisplayHeight);

    if (disp_resolution == p036_resolution::pix128x32) {
      display->displayOff();
      display->SetComPins(0x02); // according to the adafruit lib, sometimes this may need to be 0x02
      bHideFooter = true;
    }

    display->displayOn();
    LineContent->loadDisplayLines(taskIndex, LoadVersion);

    // Flip screen if required
    setOrientationRotated(Rotated);

    setContrast(Contrast);

    // Display the device name, logo, time and wifi
    # if             P036_FLAG_HIDE_LOGO

    if (!HideLogo)
    # endif // if P036_FLAG_HIDE_LOGO
    {
      display_logo();
      update_display();
    }

    // Initialize frame counter
    frameCounter                    = 0;
    currentFrameToDisplay           = 0;
    nextFrameToDisplay              = 0;
    bPageScrollDisabled             = true;  // first page after INIT without scrolling
    ScrollingPages.linesPerFrameDef = NrLines;
    bLineScrollEnabled              = false; // start without line scrolling

    // Clear scrolling line data
    for (uint8_t i = 0; i < P36_MAX_LinesPerPage; ++i) {
      ScrollingLines.SLine[i].Width     = 0;
      ScrollingLines.SLine[i].LastWidth = 0;
    }

    // prepare font and positions for page and line scrolling
    prepare_pagescrolling(ScrollSpeed, NrLines);
  }

  bRunning = NetworkConnected();

  return isInitialized();
}

void P036_data_struct::CleanEscapeCharacters(String& str, const bool ForHeaderOnly = false)
{
  stripEscapeCharacters(str);

  /***** WILL BE DEPRECATED - DO NOT USE *****/
  if (ForHeaderOnly) {
    str.replace("$", "%"); // Allow system vars to be passed to header in by using $ instead of %
  }

  /***** WILL BE DEPRECATED - DO NOT USE *****/
}

const char p036_subcommands[] PROGMEM = "display|frame"
                                        # if P036_ENABLE_LINECOUNT
                                        "|linecount"
                                        # endif // if P036_ENABLE_LINECOUNT
                                        "|restore|scroll"
                                        # if P036_ENABLE_LEFT_ALIGN
                                        "|leftalign|align"
                                        # endif // if P036_ENABLE_LEFT_ALIGN
                                        # if P036_USERDEF_HEADERS
                                        "|userdef1|userdef2"
                                        # endif // if P036_USERDEF_HEADERS
;
enum class p036_subcommands_e {
  display,
  frame,
  # if P036_ENABLE_LINECOUNT
  linecount,
  # endif // if P036_ENABLE_LINECOUNT
  restore,
  scroll,
  # if P036_ENABLE_LEFT_ALIGN
  leftalign,
  align,
  # endif // if P036_ENABLE_LEFT_ALIGN
  # if P036_USERDEF_HEADERS
  userdef1,
  userdef2
  # endif // if P036_USERDEF_HEADERS
};


bool P036_data_struct::plugin_write(struct EventStruct *event, const String& string)
{
  const String command = parseString(string, 1);

  if (!(equals(command, F("oledframedcmd"))) || !isInitialized()) {
    return false;
  }
  bool success = false;

  bool bUpdateDisplay = false;
  bool bDisplayON     = false;
  uint8_t eventId     = 0;

  const String subcommand = parseString(string, 2);
  int  LineNo             = event->Par1;
  bool Parsing            = (LineNo >= 0);

      # if P036_SEND_EVENTS
  const bool sendEvents = bitRead(P036_FLAGS_0, P036_FLAG_SEND_EVENTS); // Bit 28 Send Events
      # endif // if P036_SEND_EVENTS

  int command_i = GetCommandCode(subcommand.c_str(), p036_subcommands);

  if (command_i == -1) {
    String parseString = parseStringKeepCaseNoTrim(string, 3);

    if (!Parsing) {
      LineNo = -LineNo;
      CleanEscapeCharacters(parseString); // Allow system vars and task values to be passed to the line
    }

    if ((LineNo > 0) && (LineNo <= P36_Nlines)) {
      // content functions
      success = true;
      String *currentLine = &LineContent->DisplayLinesV1[LineNo - 1].Content;
      *currentLine = parseString;

      if (Parsing) {
        *currentLine = P36_parseTemplate(*currentLine, LineNo - 1);

        # if P036_ENABLE_TICKER

        if (!bUseTicker)
        # endif // if P036_ENABLE_TICKER
        {
          // calculate Pix length of new content, not necessary for ticker
          uint16_t PixLength = CalcPixLength(LineNo - 1);

          if (PixLength > 255) {
            addHtmlError(strformat(F("Pixel length of %d too long for line! Max. 255 pix!"), PixLength));

            const unsigned int strlen = currentLine->length();

            if (strlen > 0) {
              const float fAvgPixPerChar       = static_cast<float>(PixLength) / strlen;
              const unsigned int iCharToRemove = ceilf((static_cast<float>(PixLength - 255)) / fAvgPixPerChar);

              // shorten string because OLED controller can not handle such long strings
              *currentLine = currentLine->substring(0, strlen - iCharToRemove);
            }
          }
        }
      }
      eventId        = P036_EVENT_LINE;
      bUpdateDisplay = true;
    }
  } else {
    success = true;

    switch (static_cast<p036_subcommands_e>(command_i)) {
      case p036_subcommands_e::display: {
        // display functions
        const String para1 = parseString(string, 3);

        if (equals(para1, F("on"))) {
          displayTimer = P036_TIMER;
          display->displayOn();

          P036_SetDisplayOn(1); //  Save the fact that the display is now ON
            # if P036_SEND_EVENTS

          if (sendEvents) {
            P036_SendEvent(event, P036_EVENT_DISPLAY, 1);
          }
            # endif // if P036_SEND_EVENTS
        }

        else if (equals(para1, F("off"))) {
          displayTimer = 0;
          display->displayOff();

          P036_SetDisplayOn(0); //  Save the fact that the display is now OFF
            # if P036_SEND_EVENTS

          if (sendEvents) {
            P036_SendEvent(event, P036_EVENT_DISPLAY, 0);
          }
            # endif // if P036_SEND_EVENTS
        }

        else if (equals(para1, F("low"))) {
          setContrast(OLED_CONTRAST_LOW);
          LineNo     = 0; // is event parameter
          eventId    = P036_EVENT_CONTRAST;
          bDisplayON = true;
        }

        else if (equals(para1, F("med"))) {
          setContrast(OLED_CONTRAST_MED);
          LineNo     = 1; // is event parameter
          eventId    = P036_EVENT_CONTRAST;
          bDisplayON = true;
        }

        else if (equals(para1, F("high"))) {
          setContrast(OLED_CONTRAST_HIGH);
          LineNo     = 2; // is event parameter
          eventId    = P036_EVENT_CONTRAST;
          bDisplayON = true;
        }

        else if (equals(para1, F("user")) &&
                 (event->Par3 >= 1) && (event->Par3 <= 255) && // contrast
                 (event->Par4 >= 0) && (event->Par4 <= 255) && // precharge
                 (event->Par5 >= 0) && (event->Par5 <= 255))   // comdetect
        {
          display->setContrast(static_cast<uint8_t>(event->Par3), static_cast<uint8_t>(event->Par4),
                               static_cast<uint8_t>(event->Par5));
          LineNo     = 3; // is event parameter
          eventId    = P036_EVENT_CONTRAST;
          bDisplayON = true;
        } else {
          success = false;
        }
        break;
      }
      case p036_subcommands_e::frame:
      {
        if ((event->Par2 >= 0) &&
            (event->Par2 <= MaxFramesToDisplay + 1)) {
          if (!P036_DisplayIsOn) {
            // display was OFF, turn it ON
            display->displayOn();
            P036_SetDisplayOn(1); //  Save the fact that the display is now ON
            # if P036_SEND_EVENTS

            if (sendEvents) {
              P036_SendEvent(event, P036_EVENT_DISPLAY, 1);
            }
            # endif // if P036_SEND_EVENTS
          }
          const uint8_t nextFrame = (event->Par2 == 0 ? 0xFF : event->Par2 - 1);
          P036_JumpToPage(event, nextFrame); //  Start to display the selected page, function needs
          // 65ms!
          # if P036_SEND_EVENTS

          if (sendEvents && bitRead(P036_FLAGS_0, P036_FLAG_EVENTS_FRAME_LINE)) { // Bit 29 Send Events Frame & Line
            P036_SendEvent(event, P036_EVENT_FRAME, currentFrameToDisplay + 1);
          }
          # endif // if P036_SEND_EVENTS
        } else {
          success = false;
        }
        break;
      }
      # if P036_ENABLE_LINECOUNT
      case p036_subcommands_e::linecount:

        if ((event->Par2 >= 1) &&
            (event->Par2 <= 4)) {
          #  if P036_ENABLE_TICKER

          if (static_cast<ePageScrollSpeed>(P036_SCROLL) == ePageScrollSpeed::ePSS_Ticker) {
            // Ticker supports only 1 line, can not be changed
            success = (event->Par2 == 1);
            return success;
          }
          #  endif // if P036_ENABLE_TICKER

          if (P036_NLINES != event->Par2) {
            P036_NLINES = event->Par2;
            setNrLines(event, P036_NLINES);
            #  if P036_SEND_EVENTS

            if (sendEvents && bitRead(P036_FLAGS_0, P036_FLAG_EVENTS_FRAME_LINE)) { // Bit 29 Send Events Frame & Line
              P036_SendEvent(event, P036_EVENT_LINECNT, P036_NLINES);
            }
            #  endif // if P036_SEND_EVENTS
          } else {
            success = false;
          }
        }
        break;
      # endif // if P036_ENABLE_LINECOUNT
      case p036_subcommands_e::restore:

        if ((event->Par2 >= 0) && // 0: restore all line contents
            (event->Par2 <= P36_Nlines)) {
          // restore content functions
          LineNo = event->Par2;
          RestoreLineContent(event->TaskIndex,
                             get4BitFromUL(P036_FLAGS_0, P036_FLAG_SETTINGS_VERSION), // Bit23-20 Version CustomTaskSettings
                             LineNo);

          if (LineNo == 0) {
            LineNo = 1; // after restoring all contents start with first Line
          }
          eventId        = P036_EVENT_RESTORE;
          bUpdateDisplay = true;
        }
        break;
      case p036_subcommands_e::scroll:

        if (event->Par2 >= 1) {
          // set scroll

          switch (event->Par2) {
            case 1: P036_SCROLL = static_cast<int16_t>(ePageScrollSpeed::ePSS_VerySlow); break;
            case 2: P036_SCROLL = static_cast<int16_t>(ePageScrollSpeed::ePSS_Slow); break;
            case 3: P036_SCROLL = static_cast<int16_t>(ePageScrollSpeed::ePSS_Fast); break;
            case 4: P036_SCROLL = static_cast<int16_t>(ePageScrollSpeed::ePSS_VeryFast); break;
            case 5: P036_SCROLL = static_cast<int16_t>(ePageScrollSpeed::ePSS_Instant); break;
            # if P036_ENABLE_TICKER
            case 6: P036_SCROLL = static_cast<int16_t>(ePageScrollSpeed::ePSS_Ticker); break;
            # endif // if P036_ENABLE_TICKER
            default:
              success = false;
              break;
          }

          if (success) {
            prepare_pagescrolling(static_cast<ePageScrollSpeed>(P036_SCROLL), P036_NLINES);
            eventId        = P036_EVENT_SCROLL;
            LineNo         = 1; // after change scroll start with first Line
            bUpdateDisplay = true;
          }
        } else {
          success = false;
        }
        break;
      # if P036_ENABLE_LEFT_ALIGN
      case p036_subcommands_e::leftalign:

        if ((event->Par2 == 0) ||
            (event->Par2 == 1)) {
          eAlignment aAlignment = (event->Par2 == 1 ? eAlignment::eLeft : eAlignment::eCenter);
          setTextAlignment(aAlignment);
          uint32_t lSettings = P036_FLAGS_1;
          set2BitToUL(lSettings, P036_FLAG_LEFT_ALIGNED, static_cast<uint8_t>(aAlignment)); // Alignment
          P036_FLAGS_1 = lSettings;
        } else {
          success = false;
        }
        break;
      case p036_subcommands_e::align:

        if ((event->Par2 >= 0) &&
            (event->Par2 <= 2)) {
          const eAlignment aAlignment = static_cast<eAlignment>(event->Par2);

          setTextAlignment(aAlignment);
          uint32_t lSettings = P036_FLAGS_1;
          set2BitToUL(lSettings, P036_FLAG_LEFT_ALIGNED, static_cast<uint8_t>(aAlignment)); // Alignment
          P036_FLAGS_1 = lSettings;
        } else {
          success = false;
        }
        break;
      # endif // if P036_ENABLE_LEFT_ALIGN
      # if P036_USERDEF_HEADERS

      case p036_subcommands_e::userdef1:
      {
        userDef1 = parseStringKeepCase(string, 3);
        CleanEscapeCharacters(userDef1, true); // Allow system vars and task values to be passed to the
        // user defined header1
        break;
      }

      case p036_subcommands_e::userdef2:
      {
        userDef2 = parseStringKeepCase(string, 3);
        CleanEscapeCharacters(userDef2, true); // Allow system vars and task values to be passed to the
        // user defined header2
        break;
      }
      # endif // if P036_USERDEF_HEADERS
    }
  }


  if (success && (eventId > 0)) {
    if (bDisplayON) {
          # if P036_SEND_EVENTS

      if (sendEvents) {
        P036_SendEvent(event, eventId, LineNo);

        if (!P036_DisplayIsOn) {
          P036_SendEvent(event, P036_EVENT_DISPLAY, 1);
        }
      }
          # endif // if P036_SEND_EVENTS
      P036_SetDisplayOn(1); //  Save the fact that the display is now ON
    }

    if (bUpdateDisplay) {
      MaxFramesToDisplay = 0xff; // update frame count

      # if P036_SEND_EVENTS
      const uint8_t currentFrame = currentFrameToDisplay;
      # endif // if P036_SEND_EVENTS

      if (!P036_DisplayIsOn &&
          (!bitRead(P036_FLAGS_0, P036_FLAG_NODISPLAY_ONRECEIVE) || // Bit 18 NoDisplayOnReceivedText
           (eventId == P036_EVENT_SCROLL))) {
        // display was OFF, turn it ON
        display->displayOn();
        P036_SetDisplayOn(1); //  Save the fact that the display is now ON
        # if P036_SEND_EVENTS

        if (sendEvents) {
          P036_SendEvent(event, P036_EVENT_DISPLAY, 1);

          if (bitRead(P036_FLAGS_0, P036_FLAG_EVENTS_FRAME_LINE)) { // Bit 29 Send Events Frame & Line
            P036_SendEvent(event, P036_EVENT_LINE, LineNo);
          }
        }
        # endif // if P036_SEND_EVENTS
      }

      if (P036_DisplayIsOn) {
        bLineScrollEnabled = false; // disable scrolling temporary
        # if P036_ENABLE_TICKER

        if (bUseTicker) {
          P036_JumpToPage(event, 0); // Restart the Ticker
        }
        else
        # endif // if P036_ENABLE_TICKER
        P036_JumpToPageOfLine(event, LineNo - 1); // Start to display the selected page, function needs 65ms!
        # if P036_SEND_EVENTS

        if (sendEvents && bitRead(P036_FLAGS_0, P036_FLAG_EVENTS_FRAME_LINE) && (currentFrame != currentFrameToDisplay)) {
          P036_SendEvent(event, P036_EVENT_FRAME, currentFrameToDisplay + 1);
        }
        # endif // if P036_SEND_EVENTS
      }

      # ifdef PLUGIN_036_DEBUG

      if (eventId == P036_EVENT_LINE) {
        if (loglevelActiveFor(LOG_LEVEL_INFO)) { // estimated
          addLogMove(LOG_LEVEL_INFO,
                     strformat(F("[P036] Line: %d Content:%s Length:%d Pix: %d Reserved:%d"),
                               LineNo,
                               LineContent->DisplayLinesV1[LineNo - 1].Content,
                               LineContent->DisplayLinesV1[LineNo - 1].Content.length(),
                               display->getStringWidth(LineContent->DisplayLinesV1[LineNo - 1].Content),
                               LineContent->DisplayLinesV1[LineNo - 1].reserved));
          delay(5); // FIXME otherwise it is maybe too fast for the serial monitor
        }
      }
      # endif // PLUGIN_036_DEBUG
    }
  }
  # ifdef PLUGIN_036_DEBUG

  if (!success && loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO,
               strformat(F("[P036] Cmd: %s SubCmd:%s Success:false"), command.c_str(), subcommand.c_str()));
  }
  # endif // PLUGIN_036_DEBUG
  return success;
}

bool P036_data_struct::isInitialized() const {
  return (display != nullptr) && (LineContent != nullptr);
}

void P036_data_struct::setContrast(uint8_t OLED_contrast) {
  OLedSetContrast(display, OLED_contrast);
}

void P036_data_struct::setOrientationRotated(bool rotated) {
  if (rotated) {
    display->flipScreenVertically();
    TopLineOffset = P36_MaxDisplayHeight - getDisplaySizeSettings(disp_resolution).Height;
  } else {
    TopLineOffset = 0;
  }
}

void P036_data_struct::RestoreLineContent(taskIndex_t taskIndex,
                                          uint8_t     LoadVersion,
                                          uint8_t     LineNo) {
  // Try to allocate in PSRAM or 2nd heap if possible
  constexpr unsigned size = sizeof(P036_LineContent);
  void *ptr               = special_calloc(1, size);

  if (!ptr) {
    return;
  }

  P036_LineContent *TempContent = new (ptr) P036_LineContent();

  if (TempContent != nullptr) {
    TempContent->loadDisplayLines(taskIndex, LoadVersion);

    if (LineNo == 0) {
      for (int i = 0; i < P36_Nlines; ++i) {
        *(&LineContent->DisplayLinesV1[i].Content) = TempContent->DisplayLinesV1[i].Content;
      }
    }
    else {
      *(&LineContent->DisplayLinesV1[LineNo - 1].Content) = TempContent->DisplayLinesV1[LineNo - 1].Content;
    }
    delete TempContent;
  }
}

# if P036_ENABLE_LINECOUNT
void P036_data_struct::setNrLines(struct EventStruct *event, uint8_t NrLines) {
  if ((NrLines >= 1) && (NrLines <= P36_MAX_LinesPerPage)) {
    prepare_pagescrolling(static_cast<ePageScrollSpeed>(P036_SCROLL), NrLines); // Recalculate font
    MaxFramesToDisplay = 0xFF;                                                  // Recalculate page indicator
    CalcMaxPageCount();                                                         // Update max page count
    nextFrameToDisplay = 0;                                                     // Reset to first page
  }
}

# endif // if P036_ENABLE_LINECOUNT

String P036_data_struct::create_display_header_text(eHeaderContent iHeaderContent) const
{
  String newString, strHeader;
  const __FlashStringHelper *newString_f = F("%sysname%");
  bool use_newString_f                   = true;

  switch (iHeaderContent) {
    case eHeaderContent::eSSID:

      if (NetworkConnected()) {
        strHeader       = WiFi.SSID();
        use_newString_f = false;
      }

      //      else {
      //        newString_f = F("%sysname%");
      //      }
      break;
    case eHeaderContent::eSysName:
      //      newString_f = F("%sysname%");
      break;
    case eHeaderContent::eTime:
      newString_f = F("%systime%");
      break;
    case eHeaderContent::eDate:
      newString_f = F("%sysday_0%.%sysmonth_0%.%sysyear%");
      break;
    case eHeaderContent::eIP:
      newString_f = F("%ip%");
      break;
    case eHeaderContent::eMAC:
      newString_f = F("%mac%");
      break;
    case eHeaderContent::eRSSI:
      newString_f = F("%rssi%dBm");
      break;
    case eHeaderContent::eBSSID:
      newString_f = F("%bssid%");
      break;
    case eHeaderContent::eWiFiCh:
      newString_f = F("Channel: %wi_ch%");
      break;
    case eHeaderContent::eUnit:
      newString_f = F("Unit: %unit%");
      break;
    case eHeaderContent::eSysLoad:
      newString_f = F("Load: %sysload%%");
      break;
    case eHeaderContent::eSysHeap:
      newString_f = F("Mem: %sysheap%");
      break;
    case eHeaderContent::eSysStack:
      newString_f = F("Stack: %sysstack%");
      break;
    case eHeaderContent::ePageNo:
      use_newString_f = false;
      strHeader       = strformat(F("page %d"), currentFrameToDisplay + 1);

      if (MaxFramesToDisplay != 0xFF) {
        strHeader += F("/");
        strHeader += (MaxFramesToDisplay + 1);
      }
      break;
    # if P036_USERDEF_HEADERS
    case eHeaderContent::eUserDef1:
      use_newString_f = false;
      newString       = userDef1;
      break;
    case eHeaderContent::eUserDef2:
      use_newString_f = false;
      newString       = userDef2;
      break;
    # endif // if P036_USERDEF_HEADERS
    case eHeaderContent::eNone:
      return EMPTY_STRING;
  }

  if (use_newString_f) {
    newString = newString_f;
  }

  if (newString.length() > 0) {
    // Right now only systemvariables have been used, so we don't have to call the parseTemplate.
    parseSystemVariables(newString, false);
    strHeader = newString;
  }

  strHeader.trim();
  return strHeader;
}

void P036_data_struct::display_header() {
  if (!isInitialized()) {
    return;
  }

  if (bHideHeader) { //  hide header
    return;
  }

  const eHeaderContent iHeaderContent = ((HeaderContentAlternative == HeaderContent) || !bAlternativHeader)
    ? HeaderContent
    : HeaderContentAlternative;
  const String title = create_display_header_text(iHeaderContent);

  display_title(title);

  // Display time and wifibars both clear area below, so paint them after the title.
  if (getDisplaySizeSettings(disp_resolution).Width == P36_MaxDisplayWidth) {
    display_time(); // only for 128pix wide displays
  }
  display_wifibars();

  # ifdef OLEDDISPLAY_DOUBLE_BUFFER

  // Update only small sections of the display, reducing the amount of data to be sent to the display
  update_display();
  # endif // ifdef OLEDDISPLAY_DOUBLE_BUFFER
}

void P036_data_struct::display_time() {
  if (!isInitialized()) {
    return;
  }

  # if P036_ENABLE_TIME_FORMAT
  SystemVariables::Enum timeOptions[] = {
    SystemVariables::SYSTIME,
    SystemVariables::SYSTM_HM_0,
    SystemVariables::SYSTIME_AM_0,
    SystemVariables::SYSTM_HM_AM_0,
  };
  # endif // if P036_ENABLE_TIME_FORMAT

  const String dtime = SystemVariables::getSystemVariable(
    # if P036_ENABLE_TIME_FORMAT
    timeOptions[timeFormat]
    # else // if P036_ENABLE_TIME_FORMAT
    SystemVariables::SYSTIME
    # endif // if P036_ENABLE_TIME_FORMAT
    );

  display->setTextAlignment(TEXT_ALIGN_LEFT);
  display->setFont(getArialMT_Plain_10());
  display->setColor(BLACK);
  display->fillRect(0, TopLineOffset, 28, GetHeaderHeight() - 2);
  display->setColor(WHITE);
  display->drawString(0, TopLineOffset, dtime);
}

void P036_data_struct::display_title(const String& title) {
  if (!isInitialized()) {
    return;
  }
  display->setColor(BLACK);
  display->fillRect(0, TopLineOffset, P36_MaxDisplayWidth, GetHeaderHeight()); // don't clear line under title.
  display->setColor(WHITE);

  if (title.isEmpty()) {
    return;
  }
  display->setFont(getArialMT_Plain_10());

  if (getDisplaySizeSettings(disp_resolution).Width == P36_MaxDisplayWidth) {
    display->setTextAlignment(TEXT_ALIGN_CENTER);
    display->drawString(P36_DisplayCentre, TopLineOffset, title);
  } else {
    display->setTextAlignment(TEXT_ALIGN_LEFT); // Display right of WiFi bars
    display->drawString(getDisplaySizeSettings(disp_resolution).PixLeft + getDisplaySizeSettings(disp_resolution).WiFiIndicatorWidth + 3,
                        TopLineOffset,
                        title);
  }
}

void P036_data_struct::display_logo() {
  if (!isInitialized()) {
    return;
  }
  # ifdef PLUGIN_036_DEBUG
  addLog(LOG_LEVEL_INFO, F("P036_DisplayLogo"));
  # endif // PLUGIN_036_DEBUG

  int left = 24;
  int top;
  const tFontSettings iFontsettings = CalculateFontSettings(2); // get font with max. height for displaying "ESP Easy"

  bDisplayingLogo = true;                                       // next time the display must be cleared completely
  display->setTextAlignment(TEXT_ALIGN_LEFT);
  display->setFont(FontSizes[iFontsettings.fontIdx].fontData);
  display->clear();                                             // resets all pixels to black
  display->setColor(WHITE);
  display->drawString(65, iFontsettings.Top + TopLineOffset,                                              F("ESP"));
  display->drawString(65, iFontsettings.Top + iFontsettings.Height + iFontsettings.Space + TopLineOffset, F("Easy"));

  if (getDisplaySizeSettings(disp_resolution).PixLeft < left) { left = getDisplaySizeSettings(disp_resolution).PixLeft; }
  top = (getDisplaySizeSettings(disp_resolution).Height - espeasy_logo_height) / 2;

  if (top < 0) { top = 0; }
  display->drawXbm(left,
                   top + TopLineOffset,
                   espeasy_logo_width,
                   espeasy_logo_height,
                   espeasy_logo_bits); // espeasy_logo_width=espeasy_logo_height=36
}

// Draw the frame position

void P036_data_struct::display_indicator() {
  if (!isInitialized()) {
    return;
  }

  if (bHideFooter) { //  hide footer
    return;
  }

  int frameCount = MaxFramesToDisplay + 1;

  //  Erase Indicator Area
  display->setColor(BLACK);
  display->fillRect(0, P036_IndicatorTop + TopLineOffset, P36_MaxDisplayWidth, P036_IndicatorHeight);

  // Only display when there is something to display.
  if ((frameCount <= 1) || (frameCount > P36_Nlines)) { return; }

  display->setColor(WHITE);

  // Display chars as required
  for (uint8_t i = 0; i < frameCount; ++i) {
    const char *image;

    if (currentFrameToDisplay == i) {
      image = activeSymbole;
    } else {
      image = inactiveSymbole;
    }

    int x;
    const int y = P036_IndicatorTop + TopLineOffset; // 2022-01-31 Removed unneeded offset '+ 2'

    // I would like a margin of 20 pixels on each side of the indicator.
    // Therefore the width of the indicator should be 128-40=88 and so space between indicator dots is 88/(framecount-1)
    // The width of the display is 128 and so the first dot must be at x=20 if it is to be centred at 64
    const int number_spaces = frameCount - 1;

    if (number_spaces <= 0) {
      return;
    }
    int margin  = 20;
    int spacing = (P36_MaxDisplayWidth - 2 * margin) / number_spaces;

    // Now apply some max of 30 pixels between the indicators and center the row of indicators.
    if (spacing > 30) {
      spacing = 30;
      margin  = (P36_MaxDisplayWidth - number_spaces * spacing) / 2;
    }

    x = margin + (spacing * i);
    display->drawXbm(x, y, 8, 8, image);
  }
}

int16_t P036_data_struct::GetHeaderHeight() const {
  if (bHideHeader) {
    // no header
    return 0;
  }
  return P36_HeaderHeight;
}

int16_t P036_data_struct::GetIndicatorTop() const {
  if (bHideFooter
      # if P036_ENABLE_TICKER
      || bUseTicker
      # endif // if P036_ENABLE_TICKER
      ) {
    // no footer (indicator) -> returm max. display height
    return getDisplaySizeSettings(disp_resolution).Height;
  }
  return P036_IndicatorTop;
}

tIndividualFontSettings P036_data_struct::CalculateIndividualFontSettings(uint8_t LineNo,
                                                                          uint8_t FontIndex,
                                                                          uint8_t LinesPerFrame,
                                                                          uint8_t FrameNo,
                                                                          int8_t  MaxHeight,
                                                                          uint8_t IdxForBiggestFont) {
  // returns the next LineNo or 0xFF if the setings do not fit
  tIndividualFontSettings result;
  uint8_t NextLineNo     = LineNo;
  uint8_t lLinesPerFrame = 0;
  int8_t  lHeight        = 0;
  int8_t  lSpace         = 0;
  int8_t  lTop           = 0;

  result.IdxForBiggestFontUsed = FontIndex;

  if (NextLineNo >= P36_Nlines) {
    // just in case
    result.NextLineNo = NextLineNo; // default, settings do fit
    return result;                  // finished
  }

  for (uint8_t i = LineNo; i < P36_Nlines; ++i) {
    // calculate individual font settings
    uint8_t lFontIndex            = FontIndex;
    const eModifyFont iModifyFont =
      static_cast<eModifyFont>(get3BitFromUL(LineContent->DisplayLinesV1[i].ModifyLayout, P036_FLAG_ModifyLayout_Font));

    switch (iModifyFont) {
      case eModifyFont::eEnlarge:

        if (ScrollingPages.linesPerFrameDef > 1) {
          // Font can only be enlarged if more than 1 line is displayed
          if (lFontIndex > IdxForBiggestFont) {
            lFontIndex--;
          } else {
            lFontIndex = IdxForBiggestFont;
          }
          result.IdxForBiggestFontUsed = lFontIndex;
        }
        break;
      case eModifyFont::eMaximize:

        if (ScrollingPages.linesPerFrameDef > 1) {
          // Font can only be maximized if more than 1 line is displayed
          lFontIndex                   = IdxForBiggestFont;
          result.IdxForBiggestFontUsed = lFontIndex;
        }
        break;
      case eModifyFont::eReduce:
        lFontIndex++;

        if (lFontIndex >= NR_ELEMENTS(FontSizes)) {
          lFontIndex = NR_ELEMENTS(FontSizes) - 1;
        }
        break;
      case eModifyFont::eMinimize:
        lFontIndex = NR_ELEMENTS(FontSizes) - 1;
        break;
      case eModifyFont::eNone:
        lFontIndex = FontIndex;
        break;
    }
    LineSettings[i].FontHeight = FontSizes[lFontIndex].Height;
    lHeight                   += FontSizes[lFontIndex].Height;
    LineSettings[i].frame      = FrameNo;
    LineSettings[i].fontIdx    = lFontIndex;
    lLinesPerFrame++;

    NextLineNo++;

    if (lLinesPerFrame == LinesPerFrame) { break; }
  }
  result.NextLineNo = NextLineNo; // default, settings do fit

  const int8_t deltaHeight = (MaxHeight - lHeight);

  if (lLinesPerFrame <= 1) {
    // just one lines per frame -> no space inbetween
    lSpace = 0;
    lTop   = (MaxHeight - lHeight) / 2;

    if (lHeight > MaxHeight) {
      result.NextLineNo = 0xFF; // settings do not fit
    }
  } else {
    if (deltaHeight >= (lLinesPerFrame - 1)) {
      // individual line setting fits
      // more than one lines per frame -> calculate space inbetween
      lSpace = deltaHeight / lLinesPerFrame;
      lTop   = (MaxHeight - (lHeight + (lSpace * (lLinesPerFrame - 1)))) / 2;
    } else {
      // individual line setting do not fit
      lTop   = 0;
      lSpace = -1; // allow overlapping by 1 pix

      if (deltaHeight < (-1 * (lLinesPerFrame - 1))) {
        if ((result.IdxForBiggestFontUsed == (NR_ELEMENTS(FontSizes) - 1)) &&
            (LinesPerFrame == SizeSettings[static_cast<int>(disp_resolution)].MaxLines)) {
          // max lines for used display and smallest font reached -> use special space between the lines and return 'fits'
          // overlapping (lSpace<0) depends on the absolute display height
          switch (disp_resolution) {
            case p036_resolution::pix128x64: lSpace = bHideFooter ? 0 : -2;
              break;
            case p036_resolution::pix128x32: lSpace = -2;
              break;
            case p036_resolution::pix64x48: lSpace = -1;
              break;
          }
        } else {
          result.NextLineNo = 0xFF; // settings do not fit
        }
      }
    }
  }
  LineSettings[LineNo].ypos = lTop + GetHeaderHeight() + TopLineOffset;

  if (lLinesPerFrame > 1) {
    for (uint8_t k = (LineNo + 1); k < NextLineNo; ++k) {
      LineSettings[k].ypos = LineSettings[k - 1].ypos + FontSizes[LineSettings[k - 1].fontIdx].Height + lSpace;
    }
  }
  # ifdef P036_CHECK_INDIVIDUAL_FONT

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    String log1;

    if (log1.reserve(140)) { // estimated
      delay(10);             // FIXME otherwise it is maybe too fast for the serial monitor
      log1 = strformat(F("IndividualFontSettings: result.NextLineNo:%d result.IdxForBiggestFontUsed:%d LineNo:%d LinesPerFrame:%d"),
                       result.NextLineNo,
                       result.IdxForBiggestFontUsed,
                       LineNo,
                       LinesPerFrame);

      if (result.NextLineNo != 0xFF) {
        log1 += strformat(F(" FrameNo:%d lTop:%d lSpace:%d"), FrameNo, lTop, lSpace);
      }
      addLogMove(LOG_LEVEL_INFO, log1);
    }
  }
  # endif // # ifdef P036_CHECK_INDIVIDUAL_FONT
  return result;
}

tFontSettings P036_data_struct::CalculateFontSettings(uint8_t lDefaultLines) {
  tFontSettings result;
  int iHeight;
  int8_t  iFontIndex = -1;
  uint8_t iMaxHeightForFont;
  uint8_t iLinesPerFrame;
  uint8_t i;

  if (lDefaultLines == 0) {
    // number of lines can be reduced if no font fits the setting
    iLinesPerFrame = ScrollingPages.linesPerFrameDef;
    iHeight        = GetIndicatorTop() - GetHeaderHeight();
  } else {
    // number of lines is fixed (e.g. for splash screen)
    iLinesPerFrame = lDefaultLines;
    iHeight        = getDisplaySizeSettings(disp_resolution).Height;
  }
  uint8_t iUsedHeightForFonts = iHeight;

  # ifdef P036_FONT_CALC_LOG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO,
           strformat(F("P036 CalculateFontSettings lines: %d, height: %d, header: %d, footer: %d"),
                     iLinesPerFrame,
                     iHeight,
                     !bHideHeader,
                     !bHideFooter));
  }
  # endif // ifdef P036_FONT_CALC_LOG

  iMaxHeightForFont = lround(iHeight / (iLinesPerFrame * 1.0f)); // no extra space between lines
  // Fonts already have their own extra space, no need to add an extra pixel space

  # ifdef P036_FONT_CALC_LOG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO,
           strformat(F("CalculateFontSettings LinesPerFrame: %d, iHeight: %d, maxFontHeight: %d"),
                     iLinesPerFrame, iHeight, iMaxHeightForFont));
  }
  # endif // ifdef P036_FONT_CALC_LOG

  while (iFontIndex < 0) {
    # ifdef P036_FONT_CALC_LOG
    String log1;
    log1.reserve(80);
    # endif // ifdef P036_FONT_CALC_LOG

    for (i = 0; i < NR_ELEMENTS(FontSizes) - 1; ++i) {
      // check available fonts for the line setting
      # ifdef P036_FONT_CALC_LOG

      // Appending a string won't need a delay(5) call...
      log1 = strformat(F(" -> i: %d, h: %d"), i, FontSizes[i].Height);
      # endif // ifdef P036_FONT_CALC_LOG

      if (FontSizes[i].Height > iMaxHeightForFont) {
        // height of font is too big
        continue;
      }
      iFontIndex = i; // save the current index
      # ifdef P036_FONT_CALC_LOG
      log1 += concat(F(", fontIdx: "), iFontIndex);
      # endif // ifdef P036_FONT_CALC_LOG
      break;
    }

    if (iFontIndex < 0) {
      # ifdef P036_FONT_CALC_LOG
      log1 += concat(F(", no font fits, fontIdx: "), iFontIndex);
      addLogMove(LOG_LEVEL_INFO, log1);
      delay(5); // FIXME otherwise it is maybe too fast for the serial monitor
      # endif // ifdef P036_FONT_CALC_LOG
      break;
    }
    # ifdef P036_FONT_CALC_LOG
    log1 += F(", font fits");
    addLogMove(LOG_LEVEL_INFO, log1);
    # endif // ifdef P036_FONT_CALC_LOG
  }

  if (iFontIndex >= 0) {
    // font found -> calculate top position and space between lines
    iUsedHeightForFonts = FontSizes[iFontIndex].Height * iLinesPerFrame;

    if (iLinesPerFrame > 1) {
      // more than one lines per frame -> calculate space inbetween
      result.Space = (iHeight - iUsedHeightForFonts) / iLinesPerFrame;
    } else {
      // just one lines per frame -> no space inbetween
      result.Space = 0;
    }
    result.Top = (iHeight - (iUsedHeightForFonts + (result.Space * (iLinesPerFrame - 1)))) / 2;
  } else {
    // no font found -> return font with shortest height
    // FIXED: tonhuisman Tweaked to match the 13 pix font to fit for 4 lines display
    result.Top = 0;

    // overlapping (lSpace<0) depends on the absolute display height
    switch (disp_resolution) {
      case p036_resolution::pix128x64:  result.Space = bHideFooter ? 0 : -2;
        break;
      case p036_resolution::pix128x32:  result.Space = -2;
        break;
      case p036_resolution::pix64x48:  result.Space = -1;
        break;
    }
    iFontIndex = NR_ELEMENTS(FontSizes) - 1;
  }

  if (lDefaultLines == 0) {
    // calculate height for individual line font setting
    uint8_t currentLinesPerFrame = iLinesPerFrame;
    tIndividualFontSettings IndividualFontSettings;
    uint8_t currentFrame       = 0;
    uint8_t currentLine        = 0;
    uint8_t iIdxForBiggestFont = 0;

    while (currentLine < P36_Nlines) {
      # if P036_ENABLE_TICKER

      if (bUseTicker && (currentLine > 0)) {
        // for ticker only the first line defines the font
        break;
      }
      # endif // if P036_ENABLE_TICKER
      // calculate individual font settings
      IndividualFontSettings = CalculateIndividualFontSettings(currentLine,
                                                               iFontIndex,
                                                               currentLinesPerFrame,
                                                               currentFrame,
                                                               iHeight,
                                                               iIdxForBiggestFont);

      if (IndividualFontSettings.NextLineNo == 0xFF) {
        // individual settings do not fit
        if ((bReduceLinesPerFrame) && (currentLinesPerFrame > 1)) {
          currentLinesPerFrame--;                                                // reduce number of lines per frame
        } else {
          iIdxForBiggestFont = IndividualFontSettings.IdxForBiggestFontUsed + 1; // use smaller font size as maximum
        }
      } else {
        // individual line setting do fits
        currentLinesPerFrame = iLinesPerFrame; // default for next call of CalculateIndividualFontSettings()
        iIdxForBiggestFont   = 0;              // default for next call of CalculateIndividualFontSettings()
        currentLine          = IndividualFontSettings.NextLineNo;
        currentFrame++;                        // next frame
      }
    }

    # ifdef P036_CHECK_INDIVIDUAL_FONT

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      String log1;

      if (log1.reserve(140)) { // estimated
        for (uint8_t i = 0; i < P36_Nlines; ++i) {
          delay(5);            // FIXME otherwise it is maybe too fast for the serial monitor
          log1 = strformat(F("Line[%d]: Frame:%d FontIdx:%d ypos:%d FontHeight:%d"), i, LineSettings[i].frame,
                           LineSettings[i].fontIdx, LineSettings[i].ypos - TopLineOffset, LineSettings[i].FontHeight);
          addLogMove(LOG_LEVEL_INFO, log1);
        }
      }
    }
    # endif // ifdef P036_CHECK_INDIVIDUAL_FONT
  }
  result.fontIdx = iFontIndex;
  result.Height  = FontSizes[iFontIndex].Height;

  # ifdef P036_FONT_CALC_LOG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    String log1;

    if (log1.reserve(140)) { // estimated
      delay(5);              // FIXME otherwise it is maybe too fast for the serial monitor
      log1  = concat(F("CalculateFontSettings: Font:"), result.FontName());
      log1 += strformat(F(" Idx:%d Top:%d FontHeight:%d Space:%d"), iFontIndex, result.Top, result.Height, result.Space);
      log1 += concat(F(" HeightForLines:"), iHeight);
      log1 += concat(F(" LinesPerFrame:"), iLinesPerFrame);

      if (lDefaultLines == 0) {
        log1 += concat(F(" DefaultLinesPerFrame:"), ScrollingPages.linesPerFrameDef);
      }
      else {
        log1 += concat(F(" DefaultLines:"), lDefaultLines);
      }
      addLogMove(LOG_LEVEL_INFO, log1);
    }
  }
  # endif // P036_FONT_CALC_LOG

  return result;
}

void P036_data_struct::prepare_pagescrolling(ePageScrollSpeed lscrollspeed,
                                             uint8_t          NrLines) {
  if (!isInitialized()) {
    return;
  }
  # if P036_ENABLE_TICKER
  bUseTicker = (lscrollspeed == ePageScrollSpeed::ePSS_Ticker);

  if (bUseTicker) {
    ScrollingPages.linesPerFrameDef = 1;
  }
  else
  # endif // if P036_ENABLE_TICKER
  {
    ScrollingPages.linesPerFrameDef = NrLines;
  }
  CalculateFontSettings(0);
}

uint8_t P036_data_struct::display_scroll(ePageScrollSpeed lscrollspeed, int lTaskTimer)
{
  if (!isInitialized()) {
    return 0;
  }

  int iPageScrollTime;
  int iCharToRemove = 0;

  # ifdef PLUGIN_036_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, concat(F("Start Scrolling: Speed: "), static_cast<int>(lscrollspeed)));
  }
  # endif // PLUGIN_036_DEBUG

  ScrollingLines.wait = 0;

  // calculate total page scrolling time
  if ((lscrollspeed == ePageScrollSpeed::ePSS_Instant) // no scrolling, just the handling time to build the new page
      # if P036_ENABLE_TICKER                          // for ticker, no scrolling, just the handling time to build the new page
      || (lscrollspeed == ePageScrollSpeed::ePSS_Ticker)
      # endif // if P036_ENABLE_TICKER
      ) {
    iPageScrollTime = P36_PageScrollTick - P36_PageScrollTimer;
  } else {
    iPageScrollTime = (P36_MaxDisplayWidth / (P36_PageScrollPix * static_cast<int>(lscrollspeed))) * P36_PageScrollTick;
  }
  int iScrollTime = static_cast<float>(lTaskTimer * 1000 - iPageScrollTime - 2 * P36_WaitScrollLines * 100) / 100; // scrollTime in ms

  # ifdef PLUGIN_036_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, concat(F("PageScrollTime: "), iPageScrollTime));
  }
  # endif // PLUGIN_036_DEBUG

  uint16_t MaxPixWidthForPageScrolling = P36_MaxDisplayWidth;

  if (bLineScrollEnabled) {
    // Reduced scrolling width because line is displayed left or right aligned
    MaxPixWidthForPageScrolling -= getDisplaySizeSettings(disp_resolution).PixLeft;
  }

  # if P036_ENABLE_TICKER

  if (bUseTicker) {
    ScrollingLines.Ticker.Tcontent = EMPTY_STRING;
    ScrollingLines.Ticker.IdxEnd   = 0;
    ScrollingLines.Ticker.IdxStart = 0;

    for (uint8_t i = 0; i < P36_Nlines; ++i) {
      String tmpString(LineContent->DisplayLinesV1[i].Content);
      tmpString.replace(F("<|>"), "   "); // replace the split token with three space char
      ScrollingLines.Ticker.Tcontent += P36_parseTemplate(tmpString, i);
    }
    ScrollingLines.Ticker.len = ScrollingLines.Ticker.Tcontent.length();
  }
  # endif // if P036_ENABLE_TICKER

  for (uint8_t j = 0; j < ScrollingPages.linesPerFrameDef; ++j) {
    // default no line scrolling and strings are centered
    uint16_t PixLengthLineOut = 0; // pix length of line out
    uint16_t PixLengthLineIn  = 0; // pix length of line in
    ScrollingLines.SLine[j].LastWidth = 0;
    ScrollingLines.SLine[j].Width     = 0;

    // get last and new line width
    if (j < ScrollingPages.linesPerFrameIn) {
      PixLengthLineIn = TrimStringTo255Chars(&ScrollingPages.In[j]);
    }

    if (j < ScrollingPages.linesPerFrameIn) {
      PixLengthLineOut = TrimStringTo255Chars(&ScrollingPages.Out[j]);
    }

    if (bLineScrollEnabled) {
      // settings for following line scrolling
      if (PixLengthLineOut > getDisplaySizeSettings(disp_resolution).Width) {
        ScrollingLines.SLine[j].LastWidth = PixLengthLineOut; // while page scrolling this line is right aligned
      }

      if ((
            # if P036_ENABLE_TICKER
            bUseTicker ||
            # endif // if P036_ENABLE_TICKER
            (PixLengthLineIn > getDisplaySizeSettings(disp_resolution).Width)) &&
          (iScrollTime > 0)) {
        // width of the line > display width -> scroll line
        # if P036_ENABLE_TICKER

        if (bUseTicker) {
          ScrollingLines.SLine[j].Width = 0;
          uint16_t AddPixTicker = 0;

          switch (textAlignment) {
            case TEXT_ALIGN_CENTER: ScrollingLines.SLine[j].CurrentLeft = getDisplaySizeSettings(disp_resolution).PixLeft +
                                                                          getDisplaySizeSettings(disp_resolution).Width / 2;
              AddPixTicker = getDisplaySizeSettings(disp_resolution).Width / 2; // half width at begin
              break;
            case TEXT_ALIGN_RIGHT:  ScrollingLines.SLine[j].CurrentLeft = getDisplaySizeSettings(disp_resolution).PixLeft +
                                                                          getDisplaySizeSettings(disp_resolution).Width;
              AddPixTicker = getDisplaySizeSettings(disp_resolution).Width; // full width at begin
              break;
            default: ScrollingLines.SLine[j].CurrentLeft = getDisplaySizeSettings(disp_resolution).PixLeft;
              break;
          }
          ScrollingLines.SLine[j].fPixSum = ScrollingLines.SLine[j].CurrentLeft;

          display->setFont(FontSizes[LineSettings[j].fontIdx].fontData);
          ScrollingLines.SLine[j].dPix = (static_cast<float>(display->getStringWidth(ScrollingLines.Ticker.Tcontent) + AddPixTicker)) /
                                         static_cast<float>(iScrollTime);
          ScrollingLines.SLine[j].SLcontent = EMPTY_STRING;

          ScrollingLines.Ticker.TickerAvgPixPerChar = lround(static_cast<float>(display->getStringWidth(
                                                                                  ScrollingLines.Ticker.Tcontent)) /
                                                             static_cast<float>(ScrollingLines.Ticker.len));

          if (ScrollingLines.Ticker.TickerAvgPixPerChar < ScrollingLines.SLine[j].dPix) {
            ScrollingLines.Ticker.TickerAvgPixPerChar = round(2 * ScrollingLines.SLine[j].dPix);
          }
          ScrollingLines.Ticker.MaxPixLen = getDisplaySizeSettings(disp_resolution).Width + 2 * ScrollingLines.Ticker.TickerAvgPixPerChar;

          // add more characters to display
          while (true) {
            char c             = ScrollingLines.Ticker.Tcontent.charAt(ScrollingLines.Ticker.IdxEnd);
            uint8_t PixForChar = display->getCharWidth(c);

            if ((ScrollingLines.SLine[0].Width + PixForChar) >= ScrollingLines.Ticker.MaxPixLen) {
              break; // no more characters necessary to add
            }
            ScrollingLines.Ticker.IdxEnd++;
            ScrollingLines.SLine[j].Width += PixForChar;
          }
        } else
        # endif // if P036_ENABLE_TICKER
        {
          ScrollingLines.SLine[j].SLcontent   = ScrollingPages.In[j].SPLcontent;
          ScrollingLines.SLine[j].SLidx       = ScrollingPages.In[j].SPLidx; // index to LineSettings[]
          ScrollingLines.SLine[j].Width       = PixLengthLineIn;             // while page scrolling this line is left aligned
          ScrollingLines.SLine[j].CurrentLeft = getDisplaySizeSettings(disp_resolution).PixLeft;
          ScrollingLines.SLine[j].fPixSum     = getDisplaySizeSettings(disp_resolution).PixLeft;

          // pix change per scrolling line tick
          ScrollingLines.SLine[j].dPix =
            (static_cast<float>(PixLengthLineIn - getDisplaySizeSettings(disp_resolution).Width)) / iScrollTime;
        }

        # ifdef P036_SCROLL_CALC_LOG

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          addLog(LOG_LEVEL_INFO, strformat(F("Line: %d width: %d dPix: %d"),
                                           j + 1, ScrollingLines.SLine[j].Width, ScrollingLines.SLine[j].dPix));
          delay(5); // FIXME otherwise it is maybe too fast for the serial monitor
          #  if P036_ENABLE_TICKER

          if (bUseTicker) {
            addLogMove(LOG_LEVEL_INFO,
                       strformat(F("+++ iScrollTime: %d StrLength: %d StrInPix: %d PixPerChar: %d"),
                                 iScrollTime,
                                 ScrollingLines.Ticker.len,
                                 display->getStringWidth(ScrollingLines.Ticker.Tcontent),
                                 ScrollingLines.Ticker.TickerAvgPixPerChar));
            delay(5); // FIXME otherwise it is maybe too fast for the serial monitor
          }
          #  endif // if P036_ENABLE_TICKER
        }
        # endif    // P036_SCROLL_CALC_LOG
      }
    }

    // reduce line content for page scrolling to max width
    if (PixLengthLineIn > MaxPixWidthForPageScrolling) {
      const int strlen = ScrollingPages.In[j].SPLcontent.length();
      # ifdef P036_SCROLL_CALC_LOG
      const String LineInStr = ScrollingPages.In[j].SPLcontent;
      # endif // P036_SCROLL_CALC_LOG
      float fAvgPixPerChar = static_cast<float>(PixLengthLineIn) / strlen;

      if (bLineScrollEnabled) {
        // shorten string on right side because line is displayed left aligned while scrolling
        // using floor() because otherwise empty space on right side
        iCharToRemove = floor(
          (static_cast<float>(PixLengthLineIn - MaxPixWidthForPageScrolling)) / fAvgPixPerChar);
        ScrollingPages.In[j].SPLcontent = ScrollingPages.In[j].SPLcontent.substring(0, strlen - iCharToRemove);
      } else {
        switch (ScrollingPages.In[j].Alignment) {
          case TEXT_ALIGN_LEFT:
            // shorten string on right side because line is left aligned
            // using floor() because otherwise empty space on right side
            iCharToRemove = floor(
              (static_cast<float>(PixLengthLineIn - MaxPixWidthForPageScrolling)) / fAvgPixPerChar);
            ScrollingPages.In[j].SPLcontent = ScrollingPages.In[j].SPLcontent.substring(0, strlen - iCharToRemove);
            break;
          case TEXT_ALIGN_RIGHT:
            // shorten string on left side because line is right aligned
            // using floor() because otherwise empty space on left side
            iCharToRemove = floor(
              (static_cast<float>(PixLengthLineIn - MaxPixWidthForPageScrolling)) / fAvgPixPerChar);
            ScrollingPages.In[j].SPLcontent = ScrollingPages.In[j].SPLcontent.substring(iCharToRemove);
            break;
          default:
            // shorten string on both sides because line is displayed centered
            // using floor() because otherwise empty space on both sides
            iCharToRemove =
              floor((static_cast<float>(PixLengthLineIn - MaxPixWidthForPageScrolling)) / (2 * fAvgPixPerChar));
            ScrollingPages.In[j].SPLcontent = ScrollingPages.In[j].SPLcontent.substring(0, strlen - iCharToRemove);
            ScrollingPages.In[j].SPLcontent = ScrollingPages.In[j].SPLcontent.substring(iCharToRemove);
            break;
        }
      }
      # ifdef P036_SCROLL_CALC_LOG
      String log;

      if (loglevelActiveFor(LOG_LEVEL_INFO) &&
          log.reserve(128)) {
        addLog(LOG_LEVEL_INFO,
               strformat(F("Line: %d LineIn: %s Length: %d PixLength: %d AvgPixPerChar: %d CharsRemoved: %d"),
                         j + 1,
                         LineInStr.c_str(),
                         strlen,
                         PixLengthLineIn,
                         fAvgPixPerChar, iCharToRemove));
        delay(5); // FIXME otherwise it is maybe too fast for the serial monitor
        display->setFont(FontSizes[LineSettings[ScrollingPages.In[j].SPLidx].fontIdx].fontData);
        addLogMove(LOG_LEVEL_INFO,
                   strformat(F(" -> Changed to: %s Length: %d PixLength: %d"),
                             ScrollingPages.In[j].SPLcontent.c_str(),
                             ScrollingPages.In[j].SPLcontent.length(),
                             display->getStringWidth(ScrollingPages.In[j].SPLcontent)));
      }
      # endif // P036_SCROLL_CALC_LOG
    }

    // reduce line content for page scrolling to max width

    if (PixLengthLineOut > MaxPixWidthForPageScrolling) {
      const int strlen = ScrollingPages.Out[j].SPLcontent.length();
      # ifdef P036_SCROLL_CALC_LOG
      const String LineOutStr = ScrollingPages.Out[j].SPLcontent;
      # endif // P036_SCROLL_CALC_LOG
      const float fAvgPixPerChar = static_cast<float>(PixLengthLineOut) / strlen;

      bool bCheckLengthLeft  = false;
      bool bCheckLengthRight = false;

      if (bLineScrollEnabled) {
        // shorten string on left side because line is displayed right aligned while scrolling
        // using ceilf() because otherwise overlapping the new text
        iCharToRemove = ceilf(
          (static_cast<float>(PixLengthLineOut - MaxPixWidthForPageScrolling)) / fAvgPixPerChar);
        ScrollingPages.Out[j].SPLcontent = ScrollingPages.Out[j].SPLcontent.substring(iCharToRemove);
        bCheckLengthRight                = true;
      } else {
        switch (ScrollingPages.Out[j].Alignment) {
          case TEXT_ALIGN_LEFT:
            // shorten string on right side because line is left aligned
            // using ceilf() because otherwise overlapping the max of 255 pixels
            iCharToRemove = ceilf(
              (static_cast<float>(PixLengthLineOut - MaxPixWidthForPageScrolling)) / fAvgPixPerChar);
            ScrollingPages.Out[j].SPLcontent = ScrollingPages.Out[j].SPLcontent.substring(0, strlen - iCharToRemove);
            bCheckLengthLeft                 = true;
            break;
          case TEXT_ALIGN_RIGHT:
            // shorten string on left side because line is right aligned
            // using ceilf() because otherwise overlapping the new text
            iCharToRemove = ceilf(
              (static_cast<float>(PixLengthLineOut - MaxPixWidthForPageScrolling)) / fAvgPixPerChar);
            ScrollingPages.Out[j].SPLcontent = ScrollingPages.Out[j].SPLcontent.substring(iCharToRemove);
            bCheckLengthRight                = true;
            break;
          default: // TEXT_ALIGN_CENTER/TEST_ALIGN_CENTER_BOTH
            // shorten string on both sides because line is displayed centered
            // using ceilf() because otherwise overlapping the new text on left side
            // using ceilf() because otherwise overlapping the max of 255 pixels on right side
            iCharToRemove = ceilf(
              (static_cast<float>(PixLengthLineOut - MaxPixWidthForPageScrolling)) / (2 * fAvgPixPerChar));
            ScrollingPages.Out[j].SPLcontent = ScrollingPages.Out[j].SPLcontent.substring(0, strlen - iCharToRemove);
            ScrollingPages.Out[j].SPLcontent = ScrollingPages.Out[j].SPLcontent.substring(iCharToRemove);
            bCheckLengthLeft                 = true;
            bCheckLengthRight                = true;
            break;
        }
      }

      if (bCheckLengthLeft || bCheckLengthRight) {
        // set font only once if necessary
        display->setFont(FontSizes[LineSettings[ScrollingPages.Out[j].SPLidx].fontIdx].fontData);
      }

      while (bCheckLengthLeft || bCheckLengthRight) {
        uint16_t StringWidthPix = display->getStringWidth(ScrollingPages.Out[j].SPLcontent);

        if (bCheckLengthLeft) {
          if (StringWidthPix > MaxPixWidthForPageScrolling) {
            // remove one more character because still overlapping the new text
            ScrollingPages.Out[j].SPLcontent = ScrollingPages.Out[j].SPLcontent.substring(1, iCharToRemove - 1);

            if (bCheckLengthRight) {
              // get new value for CheckLengthRight
              StringWidthPix = display->getStringWidth(ScrollingPages.Out[j].SPLcontent);
            }
          } else {
            bCheckLengthLeft = false;
          }
        }

        if (bCheckLengthRight) {
          if (StringWidthPix > MaxPixWidthForPageScrolling) {
            // remove one more character because otherwise overlapping the max of 255 pixels on right side
            ScrollingPages.Out[j].SPLcontent = ScrollingPages.Out[j].SPLcontent.substring(1 + 1);
          } else {
            bCheckLengthRight = false;
          }
        }
      }

      # ifdef P036_SCROLL_CALC_LOG

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLog(LOG_LEVEL_INFO,
               strformat(F("Line: %d LineOut: %s Length: %d PixLength: %d AvgPixPerChar: %.2f CharsRemoved: %d"),
                         j + 1,
                         LineOutStr.c_str(),
                         strlen,
                         PixLengthLineOut,
                         fAvgPixPerChar,
                         iCharToRemove));
        delay(5); // FIXME otherwise it is maybe too fast for the serial monitor
        display->setFont(FontSizes[LineSettings[ScrollingPages.Out[j].SPLidx].fontIdx].fontData);
        addLogMove(LOG_LEVEL_INFO,
                   strformat(F(" -> Changed to: %s Length: %d PixLength: %d"),
                             ScrollingPages.Out[j].SPLcontent.c_str(),
                             ScrollingPages.Out[j].SPLcontent.length(),
                             display->getStringWidth(ScrollingPages.Out[j].SPLcontent)));
        delay(5); // FIXME otherwise it is maybe too fast for the serial monitor
      }
      # endif // P036_SCROLL_CALC_LOG
    }
  }

  ScrollingPages.dPix    = P36_PageScrollPix * static_cast<int>(lscrollspeed); // pix change per scrolling page tick
  ScrollingPages.dPixSum = ScrollingPages.dPix;

  display_scroll_timer(true, lscrollspeed);                                    // Initial display of the page

  # ifdef PLUGIN_036_DEBUG
  addLog(LOG_LEVEL_INFO, F("Scrolling finished"));
  # endif // PLUGIN_036_DEBUG
  return ScrollingPages.Scrolling;
}

uint8_t P036_data_struct::display_scroll_timer(bool             initialScroll,
                                               ePageScrollSpeed lscrollspeed) {
  if (!isInitialized()) {
    return 0;
  }

  // page scrolling (using PLUGIN_TASKTIMER_IN)
  display->setColor(BLACK);

  // We allow 12 pixels (including underline) at the top because otherwise the wifi indicator gets too squashed!!
  // scrolling window is 44 pixels high - ie 64 less margin of 12 at top and 8 at bottom
  display->fillRect(0, GetHeaderHeight() + (initialScroll ? 0 : 1) + TopLineOffset, P36_MaxDisplayWidth,
                    GetIndicatorTop() - GetHeaderHeight());
  display->setColor(WHITE);

  if (initialScroll) {
    if (!bHideHeader) {
      display->drawLine(0,
                        GetHeaderHeight() + TopLineOffset,
                        P36_MaxDisplayWidth,
                        GetHeaderHeight() + TopLineOffset); // line below title
    }
  }

  # if P036_ENABLE_TICKER

  if (bUseTicker) {
    // for Ticker start with the set alignment
    display->setTextAlignment(TEXT_ALIGN_LEFT);
    display->setFont(FontSizes[LineSettings[ScrollingLines.SLine[0].SLidx].fontIdx].fontData);
    display->drawString(ScrollingLines.SLine[0].CurrentLeft,
                        LineSettings[ScrollingLines.SLine[0].SLidx].ypos,
                        ScrollingLines.Ticker.Tcontent.substring(ScrollingLines.Ticker.IdxStart, ScrollingLines.Ticker.IdxEnd));
  } else
  # endif // if P036_ENABLE_TICKER
  {
    for (uint8_t j = 0; j < ScrollingPages.linesPerFrameOut; ++j) {
      if ((initialScroll && (lscrollspeed < ePageScrollSpeed::ePSS_Instant)) ||
          !initialScroll) {
        // scrolling, prepare scrolling page out to right
        DrawScrollingPageLine(&ScrollingPages.Out[j], ScrollingLines.SLine[j].LastWidth, TEXT_ALIGN_RIGHT);
      }
    }

    for (uint8_t j = 0; j < ScrollingPages.linesPerFrameIn; ++j) {
      // non-scrolling or scrolling prepare scrolling page in from left
      DrawScrollingPageLine(&ScrollingPages.In[j], ScrollingLines.SLine[j].Width, TEXT_ALIGN_LEFT);
    }
  }

  update_display();

  if ((initialScroll && (lscrollspeed < ePageScrollSpeed::ePSS_Instant)) ||
      (!initialScroll && (ScrollingPages.dPixSum < P36_MaxDisplayWidth))) { // scrolling
    // page still scrolling
    ScrollingPages.dPixSum += ScrollingPages.dPix;
  } else {
    // page scrolling finished
    ScrollingPages.Scrolling = 0; // allow following line scrolling
  }
  return ScrollingPages.Scrolling;
}

// Draw scrolling line (1pix/s)
void P036_data_struct::display_scrolling_lines() {
  if (!isInitialized()) {
    return;
  }

  // line scrolling (using PLUGIN_TEN_PER_SECOND)

  uint8_t i;
  bool    bscroll       = false;
  bool    updateDisplay = false;
  int     iCurrentLeft;

  for (i = 0; i < ScrollingPages.linesPerFrameIn; ++i) {
    if (ScrollingLines.SLine[i].Width != 0) {
      bscroll = true;
      break;
    }
  }

  if (bscroll) {
    if (ScrollingLines.wait < P36_WaitScrollLines) {
      ScrollingLines.wait++;
      return; // wait before scrolling line not finished
    }

    for (i = 0; i < ScrollingPages.linesPerFrameIn; ++i) {
      if (ScrollingLines.SLine[i].Width != 0) {
        // scroll this line
        ScrollingLines.SLine[i].fPixSum -= ScrollingLines.SLine[i].dPix;
        iCurrentLeft                     = lround(ScrollingLines.SLine[i].fPixSum);

        if (iCurrentLeft != ScrollingLines.SLine[i].CurrentLeft) {
          // still scrolling
          ScrollingLines.SLine[i].CurrentLeft = iCurrentLeft;
          updateDisplay                       = true;
          display->setColor(BLACK);
          display->fillRect(0, LineSettings[ScrollingLines.SLine[i].SLidx].ypos + 1, P36_MaxDisplayWidth,
                            FontSizes[LineSettings[ScrollingLines.SLine[i].SLidx].fontIdx].Height);
          display->setColor(WHITE);

          display->setFont(FontSizes[LineSettings[ScrollingLines.SLine[i].SLidx].fontIdx].fontData);

          if (
            # if P036_ENABLE_TICKER
            bUseTicker ||
            # endif // if P036_ENABLE_TICKER
            (((iCurrentLeft - getDisplaySizeSettings(disp_resolution).PixLeft) +
              ScrollingLines.SLine[i].Width) >= getDisplaySizeSettings(disp_resolution).Width)) {
            display->setTextAlignment(TEXT_ALIGN_LEFT);

            # if P036_ENABLE_TICKER

            if (bUseTicker) {
              display->drawString(iCurrentLeft,
                                  LineSettings[ScrollingLines.SLine[0].SLidx].ypos,
                                  ScrollingLines.Ticker.Tcontent.substring(ScrollingLines.Ticker.IdxStart, ScrollingLines.Ticker.IdxEnd));

              // add more characters to display
              iCurrentLeft -= getDisplaySizeSettings(disp_resolution).PixLeft;

              while (true) {
                if (ScrollingLines.Ticker.IdxEnd >= ScrollingLines.Ticker.len) { // end of string
                  break;
                }
                const uint8_t c          = ScrollingLines.Ticker.Tcontent.charAt(ScrollingLines.Ticker.IdxEnd);
                const uint8_t PixForChar = display->getCharWidth(c); // PixForChar can be 0 if c is non ascii

                if ((static_cast<int>(ScrollingLines.SLine[0].Width + PixForChar) + iCurrentLeft) >= ScrollingLines.Ticker.MaxPixLen) {
                  break;                                             // no more characters necessary to add
                }
                ScrollingLines.Ticker.IdxEnd++;
                ScrollingLines.SLine[0].Width += PixForChar;
              }

              // remove already displayed characters
              const float fCurrentPixLeft = static_cast<float>(getDisplaySizeSettings(disp_resolution).PixLeft) - 2.0f *
                                            ScrollingLines.Ticker.TickerAvgPixPerChar;

              while (ScrollingLines.SLine[0].fPixSum < fCurrentPixLeft) {
                const uint8_t c          = ScrollingLines.Ticker.Tcontent.charAt(ScrollingLines.Ticker.IdxStart);
                const uint8_t PixForChar = display->getCharWidth(c); // PixForChar can be 0 if c is non ascii
                ScrollingLines.SLine[0].fPixSum += static_cast<float>(PixForChar);
                ScrollingLines.Ticker.IdxStart++;

                if (ScrollingLines.Ticker.IdxStart >= ScrollingLines.Ticker.IdxEnd) {
                  ScrollingLines.SLine[0].Width = 0; // Stop scrolling

                  #  ifdef PLUGIN_036_DEBUG

                  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
                    addLog(LOG_LEVEL_INFO, F("Ticker finished"));
                  }
                  #  endif // PLUGIN_036_DEBUG
                  break;
                }

                if (ScrollingLines.SLine[0].Width > PixForChar) {
                  ScrollingLines.SLine[0].Width -= PixForChar;
                }
              }
              break;
            } else
            # endif // if P036_ENABLE_TICKER
            {
              display->drawString(iCurrentLeft,
                                  LineSettings[ScrollingLines.SLine[i].SLidx].ypos,
                                  ScrollingLines.SLine[i].SLcontent);
            }
          } else {
            # if P036_ENABLE_TICKER

            if (!bUseTicker)
            # endif // if P036_ENABLE_TICKER
            {
              // line scrolling finished -> line is shown as aligned right
              display->setTextAlignment(TEXT_ALIGN_RIGHT);
              display->drawString(P36_MaxDisplayWidth - getDisplaySizeSettings(disp_resolution).PixLeft,
                                  LineSettings[ScrollingLines.SLine[i].SLidx].ypos,
                                  ScrollingLines.SLine[i].SLcontent);
            }
            ScrollingLines.SLine[i].Width = 0; // Stop scrolling
          }
        }
      }
    }

    if (updateDisplay && (ScrollingPages.Scrolling == 0)) {
      update_display();
    }
  }
}

// Draw Signal Strength Bars, return true when there was an update.
bool P036_data_struct::display_wifibars() {
  if (!isInitialized()) {
    return false;
  }

  if (bHideHeader) { //  hide header
    return false;
  }

  const bool connected    = NetworkConnected();
  const int  nbars_filled = (WiFi.RSSI() + 100) / 12; // all bars filled if RSSI better than -46dB
  const int  newState     = connected ? nbars_filled : P36_WIFI_STATE_NOT_CONNECTED;

  if (newState == lastWiFiState) {
    return false; // nothing to do.
  }
  const int x         = getDisplaySizeSettings(disp_resolution).WiFiIndicatorLeft;
  const int y         = TopLineOffset;
  int size_x          = getDisplaySizeSettings(disp_resolution).WiFiIndicatorWidth;
  const int size_y    = GetHeaderHeight() - 2;
  const int nbars     = 5;
  const int16_t width = (size_x / nbars);

  size_x = width * nbars - 1; // Correct for round errors.

  //  x,y are the x,y locations
  //  sizex,sizey are the sizes (should be a multiple of the number of bars)
  //  nbars is the number of bars and nbars_filled is the number of filled bars.

  //  We leave a 1 pixel gap between bars
  display->setColor(BLACK);
  display->fillRect(x, y, size_x, size_y);
  display->setColor(WHITE);

  if (NetworkConnected()) {
    for (uint8_t ibar = 0; ibar < nbars; ++ibar) {
      const int16_t height = size_y * (ibar + 1) / nbars;
      const int16_t xpos   = x + ibar * width;
      const int16_t ypos   = y + size_y - height;

      if (ibar <= nbars_filled) {
        // Fill complete bar
        display->fillRect(xpos, ypos, width - 1, height);
      } else {
        // Only draw top and bottom.
        display->fillRect(xpos, ypos,           width - 1, 1);
        display->fillRect(xpos, y + size_y - 1, width - 1, 1);
      }
    }
  } else {
    // Draw a not connected sign (empty rectangle)
    display->drawRect(x, y, size_x, size_y - 1);
  }
  return true;
}

void P036_data_struct::update_display()
{
  if (isInitialized()) {
    display->display();
  }
}

void P036_data_struct::P036_JumpToPage(struct EventStruct *event, uint8_t nextFrame)
{
  if (!isInitialized()) {
    return;
  }

  // reschedule page change
  Scheduler.schedule_task_device_timer(event->TaskIndex,
                                       millis() + (Settings.TaskDeviceTimer[event->TaskIndex] * 1000));
  nextFrameToDisplay    = nextFrame;
  bPageScrollDisabled   = true; //  show next page without scrolling
  disableFrameChangeCnt = 2;    //  disable next page change in PLUGIN_READ if PLUGIN_READ was already scheduled
  P036_DisplayPage(event);      //  Display the selected page, function needs 65ms!
  displayTimer = P036_TIMER;    //  Restart timer
}

void P036_data_struct::P036_JumpToPageOfLine(struct EventStruct *event, uint8_t LineNo)
{
  CalcMaxPageCount(); // Update max page count and DisplayedPageNo
  P036_JumpToPage(event, LineSettings[LineNo].DisplayedPageNo);
}

// Defines the Scroll area layout
// Displays the selected page, function needs 65ms!
// Called by PLUGIN_READ and P036_JumpToPage()
void P036_data_struct::P036_DisplayPage(struct EventStruct *event)
{
  # ifdef PLUGIN_036_DEBUG
  addLog(LOG_LEVEL_INFO, F("P036_DisplayPage"));
  # endif // PLUGIN_036_DEBUG

  if (!isInitialized()) {
    return;
  }

  uint8_t NFrames; // the number of frames
  uint8_t lineCounter = 0;

  if (P036_DisplayIsOn) {
    // Display is on.
    ScrollingPages.Scrolling = 1;                                  // page scrolling running -> no line scrolling allowed
    NFrames                  = LineSettings[P36_Nlines - 1].frame; // last prepared page in prepare_pagescrolling()
    HeaderContent            = static_cast<eHeaderContent>(get8BitFromUL(PCONFIG_LONG(0), P036_FLAG_HEADER));
    HeaderContentAlternative = static_cast<eHeaderContent>(get8BitFromUL(PCONFIG_LONG(0), P036_FLAG_HEADER_ALTERNATIVE));

    // Construct the outgoing string
    for (uint8_t i = 0; i < P36_Nlines; ++i) {
      if (LineSettings[i].frame == frameCounter) {
        lineCounter = i;
        break;
      }
    }

    for (uint8_t i = 0; i < ScrollingPages.linesPerFrameDef; ++i)
    {
      if (LineSettings[lineCounter + i].frame != frameCounter) {
        continue;
      }
      ScrollingPages.linesPerFrameOut = i + 1;
      CreateScrollingPageLine(&ScrollingPages.Out[i], lineCounter + i);
    }

    // now loop round looking for the next frame with some content
    //   skip this frame if all lines in frame are blank
    // - we exit the while loop if any line is not empty
    bool foundText = false;
    int  ntries    = 0;

    while (!foundText) {
      //        Stop after framecount loops if no data found
      ntries++;

      if (ntries > (NFrames + 1)) {
        // do not leave the while loop to early
        // it needs to loop back to frameCounter=0 if just one frame is having text
        break;
      }

      if (nextFrameToDisplay == 0xff) {
        // Increment the frame counter
        frameCounter++;

        if (frameCounter > NFrames) {
          frameCounter          = 0;
          currentFrameToDisplay = 0;
        }
      } else {
        // next frame because content changed in PLUGIN_WRITE
        frameCounter = nextFrameToDisplay;
      }

      //        Contruct incoming strings
      for (uint8_t i = 0; i < P36_Nlines; ++i) {
        if (nextFrameToDisplay == 0xff) {
          // showing next page
          if (LineSettings[i].frame == frameCounter) {
            lineCounter = i;
            break;
          }
        } else {
          // jump to a displayed page from PlugIn
          if (LineSettings[i].DisplayedPageNo == nextFrameToDisplay) {
            frameCounter = LineSettings[i].frame;
            lineCounter  = i;
            break;
          }
        }
      }

      for (uint8_t i = 0; i < ScrollingPages.linesPerFrameDef; ++i)
      {
        if (LineSettings[lineCounter + i].frame != frameCounter) {
          continue;
        }
        ScrollingPages.linesPerFrameIn = i + 1;
        CreateScrollingPageLine(&ScrollingPages.In[i], lineCounter + i);
        # if P036_FEATURE_DISPLAY_PREVIEW
        currentLines[i] = ScrollingPages.In[i].SPLcontent;
        # endif // if P036_FEATURE_DISPLAY_PREVIEW

        if (ScrollingPages.In[i].SPLcontent.length() > 0) {
          foundText = true;

          # if P036_FEATURE_DISPLAY_PREVIEW && P036_FEATURE_ALIGN_PREVIEW

          #  if P036_ENABLE_TICKER

          if (!bUseTicker)
          #  endif // if P036_ENABLE_TICKER
          {
            // Preview: Center or Right-Align add spaces on the left
            const bool isAlignCenter = ScrollingPages.In[i].Alignment == OLEDDISPLAY_TEXT_ALIGNMENT::TEXT_ALIGN_CENTER;
            const bool isAlignRight  = ScrollingPages.In[i].Alignment == OLEDDISPLAY_TEXT_ALIGNMENT::TEXT_ALIGN_RIGHT;

            if (isAlignRight || isAlignCenter) {
              const uint16_t maxlength  = getDisplaySizeSettings(disp_resolution).Width;
              const uint16_t pixlength  = display->getStringWidth(currentLines[i]); // pix length for entire string
              const uint16_t charlength = display->getStringWidth(F(" "));          // pix length for a space char
              int16_t addSpaces         = (maxlength - pixlength) / charlength;

              if (isAlignCenter) {
                addSpaces /= 2;
              }

              if (addSpaces > 0) {
                currentLines[i].reserve(currentLines[i].length() + addSpaces);

                while (addSpaces > 0) {
                  currentLines[i] = ' ' + currentLines[i];
                  addSpaces--;
                }
              }
            }
          }
          # endif // if P036_FEATURE_DISPLAY_PREVIEW && P036_FEATURE_ALIGN_PREVIEW
        }
      }

      if (foundText) {
        if (nextFrameToDisplay == 0xff) {
          if (frameCounter != 0) {
            ++currentFrameToDisplay;
          }
        } else {
          currentFrameToDisplay = nextFrameToDisplay;
        }
      } else {
        nextFrameToDisplay = 0xff; // no text on the page -> jump to this page is not possible
      }
    }
    nextFrameToDisplay = 0xFF;

    CalcMaxPageCount(); // Update max page count

    // Update display
    if (bDisplayingLogo) {
      bDisplayingLogo = false;
      display->clear();        // resets all pixels to black
    }

    bAlternativHeader = false; // start with first header content
    HeaderCount       = 0;     // reset header count
    display_header();

    display_indicator();

    update_display();

    const bool bScrollWithoutWifi = bitRead(PCONFIG_LONG(0), 24); // Bit 24
    const bool bScrollLines       = bitRead(PCONFIG_LONG(0), 17); // Bit 17
    bRunning           = NetworkConnected() || bScrollWithoutWifi;
    bLineScrollEnabled = ((bScrollLines
                           # if P036_ENABLE_TICKER
                           || bUseTicker
                           # endif // if P036_ENABLE_TICKER
                           ) && bRunning); // scroll lines only if WifiIsConnected,
    // WifiIsConnected,
    // otherwise too slow

    ePageScrollSpeed lscrollspeed = static_cast<ePageScrollSpeed>(P036_SCROLL);

    if (bPageScrollDisabled) { lscrollspeed = ePageScrollSpeed::ePSS_Instant; } // first page after INIT without scrolling

    int lTaskTimer = Settings.TaskDeviceTimer[event->TaskIndex];

    if (display_scroll(lscrollspeed, lTaskTimer)) {
      Scheduler.setPluginTaskTimer(P36_PageScrollTimer, event->TaskIndex, event->Par1); // calls next page scrollng tick
    }

    if (bRunning) {
      // scroll lines only if WifiIsConnected, otherwise too slow
      bPageScrollDisabled = false; // next PLUGIN_READ will do page scrolling
    }
  # ifdef PLUGIN_036_DEBUG
  } else {
    addLog(LOG_LEVEL_INFO, F("P036_DisplayPage Display off"));
  # endif // PLUGIN_036_DEBUG
  }
}

// Perform some specific changes for OLED display
String P036_data_struct::P36_parseTemplate(String& tmpString, uint8_t lineIdx) {
  if (tmpString.length() == 0) {
    return EMPTY_STRING;
  }
  String result = parseTemplate_padded(tmpString, 20);

  result.trim();

  // OLED lib uses this routine to convert UTF8 to extended ASCII
  // http://playground.arduino.cc/Main/Utf8ascii
  // Attempt to display euro sign (FIXME)

  /*
     const char euro[4] = {0xe2, 0x82, 0xac, 0}; // Unicode euro symbol
     const char euro_oled[3] = {0xc2, 0x80, 0}; // Euro symbol OLED display font
     result.replace(euro, euro_oled);
   */
  const eAlignment iAlignment =
    static_cast<eAlignment>(get3BitFromUL(LineContent->DisplayLinesV1[lineIdx].ModifyLayout, P036_FLAG_ModifyLayout_Alignment));

  OLEDDISPLAY_TEXT_ALIGNMENT iTextAlignment = getTextAlignment(static_cast<eAlignment>(iAlignment));

  # if P036_ENABLE_TICKER

  if (bUseTicker) {
    iTextAlignment = TEXT_ALIGN_RIGHT; // ticker is always right aligned
  }
  # endif // if P036_ENABLE_TICKER

  switch (iTextAlignment) {
    case TEXT_ALIGN_LEFT:

      // add leading spaces from tmpString to the result
      for (uint16_t l = 0; l < tmpString.length(); ++l) {
        if (tmpString[l] != ' ') {
          break;
        }
        result = ' ' + result;
      }
      break;
    case TEXT_ALIGN_RIGHT:

      // add trailing spaces from tmpString to the result
      for (int16_t l = tmpString.length() - 1; l >= 0; --l) {
        if (tmpString[l] != ' ') {
          break;
        }
        result += ' ';
      }
      break;
    default:
      break;
  }
  return result;
}

# if P036_ENABLE_LEFT_ALIGN
void P036_data_struct::setTextAlignment(eAlignment aAlignment) {
  switch (aAlignment) {
    case eAlignment::eLeft:   textAlignment = TEXT_ALIGN_LEFT; break;
    case eAlignment::eRight:  textAlignment = TEXT_ALIGN_RIGHT; break;
    case eAlignment::eCenter: // Fall through
    case eAlignment::eGlobal: textAlignment = TEXT_ALIGN_CENTER; break;
  }

  MaxFramesToDisplay = 0xFF; // Recalculate page indicator
  nextFrameToDisplay = 0;    // Reset to first page
}

OLEDDISPLAY_TEXT_ALIGNMENT P036_data_struct::getTextAlignment(eAlignment aAlignment) const {
  switch (aAlignment) {
    case eAlignment::eLeft:   return TEXT_ALIGN_LEFT;
    case eAlignment::eRight:  return TEXT_ALIGN_RIGHT;
    case eAlignment::eCenter: return TEXT_ALIGN_CENTER;
    case eAlignment::eGlobal: break;
  }
  return textAlignment;
}

uint8_t P036_data_struct::GetTextLeftMargin(OLEDDISPLAY_TEXT_ALIGNMENT _textAlignment) const {
  // left margin must be offset with PixLeft (the first shown left pixel on 64x48 displays is 32!)
  if (_textAlignment == TEXT_ALIGN_LEFT) {
    return getDisplaySizeSettings(disp_resolution).PixLeft;
  }

  if (_textAlignment == TEXT_ALIGN_CENTER) {
    return (getDisplaySizeSettings(disp_resolution).Width / 2) + getDisplaySizeSettings(disp_resolution).PixLeft;
  }

  if (_textAlignment == TEXT_ALIGN_RIGHT) {
    return getDisplaySizeSettings(disp_resolution).Width + getDisplaySizeSettings(disp_resolution).PixLeft;
  }
  return getDisplaySizeSettings(disp_resolution).PixLeft;
}

# endif // if P036_ENABLE_LEFT_ALIGN

void P036_data_struct::registerButtonState(uint8_t newButtonState, bool bPin3Invers) {
  if ((ButtonLastState == 0xFF) || (bPin3Invers != (!!newButtonState))) {
    ButtonLastState = newButtonState;
    DebounceCounter++;

    if (RepeatCounter > 0) {
      RepeatCounter--;      // decrease the repeat count
    }
  } else {
    ButtonLastState = 0xFF; // Reset
    DebounceCounter = 0;
    RepeatCounter   = 0;
    ButtonState     = false;
  }

  if ((ButtonLastState == newButtonState) &&
      (DebounceCounter >= P36_DebounceTreshold) &&
      (RepeatCounter == 0)) {
    ButtonState = true;
  }
}

void P036_data_struct::markButtonStateProcessed() {
  ButtonState     = false;
  DebounceCounter = 0;
  RepeatCounter   = P36_RepeatDelay; //  Wait a bit before repeating the button action
}

uint16_t P036_data_struct::CalcPixLength(uint8_t LineNo) {
  if (LineContent->DisplayLinesV1[LineNo].Content.isEmpty()) {
    // empty string
    return 0;
  }
  display->setFont(FontSizes[LineSettings[LineNo].fontIdx].fontData);
  return display->getStringWidth(LineContent->DisplayLinesV1[LineNo].Content);
}

void P036_data_struct::CalcMaxPageCount(void) {
  // Update max page count
  if (MaxFramesToDisplay == 0xFF) {
    // not updated yet
    uint8_t iFrame = 0;

    for (uint8_t i = 0; i < P36_Nlines; ++i) {
      if (LineContent->DisplayLinesV1[i].Content[0] != 0) {   // line is not empty
        LineSettings[i].DisplayedPageNo = MaxFramesToDisplay; // current MaxFramesToDisplay is the number of the shown page
      } else {
        LineSettings[i].DisplayedPageNo = 0xff;               // line is not shown
      }

      if (LineSettings[i].frame != iFrame) { continue; } // line is not yet on the next page

      for (uint8_t k = 0; k < ScrollingPages.linesPerFrameDef; ++k) {
        if ((i + k) >= P36_Nlines) { break; }

        if ((LineSettings[i + k].frame) != iFrame) { // line is already on the next page
          iFrame++;                                  // next frame to check
          break;
        }

        if (LineContent->DisplayLinesV1[i + k].Content[0] != 0) { // line is not empty
          iFrame++;                                               // next frame to check

          if (MaxFramesToDisplay == 0xFF) {
            MaxFramesToDisplay = 0;
          } else {
            MaxFramesToDisplay++;
          }
          LineSettings[i + k].DisplayedPageNo = MaxFramesToDisplay; // current MaxFramesToDisplay is the number of the shown page
          break;
        }
      }
    }
    # ifdef P036_CHECK_INDIVIDUAL_FONT

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO,
             concat(F("CalcMaxPageCount: MaxFramesToDisplay:"), MaxFramesToDisplay));

      for (uint8_t i = 0; i < P36_Nlines; ++i) {
        delay(5); // FIXME otherwise it is maybe too fast for the serial monitor
        addLogMove(LOG_LEVEL_INFO,
                   strformat(F("Line[%d]: Frame:%d DisplayedPageNo:%d FontIdx:%d ypos:%d FontHeight:%d"),
                             i,
                             LineSettings[i].frame,
                             LineSettings[i].DisplayedPageNo,
                             LineSettings[i].fontIdx,
                             LineSettings[i].ypos - TopLineOffset,
                             LineSettings[i].FontHeight));
      }
    }
    # endif // ifdef P036_CHECK_INDIVIDUAL_FONT
  }
}

uint16_t P036_data_struct::TrimStringTo255Chars(tScrollingPageLines *ScrollingPageLine) {
  uint16_t PixLengthLine = 0;

  if (ScrollingPageLine->SPLcontent.length() > 0) {
    display->setFont(FontSizes[LineSettings[ScrollingPageLine->SPLidx].fontIdx].fontData);
    PixLengthLine = display->getStringWidth(ScrollingPageLine->SPLcontent);

    if (PixLengthLine > 255) {
      // shorten string because OLED controller can not handle such long strings
      const int   strlen         = ScrollingPageLine->SPLcontent.length();
      const float fAvgPixPerChar = static_cast<float>(PixLengthLine) / strlen;
      const int   iCharToRemove  = ceilf((static_cast<float>(PixLengthLine - 255)) / fAvgPixPerChar);
      ScrollingPageLine->SPLcontent = ScrollingPageLine->SPLcontent.substring(0, strlen - iCharToRemove);
      PixLengthLine                 = display->getStringWidth(ScrollingPageLine->SPLcontent);
    }
  }
  return PixLengthLine;
}

void P036_data_struct::DrawScrollingPageLine(tScrollingPageLines       *ScrollingPageLine,
                                             uint16_t                   Width,
                                             OLEDDISPLAY_TEXT_ALIGNMENT textAlignment) {
  int16_t LeftOffset = 0;

  switch (textAlignment) {
    case TEXT_ALIGN_LEFT: LeftOffset  = -P36_MaxDisplayWidth; break;
    case TEXT_ALIGN_RIGHT: LeftOffset = 0; break;
    default: LeftOffset               = 0; break;
  }
  display->setFont(FontSizes[LineSettings[ScrollingPageLine->SPLidx].fontIdx].fontData);

  if (Width > 0) {
    // textAlignment=TEXT_ALIGN_LEFT:  width of LineIn[j] > display width -> line is left aligned while scrolling page
    // textAlignment=TEXT_ALIGN_RIGHT: width of LineOut[j] > display width -> line is right aligned while scrolling page
    display->setTextAlignment(textAlignment);
    display->drawString(-P36_MaxDisplayWidth + getDisplaySizeSettings(disp_resolution).PixLeft + ScrollingPages.dPixSum,
                        LineSettings[ScrollingPageLine->SPLidx].ypos,
                        ScrollingPageLine->SPLcontent);
  } else {
    // line is kept aligned while scrolling page
    display->setTextAlignment(ScrollingPageLine->Alignment);

    // textAlignment=TEXT_ALIGN_LEFT: for non-scrolling pages ScrollingPages.dPixSum=P36_MaxDisplayWidth -> therefore the calculation must
    // use P36_MaxDisplayWidth, too
    display->drawString(LeftOffset + GetTextLeftMargin(ScrollingPageLine->Alignment) + ScrollingPages.dPixSum,
                        LineSettings[ScrollingPageLine->SPLidx].ypos,
                        ScrollingPageLine->SPLcontent);
  }
}

void P036_data_struct::CreateScrollingPageLine(tScrollingPageLines *ScrollingPageLine, uint8_t Counter) {
  # if P036_ENABLE_TICKER

  if (bUseTicker) {
    ScrollingPageLine->SPLcontent = EMPTY_STRING;
  } else
  # endif // if P036_ENABLE_TICKER
  {
    String tmpString(LineContent->DisplayLinesV1[Counter].Content);

    ScrollingPageLine->SPLcontent = P36_parseTemplate(tmpString, Counter);

    if (ScrollingPageLine->SPLcontent.length() > 0) {
      const int splitIdx = ScrollingPageLine->SPLcontent.indexOf(F("<|>")); // check for split token

      if (splitIdx >= 0) {
        // split line into left and right part
        tmpString = ScrollingPageLine->SPLcontent;
        tmpString.replace(F("<|>"), F(" "));                            // replace in tmpString the split token with one space char
        display->setFont(FontSizes[LineSettings[Counter].fontIdx].fontData);
        uint16_t pixlength = display->getStringWidth(tmpString);        // pixlength without split token but with one space char
        tmpString = ' ';
        const uint16_t charlength = display->getStringWidth(tmpString); // pix length for a space char
        pixlength += charlength;

        while (pixlength <= getDisplaySizeSettings(disp_resolution).Width) {
          // add more space chars until pixlength of the final line is almost the display width
          tmpString += ' ';                                         // add another space char
          pixlength += charlength;
        }
        ScrollingPageLine->SPLcontent.replace(F("<|>"), tmpString); // replace in final line the split token with space chars
      }
    }
    const eAlignment iAlignment =
      static_cast<eAlignment>(get3BitFromUL(LineContent->DisplayLinesV1[Counter].ModifyLayout, P036_FLAG_ModifyLayout_Alignment));

    ScrollingPageLine->Alignment = getTextAlignment(iAlignment);
    ScrollingPageLine->SPLidx    = Counter; // index to LineSettings[]
  }
}

# if P036_FEATURE_DISPLAY_PREVIEW
bool P036_data_struct::web_show_values() {
  addHtml(F("<pre>")); // To keep spaces etc. in the shown output

  for (uint8_t i = 0; i < ScrollingPages.linesPerFrameDef; ++i) {
    addHtmlDiv(F("div_l"), currentLines[i], EMPTY_STRING, F("style='font-size:75%;'"));

    if (i != ScrollingPages.linesPerFrameDef - 1) {
      addHtmlDiv(F("div_br"));
    }
  }
  addHtml(F("</pre>"));
  return true;
}

# endif // if P036_FEATURE_DISPLAY_PREVIEW


# if P036_SEND_EVENTS
void P036_data_struct::P036_SendEvent(struct EventStruct *event, uint8_t eventId, int16_t eventValue) {
  const __FlashStringHelper *eventid_str = F("");

  switch (eventId) {
    case P036_EVENT_DISPLAY:   eventid_str =  F("display");  break;
    case P036_EVENT_CONTRAST:  eventid_str =  F("contrast"); break;
    case P036_EVENT_FRAME:     eventid_str =  F("frame");    break;
    case P036_EVENT_LINE:      eventid_str =  F("line");     break;
    #  if P036_ENABLE_LINECOUNT
    case P036_EVENT_LINECNT:   eventid_str =  F("linecount"); break;
    #  endif // if P036_ENABLE_LINECOUNT
    case P036_EVENT_RESTORE:   eventid_str =  F("restore");   break;
    case P036_EVENT_SCROLL:    eventid_str =  F("scroll");    break;
    default:
      return;
  }


  eventQueue.add(event->TaskIndex, eventid_str, eventValue);
}

# endif // if P036_SEND_EVENTS


#endif  // ifdef USES_P036

#include "../PluginStructs/P037_data_struct.h"

#ifdef USES_P037

# include "../Helpers/ESPEasy_Storage.h"
# include "../Helpers/Numerical.h"
# include "../Helpers/RulesMatcher.h"
# include "../WebServer/Markup_Forms.h"
# include "../WebServer/ESPEasy_WebServer.h"
# include "../WebServer/Markup.h"
# include "../WebServer/HTML_wrappers.h"
# include "../ESPEasyCore/ESPEasyRules.h"


P037_data_struct::P037_data_struct(taskIndex_t taskIndex) : _taskIndex(taskIndex)
{}

P037_data_struct::~P037_data_struct() {
  # if P037_JSON_SUPPORT

  if (nullptr != root) {
    root->clear();
    delete root;
    root = nullptr;
  }
  # endif // if P037_JSON_SUPPORT
}

/**
 * Load the settings from file
 */
bool P037_data_struct::loadSettings() {

  if (_taskIndex < TASKS_MAX) {
    # ifdef USE_SECOND_HEAP
//    HeapSelectIram ephemeral;
    #endif
    size_t offset = 0;
    LoadCustomTaskSettings(_taskIndex, mqttTopics,
                           VARS_PER_TASK, 41, offset);
    offset += VARS_PER_TASK * 41;

    LoadCustomTaskSettings(_taskIndex, jsonAttributes,
                           VARS_PER_TASK, 21, offset);
    offset += VARS_PER_TASK * 21;

    {
      String tmp[1];

      LoadCustomTaskSettings(_taskIndex, tmp,
                             1, 41, offset);
      move_special(globalTopicPrefix, std::move(tmp[0]));
      offset           += 41;
    }


    LoadCustomTaskSettings(_taskIndex, valueArray,
                           P037_ARRAY_SIZE, 0, offset + 1);
    return true;
  }
  return false;
}

String P037_data_struct::saveSettings() {
  String res;

  if (_taskIndex < TASKS_MAX) { // TODO tonhuisman: Combine saving the settings into 1 call
    size_t offset = 0;
    res += SaveCustomTaskSettings(_taskIndex, mqttTopics,
                                  VARS_PER_TASK, 41, offset);
    offset += VARS_PER_TASK * 41;

    res += SaveCustomTaskSettings(_taskIndex, jsonAttributes,
                                  VARS_PER_TASK, 21, offset);
    offset += VARS_PER_TASK * 21;

    {
      String tmp[1];
      tmp[0] = globalTopicPrefix;
      res   += SaveCustomTaskSettings(_taskIndex, tmp,
                                      1, 41, offset);
      offset += 41;
    }


    res += SaveCustomTaskSettings(_taskIndex, valueArray,
                                  P037_ARRAY_SIZE, 0, offset + 1);
  }
  return res;
}

String P037_data_struct::getFullMQTTTopic(uint8_t taskValueIndex) const {
  String topic;

  if ((taskValueIndex < VARS_PER_TASK) && (mqttTopics[taskValueIndex].length() > 0)) {
    topic.reserve(globalTopicPrefix.length() + mqttTopics[taskValueIndex].length());
    topic = globalTopicPrefix;
    topic.trim();
    topic += mqttTopics[taskValueIndex];
    topic.trim();
  }
  return topic;
}

bool P037_data_struct::shouldSubscribeToMQTTtopic(const String& topic) const {
  if (topic.length() == 0) { return false; }

  for (uint8_t x = 0; x < VARS_PER_TASK; x++) {
    if (topic.equalsIgnoreCase(getFullMQTTTopic(x))) {
      return true;
    }
  }
  return false;
}

# if P037_MAPPING_SUPPORT || P037_FILTER_SUPPORT

/**
 * Parse the mappings and filters from the settings-string into arrays
 */
void P037_data_struct::parseMappings() {
  if (
    #  if P037_MAPPING_SUPPORT
    _maxIdx == -1
    #  endif // if P037_MAPPING_SUPPORT
    #  if P037_MAPPING_SUPPORT && P037_FILTER_SUPPORT
    ||
    #  endif // if P037_MAPPING_SUPPORT && P037_FILTER_SUPPORT
    #  if P037_FILTER_SUPPORT
    _maxFilter == -1
    #  endif // if P037_FILTER_SUPPORT
    ) {
    #  if P037_MAPPING_SUPPORT
    _maxIdx = 0;    // Initialize to empty
    #  endif // if P037_MAPPING_SUPPORT
    #  if P037_FILTER_SUPPORT
    _maxFilter = 0; // Initialize to empty
    #  endif // if P037_FILTER_SUPPORT

    #  if P037_MAPPING_SUPPORT || P037_FILTER_SUPPORT
    int8_t idx;
    #  endif // if P037_MAPPING_SUPPORT || P037_FILTER_SUPPORT
    #  if P037_MAPPING_SUPPORT
    idx = P037_MAX_MAPPINGS;

    for (uint8_t mappingOffset = P037_END_MAPPINGS; mappingOffset >= P037_START_MAPPINGS && _maxIdx == 0; mappingOffset--) {
      if (!valueArray[mappingOffset].isEmpty()) {
        _maxIdx = idx;
      }
      idx--;
    }
    #  endif // if P037_MAPPING_SUPPORT

    #  if P037_FILTER_SUPPORT
    idx = P037_MAX_FILTERS;

    for (uint8_t filterOffset = P037_END_FILTERS; filterOffset >= P037_START_FILTERS && _maxFilter == 0; filterOffset--) {
      if (!valueArray[filterOffset].isEmpty()) {
        _maxFilter = idx;
      }
      idx--;
    }
    #   ifdef P037_FILTER_PER_TOPIC

    if (_maxFilter > 0) { // For Filter-per-topic: Only activate filtering if at least 1 filter is defined
      _maxFilter = VARS_PER_TASK;
    }
    #   endif // ifdef P037_FILTER_PER_TOPIC
    #  endif // if P037_FILTER_SUPPORT
  }
} // parseMappings

# endif // if P037_MAPPING_SUPPORT || P037_FILTER_SUPPORT

bool P037_data_struct::webform_load(
  # if P037_MAPPING_SUPPORT
  bool mappingEnabled
  # endif // if P037_MAPPING_SUPPORT
  # if P037_MAPPING_SUPPORT&& P037_FILTER_SUPPORT
  ,
  # endif // if P037_MAPPING_SUPPORT && P037_FILTER_SUPPORT
  # if P037_FILTER_SUPPORT
  bool filterEnabled
  # endif // if P037_FILTER_SUPPORT
  # if (P037_MAPPING_SUPPORT || P037_FILTER_SUPPORT)&& defined(P037_JSON_SUPPORT)
  ,
  # endif // if (P037_MAPPING_SUPPORT || P037_FILTER_SUPPORT)&& defined(P037_JSON_SUPPORT)
  # ifdef P037_JSON_SUPPORT
  bool jsonEnabled
  # endif // ifdef P037_JSON_SUPPORT
  ) {
  bool success = false;

  addFormSubHeader(F("Topic subscriptions"));

  // Global topic prefix
  addFormTextBox(F("Prefix for all topics"), F("pprefix"), globalTopicPrefix, 40);

  # ifdef P037_JSON_SUPPORT

  if (jsonEnabled) {
    addRowLabel(F("MQTT Topic"));
    html_table(EMPTY_STRING, false); // Sub-table
    html_table_header(F("&nbsp;#&nbsp;"),  30);
    html_table_header(F("Topic"),          350);
    html_table_header(F("JSON Attribute"), 200);
  }
  # endif // ifdef P037_JSON_SUPPORT

  for (int varNr = 0; varNr < VARS_PER_TASK; varNr++) {
    # ifdef P037_JSON_SUPPORT

    if (jsonEnabled) { // Add a column with the json attribute to use for value
      html_TR_TD();
      addHtml(F("&nbsp;"));
      addHtmlInt(varNr + 1);
      html_TD(F("padding-right: 8px"));
      addTextBox(concat(F("template"), varNr + 1),
                 mqttTopics[varNr],
                 40,
                 F("xwide"));
      html_TD(F("padding-right: 8px"));
      addTextBox(concat(F("attribute"), varNr + 1),
                 jsonAttributes[varNr],
                 20,
                 F("xwide"));
      html_TD();
    } else
    # endif // ifdef P037_JSON_SUPPORT
    {
      addFormTextBox(concat(F("MQTT Topic "), varNr + 1), concat(F("template"), varNr + 1), mqttTopics[varNr], 40);
    }
  }
  # ifdef P037_JSON_SUPPORT

  if (jsonEnabled) {
    html_end_table();
  }
  # endif // ifdef P037_JSON_SUPPORT

  # if P037_MAPPING_SUPPORT || P037_FILTER_SUPPORT
  parseMappings();
  # endif // if P037_MAPPING_SUPPORT || P037_FILTER_SUPPORT

  # if P037_FILTER_SUPPORT

  if (filterEnabled) {
    addFormSubHeader(F("Name - value filters"));

    #  ifdef P037_FILTER_PER_TOPIC
    addRowLabel(F("Filter for MQTT Topic"));
    #  else // ifdef P037_FILTER_PER_TOPIC
    addRowLabel(F("Filter"));
    #  endif // ifdef P037_FILTER_PER_TOPIC
    html_table(F(""), false); // Sub-table
    html_table_header(F("&nbsp;#&nbsp;"), 30);
    html_table_header(F("Name[;Index]"),  350);
    html_table_header(F("Operand"),       180);
    html_table_header(F("Value"),         550);

    const __FlashStringHelper *filterOptions[] = {
      F("equals"), // map name to value
      F("range")   // between 2 values
      #  if P037_FILTER_COUNT >= 3
      , F("list")  // list of multiple values
      #  endif // if P037_FILTER_COUNT >= 3
    };
    const int filterIndices[] = { 0, 1
      #  if P037_FILTER_COUNT >= 3
                                  , 2
      #  endif // if P037_FILTER_COUNT >= 3
    };

    const FormSelectorOptions selector(P037_FILTER_COUNT, filterOptions, filterIndices);


    String filters = P037_FILTER_LIST; // Anticipate more filters
    int8_t filterIndex;

    int8_t idx      = 0;
    int8_t filterNr = 1;
    #  ifdef P037_FILTER_PER_TOPIC

    if (_maxFilter <= 0) { _maxFilter = (VARS_PER_TASK * 3); }
    #  endif // ifdef P037_FILTER_PER_TOPIC

    for (uint8_t filterOffset = P037_START_FILTERS; filterOffset <= P037_END_FILTERS && idx < (_maxFilter * 3); filterOffset++) {
      {
        html_TR_TD();
        addHtml(F("&nbsp;"));
        addHtmlInt(filterNr);
        html_TD();
        addTextBox(getPluginCustomArgName(idx + 100 + 0),
                   parseStringKeepCase(valueArray[filterOffset], 1, P037_VALUE_SEPARATOR),
                   32, 
                   F("xwide"));
      }
      {
        html_TD();
        filterIndex = filters.indexOf(parseString(valueArray[filterOffset], 2, P037_VALUE_SEPARATOR));
        selector.addSelector(getPluginCustomArgName(idx + 100 + 1), filterIndex);
        html_TD();

        addTextBox(getPluginCustomArgName(idx + 100 + 2), parseStringKeepCase(valueArray[filterOffset], 3, P037_VALUE_SEPARATOR),
                   32, 
                   F(""));
        addUnit(F("Range/List: separate values with ; "));
        html_TD();
      }

      filterNr++;
      idx += 3;
    }
    #  ifdef PLUGIN_037_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      String info;
      info.reserve(25);
      info += concat(F("P037 maxFilter: "), (int)_maxFilter);
      info += concat(F(" idx: "), (int)idx);
      addLogMove(LOG_LEVEL_INFO, info);
    }
    #  endif // ifdef PLUGIN_037_DEBUG
    #  ifndef P037_FILTER_PER_TOPIC
    filterIndex = 0;
    uint8_t extraFilters = 0;
    
    while (extraFilters < P037_EXTRA_VALUES && idx < P037_MAX_FILTERS * 3) {
      {
        html_TR_TD();
        addHtml(F("&nbsp;"));
        addHtmlInt(filterNr);
        html_TD();
        addTextBox(getPluginCustomArgName(idx + 100 + 0), EMPTY_STRING,
                   32, 
                   F("xwide"));
      }
      {
        html_TD();
        selector.addSelector(getPluginCustomArgName(idx + 100 + 1), filterIndex);
        html_TD();
        addTextBox(getPluginCustomArgName(idx + 100 + 2), EMPTY_STRING,
                   32, 
                   F(""));
        addUnit(F("Range/List: separate values with ; "));
        html_TD();
      }
      idx += 3;
      extraFilters++;
      filterNr++;
    }
    #  endif // ifndef P037_FILTER_PER_TOPIC
    html_end_table();
    #  ifndef P037_FILTER_PER_TOPIC
    #   ifdef PLUGIN_037_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      info  = concat(F("P037 extraFilters: "), (int)extraFilters);
      info += concat(F(" idx: "), (int)idx);
      addLogMove(LOG_LEVEL_INFO, info);
    }
    #   endif // ifdef PLUGIN_037_DEBUG
    #  endif  // ifndef P037_FILTER_PER_TOPIC
    addFormNote(F("Both Name and Value must be filled for a valid filter. Filters are case-sensitive."));
    #  ifndef P037_FILTER_PER_TOPIC

    if (extraFilters == P037_EXTRA_VALUES) {
      String moreMessage = concat(F("After filling all filters, submitting this page will make extra filters available (up to "),
                                  P037_MAX_FILTERS);
      moreMessage += F(").");
      addFormNote(moreMessage);
    }
    #  endif // ifndef P037_FILTER_PER_TOPIC
  }

  # endif    // if P037_FILTER_SUPPORT

  # if P037_MAPPING_SUPPORT

  if (mappingEnabled) {
    addFormSubHeader(F("Name - value mappings"));

    addRowLabel(F("Mapping"));
    html_table(F(""), false); // Sub-table
    html_table_header(F("&nbsp;#&nbsp;"), 30);
    html_table_header(F("Name"),          300);
    html_table_header(F("Operand"),       180);
    html_table_header(F("Value"),         300);

    const __FlashStringHelper *operandOptions[] = {
      F("map"),                          // map name to int
      F("percentage") };                 // map attribute value to percentage of provided value
    const int operandIndices[] = { 0, 1 };
    const FormSelectorOptions selector(P037_OPERAND_COUNT, operandOptions, operandIndices);


    String operands = P037_OPERAND_LIST; // Anticipate more operations
    int8_t operandIndex;

    int8_t idx   = 0;
    int8_t mapNr = 1;

    for (uint8_t mappingOffset = P037_START_MAPPINGS; mappingOffset <= P037_END_MAPPINGS && idx < (_maxIdx * 3); mappingOffset++) {
      {
        html_TR_TD();
        addHtml(F("&nbsp;"));
        addHtmlInt(mapNr);
        html_TD();
        addTextBox(getPluginCustomArgName(idx + 0),
                   parseStringKeepCase(valueArray[mappingOffset], 1, P037_VALUE_SEPARATOR),
                   32, 
                   F(""));
      }
      {
        html_TD();
        operandIndex = operands.indexOf(parseString(valueArray[mappingOffset], 2, P037_VALUE_SEPARATOR));

        selector.addSelector(getPluginCustomArgName(idx + 1), operandIndex);
        html_TD();
        addTextBox(getPluginCustomArgName(idx + 2),
                   parseStringKeepCase(valueArray[mappingOffset], 3, P037_VALUE_SEPARATOR),
                   32, 
                   F(""));
        html_TD();
      }
      mapNr++;
      idx += 3;
    }
    #  ifdef PLUGIN_037_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      String info;
      info.reserve(25);
      info += concat(F("P037 maxIdx: "), (int)_maxIdx);
      info += concat(F(" idx: "), (int)idx);
      addLogMove(LOG_LEVEL_INFO, info);
    }
    #  endif // ifdef PLUGIN_037_DEBUG
    operandIndex = 0;
    uint8_t extraMappings = 0;

    while (extraMappings < P037_EXTRA_VALUES && idx < P037_MAX_MAPPINGS * 3) {
      {
        html_TR_TD();
        addHtml(F("&nbsp;"));
        addHtmlInt(mapNr);
        html_TD();
        addTextBox(getPluginCustomArgName(idx + 0), 
                   EMPTY_STRING,
                   32, 
                   F(""));
      }
      {
        html_TD();
        selector.addSelector(getPluginCustomArgName(idx + 1), operandIndex);
        html_TD();
        addTextBox(getPluginCustomArgName(idx + 2), 
                   EMPTY_STRING,
                   32, 
                   F(""));
        html_TD();
      }
      idx += 3;
      extraMappings++;
      mapNr++;
    }
    html_end_table();
    #  ifdef PLUGIN_037_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      String info;
      info.reserve(35);
      info += concat(F("P037 extraMappings: "), (int)extraMappings);
      info += concat(F(" idx: "), (int)idx);
      addLogMove(LOG_LEVEL_INFO, info);
    }
    #  endif // ifdef PLUGIN_037_DEBUG
    addFormNote(F("Both Name and Value must be filled for a valid mapping. Mappings are case-sensitive."));

    if (extraMappings == P037_EXTRA_VALUES) {
      addFormNote(strformat(
        F("After filling all mappings, submitting this page will make extra mappings available (up to %d)."),
                                  P037_MAX_MAPPINGS));
    }
  }
  # endif // if P037_MAPPING_SUPPORT

  success = true;
  return success;
} // webform_load

bool P037_data_struct::webform_save(
  # if P037_FILTER_SUPPORT
  bool filterEnabled
  # endif // if P037_FILTER_SUPPORT
  # if P037_FILTER_SUPPORT&& defined(P037_JSON_SUPPORT)
  ,
  # endif // if P037_FILTER_SUPPORT && defined(P037_JSON_SUPPORT)
  # ifdef P037_JSON_SUPPORT
  bool jsonEnabled
  # endif // ifdef P037_JSON_SUPPORT
  ) {
  bool success = false;

  String error;

  error.reserve(80); // Estimated

  for (int varNr = 0; varNr < VARS_PER_TASK; varNr++) {
    mqttTopics[varNr] = webArg(concat(F("template"), varNr + 1));

    # ifdef P037_JSON_SUPPORT

    if (jsonEnabled) {
      jsonAttributes[varNr] = webArg(concat(F("attribute"), varNr + 1));
    }
    # endif // P037_JSON_SUPPORT
  }

  globalTopicPrefix = webArg(F("pprefix"));

  # if P037_MAPPING_SUPPORT || P037_FILTER_SUPPORT
  String left, right;
  bool   firstError;
  int8_t idx = 0;
  # endif // if P037_MAPPING_SUPPORT || P037_FILTER_SUPPORT

  // Mappings are processed first
  # if P037_MAPPING_SUPPORT
  firstError = true;
  String  operands = P037_OPERAND_LIST;
  uint8_t mapNr    = 1;
  left.reserve(32);
  right.reserve(32);

  for (uint8_t mappingOffset = P037_START_MAPPINGS; mappingOffset <= P037_END_MAPPINGS; mappingOffset++) {
    left.clear();
    left +=  webArg(getPluginCustomArgName(idx + 0));
    left.trim();
    right.clear();
    right += webArg(getPluginCustomArgName(idx + 2));
    right.trim();

    if (!left.isEmpty() || !right.isEmpty()) {
      valueArray[mappingOffset]  = wrapWithQuotes(left);
      valueArray[mappingOffset] += P037_VALUE_SEPARATOR;
      uint8_t oper = getFormItemIntCustomArgName(idx + 1);
      valueArray[mappingOffset] += operands.substring(oper, oper + 1);
      valueArray[mappingOffset] += P037_VALUE_SEPARATOR;
      valueArray[mappingOffset] += wrapWithQuotes(right);
    } else {
      valueArray[mappingOffset] = EMPTY_STRING;
    }

    if (left.isEmpty() != right.isEmpty()) {
      if (firstError) {
        error     += F("Name and value should both be filled for mapping ");
        firstError = false;
      } else {
        error += ',';
      }
      error += mapNr;
    }
    mapNr++;
    idx += 3;
    delay(0); // leave some yield
  }

  if (!firstError) {
    error += '\n';
  }
  # endif // if P037_MAPPING_SUPPORT

  # if P037_FILTER_SUPPORT
  String filters = P037_FILTER_LIST;
  firstError = true;
  uint8_t filterNr = 1;
  idx = 0;

  for (uint8_t filterOffset = P037_START_FILTERS; filterOffset <= P037_END_FILTERS; filterOffset++) {
    left =  webArg(getPluginCustomArgName(idx + 100 + 0));
    left.trim();
    right = webArg(getPluginCustomArgName(idx + 100 + 2));
    right.trim();

    if (!left.isEmpty() || !right.isEmpty()
        #  ifdef P037_FILTER_PER_TOPIC
        || true // Store all filters and in the same order, including empty filters
        #  endif // ifdef P037_FILTER_PER_TOPIC
        ) {
      valueArray[filterOffset]  = wrapWithQuotes(left);
      valueArray[filterOffset] += P037_VALUE_SEPARATOR;
      uint8_t oper = getFormItemIntCustomArgName(idx + 100 + 1);
      valueArray[filterOffset] += filters.substring(oper, oper + 1);
      valueArray[filterOffset] += P037_VALUE_SEPARATOR;
      valueArray[filterOffset] += wrapWithQuotes(right);
    } else {
      valueArray[filterOffset] = EMPTY_STRING;
    }

    if (left.isEmpty() != right.isEmpty()) {
      if (firstError) {
        error     += F("Name and value should both be filled for filter ");
        firstError = false;
      } else {
        error += ',';
      }
      error += filterNr;
    }
    filterNr++;
    idx += 3;
    delay(0); // leave some yield
  }
  #  ifndef P037_FILTER_PER_TOPIC

  if (!firstError) {
    error += '\n';
  }
  #  endif // ifndef P037_FILTER_PER_TOPIC
  # endif  // if P037_FILTER_SUPPORT

  error += saveSettings();

  if (!error.isEmpty()) {
    addHtmlError(error);
  }
  # if P037_MAPPING_SUPPORT
  _maxIdx = -1; // Invalidate current mappings and filters
  # endif // if P037_MAPPING_SUPPORT
  # if P037_FILTER_SUPPORT
  _maxFilter = -1;
  # endif // if P037_FILTER_SUPPORT

  success = true;

  return success;
} // webform_save

# if P037_MAPPING_SUPPORT
#  ifdef PLUGIN_037_DEBUG
void P037_data_struct::logMapValue(const String& input, const String& result) {
  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    String info;
    info.reserve(45);
    info += concat(F("IMPT : MQTT mapped value '"), input);
    info += concat(F("' to '"), result);
    info += '\'';
    addLogMove(LOG_LEVEL_INFO, info);
  }
} // logMapValue

#  endif // ifdef PLUGIN_037_DEBUG

/**
 * Map a string to a (numeric) value, unchanged if no mapping found
 */
String P037_data_struct::mapValue(const String& input, const String& attribute) {
  String result = String(input); // clone

  if (!input.isEmpty()) {
    parseMappings();
    const String operands = P037_OPERAND_LIST;

    int8_t idx = 0;

    for (uint8_t mappingOffset = P037_START_MAPPINGS; mappingOffset <= P037_END_MAPPINGS && idx <= _maxIdx; mappingOffset++) {
      const String name = parseStringKeepCase(valueArray[mappingOffset], 1, P037_VALUE_SEPARATOR);
      const String oper = parseString(valueArray[mappingOffset], 2, P037_VALUE_SEPARATOR);
      const String valu = parseStringKeepCase(valueArray[mappingOffset], 3, P037_VALUE_SEPARATOR);

      if ((name == input) || ((!attribute.isEmpty()) && (name == attribute))) {
        int8_t operandIndex = operands.indexOf(oper);

        switch (operandIndex) {
          case 0: // = => 1:1 mapping
          {
            if (!valu.isEmpty()) {
              result = valu;
              #  ifdef PLUGIN_037_DEBUG
              logMapValue(input, result);
              #  endif // ifdef PLUGIN_037_DEBUG
            }
            break;
          }
          case 1: // % => percentage of mapping
          {
            ESPEASY_RULES_FLOAT_TYPE inputDouble;
            ESPEASY_RULES_FLOAT_TYPE mappingDouble;

            if (validDoubleFromString(input, inputDouble) &&
                validDoubleFromString(valu, mappingDouble)) {
              if (compareDoubleValues('>', mappingDouble, 0.0)) {
                ESPEASY_RULES_FLOAT_TYPE resultDouble = (static_cast<ESPEASY_RULES_FLOAT_TYPE>(100) / mappingDouble) * inputDouble; // Simple calculation to percentage
                int8_t decimals     = 0;
                int8_t dotPos       = input.indexOf('.');

                if (dotPos > -1) {
                  String decPart = input.substring(dotPos + 1);
                  decimals = decPart.length();             // Take the number of decimals to the output value
                }
                result = toString(resultDouble, decimals); // Percentage with same decimals as input
                #  ifdef PLUGIN_037_DEBUG
                logMapValue(input, result);
                #  endif // ifdef PLUGIN_037_DEBUG
              }
            }
            break;
          }
          default:
            break;
        }
      }
      idx++;
    }
  }

  return result;
} // mapValue

# endif // if P037_MAPPING_SUPPORT

# if P037_FILTER_SUPPORT

/**
 * do we have filter values?
 */
bool P037_data_struct::hasFilters() {
  parseMappings(); // When not parsed yet
  #  ifdef PLUGIN_037_DEBUG
  addLogMove(LOG_LEVEL_INFO, concat(F("p037 hasFilter: "), (int)_maxFilter));
  #  endif // ifdef PLUGIN_037_DEBUG
  return _maxFilter > 0;
} // hasFilters

#  ifdef P037_FILTER_PER_TOPIC
String P037_data_struct::getFilterAsTopic(uint8_t topicId) {
  String result;

  if (hasFilters() &&
      (topicId > 0) &&
      (topicId <= VARS_PER_TASK)) {
    result.reserve(32);
    uint8_t fltBase = (topicId - 1) + P037_START_FILTERS;
    String  name    = parseStringKeepCase(valueArray[fltBase], 1, P037_VALUE_SEPARATOR);
    String  valu    = parseStringKeepCase(valueArray[fltBase], 3, P037_VALUE_SEPARATOR);

    if ((!name.isEmpty()) &&
        (!valu.isEmpty())) {
      result += '/';
      result += name;
      result += '/';

      if (!_filterListItem.isEmpty()) {
        result += _filterListItem;
      } else {
        result += valu;
      }
    }
  }
  return result;
}

#  endif // P037_FILTER_PER_TOPIC

#  ifdef PLUGIN_037_DEBUG
void P037_data_struct::logFilterValue(const String& text, const String& key, const String& value, const String& match) {
  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    String log;
    log.reserve(text.length() + key.length() + value.length() + match.length() + 16);
    log += text;
    log += key;
    log += concat(F(" value: "), value);
    log += concat(F(" match: "), match);
    addLogMove(LOG_LEVEL_INFO, log);
  }
} // logFilterValue

#  endif // PLUGIN_037_DEBUG

/**
 * checkFilters
 * Algorithm: (all comparisons are case-sensitive)
 * - If key is not found in the list, return true
 * - If key is found and value matches, return true
 * - if key is found but value doesn't match, return false
 * key can be in the list multiple times
 */
bool P037_data_struct::checkFilters(const String& key, const String& value, int8_t topicId) {
  bool result = true;

  if ((!key.isEmpty()) &&
      (!value.isEmpty())) { // Ignore empty input(s)
    String  filters = P037_FILTER_LIST;
    String  valueData = value;
    String  fltKey, fltIndex, filterData, fltOper;
    ESPEASY_RULES_FLOAT_TYPE  from, to, doubleValue;
    int8_t  rangeSeparator;
    bool    accept       = true;
    bool    matchTopicId = true;
    uint8_t fltFrom      = P037_START_FILTERS;
    uint8_t fltMax       = P037_START_FILTERS + _maxFilter;
    #  ifdef P037_FILTER_PER_TOPIC

    if (topicId > 0) {
      fltFrom = (topicId - 1) + P037_START_FILTERS;
      fltMax  = topicId + P037_START_FILTERS;
    }
    #  endif // ifdef P037_FILTER_PER_TOPIC

    for (uint8_t flt = fltFrom; flt < fltMax; flt++) {
      fltOper        = parseStringKeepCase(valueArray[flt], 2, P037_VALUE_SEPARATOR);
      fltKey         = parseStringKeepCase(valueArray[flt], 1, P037_VALUE_SEPARATOR);
      rangeSeparator = parseString(fltKey, 2).toInt();

      if (rangeSeparator > 0) {
        valueData.replace(';', ',');
        valueData = parseString(valueData, rangeSeparator);
      }
      fltKey = parseString(fltKey, 1);
      fltKey.trim();

      if (fltKey == key) {
        result = false;                          // Matched key, so now we are looking for matching value
        int8_t filterIndex = filters.indexOf(fltOper);
        filterData = parseStringKeepCase(valueArray[flt], 3, P037_VALUE_SEPARATOR);
        parseSystemVariables(filterData, false); // Replace system variables

        switch (filterIndex) {
          case 0:                                // = => equals
          {
            _filterListItem = EMPTY_STRING;

            if (filterData == valueData) {
              #  ifdef PLUGIN_037_DEBUG
              String match;
              match.reserve(30);
              match += parseStringKeepCase(valueArray[flt], 3, P037_VALUE_SEPARATOR); // re-parse

              if (topicId > 0) {
                match += F(" topic match: ");
                match += matchTopicId ? F("yes") : F("no");
              }
              logFilterValue(F("P037 filter equals key: "), key, valueData, match);
              #  endif // ifdef PLUGIN_037_DEBUG
              return matchTopicId; // Match, don't look any further
            }
            break;
          }
          case 1:                                       // - => range x-y (inside) or y-x (outside)
          {
            _filterListItem = EMPTY_STRING;
            rangeSeparator  = filterData.indexOf(';');  // Semicolons

            if (rangeSeparator == -1) {
              rangeSeparator = filterData.indexOf('-'); // Fall-back test for dash
            }

            if (rangeSeparator > -1) {
              accept = false;

              if (validDoubleFromString(filterData.substring(0, rangeSeparator),  from) &&
                  validDoubleFromString(filterData.substring(rangeSeparator + 1), to) &&
                  validDoubleFromString(valueData,                                doubleValue)) {
                if (compareDoubleValues('>' + '=', to, from)) { // Normal low - high range: between low and high
                  if (compareDoubleValues('>' + '=', doubleValue, from) &&
                      compareDoubleValues('<' + '=', doubleValue, to)) {
                    accept = true;
                  }
                } else { // Alternative high - low range: outside low and high values
                  if (compareDoubleValues('>' + '=', doubleValue, from) ||
                      compareDoubleValues('<' + '=', doubleValue, to)) {
                    accept = true;
                  }
                }

                #  ifdef PLUGIN_037_DEBUG

                if (loglevelActiveFor(LOG_LEVEL_INFO)) {
                  String match;
                  match.reserve(30);
                  match += F("P037 filter ");
                  match += accept ? EMPTY_STRING : F("NOT ");
                  match += F("in range key: ");
                  logFilterValue(match, key, valueData, parseStringKeepCase(valueArray[flt], 3, P037_VALUE_SEPARATOR));
                }
                #  endif // ifdef PLUGIN_037_DEBUG

                if (accept) {
                  return matchTopicId; // bail out, we're done
                }
              }
            }
            break;
          }
          #  if P037_FILTER_COUNT >= 3
          case 2: // : => Match against a semicolon-separated list
          {
            _filterListItem = EMPTY_STRING;
            String item;
            rangeSeparator = filterData.indexOf(';');

            if ((rangeSeparator > -1) &&
                validDoubleFromString(valueData, doubleValue)) {
              accept = false;

              do {
                item = filterData.substring(0, rangeSeparator);
                item.trim();
                filterData = filterData.substring(rangeSeparator + 1);
                filterData.trim();
                rangeSeparator = filterData.indexOf(';');

                if (rangeSeparator == -1) {
                  rangeSeparator = filterData.length(); // Last value
                }

                if (validDoubleFromString(item, from) &&
                    compareDoubleValues('=', doubleValue, from)) {
                  accept          = true;
                  _filterListItem = item;
                }
              } while (!filterData.isEmpty() && !accept);

              #   ifdef PLUGIN_037_DEBUG

              if (loglevelActiveFor(LOG_LEVEL_INFO)) {
                String match;
                match.reserve(30);
                match += F("P037 filter ");
                match += accept ? EMPTY_STRING : F("NOT ");
                match += F("in list key: ");
                logFilterValue(match, key, valueData, parseStringKeepCase(valueArray[flt], 3, P037_VALUE_SEPARATOR));
              }
              #   endif // ifdef PLUGIN_037_DEBUG

              if (accept) {
                return matchTopicId; // bail out, we're done
              }
            }
            break;
          }
          #  endif // if P037_FILTER_COUNT >= 3
          default:
            break;
        }
      }
      delay(0); // Allow some yield
    }
  }
  return result;
}

# endif // if P037_FILTER_SUPPORT

# ifdef P037_JSON_SUPPORT

/**
 * Allocate a DynamicJsonDocument and parse the message.
 * Returns true if the operation succeeded, and doc and iter can be used, when n ot successful the state of those variables is undefined.
 */
bool P037_data_struct::parseJSONMessage(const String& message) {
  bool result = false;

  if ((nullptr != root) &&
      (message.length() * 1.5 > lastJsonMessageLength)) {
    cleanupJSON();
  }

  if (message.length() * 1.5 > lastJsonMessageLength) {
    lastJsonMessageLength = message.length() * 1.5;
    #  ifdef PLUGIN_037_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, concat(F("IMPT : JSON buffer increased to "), (int)lastJsonMessageLength));
    }
    #  endif // ifdef PLUGIN_037_DEBUG
  }

  if (nullptr == root) {
    // Try to allocate in PSRAM or 2nd heap if possible
    constexpr unsigned size = sizeof(DynamicJsonDocument);
    void *ptr               = special_calloc(1, size);
    if (ptr) {    
      root = new (ptr) DynamicJsonDocument(lastJsonMessageLength); // Dynamic allocation
    }
  }

  if (nullptr != root) {
    deserializeJson(*root, message);

    if (!root->isNull()) {
      # ifdef USE_SECOND_HEAP
      HeapSelectIram ephemeral;
      # endif // ifdef USE_SECOND_HEAP

      result = true;
      doc    = root->as<JsonObject>();
      iter   = doc.begin();
    }
  }
  return result;
}

/**
 * Release the created DynamicJsonDocument (if it was allocated)
 */
void P037_data_struct::cleanupJSON() {
  if (nullptr != root) {
    root->clear();
    delete root;
    root = nullptr;
  }
}

# endif // P037_JSON_SUPPORT

#endif  // ifdef USES_P037

#include "../PluginStructs/P120_data_struct.h"

#if defined(USES_P120) || defined(USES_P125)

# define P120_RAD_TO_DEG        57.295779f // 180.0/M_PI


P120_data_struct::P120_data_struct(uint8_t aSize)
  : _aSize(aSize)
{
  if (_aSize == 0) { _aSize = 1; }
  _XA.resize(_aSize, 0);
  _YA.resize(_aSize, 0);
  _ZA.resize(_aSize, 0);
  _aUsed = 0;
  _aMax  = 0;
}

// **************************************************************************/
// Destructor
// **************************************************************************/
P120_data_struct::~P120_data_struct() {
  if (adxl345 != nullptr) {
    delete adxl345;
    adxl345 = nullptr;
  }
}

void P120_data_struct::setI2Caddress(uint8_t i2c_addr)
{
  _i2c_addr = i2c_addr;
  i2c_mode  = true;
}

void P120_data_struct::setSPI_CSpin(int cs_pin)
{
  _cs_pin  = cs_pin;
  i2c_mode = false;
}

// **************************************************************************/
// Initialize sensor and read data from ADXL345
// **************************************************************************/
bool P120_data_struct::read_sensor(struct EventStruct *event) {
  # if PLUGIN_120_DEBUG
  String log;
  # endif // if PLUGIN_120_DEBUG

  if (!initialized()) {
    init_sensor(event);
    # if PLUGIN_120_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG) &&
        log.reserve(55)) {
      if (i2c_mode) {
        #  ifdef USES_P120
        log = strformat(F("ADXL345: i2caddress: 0x%02x"), _i2cAddress);
        #  endif // ifdef USES_P120
      } else {
        #  ifdef USES_P125
        log = concat(F("ADXL345: CS-pin: "), _cs_pin);
        #  endif // ifdef USES_P125
      }
      log += strformat(F(", initialized: %s, ID=0x%02x"),
                       String(initialized() ? F("true") : F("false")),
                       adxl345->getDevID());
      addLogMove(LOG_LEVEL_DEBUG, log);
    }
    # endif // if PLUGIN_120_DEBUG
  }

  if (initialized()) {
    _x = 0; _y = 0; _z = 0;
    adxl345->readAccel(&_x, &_y, &_z);
    _XA[_aUsed] = _x;
    _YA[_aUsed] = _y;
    _ZA[_aUsed] = _z;

    _aUsed++;

    if ((_aMax < _aUsed) && (_aUsed < _aSize)) {
      _aMax = _aUsed;
    }

    if (_aUsed == _aSize) {
      _aUsed = 0;
    }

    # if PLUGIN_120_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      addLogMove(LOG_LEVEL_DEBUG, strformat(F("ADXL345: X: %d, Y: %d, Z: %d"), _x, _y, _z));
    }
    # endif // if PLUGIN_120_DEBUG

    sensor_check_interrupt(event); // Process any interrupt

    return true;
  }
  return false;
}

// **************************************************************************/
// Average the measurements and return the results
// **************************************************************************/
bool P120_data_struct::read_data(struct EventStruct *event) const
{
  float pitch, roll;

  getPitchRoll(event, pitch, roll);
  float X, Y, Z;

  if (!get_XYZ(X, Y, Z)) { return false; }

  // At 2.5V the sensor has 256 LSB/g
  // However with higher voltage the sensitivity may change
  // Assume on average we're at 1g environment, regardless the sensor orientation.
  // Thus we need to compute the length of the vector and use that as scale factor for 1g.
  float scaleFactor_g = sqrtf(X * X + Y * Y + Z * Z);

  if (last_scale_factor_g > 0.1f) {
    // IIR filter to adapt factor slowly for variations in the supply voltage.
    // This will also prevent "shocks" to affect the scale factor a lot.
    scaleFactor_g += last_scale_factor_g * 15;
    scaleFactor_g /= 16;
  }
  last_scale_factor_g = scaleFactor_g;

  const uint8_t valueCount = P120_NR_OUTPUT_VALUES;

  for (uint8_t i = 0; i < VARS_PER_TASK; ++i) {
    if (i < valueCount) {
      const uint8_t pconfigIndex = i + P120_QUERY1_CONFIG_POS;
      float value                = 0.0f;

      switch (static_cast<valueType>(PCONFIG(pconfigIndex))) {
        case valueType::Empty:
          break;
        case valueType::X_RAW:
          value = X;
          break;
        case valueType::Y_RAW:
          value = Y;
          break;
        case valueType::Z_RAW:
          value = Z;
          break;
        case valueType::X_g:
          value = X / scaleFactor_g;
          break;
        case valueType::Y_g:
          value = Y / scaleFactor_g;
          break;
        case valueType::Z_g:
          value = Z / scaleFactor_g;
          break;
        case valueType::Pitch:
          value = pitch;
          break;
        case valueType::Roll:
          value = roll;
          break;
        case valueType::NR_ValueTypes:
          break;
      }
      UserVar.setFloat(event->TaskIndex, i, value);
    }
  }
  return true;
}

bool P120_data_struct::get_XYZ(float& X, float& Y, float& Z) const
{
  X = 0.0f;
  Y = 0.0f;
  Z = 0.0f;

  if (initialized()) {
    for (uint8_t n = 0; n <= _aMax; n++) {
      X += _XA[n];
      Y += _YA[n];
      Z += _ZA[n];
    }

    if (_aMax > 0) {
      X /= _aMax; // Average available measurements
      Y /= _aMax;
      Z /= _aMax;
    }

    # if PLUGIN_120_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      String log;

      if (log.reserve(40)) {
        log  = F("ADXL345: averages, X: ");
        log += X;
        log += F(", Y: ");
        log += Y;
        log += F(", Z: ");
        log += Z;
        addLogMove(LOG_LEVEL_DEBUG, log);
      }
    }
    # endif // if PLUGIN_120_DEBUG
    return true;
  }
  return false;
}

bool P120_data_struct::getPitchRoll(struct EventStruct *event, float& pitch, float& roll) const
{
  float X, Y, Z;

  if (get_XYZ(X, Y, Z)) {
    roll  = atan2(-Y, Z);
    pitch = atan2(X, sqrt(Y * Y + Z * Z));

    if (!bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_ANGLE_IN_RAD)) {
      roll  *= P120_RAD_TO_DEG;
      pitch *= P120_RAD_TO_DEG;
    }
    return true;
  }
  roll  = 0.0f;
  pitch = 0.0f;
  return false;
}

// **************************************************************************/
// Initialize ADXL345
// **************************************************************************/
bool P120_data_struct::init_sensor(struct EventStruct *event) {
  if (i2c_mode) {
    adxl345 = new (std::nothrow) ADXL345(_i2c_addr); // Init using I2C
  } else {
    adxl345 = new (std::nothrow) ADXL345(_cs_pin);   // Init using SPI
  }

  if (initialized()) {
    uint8_t act = 0, freeFall = 0, singleTap = 0, doubleTap = 0;

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      String log = F("ADXL345: Initializing sensor for ");

      if (i2c_mode) {
        log += F("I2C");
      } else {
        log += F("SPI");
      }
      log += F("...");
      addLogMove(LOG_LEVEL_INFO, log);
    }
    adxl345->powerOn();
    adxl345->setRangeSetting(2 ^ (get2BitFromUL(P120_CONFIG_FLAGS1, P120_FLAGS1_RANGE) + 1)); // Range is stored in 2 bits, only 4 possible
                                                                                              // options
    // Activity triggering
    // Inactivity triggering, same axis
    adxl345->setActivityXYZ(bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_ACTIVITY_X),
                            bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_ACTIVITY_Y),
                            bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_ACTIVITY_Z));
    adxl345->setInactivityXYZ(bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_ACTIVITY_X),
                              bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_ACTIVITY_Y),
                              bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_ACTIVITY_Z));

    if (bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_ACTIVITY_X) ||
                              bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_ACTIVITY_Y) ||
                              bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_ACTIVITY_Z)) {
      adxl345->setActivityThreshold(get8BitFromUL(P120_CONFIG_FLAGS1, P120_FLAGS1_ACTIVITY_TRESHOLD));
      adxl345->setInactivityThreshold(get8BitFromUL(P120_CONFIG_FLAGS1, P120_FLAGS1_INACTIVITY_TRESHOLD));
      act = 1;
    }

    // FIXME TD-er: This offset only has 1/4th the resolution of the raw data
    adxl345->setAxisOffset(get8BitFromUL(P120_CONFIG_FLAGS4, P120_FLAGS4_OFFSET_X) - 0x80,
                           get8BitFromUL(P120_CONFIG_FLAGS4, P120_FLAGS4_OFFSET_Y) - 0x80,
                           get8BitFromUL(P120_CONFIG_FLAGS4, P120_FLAGS4_OFFSET_Z) - 0x80);


    // Tap triggering
    adxl345->setTapDetectionOnXYZ(bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_TAP_X),
                                  bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_TAP_Y),
                                  bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_TAP_Z));

    // Tap detection
    if (bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_TAP_X) ||
                                  bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_TAP_Y) ||
                                  bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_TAP_Z)) {
      adxl345->setTapThreshold(get8BitFromUL(P120_CONFIG_FLAGS2, P120_FLAGS2_TAP_TRESHOLD));
      adxl345->setTapDuration(get8BitFromUL(P120_CONFIG_FLAGS2, P120_FLAGS2_TAP_DURATION));
      singleTap = 1;

      // Double-tap detection
      if (bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_DBL_TAP)) {
        adxl345->setDoubleTapLatency(get8BitFromUL(P120_CONFIG_FLAGS2, P120_FLAGS2_DBL_TAP_LATENCY));
        adxl345->setDoubleTapWindow(get8BitFromUL(P120_CONFIG_FLAGS2, P120_FLAGS2_DBL_TAP_WINDOW));
        doubleTap = 1;
      } else {
        adxl345->setDoubleTapLatency(0); // Off
        adxl345->setDoubleTapWindow(0);
      }
    } else {
      adxl345->setTapThreshold(0); // Off
      adxl345->setTapDuration(0);
      adxl345->setDoubleTapLatency(0);
      adxl345->setDoubleTapWindow(0);
    }

    // Free-fall detection
    if (bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_FREE_FALL)) {
      adxl345->setFreeFallThreshold(get8BitFromUL(P120_CONFIG_FLAGS3, P120_FLAGS3_FREEFALL_TRESHOLD));
      adxl345->setFreeFallDuration(get8BitFromUL(P120_CONFIG_FLAGS3,  P120_FLAGS3_FREEFALL_DURATION));
      freeFall = 1;
    } else {
      adxl345->setFreeFallThreshold(0);
      adxl345->setFreeFallDuration(0);
    }

    // Enable interrupts
    adxl345->setImportantInterruptMapping(singleTap, doubleTap, freeFall, act, act);
    adxl345->ActivityINT(act);
    adxl345->InactivityINT(act);
    adxl345->singleTapINT(singleTap);
    adxl345->doubleTapINT(doubleTap);
    adxl345->FreeFallINT(freeFall);

    addLog(LOG_LEVEL_INFO, F("ADXL345: Initialization done."));
  } else {
    addLog(LOG_LEVEL_ERROR, F("ADXL345: Initialization of sensor failed."));
    return false;
  }

  # ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    String log;

    if (log.reserve(25)) {
      if (i2c_mode) {
        #  ifdef USES_P120
        log  = F("ADXL345: Address: 0x");
        log += String(_i2c_addr, HEX);
        #  endif // ifdef USES_P120
      } else {
        #  ifdef USES_P125
        log  = F("ADXL345: CS-pin: ");
        log += _cs_pin;
        #  endif // ifdef USES_P125
      }
      addLogMove(LOG_LEVEL_DEBUG, log);
    }
  }
  # endif // ifndef BUILD_NO_DEBUG

  return true;
}

/* Look for Interrupts and Triggered Action    */
void P120_data_struct::sensor_check_interrupt(struct EventStruct *event) {
  // getInterruptSource clears all triggered actions after returning value
  // Do not call again until you need to recheck for triggered actions
  uint8_t interrupts = adxl345->getInterruptSource();
  String  payload;

  payload.reserve(30);

  // Free Fall Detection
  if (adxl345->triggered(interrupts, ADXL345_FREE_FALL)) {
    if (bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_LOG_ACTIVITY)) {
      addLog(LOG_LEVEL_INFO, F("ADXL345: *** FREE FALL ***"));
    }
    payload = F("FreeFall=");
    appendPayloadXYZ(event, payload, 1u, 1u, 1u); // Use all values
    send_task_event(event, payload);
  }

  // Inactivity
  if (adxl345->triggered(interrupts, ADXL345_INACTIVITY) &&
      bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_SEND_ACTIVITY)) {
    if (!inactivityTriggered) {
      if (bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_LOG_ACTIVITY)) {
        addLog(LOG_LEVEL_INFO, F("ADXL345: *** INACTIVITY ***"));
      }
      payload = F("Inactivity=");
      appendPayloadXYZ(event, payload,
                       bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_ACTIVITY_X),
                       bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_ACTIVITY_Y),
                       bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_ACTIVITY_Z));
      send_task_event(event, payload);
    }
    inactivityTriggered = true;
    activityTriggered   = false;
  }

  // Activity
  if (adxl345->triggered(interrupts, ADXL345_ACTIVITY) &&
      bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_SEND_ACTIVITY)) {
    if (!activityTriggered) {
      if (bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_LOG_ACTIVITY)) {
        addLog(LOG_LEVEL_INFO, F("ADXL345: *** ACTIVITY ***"));
      }
      payload = F("Activity=");
      appendPayloadXYZ(event, payload,
                       bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_ACTIVITY_X),
                       bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_ACTIVITY_Y),
                       bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_ACTIVITY_Z));
      send_task_event(event, payload);
    }
    activityTriggered   = true;
    inactivityTriggered = false;
  }

  // Double Tap Detection
  // Tap Detection
  if (adxl345->triggered(interrupts, ADXL345_DOUBLE_TAP) ||
      (adxl345->triggered(interrupts, ADXL345_SINGLE_TAP))) {
    if (adxl345->triggered(interrupts, ADXL345_SINGLE_TAP)) {
      if (bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_LOG_ACTIVITY)) {
        addLog(LOG_LEVEL_INFO, F("ADXL345: *** TAP ***"));
      }
      payload = F("Tapped");
    }

    if (adxl345->triggered(interrupts, ADXL345_DOUBLE_TAP)) {      // tonhuisman: Double-tap overrides single-tap event
      if (bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_LOG_ACTIVITY)) { // This is on purpose and as intended!
        addLog(LOG_LEVEL_INFO, F("ADXL345: *** DOUBLE TAP ***"));
      }
      payload = F("DoubleTapped");
    }
    payload += '=';

    appendPayloadXYZ(event, payload,
                     bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_TAP_X),
                     bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_TAP_Y),
                     bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_TAP_Z));
    send_task_event(event, payload);
  }
}

// *******************************************************************
// Append X, Y and Z arguments where configured
// *******************************************************************
void P120_data_struct::appendPayloadXYZ(struct EventStruct *event, String& payload,
                                        uint8_t useX,
                                        uint8_t useY,
                                        uint8_t useZ) {
  if (useX == 1) {
    if (bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_EVENT_RAW_VALUES)) {
      payload += _x;
    } else {
      payload += formatUserVarNoCheck(event, 0);
    }
  } else {
    payload += 0;
  }
  payload += ',';

  if (useY == 1) {
    if (bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_EVENT_RAW_VALUES)) {
      payload += _y;
    } else {
      payload += formatUserVarNoCheck(event, 1);
    }
  } else {
    payload += 0;
  }
  payload += ',';

  if (useZ == 1) {
    if (bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_EVENT_RAW_VALUES)) {
      payload += _z;
    } else {
      payload += formatUserVarNoCheck(event, 2);
    }
  } else {
    payload += 0;
  }
}

// *******************************************************************
// Send out an event for the current event, aith provided payload
// *******************************************************************
void P120_data_struct::send_task_event(struct EventStruct *event,
                                       String            & eventPayload) {
  if (Settings.UseRules &&
      !eventPayload.isEmpty()) {
    String RuleEvent;
    RuleEvent += getTaskDeviceName(event->TaskIndex);
    RuleEvent += '#';
    RuleEvent += eventPayload;
    eventQueue.addMove(std::move(RuleEvent));
  }
}

// *******************************************************************
// Load the configuration interface
// *******************************************************************
bool P120_data_struct::plugin_webform_loadOutputSelector(struct EventStruct *event) {
  {
    const __FlashStringHelper *options[P120_NR_OUTPUT_OPTIONS];

    for (uint8_t i = 0; i < P120_NR_OUTPUT_OPTIONS; ++i) {
      options[i] = P120_data_struct::valuename(i, true);
    }

    const uint8_t valueCount = P120_NR_OUTPUT_VALUES;

    for (uint8_t i = 0; i < valueCount; ++i) {
      const uint8_t pconfigIndex = i + P120_QUERY1_CONFIG_POS;
      sensorTypeHelper_loadOutputSelector(event, pconfigIndex, i, P120_NR_OUTPUT_OPTIONS, options);
    }
  }
  addFormSeparator(4);
  {
    const __FlashStringHelper *options[] = {
      F("Degrees"),
      F("Radians") };
    const int values[] = { 0,
                           1 };
    const int choice = bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_ANGLE_IN_RAD);
    const FormSelectorOptions selector(2, options, values);
    selector.addFormSelector(F("Angle Units"), F("angle_rad"), choice);
  }
  return true;
}

bool P120_data_struct::plugin_webform_load(struct EventStruct *event) {
  // Range
  {
    const __FlashStringHelper *rangeOptions[] = {
      F("2"),
      F("4"),
      F("8"),
      F("16") };
    int rangeValues[] = { P120_RANGE_2G, P120_RANGE_4G, P120_RANGE_8G, P120_RANGE_16G };
    FormSelectorOptions selector(4, rangeOptions, rangeValues);
    selector.default_index = P120_RANGE_16G;
    selector.addFormSelector(F("Range"), F("range"),
      get2BitFromUL(P120_CONFIG_FLAGS1, P120_FLAGS1_RANGE));
    addUnit('g');
  }

  // Axis selection
  {
    addFormCheckBox(F("X-axis activity sensing"), F("act_x"), bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_ACTIVITY_X) == 1);
    addFormCheckBox(F("Y-axis activity sensing"), F("act_y"), bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_ACTIVITY_Y) == 1);
    addFormCheckBox(F("Z-axis activity sensing"), F("act_z"), bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_ACTIVITY_Z) == 1);
    addFormNumericBox(F("Activity treshold"), F("act_thres"),
                      get8BitFromUL(P120_CONFIG_FLAGS1, P120_FLAGS1_ACTIVITY_TRESHOLD), 1, 255);
    addUnit(F("1..255 * 62.5 mg"));
    addFormNumericBox(F("In-activity treshold"), F("inact_thres"),
                      get8BitFromUL(P120_CONFIG_FLAGS1, P120_FLAGS1_INACTIVITY_TRESHOLD), 1, 255);
    addUnit(F("1..255 * 62.5 mg"));
  }

  // Activity logging and send events for (in)activity
  {
    addFormCheckBox(F("Enable (in)activity events"), F("send_act"),
                    bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_SEND_ACTIVITY) == 1);
    addFormCheckBox(F("Log sensor activity (INFO)"), F("log_act"),
                    bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_LOG_ACTIVITY) == 1);
    addFormCheckBox(F("Events with raw measurements"), F("raw_measure"),
                    bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_EVENT_RAW_VALUES) == 1);
  }

  // Tap detection
  {
    addFormSubHeader(F("Tap detection"));

    addFormCheckBox(F("X-axis"), F("tap_x"), bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_TAP_X) == 1);
    addFormCheckBox(F("Y-axis"), F("tap_y"), bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_TAP_Y) == 1);
    addFormCheckBox(F("Z-axis"), F("tap_z"), bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_TAP_Z) == 1);
    addFormNote(F("Also enables taskname#Tapped event."));
    addFormNumericBox(F("Tap treshold"), F("tap_thres"),
                      get8BitFromUL(P120_CONFIG_FLAGS2, P120_FLAGS2_TAP_TRESHOLD), 1, 255);
    addUnit(F("1..255 * 62.5 mg"));
    addFormNumericBox(F("Tap duration"), F("tap_dur"),
                      get8BitFromUL(P120_CONFIG_FLAGS2, P120_FLAGS2_TAP_DURATION), 1, 255);
    addUnit(F("1..255 * 625 &micro;s"));
  }

  // Double-tap detection
  {
    addFormCheckBox(F("Enable double-tap detection"), F("dbl_tap"), bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_DBL_TAP) == 1);
    addFormNote(F("Also enables taskname#DoubleTapped event."));
    addFormNumericBox(F("Double-tap latency"), F("dbl_tap_latency"),
                      get8BitFromUL(P120_CONFIG_FLAGS2, P120_FLAGS2_DBL_TAP_LATENCY), 1, 255);
    addUnit(F("1..255 * 1.25 ms"));
    addFormNumericBox(F("Double-tap window"), F("dbl_tap_window"),
                      get8BitFromUL(P120_CONFIG_FLAGS2, P120_FLAGS2_DBL_TAP_WINDOW), 1, 255);
    addUnit(F("1..255 * 1.25 ms"));
  }

  // Free-fall detection
  {
    addFormSubHeader(F("Free-fall detection"));

    addFormCheckBox(F("Enable free-fall detection"), F("fr_fall"), bitRead(P120_CONFIG_FLAGS1, P120_FLAGS1_FREE_FALL) == 1);
    addFormNote(F("Also enables taskname#FreeFall event."));
    addFormNumericBox(F("Free-fall treshold"), F("fr_fall_thres"),
                      get8BitFromUL(P120_CONFIG_FLAGS3, P120_FLAGS3_FREEFALL_TRESHOLD), 1, 255);
    addUnit(F("1..255 * 62.5 mg"));
    addFormNumericBox(F("Free-fall duration"), F("fr_fall_dur"),
                      get8BitFromUL(P120_CONFIG_FLAGS3, P120_FLAGS3_FREEFALL_DURATION), 1, 255);
    addUnit(F("1..255 * 625 &micro;s"));
  }

  // Axis Offsets (calibration)
  {
    addFormSubHeader(F("Axis calibration"));
    addFormNumericBox(F("X-Axis offset"), F("off_x"),
                      get8BitFromUL(P120_CONFIG_FLAGS4, P120_FLAGS4_OFFSET_X) - 0x80, -127, 127);
    addUnit(F("-127..127 * 15.6 mg"));
    addFormNumericBox(F("Y-Axis offset"), F("off_y"),
                      get8BitFromUL(P120_CONFIG_FLAGS4, P120_FLAGS4_OFFSET_Y) - 0x80, -127, 127);
    addUnit(F("-127..127 * 15.6 mg"));
    addFormNumericBox(F("Z-Axis offset"), F("off_z"),
                      get8BitFromUL(P120_CONFIG_FLAGS4, P120_FLAGS4_OFFSET_Z) - 0x80, -127, 127);
    addUnit(F("-127..127 * 15.6 mg"));
  }

  // Data retrieval options
  {
    addFormSubHeader(F("Data retrieval"));

    addFormNumericBox(F("Averaging buffer size"), F("average_buf"), P120_AVERAGE_BUFFER, 1, 100);
    addUnit(F("1..100"));

    const __FlashStringHelper *frequencyOptions[] = {
      F("10"),
      F("50") };
    int frequencyValues[] = { P120_FREQUENCY_10, P120_FREQUENCY_50 };
    const FormSelectorOptions selector( 2, frequencyOptions, frequencyValues);
    selector.addFormSelector(F("Measuring frequency"), F("frequency"), P120_FREQUENCY);
    addUnit(F("Hz"));
    addFormNote(F("Values X/Y/Z are updated 1x per second, Controller updates &amp; Value-events are based on 'Interval' setting."));
  }

  return true;
}

// *******************************************************************
// Save the configuration interface
// *******************************************************************
bool P120_data_struct::plugin_webform_save(struct EventStruct *event) {
  const uint8_t valueCount = P120_NR_OUTPUT_VALUES;

  for (uint8_t i = 0; i < valueCount; ++i) {
    const uint8_t pconfigIndex = i + P120_QUERY1_CONFIG_POS;
    const uint8_t choice       = PCONFIG(pconfigIndex);
    sensorTypeHelper_saveOutputSelector(event, pconfigIndex, i, P120_data_struct::valuename(choice, false));
  }


  P120_FREQUENCY = getFormItemInt(F("frequency"));
  uint32_t flags = 0ul;

  set2BitToUL(flags, P120_FLAGS1_RANGE, getFormItemInt(F("range")));
  bitWrite(flags, P120_FLAGS1_ACTIVITY_X,       isFormItemChecked(F("act_x")));
  bitWrite(flags, P120_FLAGS1_ACTIVITY_Y,       isFormItemChecked(F("act_y")));
  bitWrite(flags, P120_FLAGS1_ACTIVITY_Z,       isFormItemChecked(F("act_z")));
  bitWrite(flags, P120_FLAGS1_TAP_X,            isFormItemChecked(F("tap_x")));
  bitWrite(flags, P120_FLAGS1_TAP_Y,            isFormItemChecked(F("tap_y")));
  bitWrite(flags, P120_FLAGS1_TAP_Z,            isFormItemChecked(F("tap_z")));
  bitWrite(flags, P120_FLAGS1_DBL_TAP,          isFormItemChecked(F("dbl_tap")));
  bitWrite(flags, P120_FLAGS1_FREE_FALL,        isFormItemChecked(F("fr_fall")));
  bitWrite(flags, P120_FLAGS1_SEND_ACTIVITY,    isFormItemChecked(F("send_act")));
  bitWrite(flags, P120_FLAGS1_LOG_ACTIVITY,     isFormItemChecked(F("log_act")));
  bitWrite(flags, P120_FLAGS1_EVENT_RAW_VALUES, isFormItemChecked(F("raw_measure")));
  bitWrite(flags, P120_FLAGS1_ANGLE_IN_RAD,     getFormItemInt(F("angle_rad")));
  set8BitToUL(flags, P120_FLAGS1_ACTIVITY_TRESHOLD,   getFormItemInt(F("act_thres")));
  set8BitToUL(flags, P120_FLAGS1_INACTIVITY_TRESHOLD, getFormItemInt(F("inact_thres")));
  P120_CONFIG_FLAGS1 = flags;

  flags = 0ul;
  set8BitToUL(flags, P120_FLAGS2_TAP_TRESHOLD,    getFormItemInt(F("tap_thres")));
  set8BitToUL(flags, P120_FLAGS2_TAP_DURATION,    getFormItemInt(F("tap_dur")));
  set8BitToUL(flags, P120_FLAGS2_DBL_TAP_LATENCY, getFormItemInt(F("dbl_tap_latency")));
  set8BitToUL(flags, P120_FLAGS2_DBL_TAP_WINDOW,  getFormItemInt(F("dbl_tap_window")));
  P120_CONFIG_FLAGS2 = flags;

  flags = 0ul;
  set8BitToUL(flags, P120_FLAGS3_FREEFALL_TRESHOLD, getFormItemInt(F("fr_fall_thres")));
  set8BitToUL(flags, P120_FLAGS3_FREEFALL_DURATION, getFormItemInt(F("fr_fall_dur")));
  P120_CONFIG_FLAGS3 = flags;

  flags = 0ul;
  set8BitToUL(flags, P120_FLAGS4_OFFSET_X, getFormItemInt(F("off_x")) + 0x80);
  set8BitToUL(flags, P120_FLAGS4_OFFSET_Y, getFormItemInt(F("off_y")) + 0x80);
  set8BitToUL(flags, P120_FLAGS4_OFFSET_Z, getFormItemInt(F("off_z")) + 0x80);
  P120_CONFIG_FLAGS4 = flags;

  return true;
}

// *******************************************************************
// Set defaults for the configuration interface
// *******************************************************************
bool P120_data_struct::plugin_set_defaults(struct EventStruct *event) {
  uint32_t flags = 0ul;

  set2BitToUL(flags, P120_FLAGS1_RANGE, P120_RANGE_16G); // Default to 16g range for highest resolution
  bitSet(flags, P120_FLAGS1_ACTIVITY_X);                 // Detect activity on all axes
  bitSet(flags, P120_FLAGS1_ACTIVITY_Y);
  bitSet(flags, P120_FLAGS1_ACTIVITY_Z);
  set8BitToUL(flags, P120_FLAGS1_ACTIVITY_TRESHOLD,   P120_DEFAULT_ACTIVITY_TRESHOLD);
  set8BitToUL(flags, P120_FLAGS1_INACTIVITY_TRESHOLD, P120_DEFAULT_INACTIVITY_TRESHOLD);
  P120_CONFIG_FLAGS1 = flags;

  flags = 0ul;
  set8BitToUL(flags, P120_FLAGS2_TAP_TRESHOLD,    P120_DEFAULT_TAP_TRESHOLD);
  set8BitToUL(flags, P120_FLAGS2_TAP_DURATION,    P120_DEFAULT_TAP_DURATION);
  set8BitToUL(flags, P120_FLAGS2_DBL_TAP_LATENCY, P120_DEFAULT_DBL_TAP_LATENCY);
  set8BitToUL(flags, P120_FLAGS2_DBL_TAP_WINDOW,  P120_DEFAULT_DBL_TAP_WINDOW);
  P120_CONFIG_FLAGS2 = flags;

  flags = 0ul;
  set8BitToUL(flags, P120_FLAGS3_FREEFALL_TRESHOLD, P120_DEFAULT_FREEFALL_TRESHOLD);
  set8BitToUL(flags, P120_FLAGS3_FREEFALL_DURATION, P120_DEFAULT_FREEFALL_DURATION);
  P120_CONFIG_FLAGS3 = flags;

  flags = 0ul;
  set8BitToUL(flags, P120_FLAGS4_OFFSET_X, 0 + 0x80); // Offset 0 by default
  set8BitToUL(flags, P120_FLAGS4_OFFSET_Y, 0 + 0x80);
  set8BitToUL(flags, P120_FLAGS4_OFFSET_Z, 0 + 0x80);
  P120_CONFIG_FLAGS4 = flags;

  // No decimals plausible, as the outputs from the sensor are of type int
  for (uint8_t i = 0; i < VARS_PER_TASK; ++i) {
    ExtraTaskSettings.TaskDeviceValueDecimals[i] = 0;
  }

  PCONFIG(P120_SENSOR_TYPE_INDEX)     = static_cast<int16_t>(Sensor_VType::SENSOR_TYPE_TRIPLE);
  PCONFIG(P120_QUERY1_CONFIG_POS + 0) = 1; // "X RAW"
  PCONFIG(P120_QUERY1_CONFIG_POS + 1) = 2; // "Y RAW"
  PCONFIG(P120_QUERY1_CONFIG_POS + 2) = 3; // "Z RAW"
  PCONFIG(P120_QUERY1_CONFIG_POS + 3) = 0; // "Empty"

  return true;
}

bool P120_data_struct::plugin_get_config_value(struct EventStruct *event, String& string) const
{
  if (initialized()) {
    const bool isPitch =  string.equalsIgnoreCase(F("pitch"));
    const bool isRoll  =  string.equalsIgnoreCase(F("roll"));

    if (isPitch || isRoll) {
      float pitch, roll;

      if (getPitchRoll(event, pitch, roll)) {
        // FIXME TD-er: Use Cache.getTaskDeviceValueDecimals to determine nr decimals
        const int nrDecimals = 2;
        string = toString(isRoll ? roll : pitch, nrDecimals);
        return true;
      }
    }
  }
  return false;
}

void P120_data_struct::plugin_get_device_value_names(struct EventStruct *event)
{
  const uint8_t valueCount = P120_NR_OUTPUT_VALUES;

  for (uint8_t i = 0; i < VARS_PER_TASK; ++i) {
    if (i < valueCount) {
      const uint8_t pconfigIndex = i + P120_QUERY1_CONFIG_POS;
      uint8_t choice             = PCONFIG(pconfigIndex);
      ExtraTaskSettings.setTaskDeviceValueName(i, P120_data_struct::valuename(choice, false));

      // Set decimals for RAW values to 0, Others to 2 decimals
      if (choice <= 3) {
        ExtraTaskSettings.TaskDeviceValueDecimals[i] = 0;
      }
    } else {
      ExtraTaskSettings.clearTaskDeviceValueName(i);
    }
  }
}

const __FlashStringHelper * P120_data_struct::valuename(uint8_t value_nr, bool displayString) {
  switch (static_cast<valueType>(value_nr)) {
    case valueType::Empty:    return displayString ? F("Empty") : F("");
    case valueType::X_RAW:    return displayString ? F("X RAW") : F("X");
    case valueType::Y_RAW:    return displayString ? F("Y RAW") : F("Y");
    case valueType::Z_RAW:    return displayString ? F("Z RAW") : F("Z");
    case valueType::X_g:      return displayString ? F("X (g)") : F("X");
    case valueType::Y_g:      return displayString ? F("Y (g)") : F("Y");
    case valueType::Z_g:      return displayString ? F("Z (g)") : F("Z");
    case valueType::Pitch:    return displayString ? F("Pitch Angle") : F("Pitch");
    case valueType::Roll:     return displayString ? F("Roll Angle")  : F("Roll");
    case valueType::NR_ValueTypes:
      break;
  }
  return F("");
}

bool P120_data_struct::isXYZ(valueType vtype)
{
  return vtype >= valueType::X_RAW &&  vtype <= valueType::Z_g;
}

#endif // if defined(USES_P120) || defined(USES_P125)

#include "../PluginStructs/P138_data_struct.h"

#ifdef USES_P138

// **************************************************************************/
// toString: convert P138_valueOptions_e enum to value-name or display-name
// **************************************************************************/
const __FlashStringHelper* toString(const P138_valueOptions_e value, bool displayString) {
  switch (value) {
    case P138_valueOptions_e::None: return displayString ? F("None") : F("none");
    case P138_valueOptions_e::BatteryCurrent: return displayString ? F("Battery current") : F("batterycurrent");
    case P138_valueOptions_e::ChargeUnderVoltage: return displayString ? F("Charge undervoltage") : F("chargeundervoltage");
    case P138_valueOptions_e::StopVoltage: return displayString ? F("Stop voltage") : F("stopvoltage");
    case P138_valueOptions_e::InCurrent: return displayString ? F("Input current") : F("inputcurrent");
    case P138_valueOptions_e::ChargeLevel: return displayString ? F("Charge level") : F("chargelevel");
    case P138_valueOptions_e::PowerSource: return displayString ? F("Power source") : F("powersource");
  }
  return F("*Undefined*");
}

# if FEATURE_MQTT_DISCOVER
int Plugin_138_QueryVType(uint8_t value_nr) {
  const P138_valueOptions_e value = static_cast<P138_valueOptions_e>(value_nr);

  switch (value) {
    case P138_valueOptions_e::None:
    case P138_valueOptions_e::ChargeLevel:
    case P138_valueOptions_e::PowerSource:
      break;
    case P138_valueOptions_e::BatteryCurrent:
    case P138_valueOptions_e::InCurrent:
      return static_cast<int>(Sensor_VType::SENSOR_TYPE_CURRENT_ONLY);
    case P138_valueOptions_e::StopVoltage:
    case P138_valueOptions_e::ChargeUnderVoltage:
      return static_cast<int>(Sensor_VType::SENSOR_TYPE_VOLTAGE_ONLY);
  }
  return static_cast<int>(Sensor_VType::SENSOR_TYPE_NONE);
}

# endif // if FEATURE_MQTT_DISCOVER

// **************************************************************************/
// Constructor
// **************************************************************************/
P138_data_struct::P138_data_struct(struct EventStruct *event) {
  addLog(LOG_LEVEL_INFO, F("IP5306: Init."));

  _ip5306 = new (std::nothrow) arduino::ip5306(); // Default address and I2C Wire object
}

// **************************************************************************/
// Destructor
// **************************************************************************/
P138_data_struct::~P138_data_struct() {
  delete _ip5306;
  _ip5306 = nullptr;
}

// **************************************************************************/
// plugin_read: Read the values and send to controller(s)
// **************************************************************************/
bool P138_data_struct::plugin_read(struct EventStruct *event) {
  bool success             = true;
  const uint8_t valueCount = P138_NR_OUTPUT_VALUES;

  for (uint8_t i = 0; i < valueCount; i++) {
    UserVar.setFloat(event->TaskIndex, i, read_value(static_cast<P138_valueOptions_e>(PCONFIG(P138_CONFIG_BASE + i))));
  }

  return success;
}

// **************************************************************************/
// plugin_fifty_per_second: check if the powersource changed and generate an event for that
// **************************************************************************/
bool P138_data_struct::plugin_fifty_per_second(struct EventStruct *event) {
  bool success = true;

  if (bitRead(P138_CONFIG_FLAGS, P138_FLAG_POWERCHANGE) && isInitialized()) {
    int8_t src = static_cast<int8_t>(_ip5306->power_source());

    if (_lastPowerSource != src) {                              // Changed?
      eventQueue.add(event->TaskIndex, F("PowerChanged"), src); // 0 = battery, 1 = Vin
      _lastPowerSource = src;                                   // Keep current
    }
  }
  return success;
}

// **************************************************************************/
// read_value: Read the requested value
// **************************************************************************/
float P138_data_struct::read_value(P138_valueOptions_e value) {
  if (isInitialized()) {
    switch (value) {
      case P138_valueOptions_e::None: return 0.0f;
      case P138_valueOptions_e::BatteryCurrent: {
        const float charge_current[] = { 200.0f, 400.0f, 500.0f, 600.0f };
        return charge_current[static_cast<int>(_ip5306->end_charge_current_detection())];
      }
      case P138_valueOptions_e::ChargeUnderVoltage: return _ip5306->charge_under_voltage_loop();
      case P138_valueOptions_e::StopVoltage: {
        const float stop_voltage[] = { 4.14f, 4.17f, 4.185f, 4.2f };
        return stop_voltage[static_cast<int>(_ip5306->charging_full_stop_voltage())];
      }
      case P138_valueOptions_e::InCurrent: return static_cast<float>(_ip5306->vin_current());
      case P138_valueOptions_e::ChargeLevel: return _ip5306->charge_level() * 100.0f;            // Make percentage
      case P138_valueOptions_e::PowerSource: return static_cast<float>(_ip5306->power_source()); // 0 = batt, 1 = Vin
    }
  }
  return 0.0f;
}

// **************************************************************************/
// plugin_write: Process commands
// **************************************************************************/
bool P138_data_struct::plugin_write(struct EventStruct *event,
                                    const String      & string) {
  bool success = false;

  // String cmd     = parseString(string, 1);

  // if (isInitialized() && equals(cmd, F("ip5306"))) { // Command trigger
  //   cmd = parseString(string, 2);                // sub command
  //   const bool empty3 = parseString(string, 3).isEmpty();
  //   const bool empty4 = parseString(string, 4).isEmpty();
  //   success = true;

  //   if (equals(cmd, F("??")) && !empty3) {         // ip5306,??,<value>
  //   } else {
  //     success = false;
  //   }
  // }

  return success;
}

const char p138_getcommands[] PROGMEM = "none|batcurrent|chundervolt|stopvolt|inpcurrent|chargelvl|pwrsource";

/****************************************************************************
 * plugin_get_config_value: Retrieve values like [<taskname>#<valuename>]
 ***************************************************************************/
bool P138_data_struct::plugin_get_config_value(struct EventStruct *event,
                                               String            & string) {
  bool success           = true;
  const String command   = parseString(string, 1);
  const int    command_i = GetCommandCode(command.c_str(), p138_getcommands);

  if (command_i > 0) { // Ignore 'None'
    const float value = read_value(static_cast<P138_valueOptions_e>(command_i));
    string = toString(value, static_cast<unsigned int>(P138_CONFIG_DECIMALS));
  } else {
    success = false;
  }

  return success;
}

#endif // ifdef USES_P138

#include "../PluginStructs/P143_data_struct.h"

#ifdef USES_P143

/**************************************************************************
 * toString for P143_DeviceType_e
 *************************************************************************/
const __FlashStringHelper* toString(P143_DeviceType_e device) {
  switch (device) {
    case P143_DeviceType_e::AdafruitEncoder: return F("Adafruit");
    # if P143_FEATURE_INCLUDE_M5STACK
    case P143_DeviceType_e::M5StackEncoder: return F("M5Stack");
    # endif // if P143_FEATURE_INCLUDE_M5STACK
    # if P143_FEATURE_INCLUDE_DFROBOT
    case P143_DeviceType_e::DFRobotEncoder: return F("DFRobot");
    # endif // if P143_FEATURE_INCLUDE_DFROBOT
  }
  return F("");
}

# if P143_FEATURE_COUNTER_COLORMAPPING

/**************************************************************************
 * toString for P143_CounterMapping_e
 *************************************************************************/
const __FlashStringHelper* toString(P143_CounterMapping_e counter) {
  switch (counter) {
    case P143_CounterMapping_e::None: return F("None");
    case P143_CounterMapping_e::ColorMapping: return F("Color mapping");
    case P143_CounterMapping_e::ColorGradient: return F("Color gradient");
  }
  return F("");
}

# endif // if P143_FEATURE_COUNTER_COLORMAPPING

/**************************************************************************
 * toString for P143_ButtonAction_e
 *************************************************************************/
const __FlashStringHelper* toString(P143_ButtonAction_e action) {
  switch (action) {
    case P143_ButtonAction_e::PushButton: return F("Pushbutton");
    case P143_ButtonAction_e::PushButtonInverted: return F("Pushbutton (inverted)");
    case P143_ButtonAction_e::ToggleSwitch: return F("Toggle switch");
  }
  return F("");
}

/*******************************************************************
 * P143_CheckEncoderDefaultSettings: Helper to set config defaults after changing the Encoder type
 ******************************************************************/
void P143_CheckEncoderDefaultSettings(struct EventStruct *event) {
  if (P143_ENCODER_TYPE != P143_PREVIOUS_TYPE) {
    switch (static_cast<P143_DeviceType_e>(P143_ENCODER_TYPE)) {
      case P143_DeviceType_e::AdafruitEncoder:
        P143_ADAFRUIT_COLOR_AND_BRIGHTNESS = 0x0000001E; // Black, with 30 (0x1E) brightness (1..255)
        P143_OFFSET_POSITION               = 0;
        break;
      # if P143_FEATURE_INCLUDE_M5STACK
      case P143_DeviceType_e::M5StackEncoder:
        P143_ADAFRUIT_COLOR_AND_BRIGHTNESS = 0x0000001E; // Black, with 30 (0x1E) brightness (1..255)
        P143_M5STACK_COLOR_AND_SELECTION   = 0x00000000; // Black, with both Leds using Color mapping
        P143_OFFSET_POSITION               = 0;
        break;
      # endif // if P143_FEATURE_INCLUDE_M5STACK
      # if P143_FEATURE_INCLUDE_DFROBOT
      case P143_DeviceType_e::DFRobotEncoder:
        P143_DFROBOT_LED_GAIN = P143_DFROBOT_MAX_GAIN;
        P143_OFFSET_POSITION  = 0;
        break;
      # endif // if P143_FEATURE_INCLUDE_DFROBOT
    }
    P143_PREVIOUS_TYPE = P143_ENCODER_TYPE; // It's now up to date
  }
}

/**************************************************************************
 * Constructor
 *************************************************************************/
P143_data_struct::P143_data_struct(struct EventStruct *event) {
  _device          = static_cast<P143_DeviceType_e>(P143_ENCODER_TYPE);
  _i2cAddress      = P143_I2C_ADDR;
  _encoderPosition = P143_INITIAL_POSITION;
  _encoderMin      = P143_MINIMAL_POSITION;
  _encoderMax      = P143_MAXIMAL_POSITION;
  _brightness      = P143_NEOPIXEL_BRIGHTNESS;
  _buttonLongPress = P143_GET_LONGPRESS_INTERVAL;
  _enableLongPress = P143_PLUGIN_ENABLE_LONGPRESS;
  # if P143_FEATURE_INCLUDE_DFROBOT
  _initialOffset = P143_OFFSET_POSITION;
  # endif // if P143_FEATURE_INCLUDE_DFROBOT
}

/*****************************************************
 * Destructor
 ****************************************************/
P143_data_struct::~P143_data_struct() {
  delete Adafruit_Seesaw;
  delete Adafruit_Spixel;
}

/**************************************************************************
 * plugin_init Initialize sensor and prepare for reading
 *************************************************************************/
bool P143_data_struct::plugin_init(struct EventStruct *event) {
  if (!_initialized) {
    switch (_device) {
      case P143_DeviceType_e::AdafruitEncoder:
      {
        Adafruit_Seesaw = new (std::nothrow) Adafruit_seesaw();
        Adafruit_Spixel = new (std::nothrow) seesaw_NeoPixel(1, P143_SEESAW_NEOPIX, NEO_GRB + NEO_KHZ800);

        if ((nullptr != Adafruit_Seesaw) && (nullptr != Adafruit_Spixel)) {
          _initialized = Adafruit_Seesaw->begin(_i2cAddress) && Adafruit_Spixel->begin(_i2cAddress);
          uint32_t version = ((Adafruit_Seesaw->getVersion() >> 16) & 0xFFFF);

          if (_initialized && (version != P143_ADAFRUIT_ENCODER_PRODUCTID)) { // Check Adafruit product ID
            _initialized = false;
          }

          if (_initialized) {
            // use a pin for the built in encoder switch
            Adafruit_Seesaw->pinMode(P143_SEESAW_SWITCH, INPUT_PULLUP);

            // set starting position
            Adafruit_Seesaw->setEncoderPosition(_encoderPosition);

            // Enable interrupts on Switch pin
            Adafruit_Seesaw->setGPIOInterrupts((uint32_t)1 << P143_SEESAW_SWITCH, 1);
            Adafruit_Seesaw->enableEncoderInterrupt();

            // We only have 1 pixel available...
            Adafruit_Spixel->setBrightness(_brightness); // Set brightness before color!
            _red   = P143_ADAFRUIT_COLOR_RED;
            _green = P143_ADAFRUIT_COLOR_GREEN;
            _blue  = P143_ADAFRUIT_COLOR_BLUE;
            Adafruit_Spixel->setPixelColor(0, _red, _green, _blue);
            Adafruit_Spixel->show();
          }
        }
        break;
      }
      # if P143_FEATURE_INCLUDE_M5STACK
      case P143_DeviceType_e::M5StackEncoder:
      {
        // Reset, only actually supported with upgraded firmware
        I2C_write8_reg(_i2cAddress, P143_M5STACK_REG_MODE, 0x00);

        #  if P143_FEATURE_M5STACK_V1_1

        // Check if we need to use the offset method
        // - Read current counter
        // - Write incremented value, only works with upgraded firmware
        // - re-read and if not changed we use an offset to handle passing the set limits
        int16_t encoderCount = I2C_readS16_LE_reg(_i2cAddress, P143_M5STACK_REG_ENCODER);
        encoderCount++;
        I2C_write16_LE_reg(_i2cAddress, P143_M5STACK_REG_ENCODER, encoderCount);

        if (encoderCount != I2C_readS16_LE_reg(_i2cAddress, P143_M5STACK_REG_ENCODER)) {
          _useOffset       = true;
          _previousEncoder = encoderCount - 1;
          _offsetEncoder   = _previousEncoder - _encoderPosition;
        } else {
          // Don't need to use offset, set configured initial value
          encoderCount = _encoderPosition;
          I2C_write16_LE_reg(_i2cAddress, P143_M5STACK_REG_ENCODER, encoderCount);
        }
        #  else // if P143_FEATURE_M5STACK_V1_1
        _useOffset = true;                // No check needed, we need to use the offset method
        #  endif // if P143_FEATURE_M5STACK_V1_1

        _red   = P143_ADAFRUIT_COLOR_RED; // Also used for M5Stack Led 1
        _green = P143_ADAFRUIT_COLOR_GREEN;
        _blue  = P143_ADAFRUIT_COLOR_BLUE;

        // Set LED initial state
        m5stack_setPixelColor(1, _red,                    _green,                    _blue);
        m5stack_setPixelColor(2, P143_M5STACK2_COLOR_RED, P143_M5STACK2_COLOR_GREEN, P143_M5STACK2_COLOR_BLUE);
        _initialized = true;
        break;
      }
      # endif // if P143_FEATURE_INCLUDE_M5STACK
      # if P143_FEATURE_INCLUDE_DFROBOT
      case P143_DeviceType_e::DFRobotEncoder:
      {
        _initialized =  P143_DFROBOT_ENCODER_PID == I2C_read16_reg(_i2cAddress, P143_DFROBOT_ENCODER_PID_MSB_REG);

        if (_initialized) {
          // Set encoder position
          I2C_write16_reg(_i2cAddress, P143_DFROBOT_ENCODER_COUNT_MSB_REG, _initialOffset + _encoderPosition);

          // Set led gain
          I2C_write8_reg(_i2cAddress, P143_DFROBOT_ENCODER_GAIN_REG, P143_DFROBOT_LED_GAIN & 0xFF);
        }
        break;
      }
      # endif // if P143_FEATURE_INCLUDE_DFROBOT
    }

    // Set initial button state
    _buttonState = (P143_ButtonAction_e::PushButtonInverted == static_cast<P143_ButtonAction_e>(P143_PLUGIN_BUTTON_ACTION)) ? 0 : 1;

    UserVar.setFloat(event->TaskIndex, 1, _buttonState);

    # if P143_FEATURE_COUNTER_COLORMAPPING

    _mapping = static_cast<P143_CounterMapping_e>(P143_PLUGIN_COUNTER_MAPPING);

    // Load color mapping data
    LoadCustomTaskSettings(event->TaskIndex, _colorMapping, P143_STRINGS, 0);

    for (int i = P143_STRINGS - 1; i >= 0; --i) {
      _colorMapping[i].trim();

      if ((_colorMaps == -1) && !_colorMapping[i].isEmpty()) {
        _colorMaps = i;
      }
    }

    counterToColorMapping(event); // Update color
    # endif // if P143_FEATURE_COUNTER_COLORMAPPING

    if (loglevelActiveFor(_initialized ? LOG_LEVEL_INFO : LOG_LEVEL_ERROR)) {
      String log = concat(F("I2CEncoders: INIT "), toString(_device));
      log += F(", ");

      # if P143_FEATURE_INCLUDE_M5STACK

      if (_useOffset) {
        log += F("using Offset method, ");
      }
      # endif // if P143_FEATURE_INCLUDE_M5STACK

      if (!_initialized) {
        log += F("FAILED.");
      } else {
        log += F("Success.");
      }
      addLogMove(_initialized ? LOG_LEVEL_INFO : LOG_LEVEL_ERROR, log);
    }
  }

  return _initialized;
}

/**************************************************************************
 * plugin_exit De-initialize and prepare for destructor
 *************************************************************************/
bool P143_data_struct::plugin_exit(struct EventStruct *event) {
  if (_initialized) {
    _initialized = false; // We don't want any unexpected events

    switch (_device) {
      case P143_DeviceType_e::AdafruitEncoder:
      {
        // Stop interrupthandler
        if (nullptr != Adafruit_Seesaw) {
          Adafruit_Seesaw->disableEncoderInterrupt();
        }

        if ((nullptr != Adafruit_Spixel) && P143_PLUGIN_EXIT_LED_OFF) {
          // Turn off Neopixel 0 by setting the R/G/B color to black
          Adafruit_Spixel->setPixelColor(0, 0, 0, 0);
          Adafruit_Spixel->show();
        }
        break;
      }
      # if P143_FEATURE_INCLUDE_M5STACK
      case P143_DeviceType_e::M5StackEncoder:
      {
        if (P143_PLUGIN_EXIT_LED_OFF) {
          // Turn off both LEDs
          m5stack_setPixelColor(0, 0, 0, 0);
        }
        break;
      }
      # endif // if P143_FEATURE_INCLUDE_M5STACK
      # if P143_FEATURE_INCLUDE_DFROBOT
      case P143_DeviceType_e::DFRobotEncoder:

        if (P143_PLUGIN_EXIT_LED_OFF) {
          // Set encoder position to 0 will effectively turn off all LEDs
          I2C_write16_reg(_i2cAddress, P143_DFROBOT_ENCODER_COUNT_MSB_REG, 0);
        }
        break;
      # endif // if P143_FEATURE_INCLUDE_DFROBOT
    }
  }
  return true;
}

/*****************************************************
 * plugin_read
 ****************************************************/
bool P143_data_struct::plugin_read(struct EventStruct *event)           {
  if (_initialized) {
    // Last obtained values
    UserVar.setFloat(event->TaskIndex, 0, _encoderPosition);
    UserVar.setFloat(event->TaskIndex, 1, _buttonState);
    return true;
  }
  return false;
}

/*****************************************************
 * plugin_write
 ****************************************************/
bool P143_data_struct::plugin_write(struct EventStruct *event,
                                    String            & string) {
  bool success     = false;
  const String cmd = parseString(string, 1);

  if (_initialized && equals(cmd, F("i2cencoder"))) {
    const String sub  = parseString(string, 2);
    const bool   led1 = equals(sub, F("led1"));

    if ((led1 || equals(sub, F("led2")))              // led1,<r>,<g>,<b> (Adafruit and M5Stack)
        && (event->Par2 >= 0) && (event->Par2 <= 255) // led2,<r>,<g>,<b> (M5Stack only)
        && (event->Par3 >= 0) && (event->Par3 <= 255) &&
        (event->Par4 >= 0) && (event->Par4 <= 255)
        # if P143_FEATURE_INCLUDE_DFROBOT
        && _device != P143_DeviceType_e::DFRobotEncoder
        # endif // if P143_FEATURE_INCLUDE_DFROBOT
        ) {
      uint32_t lSettings = 0u;

      if (led1) {
        _red      = event->Par2;
        _green    = event->Par3;
        _blue     = event->Par4;
        lSettings = P143_ADAFRUIT_COLOR_AND_BRIGHTNESS;
        set8BitToUL(lSettings, P143_ADAFRUIT_OFFSET_RED,   _red);
        set8BitToUL(lSettings, P143_ADAFRUIT_OFFSET_GREEN, _green);
        set8BitToUL(lSettings, P143_ADAFRUIT_OFFSET_BLUE,  _blue);
        P143_ADAFRUIT_COLOR_AND_BRIGHTNESS = lSettings;
      }

      switch (_device) {
        case P143_DeviceType_e::AdafruitEncoder:
        {
          if (led1 && (nullptr != Adafruit_Spixel)) {
            Adafruit_Spixel->setPixelColor(0, _red, _green, _blue);
            Adafruit_Spixel->show();
            success = true;
          }
          break;
        }
        # if P143_FEATURE_INCLUDE_M5STACK
        case P143_DeviceType_e::M5StackEncoder:
        {
          if (!led1) {
            lSettings = P143_M5STACK_COLOR_AND_SELECTION;
            set8BitToUL(lSettings, P143_M5STACK2_OFFSET_RED,   event->Par2 & 0xFF);
            set8BitToUL(lSettings, P143_M5STACK2_OFFSET_GREEN, event->Par3 & 0xFF);
            set8BitToUL(lSettings, P143_M5STACK2_OFFSET_BLUE,  event->Par4 & 0xFF);
            P143_M5STACK_COLOR_AND_SELECTION = lSettings;
          }
          m5stack_setPixelColor(led1 ? 1 : 2, event->Par2 & 0xFF, event->Par3 & 0xFF, event->Par4 & 0xFF);
          success = true;
          break;
        }
        # endif // if P143_FEATURE_INCLUDE_M5STACK
        # if P143_FEATURE_INCLUDE_DFROBOT
        case P143_DeviceType_e::DFRobotEncoder:
          break;
        # endif // if P143_FEATURE_INCLUDE_DFROBOT
      }
    } else
    if (equals(sub, F("bright")) // bright,<b> (range 1..255, Adafruit and M5Stack only)
        && (event->Par2 >= 1) && (event->Par2 <= 255)
        # if P143_FEATURE_INCLUDE_DFROBOT
        && _device != P143_DeviceType_e::DFRobotEncoder
        # endif // if P143_FEATURE_INCLUDE_DFROBOT
        ) {
      _brightness = event->Par2;
      uint32_t lSettings = P143_ADAFRUIT_COLOR_AND_BRIGHTNESS;
      set8BitToUL(lSettings, P143_ADAFRUIT_OFFSET_BRIGHTNESS, _brightness);
      P143_ADAFRUIT_COLOR_AND_BRIGHTNESS = lSettings;

      switch (_device) {
        case P143_DeviceType_e::AdafruitEncoder:
        {
          if (nullptr != Adafruit_Spixel) {
            Adafruit_Spixel->setBrightness(_brightness);

            // Update with new brightness
            Adafruit_Spixel->setPixelColor(0, _red, _green, _blue);
            Adafruit_Spixel->show();
          }
          success = true;
          break;
        }
        # if P143_FEATURE_INCLUDE_M5STACK
        case P143_DeviceType_e::M5StackEncoder:
        {
          // Update with new brightness
          m5stack_setPixelColor(1, _red,                    _green,                    _blue);
          m5stack_setPixelColor(2, P143_M5STACK2_COLOR_RED, P143_M5STACK2_COLOR_GREEN, P143_M5STACK2_COLOR_BLUE);
          success = true;
          break;
        }
        # endif // if P143_FEATURE_INCLUDE_M5STACK
        # if P143_FEATURE_INCLUDE_DFROBOT
        case P143_DeviceType_e::DFRobotEncoder:
          break;
        # endif // if P143_FEATURE_INCLUDE_DFROBOT
      }
    # if P143_FEATURE_INCLUDE_DFROBOT
    } else
    if (equals(sub, F("gain")) // gain,<gain> (Range 1..51, DFRobot only)
        && (event->Par2 >= P143_DFROBOT_MIN_GAIN) && (event->Par2 <= P143_DFROBOT_MAX_GAIN)
        && (_device == P143_DeviceType_e::DFRobotEncoder)
        ) {
      P143_DFROBOT_LED_GAIN = event->Par2;
      success               = true;
    # endif // if P143_FEATURE_INCLUDE_DFROBOT
    } else
    if (equals(sub, F("set"))) { // set,<position>[,<initialOffset>] (initial offset only for DFRobot)
      _encoderPosition = event->Par2;

      switch (_device) {
        case P143_DeviceType_e::AdafruitEncoder:
        {
          if (nullptr != Adafruit_Seesaw) {
            Adafruit_Seesaw->setEncoderPosition(_encoderPosition);
          }
          break;
        }
        # if P143_FEATURE_INCLUDE_M5STACK
        case P143_DeviceType_e::M5StackEncoder:
        {
          if (_useOffset) { // Adjust offset
            int16_t encoderCount = I2C_readS16_LE_reg(_i2cAddress, P143_M5STACK_REG_ENCODER);
            _offsetEncoder = encoderCount - _encoderPosition;
          #  if P143_FEATURE_M5STACK_V1_1
          } else { // Set position using upgraded firmware
            I2C_write16_LE_reg(_i2cAddress, P143_M5STACK_REG_ENCODER, _encoderPosition);
          #  endif // if P143_FEATURE_M5STACK_V1_1
          }
          break;
        }
        # endif // if P143_FEATURE_INCLUDE_M5STACK
        # if P143_FEATURE_INCLUDE_DFROBOT
        case P143_DeviceType_e::DFRobotEncoder:
        {
          if (!parseString(string, 4).isEmpty() && (event->Par3 >= P143_DFROBOT_MIN_OFFSET) && (event->Par3 <= P143_DFROBOT_MAX_OFFSET)) {
            _initialOffset       = event->Par3;
            P143_OFFSET_POSITION = _initialOffset;
          }
          I2C_write16_reg(_i2cAddress, P143_DFROBOT_ENCODER_COUNT_MSB_REG, _initialOffset + _encoderPosition);
          break;
        }
        # endif // if P143_FEATURE_INCLUDE_DFROBOT
      }
    }
  }

  return success;
}

/*****************************************************
 * plugin_ten_per_second
 ****************************************************/
bool P143_data_struct::plugin_ten_per_second(struct EventStruct *event) {
  bool result = false;

  if (_initialized) {
    int32_t current = _encoderPosition;
    # if PLUGIN_143_DEBUG
    _oldPosition = _encoderPosition;
    # endif // if PLUGIN_143_DEBUG

    // Read encoder
    switch (_device) {
      case P143_DeviceType_e::AdafruitEncoder:

        if (nullptr != Adafruit_Seesaw) {
          current = Adafruit_Seesaw->getEncoderPosition();
        }
        break;
      # if P143_FEATURE_INCLUDE_M5STACK
      case P143_DeviceType_e::M5StackEncoder:
        current = I2C_readS16_LE_reg(_i2cAddress, P143_M5STACK_REG_ENCODER);
        break;
      # endif // if P143_FEATURE_INCLUDE_M5STACK
      # if P143_FEATURE_INCLUDE_DFROBOT
      case P143_DeviceType_e::DFRobotEncoder:
        current = static_cast<int16_t>(I2C_read16_reg(_i2cAddress, P143_DFROBOT_ENCODER_COUNT_MSB_REG)) - _initialOffset;
        break;
      # endif // if P143_FEATURE_INCLUDE_DFROBOT
    }
    # if P143_FEATURE_INCLUDE_M5STACK
    const int32_t rawCurrent = current;

    if (_useOffset) {
      current -= _offsetEncoder;
    }
    # endif // if P143_FEATURE_INCLUDE_M5STACK
    const int32_t orgCurrent = current;

    // Check limits
    if (_encoderMin != _encoderMax) {
      if ((current < _encoderMin) || (current > _encoderMax)) {
        // Have to check separately, as it's possible to move multiple steps within 1/10th second
        if (current < _encoderMin) {
          # if P143_FEATURE_INCLUDE_M5STACK

          if (_useOffset) {
            _offsetEncoder = rawCurrent - _encoderMin;
          }
          # endif // if P143_FEATURE_INCLUDE_M5STACK
          current = _encoderMin; // keep minimal value
        }
        else if (current > _encoderMax) {
          # if P143_FEATURE_INCLUDE_M5STACK

          if (_useOffset) {
            _offsetEncoder = rawCurrent - _encoderMax;
          }
          # endif // if P143_FEATURE_INCLUDE_M5STACK
          current = _encoderMax; // keep maximal value
        }
        _previousEncoder = current;

        // (Re)Set Encoder to current position if outside the set boundaries
        if (current != orgCurrent) {
          switch (_device) {
            case P143_DeviceType_e::AdafruitEncoder:

              if (nullptr != Adafruit_Seesaw) {
                Adafruit_Seesaw->setEncoderPosition(current);
              }
              break;
            # if P143_FEATURE_INCLUDE_M5STACK
            case P143_DeviceType_e::M5StackEncoder:

              #  if P143_FEATURE_M5STACK_V1_1

              // Set encoder position. NB: will only work if the encoder firmware is updated to v1.1
              if (!_useOffset) {
                I2C_write16_LE_reg(_i2cAddress, P143_M5STACK_REG_ENCODER, current);
              }
              #  endif // if P143_FEATURE_M5STACK_V1_1
              break;
            # endif // if P143_FEATURE_INCLUDE_M5STACK
            # if P143_FEATURE_INCLUDE_DFROBOT
            case P143_DeviceType_e::DFRobotEncoder:
              I2C_write16_reg(_i2cAddress, P143_DFROBOT_ENCODER_COUNT_MSB_REG, _initialOffset + _encoderPosition);
              break;
            # endif // if P143_FEATURE_INCLUDE_DFROBOT
          }
        }
      }
    }

    if (current != _encoderPosition) {
      // Generate event
      if (Settings.UseRules) {
        eventQueue.add(event->TaskIndex, Cache.getTaskDeviceValueName(event->TaskIndex, 0),
                       strformat(F("%d,%d"), current, current - _encoderPosition)); // Position, Delta (positive = clock-wise)
      }

      // Set task value
      _encoderPosition = current;

      UserVar.setFloat(event->TaskIndex, 0, _encoderPosition);

      result = true;

      // Calculate colormapping
      # if P143_FEATURE_COUNTER_COLORMAPPING
      counterToColorMapping(event);
      # endif // if P143_FEATURE_COUNTER_COLORMAPPING

      # if PLUGIN_143_DEBUG

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        String log = concat(F("I2CEncoder : "), toString(_device));
        log += strformat(F(", Changed: %d to: %d, delta: %d"),
                         _oldPosition, _encoderPosition, _encoderPosition - _oldPosition);
        addLogMove(LOG_LEVEL_INFO, log);
      }
      # endif // if PLUGIN_143_DEBUG
    }
  }

  return result;
}

# if P143_FEATURE_COUNTER_COLORMAPPING

/*****************************************************
 * counterToColorMapping
 ****************************************************/
void P143_data_struct::counterToColorMapping(struct EventStruct *event) {
  int16_t iRed   = -1;
  int16_t iGreen = -1;
  int16_t iBlue  = -1;
  int16_t pRed   = -1;
  int16_t pGreen = -1;
  int16_t pBlue  = -1;
  int32_t iCount = INT32_MIN;
  int32_t pCount = INT32_MIN;

  switch (_mapping) {
    case P143_CounterMapping_e::ColorMapping:
    {
      for (int i = 0; i <= _colorMaps; ++i) {
        if ((!_colorMapping[i].isEmpty()) &&
            (iCount == INT32_MIN) &&
            (parseColorMapLine(_colorMapping[i], iCount, iRed, iGreen, iBlue)) &&
            (iCount < _encoderPosition)) { // Reset, out of range
          iRed   = -1;
          iGreen = -1;
          iBlue  = -1;
          iCount = INT32_MIN;
        }
      }
      break;
    }

    case P143_CounterMapping_e::ColorGradient:
    {
      for (int i = 0; i <= _colorMaps; ++i) {
        if (!_colorMapping[i].isEmpty()) {
          if ((iCount == INT32_MIN) &&
              (parseColorMapLine(_colorMapping[i], iCount, iRed, iGreen, iBlue)) &&
              ((iCount > _encoderPosition) || !rangeCheck(iCount, _encoderMin, _encoderMax))) {
            iRed   = -1;
            iGreen = -1;
            iBlue  = -1;
            iCount = INT32_MIN;
          }

          if ((pCount == INT32_MIN) &&
              (parseColorMapLine(_colorMapping[i], pCount, pRed, pGreen, pBlue)) &&
              ((pCount < _encoderPosition) || !rangeCheck(pCount, _encoderMin, _encoderMax))) {
            pRed   = -1;
            pGreen = -1;
            pBlue  = -1;
            pCount = INT32_MIN;
          }
        }
      }

      // Calculate R/G/B gradient-values for current Counter within upper and lower range
      if ((pCount != iCount) && (iRed > -1) && (iGreen > -1) && (iBlue > -1) && (pRed > -1) && (pGreen > -1) && (pBlue > -1)) {
        iRed   = map(_encoderPosition, iCount, pCount, iRed, pRed);
        iGreen = map(_encoderPosition, iCount, pCount, iGreen, pGreen);
        iBlue  = map(_encoderPosition, iCount, pCount, iBlue, pBlue);
      }

      break;
    }
    case P143_CounterMapping_e::None:
      // Do nothing
      break;
  }

  // Updated Led color?
  if ((iRed > -1) && (iGreen > -1) && (iBlue > -1)) {
    #  if !defined(BUILD_NO_DEBUG) && PLUGIN_143_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      String log = F("P143: Change color R:");
      log += iRed;
      log += F(", G:");
      log += iGreen;
      log += F(", B:");
      log += iBlue;
      #   ifdef LOG_LEVEL_DEBUG_DEV
      log += F(", pR:"); // DEV-only from here
      log += pRed;
      log += F(", pG:");
      log += pGreen;
      log += F(", pB:");
      log += pBlue;
      log += F(", iC:");
      log += iCount;
      log += F(", pC:");
      log += pCount;
      #   endif // ifdef LOG_LEVEL_DEBUG_DEV
      addLog(LOG_LEVEL_DEBUG, log);
    }
    #  endif // if !defined(BUILD_NO_DEBUG) && PLUGIN_143_DEBUG

    switch (_device) {
      case P143_DeviceType_e::AdafruitEncoder:
      {
        _red   = iRed;
        _green = iGreen;
        _blue  = iBlue;

        if (nullptr != Adafruit_Spixel) {
          Adafruit_Spixel->setPixelColor(0, _red, _green, _blue);
          Adafruit_Spixel->show();
        }
        break;
      }
      #  if P143_FEATURE_INCLUDE_M5STACK
      case P143_DeviceType_e::M5StackEncoder:
      {
        _red   = iRed;
        _green = iGreen;
        _blue  = iBlue;
        m5stack_setPixelColor(static_cast<uint8_t>(P143_M5STACK_SELECTION), _red, _green, _blue);
        break;
      }
      #  endif // if P143_FEATURE_INCLUDE_M5STACK
      #  if P143_FEATURE_INCLUDE_DFROBOT
      case P143_DeviceType_e::DFRobotEncoder:
        break; // N/A
      #  endif // if P143_FEATURE_INCLUDE_DFROBOT
    }
  }
}

/*****************************************************
 * parseColorMapLine, line prefixed with # is comment/disabled
 ****************************************************/
bool P143_data_struct::parseColorMapLine(const String& line,
                                         int32_t     & count,
                                         int16_t     & red,
                                         int16_t     & green,
                                         int16_t     & blue) {
  bool   result = false;
  String tmp    = parseString(line, 1);

  if (!tmp.isEmpty() && !tmp.startsWith(F("#"))) {
    count = tmp.toInt();

    tmp = parseString(line, 2);

    if (!tmp.isEmpty()) {
      red = tmp.toInt();

      tmp = parseString(line, 3);

      if (!tmp.isEmpty()) {
        green = tmp.toInt();

        tmp = parseString(line, 4);

        if (!tmp.isEmpty()) {
          blue   = tmp.toInt();
          result = true;
        }
      }
    }
  }

  return result;
}

/*****************************************************
 * rangeCheck
 ****************************************************/
bool P143_data_struct::rangeCheck(int32_t count,
                                  int32_t min,
                                  int32_t max) {
  return !((min != max) &&
           ((count < min) || (count > max)));
}

# endif // if P143_FEATURE_COUNTER_COLORMAPPING

/*****************************************************
 * plugin_fifty_per_second, handle button actions
 ****************************************************/
bool P143_data_struct::plugin_fifty_per_second(struct EventStruct *event) {
  if (_initialized) {
    const uint8_t pressedState = 0; // button has this value if pressed
    uint8_t button             = _buttonLast;
    uint8_t state              = _buttonState;

    // Read button
    switch (_device) {
      case P143_DeviceType_e::AdafruitEncoder:

        if (nullptr != Adafruit_Seesaw) {
          button = Adafruit_Seesaw->digitalRead(P143_SEESAW_SWITCH);
        }
        break;
      # if P143_FEATURE_INCLUDE_M5STACK
      case P143_DeviceType_e::M5StackEncoder:
        button = I2C_read8_reg(_i2cAddress, P143_M5STACK_REG_BUTTON);
        break;
      # endif // if P143_FEATURE_INCLUDE_M5STACK
      # if P143_FEATURE_INCLUDE_DFROBOT
      case P143_DeviceType_e::DFRobotEncoder:
        uint8_t press = I2C_read8_reg(_i2cAddress, P143_DFROBOT_ENCODER_KEY_STATUS_REG);

        if ((press & 0x01) != 0) {
          I2C_write8_reg(_i2cAddress, P143_DFROBOT_ENCODER_KEY_STATUS_REG, 0x00); // Reset
          // Trigger immediately
          button      = button ? 0 : 1;
          _buttonDown = true;
          _buttonTime = (60 / 20);
        }

        break;
      # endif // if P143_FEATURE_INCLUDE_DFROBOT
    }

    if ((button == pressedState) && _buttonDown) {
      _buttonTime++; // increment 20 msec
    } else if (button == pressedState) {
      _buttonDown = true;
      _buttonTime = 0;
    }

    if ((button != _buttonLast) && (_buttonTime >= (60 / 20))) { // Short-press = 60 msec
      _buttonLast = button;
      _buttonDown = false;

      switch (static_cast<P143_ButtonAction_e>(P143_PLUGIN_BUTTON_ACTION)) {
        case P143_ButtonAction_e::PushButton:
          state = button;
          break;
        case P143_ButtonAction_e::PushButtonInverted:
          state        = button == 0 ? 1 : 0;
          _buttonState = state == 0 ? 1 : 0; // Changed
          break;
        case P143_ButtonAction_e::ToggleSwitch:

          if (button == pressedState) {
            state = _buttonState == 0 ? 1 : 0; // Toggle
          }
          _buttonTime = 0;                     // Ignore long-press
          break;
      }
    }

    if (state != _buttonState) {
      if (_enableLongPress && (_buttonTime >= (_buttonLongPress / 20))) { // Long-press
        state += 10;                                                      // Eventvalues similar to Switch plugin
      }

      // Generate event
      if (Settings.UseRules) {
        eventQueue.add(event->TaskIndex, Cache.getTaskDeviceValueName(event->TaskIndex, 1), state);
      }

      // Set task value
      _buttonState = state;

      UserVar.setFloat(event->TaskIndex, 1, _buttonState);

      return true;
    }
  }
  return false;
}

# if P143_FEATURE_INCLUDE_M5STACK

/*****************************************************
 * applyBrightness, calculate new color based on brightness, based on NeoPixel library setBrightness()
 ****************************************************/
uint8_t P143_data_struct::applyBrightness(uint8_t color) {
  if (_brightness) {
    color = (color * _brightness) >> 8;
  }

  return color;
}

/*****************************************************
 * m5stack_setPixelColor
 ****************************************************/
void P143_data_struct::m5stack_setPixelColor(uint8_t pixel,
                                             uint8_t red,
                                             uint8_t green,
                                             uint8_t blue) {
  uint8_t data[4] = {
    pixel,
    applyBrightness(red),
    applyBrightness(green),
    applyBrightness(blue) };

  I2C_writeBytes_reg(_i2cAddress, P143_M5STACK_REG_LED, data, 4);
}

# endif // if P143_FEATURE_INCLUDE_M5STACK

#endif // ifdef USES_P143


#include "../PluginStructs/P022_data_struct.h"

#ifdef USES_P022

bool P022_data_struct::p022_is_init(uint8_t address) {
  if ((address < PCA9685_ADDRESS) || (address > PCA9685_MAX_ADDRESS)) { return false; }
  uint32_t address_offset = address - PCA9685_ADDRESS;

  if (address_offset < 32) {
    return initializeState_lo & (1 << address_offset);
  } else {
    return initializeState_hi & (1 << (address_offset - 32));
  }
}

bool P022_data_struct::p022_set_init(uint8_t address) {
  if ((address < PCA9685_ADDRESS) || (address > PCA9685_MAX_ADDRESS)) { return false; }
  uint32_t address_offset = address - PCA9685_ADDRESS;

  if (address_offset < 32) {
    initializeState_lo |= (1 << address_offset);
  } else {
    initializeState_hi |= (1 << (address_offset - 32));
  }
  return true;
}

bool P022_data_struct::p022_clear_init(uint8_t address) {
  if ((address < PCA9685_ADDRESS) || (address > PCA9685_MAX_ADDRESS)) { return false; }
  uint32_t address_offset = address - PCA9685_ADDRESS;

  if (address_offset < 32) {
    initializeState_lo &= ~(1 << address_offset);
  } else {
    initializeState_hi &= ~(1 << (address_offset - 32));
  }
  return true;
}

// ********************************************************************************
// PCA9685 config
// ********************************************************************************
void P022_data_struct::Plugin_022_writeRegister(int i2cAddress, int regAddress, uint8_t data) {
  I2C_write8_reg(i2cAddress, regAddress, data);
}

// ********************************************************************************
// PCA9685 write
// ********************************************************************************
void P022_data_struct::Plugin_022_Off(int address, int pin)
{
  Plugin_022_Write(address, pin, 0);
}

void P022_data_struct::Plugin_022_On(int address, int pin)
{
  Plugin_022_Write(address, pin, PCA9685_MAX_PWM);
}

void P022_data_struct::Plugin_022_Write(int address, int Par1, int Par2)
{
  int i2cAddress = address;

  // boolean success = false;
  int regAddress = Par1 == -1
                   ? PCA9685_ALLLED_REG
                   : PCA9685_LED0 + 4 * Par1;
  uint16_t LED_ON  = 0;
  uint16_t LED_OFF = Par2;

  Wire.beginTransmission(i2cAddress);
  Wire.write(regAddress);
  Wire.write(lowByte(LED_ON));
  Wire.write(highByte(LED_ON));
  Wire.write(lowByte(LED_OFF));
  Wire.write(highByte(LED_OFF));
  Wire.endTransmission();
}

void P022_data_struct::Plugin_022_Frequency(int address, uint16_t freq)
{
  int i2cAddress = address;

  Plugin_022_writeRegister(i2cAddress, PLUGIN_022_PCA9685_MODE1, (uint8_t)0x0);
  freq *= 0.9f;

  //  prescale = 25000000 / 4096;
  uint16_t prescale = 6103;

  prescale /=  freq;
  prescale -= 1;
  const uint8_t oldmode = I2C_read8_reg(i2cAddress, PLUGIN_022_PCA9685_MODE1);
  const uint8_t newmode = (oldmode & 0x7f) | 0x10;

  Plugin_022_writeRegister(i2cAddress, PLUGIN_022_PCA9685_MODE1, (uint8_t)newmode);
  Plugin_022_writeRegister(i2cAddress, 0xfe,                     (uint8_t)prescale); // prescale register
  Plugin_022_writeRegister(i2cAddress, PLUGIN_022_PCA9685_MODE1, (uint8_t)oldmode);
  delayMicroseconds(5000);
  Plugin_022_writeRegister(i2cAddress, PLUGIN_022_PCA9685_MODE1, (uint8_t)oldmode | 0xa1);
}

void P022_data_struct::Plugin_022_initialize(int address)
{
  int i2cAddress = address;

  // default mode is open drain output, drive leds connected to VCC
  Plugin_022_writeRegister(i2cAddress, PLUGIN_022_PCA9685_MODE1, (uint8_t)0x01);      // reset the device
  delay(1);
  Plugin_022_writeRegister(i2cAddress, PLUGIN_022_PCA9685_MODE1, (uint8_t)0b10100000); // set up for auto increment
  // Plugin_022_writeRegister(i2cAddress, PCA9685_MODE2, (uint8_t)0x10); // set to output
  p022_set_init(address);
}

String P022_data_struct::P022_logPrefix(int address) {
  return concat(formatToHex(address, F("PCA 0x"), 2),  F(": "));
}

String P022_data_struct::P022_logPrefix(int address, const __FlashStringHelper * poststr)
{
  return concat(P022_logPrefix(address), poststr);
}

void P022_data_struct::initModeFreq(int address, uint8_t mode2, uint16_t freq)
{
  if (!p022_is_init(address))
  {
    Plugin_022_initialize(address);
    Plugin_022_writeRegister(address, PCA9685_MODE2, mode2);
    Plugin_022_Frequency(address, freq);
  }
}

#endif // ifdef USES_P022

#include "../PluginStructs/P112_data_struct.h"

#ifdef USES_P112

// #######################################################################################################
// #################### Plugin 112 I2C AS7265X Triad Spectroscopy Sensor and White, IR and UV LED ########
// #######################################################################################################
//
// Triad Spectroscopy Sensor and White, IR and UV LED
// like this one: https://www.sparkfun.com/products/15050
// based on this library: https://github.com/sparkfun/SparkFun_AS7265x_Arduino_Library
// this code is based on 29 Mar 2019-03-29 version of the above library
//
// 2021-03-29 heinemannj: Initial commit
//

// Needed also here for PlatformIO's library finder as the .h file
// is in a directory which is excluded in the src_filter
#include <SparkFun_AS7265X.h>

bool P112_data_struct::begin()
{
  if (!initialized) {
    initialized = sensor.begin();

    if (initialized) {
//      sensor.takeMeasurementsWithBulb();
    }
  }
  return initialized;
}

#endif // ifdef USES_P112

// Needed also here for PlatformIO's library finder as the .h file
// is in a directory which is excluded in the src_filter

#include "../PluginStructs/P050_data_struct.h"


#ifdef USES_P050

# include <Adafruit_TCS34725.h>

P050_data_struct::P050_data_struct(uint16_t integrationSetting, uint16_t gainSetting) {
  // Map integration time setting (uint16_t to enum)
  _integrationTime = static_cast<tcs34725IntegrationTime_t>(integrationSetting);

  // Map gain setting (uint16_t -> enum)
  _gain = static_cast<tcs34725Gain_t>(gainSetting);

  /* Initialise with specific int time and gain values */
  tcs = Adafruit_TCS34725(_integrationTime, _gain);

  resetTransformation();

  // String log = F("P050_data sizeof(TransformationSettings): ");
  // log += sizeof(TransformationSettings);
  // addLog(LOG_LEVEL_INFO, log);
}

/**
 * resetTransformation
 * Effectgively sets matrix[0][0], matrix[1][1] and matrix[2][2] to 1.0f, all other fields to 0.0f
 */
void P050_data_struct::resetTransformation() {
  // Initialize Transformationn defaults
  for (int i = 0; i < 3; ++i) {
    for (int j = 0; j < 3; ++j) {
      TransformationSettings.matrix[i][j] = i == j ? 1.0f : 0.0f;
    }
  }
}

/**
 * applyTransformation : calibrate r/g/b inputs (uint16_t) to rc/gc/bc outputs (float, by reference)
 */
void P050_data_struct::applyTransformation(uint16_t r, uint16_t g, uint16_t b, float *rc, float *gc, float *bc) {
  applyTransformation(static_cast<float>(r), static_cast<float>(g), static_cast<float>(b), rc, gc, bc);
}

/**
 * applyTransformation : calibrate normalized r/g/b inputs (float) to rc/gc/bc outputs (float, by reference)
 */
void P050_data_struct::applyTransformation(float nr, float ng, float nb, float *rc, float *gc, float *bc) {
  *rc = TransformationSettings.matrix[0][0] * nr +
        TransformationSettings.matrix[0][1] * ng +
        TransformationSettings.matrix[0][2] * nb;
  *gc = TransformationSettings.matrix[1][0] * nr +
        TransformationSettings.matrix[1][1] * ng +
        TransformationSettings.matrix[1][2] * nb;
  *bc = TransformationSettings.matrix[2][0] * nr +
        TransformationSettings.matrix[2][1] * ng +
        TransformationSettings.matrix[2][2] * nb;
}

bool P050_data_struct::loadSettings(taskIndex_t taskIndex) {
  LoadCustomTaskSettings(taskIndex, reinterpret_cast<uint8_t *>(&TransformationSettings), sizeof(TransformationSettings));
  return true;
}

bool P050_data_struct::saveSettings(taskIndex_t taskIndex) {
  SaveCustomTaskSettings(taskIndex, reinterpret_cast<const uint8_t *>(&TransformationSettings), sizeof(TransformationSettings));
  return true;
}

String P050_data_struct::generate_cal_id(int i, int j) {
  return strformat(F("cal_%c_%d"), static_cast<char>('a' + i), j);
}

#endif // ifdef USES_P050

#include "../PluginStructs/P126_data_struct.h"

#ifdef USES_P126

// **************************************************************************/
// Constructor
// **************************************************************************/
P126_data_struct::P126_data_struct(int8_t  dataPin,
                                   int8_t  clockPin,
                                   int8_t  latchPin,
                                   uint8_t chipCount)
  : _dataPin(dataPin), _clockPin(clockPin), _latchPin(latchPin), _chipCount(chipCount) {
  shift = new (std::nothrow) ShiftRegister74HC595_NonTemplate(_chipCount, _dataPin, _clockPin, _latchPin);
}

// **************************************************************************/
// Destructor
// **************************************************************************/
P126_data_struct::~P126_data_struct() {
  delete shift;
  shift = nullptr;
}

bool P126_data_struct::plugin_init(struct EventStruct *event) {
  if (P126_CONFIG_FLAGS_GET_VALUES_RESTORE) { // Restore only when enabled
    uint8_t idx = P126_CONFIG_SHOW_OFFSET;
    std::vector<uint8_t> value;

    value.resize(_chipCount, 0);             // Initialize vector to 0's

    const uint8_t *pvalue = shift->getAll(); // Get current state

    for (uint8_t i = 0; i < _chipCount; ++i) {
      value[i] = pvalue[i];
    }

    const uint16_t maxVar = min(static_cast<uint8_t>(VARS_PER_TASK),
                                static_cast<uint8_t>(ceil((P126_CONFIG_CHIP_COUNT - P126_CONFIG_SHOW_OFFSET) / 4.0)));
    uint32_t par;

    for (uint16_t varNr = 0; varNr < maxVar; ++varNr) {
      par = UserVar.getUint32(event->TaskIndex, varNr);

      for (uint8_t n = 0; n < 4 && idx < _chipCount; ++n, ++idx) {
        value[idx] = ((par >> (n * 8)) & 0xff);

        # ifdef P126_DEBUG_LOG

        if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
          addLogMove(LOG_LEVEL_DEBUG, strformat(F("SHIFTOUT: plugin_init: value[%d] : %d/0x%02x, n * 8: %d/%d, varNr: %d"),
                                                idx, value[idx], value[idx], n, n * 8, varNr));
        }
        # endif // ifdef P126_DEBUG_LOG
      }
    }
    shift->setAll(&value[0], false); // DO NOT SEND OUTPUT TO REGISTERS
  }
  return true;
}

const uint32_t P126_data_struct::getChannelState(uint8_t offset, uint8_t size) const {
  uint32_t result       = 0u;
  const uint8_t *pvalue = shift->getAll();
  const uint8_t  last   = offset + size;

  if (nullptr != pvalue) {
    uint16_t sft = 0u;

    for (uint8_t ofs = offset; ofs < last; ++ofs, ++sft) {
      result += (pvalue[ofs] << (8 * sft));
    }

    # ifdef P126_DEBUG_LOG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      addLogMove(LOG_LEVEL_DEBUG, strformat(F("SHIFTOUT: getChannelState offset: %d, size: %d, result: %d/0x%x"),
                                            offset, size, result, result));
    }
    # endif // ifdef P126_DEBUG_LOG
  }
  return result;
}

bool P126_data_struct::plugin_read(struct EventStruct *event) {
  const uint16_t last = P126_CONFIG_SHOW_OFFSET + (VARS_PER_TASK * 4);
  uint8_t varNr       = 0;

  for (uint16_t index = P126_CONFIG_SHOW_OFFSET; index < _chipCount && index < last && varNr < VARS_PER_TASK; index += 4, ++varNr) {
    const uint32_t result = getChannelState(index, min(VARS_PER_TASK, _chipCount - index));
    UserVar.setUint32(event->TaskIndex, varNr, result);
  }
  return true;
}

bool P126_data_struct::plugin_write(struct EventStruct *event,
                                    const String      & string) {
  bool   success = false;
  String command = parseString(string, 1);

  if (equals(command, F("shiftout"))) {
    const String subcommand = parseString(string, 2);
    const bool   hc_update  = subcommand.indexOf(F("noupdate")) == -1;

    if (equals(subcommand, F("set")) || equals(subcommand, F("setnoupdate"))) {
      const uint8_t  pin   = event->Par2;
      const uint16_t value = event->Par3;

      if (validChannel(pin) && ((value == 0) || (value == 1))) {
        shift->set(pin - 1, value, hc_update);
        success = true;
        # ifdef P126_DEBUG_LOG

        if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
          addLogMove(LOG_LEVEL_DEBUG, strformat(F("%s, pin: %d, value: %d"), command.c_str(), pin, value));
        }
        # endif // ifdef P126_DEBUG_LOG
      }
    } else if (equals(subcommand, F("update"))) {
      shift->updateRegisters();
      success = true;
    } else if (equals(subcommand, F("setall")) || equals(subcommand, F("setallnoupdate"))) {
      success = true;
      std::vector<uint8_t> value;
      value.resize(_chipCount, 0);             // Initialize vector to 0's

      const uint8_t *pvalue = shift->getAll(); // Get current state

      for (uint8_t i = 0; i < _chipCount; ++i) {
        value[i] = pvalue[i];
      }

      uint32_t par   = 0u;
      uint8_t  param = 3; // Start with an offset
      uint8_t  width = 4;
      uint8_t  idx   = 0;
      String   arg   = parseString(string, param);

      while (!arg.isEmpty() && idx < _chipCount && success) {
        int colon    = arg.indexOf(':'); // First colon: Chip-index, range 1.._chipCount
        int32_t itmp = 0;

        if (colon != -1) {
          const String cis = arg.substring(0, colon);
          arg = arg.substring(colon + 1);

          if (!cis.isEmpty() && validIntFromString(cis, itmp) && (itmp > 0) && (itmp <= _chipCount)) {
            idx = itmp - 1;       // Actual range is 0.._chipCount - 1
          } else {
            success = false;      // Cancel entire operation on error
          }
        }
        colon = arg.indexOf(':'); // Second colon: data width, range 1..4 bytes
        width = 4;                // Set default data width to 4 = 32 bits

        if (colon != -1) {
          const String lis = arg.substring(0, colon);
          arg = arg.substring(colon + 1);

          if (!lis.isEmpty() && validIntFromString(lis, itmp) && (itmp > 0) && (itmp <= 4)) {
            width = itmp;
          } else {
            success = false; // Cancel entire operation on error
          }
        }
        int64_t tmp = 0;
        par = 0u; // reset

        if (validInt64FromString(arg, tmp)) {
          par = static_cast<uint32_t>(tmp);
        }

        # ifdef P126_DEBUG_LOG

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          addLogMove(LOG_LEVEL_INFO,
                     strformat(F("%s: arg: %s, tmp: %s/0x%x, par: %d/0x%x, chip:%d, width:%d"),
                               command.c_str(), arg.c_str(), ull2String(tmp).c_str(), tmp, par, par, idx, width));
        }
        # endif // ifdef P126_DEBUG_LOG

        param++; // Process next argument
        arg = parseString(string, param);

        for (uint8_t n = 0; n < width && idx < _chipCount; ++n, ++idx) {
          value[idx] = ((par >> (n * 8)) & 0xff);

          # ifdef P126_DEBUG_LOG

          if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
            addLogMove(LOG_LEVEL_DEBUG,
                       strformat(F("%s: value[%d] : %d/0x%x, n * 8: %d/%d"),
                                 command.c_str(), idx, value[idx], value[idx], n, n * 8));
          }
          # endif // ifdef P126_DEBUG_LOG
        }
      }

      if (success) {
        shift->setAll(&value[0], hc_update);
      }
    } else if (equals(subcommand, F("setalllow"))) {
      shift->setAllLow();
      success = true;
    } else if (equals(subcommand, F("setallhigh"))) {
      shift->setAllHigh();
      success = true;
    } else if (equals(subcommand, F("setoffset"))) {
      if ((event->Par2 >= 0) && (event->Par2 <= P126_MAX_SHOW_OFFSET)) {
        uint8_t previousOffset = P126_CONFIG_SHOW_OFFSET;
        P126_CONFIG_SHOW_OFFSET = event->Par2;

        if (P126_CONFIG_SHOW_OFFSET >= P126_CONFIG_CHIP_COUNT) {
          P126_CONFIG_SHOW_OFFSET = 0;
        }
        P126_CONFIG_SHOW_OFFSET -= (P126_CONFIG_SHOW_OFFSET % 4);

        if ((P126_CONFIG_CHIP_COUNT > 4) &&
            (P126_CONFIG_SHOW_OFFSET > P126_CONFIG_CHIP_COUNT - 4) &&
            (P126_CONFIG_CHIP_COUNT < P126_MAX_SHOW_OFFSET)) {
          P126_CONFIG_SHOW_OFFSET -= 4;
        }

        // Reset State_A..D values when changing the offset
        if ((previousOffset != P126_CONFIG_SHOW_OFFSET) && P126_CONFIG_FLAGS_GET_VALUES_RESTORE) {
          for (uint8_t varNr = 0; varNr < VARS_PER_TASK; ++varNr) {
            UserVar.setUint32(event->TaskIndex, varNr, 0u);
          }
          # ifdef P126_DEBUG_LOG
          addLog(LOG_LEVEL_INFO, F("SHIFTOUT: 'Offset for display' changed: state values reset."));
          # endif // ifdef P126_DEBUG_LOG
        }
        success = true;
      }
    } else if (equals(subcommand, F("setchipcount"))) {
      if ((event->Par2 >= 1) && (event->Par2 <= P126_MAX_CHIP_COUNT)) {
        P126_CONFIG_CHIP_COUNT = event->Par2;
        _chipCount             = event->Par2;
        shift->setSize(P126_CONFIG_CHIP_COUNT);
        success = true;
      }
    # ifdef P126_SHOW_VALUES
    } else if (equals(subcommand, F("sethexbin"))) {
      if ((event->Par2 == 0) || (event->Par2 == 1)) {
        uint32_t lSettings = P126_CONFIG_FLAGS;
        bitWrite(lSettings, P126_FLAGS_VALUES_DISPLAY, event->Par2 == 1);
        P126_CONFIG_FLAGS = lSettings;
        success           = true;
      }
    # endif // ifdef P126_SHOW_VALUES
    }
    # ifdef P126_DEBUG_LOG

    if (success) {
      addLog(LOG_LEVEL_DEBUG, string);
    }
    # endif // ifdef P126_DEBUG_LOG
  }
  return success;
}

#endif // ifdef USES_P126

#include "../PluginStructs/P039_data_struct.h"

#ifdef USES_P039

/*
   P039_data_struct::P039_data_struct(
      uint16_t       l_conversionResult,
      uint8_t        l_devicefaults,
      unsigned long  l_timer,
      bool           l_sensorFault,
      bool           l_convReady)
   :  conversionResult(l_conversionResult), deviceFaults(l_devicefaults), timer(l_timer), sensorFault(l_sensorFault), convReady(l_convReady)
      {}
 */
bool P039_data_struct::begin()
{
  return false;
}

bool P039_data_struct::read()
{
  return false;
}

bool P039_data_struct::write()
{
  return false;
}

#endif // ifdef USES_P039

#include "../PluginStructs/P045_data_struct.h"

#ifdef USES_P045

# define MPU6050_RA_GYRO_CONFIG              0x1B
# define MPU6050_RA_ACCEL_CONFIG             0x1C
# define MPU6050_RA_ACCEL_XOUT_H             0x3B
# define MPU6050_RA_PWR_MGMT_1               0x6B
# define MPU6050_ACONFIG_AFS_SEL_BIT         4
# define MPU6050_ACONFIG_AFS_SEL_LENGTH      2
# define MPU6050_GCONFIG_FS_SEL_BIT          4
# define MPU6050_GCONFIG_FS_SEL_LENGTH       2
# define MPU6050_CLOCK_PLL_XGYRO             0x01
# define MPU6050_GYRO_FS_250                 0x00
# define MPU6050_ACCEL_FS_2                  0x00
# define MPU6050_PWR1_SLEEP_BIT              6
# define MPU6050_PWR1_CLKSEL_BIT             2
# define MPU6050_PWR1_CLKSEL_LENGTH          3

P045_data_struct::P045_data_struct(uint8_t i2c_addr) : i2cAddress(i2c_addr) {}

void P045_data_struct::init()
{
  // Initialize the MPU6050, for details look at the MPU6050 library: MPU6050::Initialize
  writeBits(MPU6050_RA_PWR_MGMT_1,   MPU6050_PWR1_CLKSEL_BIT,     MPU6050_PWR1_CLKSEL_LENGTH,     MPU6050_CLOCK_PLL_XGYRO);
  writeBits(MPU6050_RA_GYRO_CONFIG,  MPU6050_GCONFIG_FS_SEL_BIT,  MPU6050_GCONFIG_FS_SEL_LENGTH,  MPU6050_GYRO_FS_250);
  writeBits(MPU6050_RA_ACCEL_CONFIG, MPU6050_ACONFIG_AFS_SEL_BIT, MPU6050_ACONFIG_AFS_SEL_LENGTH, MPU6050_ACCEL_FS_2);
  writeBits(MPU6050_RA_PWR_MGMT_1,   MPU6050_PWR1_SLEEP_BIT,      1,                              0);

  // Read the MPU6050 once to clear out zeros (1st time reading MPU6050 returns all 0s)
  int16_t ax, ay, az, gx, gy, gz;

  getRaw6AxisMotion(&ax, &ay, &az, &gx, &gy, &gz);

  for (int i = 0; i < 3; ++i) {
    for (int j = 0; j < 5; ++j) {
      _axis[i][j] = 0;
    }
  }
}

void P045_data_struct::loop()
{
  // Read the sensorvalues, we run this bit every 1/10th of a second
  getRaw6AxisMotion(&_axis[0][3],
                    &_axis[1][3],
                    &_axis[2][3],
                    &_axis[0][4],
                    &_axis[1][4],
                    &_axis[2][4]);

  // Set the minimum and maximum value for each axis a-value, overwrite previous values if smaller/larger
  trackMinMax(_axis[0][3], &_axis[0][0], &_axis[0][1]);
  trackMinMax(_axis[1][3], &_axis[1][0], &_axis[1][1]);
  trackMinMax(_axis[2][3], &_axis[2][0], &_axis[2][1]);

  //          ^ current value @ 3   ^ min val @ 0         ^ max val @ 1

  /*
     // Uncomment this block if you want to debug your MPU6050, but be prepared for a log overload
     String log = F("MPU6050 : axis values: ");

     log += _axis[0][3];
     log += F(", ");
     log += _axis[1][3];
     log += F(", ");
     log += _axis[2][3];
     log += F(", g values: ");
     log += _axis[0][4];
     log += F(", ");
     log += _axis[1][4];
     log += F(", ");
     log += _axis[2][4];
     addLog(LOG_LEVEL_INFO, log);
   */

  // Run this bit every 5 seconds per deviceaddress (not per instance)
  if (timeOutReached(_timer + 5000))
  {
    _timer = millis();

    // Determine the maximum measured range of each axis
    for (uint8_t i = 0; i < 3; ++i) {
      _axis[i][2] = abs(_axis[i][1] - _axis[i][0]);
      _axis[i][0] = _axis[i][3];
      _axis[i][1] = _axis[i][3];
    }
  }
}

void P045_data_struct::trackMinMax(int16_t current, int16_t *min, int16_t *max)
{
  // From nodemcu-laundry.ino by Nolan Gilley
  if (current > *max)
  {
    *max = current;
  }
  else if (current < *min)
  {
    *min = current;
  }
}

void P045_data_struct::getRaw6AxisMotion(int16_t *ax, int16_t *ay, int16_t *az, int16_t *gx, int16_t *gy, int16_t *gz) {
  // From I2Cdev::readBytes and MPU6050::getMotion6, both by Jeff Rowberg
  uint8_t buffer[14];
  uint8_t count = 0;

  I2C_write8(i2cAddress, MPU6050_RA_ACCEL_XOUT_H);
  Wire.requestFrom(i2cAddress, (uint8_t)14);

  for (; Wire.available(); ++count) {
    buffer[count] = Wire.read();
  }
  *ax = (((int16_t)buffer[0]) << 8) | buffer[1];
  *ay = (((int16_t)buffer[2]) << 8) | buffer[3];
  *az = (((int16_t)buffer[4]) << 8) | buffer[5];
  *gx = (((int16_t)buffer[8]) << 8) | buffer[9];
  *gy = (((int16_t)buffer[10]) << 8) | buffer[11];
  *gz = (((int16_t)buffer[12]) << 8) | buffer[13];
}

void P045_data_struct::writeBits(uint8_t regAddr, uint8_t bitStart, uint8_t length, uint8_t data) {
  // From I2Cdev::writeBits by Jeff Rowberg
  //      010 value to write
  // 76543210 bit numbers
  //    xxx   args: bitStart=4, length=3
  // 00011100 mask byte
  // 10101111 original value (sample)
  // 10100011 original & ~mask
  // 10101011 masked | value
  bool is_ok = true;
  uint8_t b  = I2C_read8_reg(i2cAddress, regAddr, &is_ok);

  if (is_ok) {
    uint8_t mask = ((1 << length) - 1) << (bitStart - length + 1);
    data <<= (bitStart - length + 1); // shift data into correct position
    data  &= mask;                    // zero all non-important bits in data
    b     &= ~(mask);                 // zero all important bits in existing byte
    b     |= data;                    // combine data with existing byte
    I2C_write8_reg(i2cAddress, regAddr, b);
  }
}

#endif // ifdef USES_P045

#include "../PluginStructs/P162_data_struct.h"

#ifdef USES_P162

# include "../PluginStructs/P162_data_struct.h"

# include <SPI.h>

// Needed also here for PlatformIO's library finder as the .h file
// is in a directory which is excluded in the src_filter

P162_data_struct::P162_data_struct(int8_t csPin,
                                   int8_t rstPin,
                                   int8_t shdPin)
  : _csPin(csPin), _rstPin(rstPin), _shdPin(shdPin)
{}

P162_data_struct::~P162_data_struct() {
  //
}

bool P162_data_struct::plugin_init(struct EventStruct *event) {
  if (validGpio(_csPin) && Settings.isSPI_valid()) {
    pinMode(_csPin, OUTPUT);
    _initialized = true;
  }

  if (validGpio(_rstPin)) {
    pinMode(_rstPin, OUTPUT);
    hw_reset();
    updateUserVars(event);
  }

  if (_initialized) {
    // Set default values
    if (P162_SHUTDOWN_W0) {
      _pot0_value = P162_SHUTDOWN_VALUE;
      write_pot(P162_POT0_SHUTDOWN, _pot0_value); // Value ignored for shutdown
    } else {
      _pot0_value = P162_INIT_W0;
      write_pot(P162_POT0_SEL, _pot0_value);
    }

    if (P162_SHUTDOWN_W1) {
      _pot1_value = P162_SHUTDOWN_VALUE;
      write_pot(P162_POT1_SHUTDOWN, _pot1_value); // Value ignored for shutdown
    } else {
      _pot1_value = P162_INIT_W1;
      write_pot(P162_POT1_SEL, _pot1_value);
    }
    updateUserVars(event);

    if (validGpio(_shdPin)) {
      pinMode(_shdPin, OUTPUT);

      if (P162_SHUTDOWN_W0 && P162_SHUTDOWN_W1) {
        _shdState = LOW;
        digitalWrite(_shdPin, _shdState);
      }
    }
  } else {
    addLog(LOG_LEVEL_ERROR, F("Digipot: Initialization failed, SPI/CS not configured."));
  }

  return _initialized;
}

/****************************************************
 * Reset the chip if a reset pin is configured
 ***************************************************/
bool P162_data_struct::hw_reset() {
  if (validGpio(_rstPin)) {
    _pot0_value = P162_RESET_VALUE;
    _pot1_value = P162_RESET_VALUE;
    digitalWrite(_rstPin, LOW);
    delayMicroseconds(1); // Reset requires low signal for at least 150 nsec, so 1 microsecond should suffice
    digitalWrite(_rstPin, HIGH);
    addLog(LOG_LEVEL_INFO, F("Digipot: Hardware reset applied."));
    return true;
  }
  return false;
}

/*********************************************************************************************
 * Handle command processing
 ********************************************************************************************/
bool P162_data_struct::plugin_write(struct EventStruct *event,
                                    String            & string) {
  bool success     = false;
  const String cmd = parseString(string, 1);

  if (equals(cmd, F("digipot"))) {
    const String sub    = parseString(string, 2);
    const bool   hasPar = !parseString(string, 3).isEmpty();

    // digipot,reset : Reset via configured RST pin else via software to initial state (both channels at 128)
    if (equals(sub, F("reset"))) { // Reset the digipot
      success = hw_reset();        // if the reset pin is configured

      if (!success) {
        _pot0_value = P162_RESET_VALUE;
        _pot1_value = P162_RESET_VALUE;
        updateUserVars(event);
        write_pot(P162_BOTH_POT_SEL, _pot0_value); // Single command
        success = true;
        addLog(LOG_LEVEL_INFO, F("Digipot: Software reset applied."));
      }
    } else

    // digipit,shutdown[,pot] : Shutdown pot 0, 1 or both (2), via hardware if both and pin shutdown configured
    if (equals(sub, F("shutdown")) && (event->Par2 >= 0) && (event->Par2 <= 2)) { // Pot range: 0..2
      if (((hasPar && (2 == event->Par2)) || !hasPar) && validGpio(_shdPin)) {    // no params or param = 2 (both) and shutdown pin defined
        _shdState = LOW;
        digitalWrite(_shdPin, _shdState);
        _pot0_value = P162_SHUTDOWN_VALUE;
        _pot1_value = P162_SHUTDOWN_VALUE;
        updateUserVars(event);
        success = true;
      } else {
        uint8_t shd = P162_BOTH_POT_SHUTDOWN;

        if (hasPar && (0 == event->Par2)) {
          shd         = P162_POT0_SHUTDOWN;
          _pot0_value = P162_SHUTDOWN_VALUE;
        } else if (hasPar && (1 == event->Par2)) {
          shd         = P162_POT1_SHUTDOWN;
          _pot1_value = P162_SHUTDOWN_VALUE;
        }
        updateUserVars(event);
        write_pot(shd, 0);
        success = true;
      }
    } else

    // digipot,(0|1|2),<value> : Set pot 0, 1 or both (2) to <value>, where value = 0..255
    if (isdigit(parseString(string, 2)[0]) && (event->Par1 >= 0) && (event->Par1 <= 2)) {
      if (hasPar && (event->Par2 >= 0) && (event->Par2 <= 255)) { // Argument mandatory
        uint8_t sel = P162_BOTH_POT_SEL;

        if (0 == event->Par1) {
          sel         = P162_POT0_SEL;
          _pot0_value = event->Par2;
        } else if (1 == event->Par1) {
          sel         = P162_POT1_SEL;
          _pot1_value = event->Par2;
        } else {
          _pot0_value = event->Par2;
          _pot1_value = event->Par2;
        }
        updateUserVars(event);
        write_pot(sel, event->Par2);
        success = true;
      }
    }
  }

  return success;
}

/********************************************************************************************
 * Write to pot
 *******************************************************************************************/
void P162_data_struct::write_pot(uint8_t cmd,
                                 uint8_t val) {
  if (!_initialized) { return; }

  // set the CS pin to low to select the chip:
  digitalWrite(_csPin, LOW);

  // send the command and value via SPI:
  SPI.transfer(cmd);
  SPI.transfer(val);

  // Set the CS pin high to execute the command:
  digitalWrite(_csPin, HIGH);
}

/*********************************************************************************
 * Set current values to UserVar
 ********************************************************************************/
void P162_data_struct::updateUserVars(struct EventStruct *event) {
  const int16_t pot0_old = UserVar.getFloat(event->TaskIndex, 0, true);
  const int16_t pot1_old = UserVar.getFloat(event->TaskIndex, 1, true);

  UserVar.setFloat(event->TaskIndex, 0, _pot0_value);
  UserVar.setFloat(event->TaskIndex, 1, _pot1_value);

  if (P162_CHANGED_EVENTS && ((pot0_old != _pot0_value) || (pot1_old != _pot1_value))) {
    sendData(event);
  }

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, strformat(F("Digipot: W0 = %d, W1 = %d"), _pot0_value, _pot1_value));
  }
}

#endif // ifdef USES_P162

#include "../PluginStructs/P139_data_struct_formselectors.h"

#ifdef USES_P139


// **********************************************************************
// Charge LED settings
// **********************************************************************
AXP2101_ChargeLED_FormSelector::AXP2101_ChargeLED_FormSelector(
  AXP2101_chargeled_d selected) :
  FormSelectorOptions(5)
{
  addFormSelector(
    F("Charge LED"), F("led"),
    static_cast<int>(selected));
}

AXP2101_chargeled_d AXP2101_ChargeLED_FormSelector::get()
{
  return static_cast<AXP2101_chargeled_d>(getFormItemInt(F("led")));
}

AXP2101_chargeled_d get_AXP2101_chargeled_d(int index) {
  if (index < 0) { return AXP2101_chargeled_d::Off; }

  if (index <= static_cast<int>(AXP2101_chargeled_d::Steady_On)) {
    return static_cast<AXP2101_chargeled_d>(index);
  }
  return AXP2101_chargeled_d::Protected;
}

String AXP2101_ChargeLED_FormSelector::getOptionString(int index) const
{
  return toString(get_AXP2101_chargeled_d(index));
}

int AXP2101_ChargeLED_FormSelector::getIndexValue(int index) const
{
  return static_cast<int>(get_AXP2101_chargeled_d(index));
}

// **********************************************************************
// Reg 61: Iprechg Charger Settings
// 0 .. 200 mA in 25 mA steps
// **********************************************************************
AXP2101_PreChargeCurrentLimit_FormSelector::AXP2101_PreChargeCurrentLimit_FormSelector(
  int selected) : FormSelectorOptions(9)
{
  addFormSelector(
    F("Pre Charge Current"), F("iprechg"),
    static_cast<int>(selected));
  addUnit(F("mA"));
  addFormNote(F("When V_bat < 3V, the battery will be charged using pre-charge current"));
}

int AXP2101_PreChargeCurrentLimit_FormSelector::get()
{
  return getFormItemInt(F("iprechg"));
}

int get_AXP2101_0_to_200mA_ChargeCurrentLimit(int index) {
  if (index < 0) { return 0; }
  const int res = 25 * index;

  if (res > 200) { return 200; }
  return res;
}

String AXP2101_PreChargeCurrentLimit_FormSelector::getOptionString(int index) const
{
  return String(get_AXP2101_0_to_200mA_ChargeCurrentLimit(index));
}

int AXP2101_PreChargeCurrentLimit_FormSelector::getIndexValue(int index) const
{
  return get_AXP2101_0_to_200mA_ChargeCurrentLimit(index);
}

// **********************************************************************
// Reg 62: ICC Charger Settings
// 0 .. 200 mA in 25 mA steps
// 200 ... 1000 mA in 100 mA steps
// **********************************************************************
AXP2101_ConstChargeCurrentLimit_FormSelector::AXP2101_ConstChargeCurrentLimit_FormSelector(
  int selected) : FormSelectorOptions(17)
{
  addFormSelector(
    F("Constant Current Charge Limit"), F("iccchg"),
    static_cast<int>(selected));
  addUnit(F("mA"));
}

int AXP2101_ConstChargeCurrentLimit_FormSelector::get()
{
  return getFormItemInt(F("iccchg"));
}

int get_AXP2101_ConstChargeCurrentLimit(int index) {
  if (index < 0) { return 0; }
  const int res = index <= 8
    ? 25 * index
    : (index - 8) * 100 + 200;

  if (res > 1000) { return 1000; }
  return res;
}

String AXP2101_ConstChargeCurrentLimit_FormSelector::getOptionString(int index) const
{
  return String(get_AXP2101_ConstChargeCurrentLimit(index));
}

int AXP2101_ConstChargeCurrentLimit_FormSelector::getIndexValue(int index) const
{
  return get_AXP2101_ConstChargeCurrentLimit(index);
}

// **********************************************************************
// Reg 63: Iterm Charger Settings and Control
// 0 .. 200 mA in 25 mA steps  + enable checkbox
// **********************************************************************
AXP2101_TerminationChargeCurrentLimit_FormSelector::AXP2101_TerminationChargeCurrentLimit_FormSelector(
  int selected) : FormSelectorOptions(9)
{
  addFormSelector(
    F("Termination Charge Current"), F("iterm"),
    static_cast<int>(selected));
  addUnit(F("mA"));
  addFormNote(F("Charge current threshold to switch to CV charging or stop charging, when battery is nearly full"));
}

int AXP2101_TerminationChargeCurrentLimit_FormSelector::get()
{
  return getFormItemInt(F("iterm"));
}

String AXP2101_TerminationChargeCurrentLimit_FormSelector::getOptionString(int index) const
{
  return String(get_AXP2101_0_to_200mA_ChargeCurrentLimit(index));
}

int AXP2101_TerminationChargeCurrentLimit_FormSelector::getIndexValue(int index) const
{
  return get_AXP2101_0_to_200mA_ChargeCurrentLimit(index);
}

// **********************************************************************
// Reg 64: CV Charger Voltage Settings
// **********************************************************************
AXP2101_CV_charger_voltage_FormSelector::AXP2101_CV_charger_voltage_FormSelector(
  AXP2101_CV_charger_voltage_e selected) :
  FormSelectorOptions(static_cast<int>(AXP2101_CV_charger_voltage_e::MAX) - 1)
{
  addFormSelector(
    F("CV Charger Voltage"), F("cv_volt"),
    static_cast<int>(selected));
  addUnit(F("V"));
  addFormNote(F("Target charge voltage of battery"));
}

AXP2101_CV_charger_voltage_e AXP2101_CV_charger_voltage_FormSelector::get()
{
  return static_cast<AXP2101_CV_charger_voltage_e>(getFormItemInt(F("cv_volt")));
}

AXP2101_CV_charger_voltage_e get_AXP2101_CV_charger_voltage_e(int index) {
  if (index < 0) { return AXP2101_CV_charger_voltage_e::reserved; }
  constexpr int max = static_cast<int>(AXP2101_CV_charger_voltage_e::MAX);
  index += 1;

  if (index >= max) { return AXP2101_CV_charger_voltage_e::reserved; }
  return static_cast<AXP2101_CV_charger_voltage_e>(index);
}

String AXP2101_CV_charger_voltage_FormSelector::getOptionString(int index) const
{
  const AXP2101_CV_charger_voltage_e val = get_AXP2101_CV_charger_voltage_e(index);
  int decimal                            = index;

  if (AXP2101_CV_charger_voltage_e::limit_4_35V == val) {
    index = 35;
  }
  return concat(F("4."), index);
}

int AXP2101_CV_charger_voltage_FormSelector::getIndexValue(int index) const
{
  return static_cast<int>(get_AXP2101_CV_charger_voltage_e(index));
}

// **********************************************************************
// Reg 14: Minimum System Voltage Control
// **********************************************************************
AXP2101_Linear_Charger_Vsys_dpm_FormSelector::AXP2101_Linear_Charger_Vsys_dpm_FormSelector(
  AXP2101_Linear_Charger_Vsys_dpm_e selected) :
  FormSelectorOptions(static_cast<int>(AXP2101_Linear_Charger_Vsys_dpm_e::MAX))
{
  addFormSelector(
    F("Minimum System Voltage"), F("min_vsys"),
    static_cast<int>(selected));
  addUnit(F("V"));
  addFormNote(F("Minimum system voltage to allow charging the battery"));
}

AXP2101_Linear_Charger_Vsys_dpm_e AXP2101_Linear_Charger_Vsys_dpm_FormSelector::get()
{
  return static_cast<AXP2101_Linear_Charger_Vsys_dpm_e>(getFormItemInt(F("min_vsys")));
}

String AXP2101_Linear_Charger_Vsys_dpm_FormSelector::getOptionString(int index) const
{
  constexpr int max = static_cast<int>(AXP2101_Linear_Charger_Vsys_dpm_e::MAX);

  if ((index >= max) || (index < 0)) {
    return EMPTY_STRING;
  }
  return concat(F("4."), index + 1);
}

// **********************************************************************
// Reg 15: Input Voltage Limit
// **********************************************************************
AXP2101_Vin_DPM_FormSelector::AXP2101_Vin_DPM_FormSelector(
  AXP2101_VINDPM_e selected) :
  FormSelectorOptions(static_cast<int>(AXP2101_VINDPM_e::MAX))
{
  addFormSelector(
    F("Min Vin_dpm Voltage"), F("vin_dpm"),
    static_cast<int>(selected));
  addUnit(F("V"));
  addFormNote(F("When Vbus reaches Vin_dpm, the charge current will decrease until zero"));
}

AXP2101_VINDPM_e AXP2101_Vin_DPM_FormSelector::get()
{
  return static_cast<AXP2101_VINDPM_e>(getFormItemInt(F("vin_dpm")));
}

String AXP2101_Vin_DPM_FormSelector::getOptionString(int index) const
{
  constexpr int max = static_cast<int>(AXP2101_VINDPM_e::MAX);

  if ((index >= max) || (index < 0)) {
    return EMPTY_STRING;
  }
  return toString(3.88f + 0.08f * index, 2);
}

// **********************************************************************
// Reg 16: Input Current Limit
// **********************************************************************
AXP2101_InputCurrentLimit_FormSelector::AXP2101_InputCurrentLimit_FormSelector(
  AXP2101_InputCurrentLimit_e selected) :
  FormSelectorOptions(static_cast<int>(AXP2101_InputCurrentLimit_e::MAX))
{
  addFormSelector(
    F("Input Current Limit"), F("cur_lim_in"),
    static_cast<int>(selected));
  addUnit(F("mA"));
  // If I_sys is over the input power supply capability, V_sys will drop.
  // If V_bat is above V_sys, PMU will enter supplement mode.
}

AXP2101_InputCurrentLimit_e AXP2101_InputCurrentLimit_FormSelector::get()
{
  return static_cast<AXP2101_InputCurrentLimit_e>(getFormItemInt(F("cur_lim_in")));
}

String AXP2101_InputCurrentLimit_FormSelector::getOptionString(int index) const
{
  constexpr int max = static_cast<int>(AXP2101_InputCurrentLimit_e::MAX);

  if ((index >= max) || (index < 0)) {
    return EMPTY_STRING;
  }
  const int value = (index < 3)
    ? 100 + (400 * index)
    : 1000 + ((index - 3)) * 500;
  return String(value);
}

#endif // ifdef USES_P139

#include "../PluginStructs/P123_data_struct.h"

#ifdef USES_P123

# include "../Helpers/AdafruitGFX_helper.h"

const __FlashStringHelper* toString(P123_TouchType_e tType) {
  switch (tType) {
    case P123_TouchType_e::FT62x6: return F("FT62x6 (0x38)");
    case P123_TouchType_e::GT911_1: return F("GT911 (0x5D)");
    case P123_TouchType_e::GT911_2: return F("GT911 (0x14)");
    case P123_TouchType_e::CST820: return F("CST820 (0x15)");
    case P123_TouchType_e::CST226: return F("CST226 (0x5A)");
    case P123_TouchType_e::AXS15231: return F("AXS15231 (0x3B)");
    case P123_TouchType_e::CHSC5816: return F("CHSC5816 (0x2E)");
    case P123_TouchType_e::Automatic: return F("Auto-detect");
  }
  return F("");
}

/**
 * Order must match enum P123_TouchType_e, with Automatic (-1) ignored
 */
const uint8_t P123_i2cAddressValues[] = { FT6X36_ADDR, GT911_ADDR1, GT911_ADDR2, CST820_ADDR, CST226_ADDR, AXS15231_ADDR, CHSC5816_ADDR };

bool P123_data_struct::plugin_i2c_has_address(const int Par1) {
  return intArrayContains(NR_ELEMENTS(P123_i2cAddressValues), P123_i2cAddressValues, Par1);
}

uint8_t P123_data_struct::plugin_i2c_address(P123_TouchType_e touchType) {
  const int tType = static_cast<int>(touchType);

  if ((tType >= 0) && (tType < NR_ELEMENTS(P123_i2cAddressValues))) {
    return P123_i2cAddressValues[tType];
  }
  return 0u;
}

P123_TouchType_e P123_data_struct::getTouchType() {
  if (nullptr != touchscreen) {
    const int stype = touchscreen->sensorType();

    switch (stype) {
      case CT_TYPE_FT6X36: return P123_TouchType_e::FT62x6;
      case CT_TYPE_GT911:

        if (GT911_ADDR1 == touchscreen->getI2CAddress()) {
          return P123_TouchType_e::GT911_1;
        } else {
          return P123_TouchType_e::GT911_2;
        }
      case CT_TYPE_CST820: return P123_TouchType_e::CST820;
      case CT_TYPE_CST226: return P123_TouchType_e::CST226;
      case CT_TYPE_AXS15231: return P123_TouchType_e::AXS15231;
      case CT_TYPE_CHSC5816: return P123_TouchType_e::CHSC5816;
    }
  }
  return P123_TouchType_e::Automatic;
}

int P123_data_struct::getBBCapTouchType(P123_TouchType_e touchType) {
  switch (touchType) {
    case P123_TouchType_e::Automatic: return CT_TYPE_UNKNOWN;
    case P123_TouchType_e::FT62x6: return CT_TYPE_FT6X36;
    case P123_TouchType_e::GT911_1: // Fall through
    case P123_TouchType_e::GT911_2: return CT_TYPE_GT911;
    case P123_TouchType_e::CST820: return CT_TYPE_CST820;
    case P123_TouchType_e::CST226: return CT_TYPE_CST226;
    case P123_TouchType_e::AXS15231: return CT_TYPE_AXS15231;
    case P123_TouchType_e::CHSC5816: return CT_TYPE_CHSC5816;
  }
  return CT_TYPE_UNKNOWN;
}

/**
 * Constructor
 */
P123_data_struct::P123_data_struct(P123_TouchType_e touchType)
  : _touchType(touchType) {
  touchHandler = new (std::nothrow) ESPEasy_TouchHandler(); // Temporary object to be able to call loadTouchObjects
  _i2caddr     = plugin_i2c_address(_touchType);
}

/**
 * Destructor
 */
P123_data_struct::~P123_data_struct() {
  reset();
}

/**
 * Proper reset and cleanup.
 */
void P123_data_struct::reset() {
  # ifdef PLUGIN_123_DEBUG
  addLog(LOG_LEVEL_INFO, F("P123 DEBUG Touchscreen reset."));
  # endif // PLUGIN_123_DEBUG

  delete touchscreen;
  touchscreen = nullptr;
  delete touchHandler;
  touchHandler = nullptr;
}

/**
 * Initialize data and set up the touchscreen.
 */
bool P123_data_struct::init(struct EventStruct *event) {
  _rotation = P123_CONFIG_ROTATION;
  _ts_x_res = P123_CONFIG_X_RES;
  _ts_y_res = P123_CONFIG_Y_RES;

  reset();

  touchHandler = new (std::nothrow) ESPEasy_TouchHandler(static_cast<taskIndex_t>(P123_CONFIG_DISPLAY_TASK),
                                                         static_cast<AdaGFXColorDepth>(P123_COLOR_DEPTH));

  if (nullptr != touchHandler) {
    touchHandler->init(event);

    if (touchHandler->touchEnabled()) {
      touchscreen = new (std::nothrow) BBCapTouch();

      if (nullptr != touchscreen) {
        touchscreen->setThreshold(P123_CONFIG_THRESHOLD);

        P123_TouchType_e touchType = static_cast<P123_TouchType_e>(P123_GET_TOUCH_TYPE);

        if (P123_TouchType_e::Automatic != touchType) { // Manual override
          touchscreen->sensorType(getBBCapTouchType(touchType));
          touchscreen->setI2CAddress(plugin_i2c_address(touchType));
        }

        if (touchscreen->init(-1, -1, P123_RESETPIN, P123_INTERRUPTPIN) != CT_SUCCESS) {
          delete touchscreen;
          touchscreen = nullptr;
        } else {
          setRotation(_rotation);
        }
      }
    }

  # ifdef PLUGIN_123_DEBUG
    addLogMove(LOG_LEVEL_INFO,
               concat(concat(F("P123 DEBUG Plugin"), nullptr != touchscreen ? F(" & touchscreen") : F("")), F(" initialized.")));
  } else {
    addLogMove(LOG_LEVEL_INFO, F("P123 DEBUG Touchscreen initialization FAILED."));
  # endif // PLUGIN_123_DEBUG
  }
  return isInitialized();
}

/**
 * mode: -2 = clear buttons in group, -3 = clear all buttongroups, -1 = draw buttons in group, 0 = initialize buttons
 */
void P123_data_struct::displayButtonGroup(struct EventStruct *event,
                                          int16_t             buttonGroup,
                                          int8_t              mode) {
  # if TOUCH_FEATURE_EXTENDED_TOUCH

  if (nullptr != touchHandler) {
    touchHandler->displayButtonGroup(event, buttonGroup, mode);
  }
  # endif // if TOUCH_FEATURE_EXTENDED_TOUCH
}

/**
 * (Re)Display a button
 */
bool P123_data_struct::displayButton(struct EventStruct *event,
                                     const int8_t      & buttonNr,
                                     int16_t             buttonGroup,
                                     int8_t              mode) {
  # if TOUCH_FEATURE_EXTENDED_TOUCH

  if (nullptr != touchHandler) {
    return touchHandler->displayButton(event, buttonNr, buttonGroup, mode);
  }
  # endif // if TOUCH_FEATURE_EXTENDED_TOUCH
  return false;
}

/**
 * Properly initialized? then true
 */
bool P123_data_struct::isInitialized() const {
  return touchHandler != nullptr && (!touchHandler->touchEnabled() || touchscreen != nullptr);
}

/**
 * Load the settings onto the webpage
 */
bool P123_data_struct::plugin_webform_load(struct EventStruct *event) {
  if (nullptr != touchHandler) {
    return touchHandler->plugin_webform_load(event);
  }
  return false;
}

/**
 * Save the settings from the web page to flash
 */
bool P123_data_struct::plugin_webform_save(struct EventStruct *event) {
  if (nullptr != touchHandler) {
    const bool result = touchHandler->plugin_webform_save(event);
    P123_CONFIG_VTYPE = touchHandler->get_device_valuecount(event); // Store 'locally'
    return result;
  }
  return false;
}

/**
 * Parse and execute the plugin commands, delegated to ESPEasy_TouchHandler
 */
bool P123_data_struct::plugin_write(struct EventStruct *event,
                                    const String      & string) {
  bool   success = false;
  String command;
  String subcommand;

  command    = parseString(string, 1);
  subcommand = parseString(string, 2);

  if (isInitialized() && equals(command, F("touch"))) {
    # ifdef PLUGIN_123_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLog(LOG_LEVEL_INFO, strformat(F("P123 WRITE arguments Par1: %d, 2: %d, 3: %d, 4: %d"),
                                       event->Par1, event->Par2, event->Par3, event->Par4));
    }
    # endif // ifdef PLUGIN_123_DEBUG

    if (equals(subcommand, F("rot"))) {         // touch,rot,<0..3> : Set rotation to 0, 90, 180, 270 degrees
      setRotation(static_cast<uint8_t>(event->Par2 % 4));
      success = true;
    } else if (equals(subcommand, F("flip"))) { // touch,flip,<0|1> : Flip rotation by 0 or 180 degrees
      setRotationFlipped(event->Par2 > 0);
      success = true;
    } else {                                    // Rest of the commands handled by ESPEasy_TouchHandler
      success = touchHandler->plugin_write(event, string);
    }
  }
  return success;
}

/**
 * Every 1/50th second we check if the screen is touched
 */
bool P123_data_struct::plugin_fifty_per_second(struct EventStruct *event) {
  if (isInitialized() && touchHandler->touchEnabled()) {
    if (touched()) {
      int16_t x  = 0;
      int16_t y  = 0;
      int16_t z  = 0;
      int16_t ox = 0;
      int16_t oy = 0;
      readData(x, y, z, ox, oy);

      int16_t rx = x;             // Keep raw values
      int16_t ry = y;
      scaleRawToCalibrated(x, y); // Map to screen coordinates if so configured

      return touchHandler->plugin_fifty_per_second(event, x, y, ox, oy, rx, ry, z);
    } else {
      touchHandler->releaseTouch(event);
    }
  }
  return false;
}

/**
 * Handle getting config values, delegated to ESPEasy_TouchHandler
 */
bool P123_data_struct::plugin_get_config_value(struct EventStruct *event,
                                               String            & string) {
  if (nullptr != touchHandler) {
    return touchHandler->plugin_get_config_value(event, string);
  }
  return false;
}

/**
 * Load the touch objects from the settings, and initialize then properly where needed.
 */
void P123_data_struct::loadTouchObjects(struct EventStruct *event) {
  if (nullptr != touchHandler) {
    touchHandler->loadTouchObjects(event);
  }
}

/**
 * Check if the screen is touched.
 */
bool P123_data_struct::touched() {
  if (isInitialized()) {
    return touchscreen->getSamples(&touchInfo) > 0; // 1 or more points touched
  }
  return false;
}

/**
 * Read the raw data if the touchscreen is initialized.
 */
void P123_data_struct::readData(int16_t& x,
                                int16_t& y,
                                int16_t& z,
                                int16_t& ox,
                                int16_t& oy) {
  if (isInitialized()) {
    x  = touchInfo.x[0]; // Only 1 point used for now.
    y  = touchInfo.y[0];
    z  = touchInfo.pressure[0];
    ox = touchInfo.x[0]; // Change of touch driver has made these arguments obsolete, but in use for touchHandler...
    oy = touchInfo.y[0];
  }
}

/**
 * Set rotation
 */
void P123_data_struct::setRotation(uint8_t n) {
  _rotation = n;

  if (isInitialized()) {
    const bool fl = touchHandler->_flipped;

    switch (_rotation) { // Rotation is handled by touch driver, flipped handled here
      case 0: touchscreen->setOrientation(fl ? 180 : 0, _ts_x_res, _ts_y_res); break;
      case 1: touchscreen->setOrientation(fl ? 270 : 90, _ts_x_res, _ts_y_res); break;
      case 2: touchscreen->setOrientation(fl ? 0 : 180, _ts_x_res, _ts_y_res); break;
      case 3: touchscreen->setOrientation(fl ? 90 : 270, _ts_x_res, _ts_y_res); break;
    }
  }
  # ifdef PLUGIN_123_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO, concat(F("P123 DEBUG Rotation set: "), _rotation));
  }
  # endif // PLUGIN_123_DEBUG
}

/**
 * Set rotationFlipped
 */
void P123_data_struct::setRotationFlipped(bool flipped) {
  touchHandler->_flipped = flipped;
  # ifdef PLUGIN_123_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO, concat(F("P123 DEBUG RotationFlipped set: "), boolToString(flipped)));
  }
  # endif // PLUGIN_123_DEBUG
}

/**
 * Check within the list of defined objects if we touched one of them.
 * The smallest matching surface is selected if multiple objects overlap.
 * Returns state, and sets selectedObjectName to the best matching object
 */
bool P123_data_struct::isValidAndTouchedTouchObject(const int16_t& x,
                                                    const int16_t& y,
                                                    String       & selectedObjectName,
                                                    int8_t       & selectedObjectIndex) {
  if (nullptr != touchHandler) {
    return touchHandler->isValidAndTouchedTouchObject(x, y, selectedObjectName, selectedObjectIndex);
  }
  return false;
}

/**
 * Get the index of a touch object by name or number
 */
int8_t P123_data_struct::getTouchObjectIndex(struct EventStruct *event,
                                             const String      & touchObject,
                                             bool                isButton) {
  if (nullptr != touchHandler) {
    return touchHandler->getTouchObjectIndex(event, touchObject, isButton);
  }
  return -1;
}

/**
 * Set the enabled/disabled state of an object.
 */
bool P123_data_struct::setTouchObjectState(struct EventStruct *event,
                                           const String      & touchObject,
                                           bool                state) {
  if (nullptr != touchHandler) {
    return touchHandler->setTouchObjectState(event, touchObject, state);
  }
  return false;
}

/**
 * Set the on/off state of a touch-button object.
 */
bool P123_data_struct::setTouchButtonOnOff(struct EventStruct *event,
                                           const String      & touchObject,
                                           bool                state) {
  if (nullptr != touchHandler) {
    return touchHandler->setTouchButtonOnOff(event, touchObject, state);
  }
  return false;
}

/**
 * Scale the provided raw coordinates to screen-resolution coordinates if calibration is enabled/configured
 */
void P123_data_struct::scaleRawToCalibrated(int16_t& x,
                                            int16_t& y) {
  if ((nullptr != touchHandler) && touchHandler->isCalibrationActive()) {
    int16_t lx = x - touchHandler->Touch_Settings.top_left.x;

    if (lx <= 0) {
      x = 0;
    } else {
      if (lx > touchHandler->Touch_Settings.bottom_right.x) {
        lx = touchHandler->Touch_Settings.bottom_right.x;
      }
      float x_fact = static_cast<float>(touchHandler->Touch_Settings.bottom_right.x - touchHandler->Touch_Settings.top_left.x) /
                     static_cast<float>(_ts_x_res);
      x = static_cast<int16_t>(round(lx / x_fact));
    }
    int16_t ly = y - touchHandler->Touch_Settings.top_left.y;

    if (ly <= 0) {
      y = 0;
    } else {
      if (ly > touchHandler->Touch_Settings.bottom_right.y) {
        ly = touchHandler->Touch_Settings.bottom_right.y;
      }
      float y_fact = (touchHandler->Touch_Settings.bottom_right.y - touchHandler->Touch_Settings.top_left.y) / _ts_y_res;
      y = static_cast<int16_t>(round(ly / y_fact));
    }
  }
}

/**
 * Get the current button group
 */
int16_t P123_data_struct::getButtonGroup() const {
  if (nullptr != touchHandler) {
    return touchHandler->getButtonGroup();
  }
  return 0;
}

/**
 * Check if a valid button group, optionally ignoring group 0
 */
bool P123_data_struct::validButtonGroup(int16_t buttonGroup,
                                        bool    ignoreZero) {
  # if TOUCH_FEATURE_EXTENDED_TOUCH

  if (nullptr != touchHandler) {
    return touchHandler->validButtonGroup(buttonGroup, ignoreZero);
  }
  # endif // if TOUCH_FEATURE_EXTENDED_TOUCH
  return false;
}

/**
 * Set the desired button group, must be between the minimum and maximum found values
 */
bool P123_data_struct::setButtonGroup(struct EventStruct *event,
                                      int16_t             buttonGroup) {
  # if TOUCH_FEATURE_EXTENDED_TOUCH

  if (nullptr != touchHandler) {
    return touchHandler->setButtonGroup(event, buttonGroup);
  }
  # endif // if TOUCH_FEATURE_EXTENDED_TOUCH
  return false;
}

/**
 * Increment button group, if max. group > 0 then min. group = 1
 */
bool P123_data_struct::nextButtonGroup(struct EventStruct *event) {
  # if TOUCH_FEATURE_EXTENDED_TOUCH

  if (nullptr != touchHandler) {
    return touchHandler->nextButtonGroup(event);
  }
  # endif // if TOUCH_FEATURE_EXTENDED_TOUCH
  return false;
}

/**
 * Decrement button group, if max. group > 0 then min. group = 1
 */
bool P123_data_struct::prevButtonGroup(struct EventStruct *event) {
  # if TOUCH_FEATURE_EXTENDED_TOUCH

  if (nullptr != touchHandler) {
    return touchHandler->prevButtonGroup(event);
  }
  # endif // if TOUCH_FEATURE_EXTENDED_TOUCH
  return false;
}

/**
 * Increment button group page (+10), if max. group > 0 then min. group page (+10) = 1
 */
bool P123_data_struct::nextButtonPage(struct EventStruct *event) {
  # if TOUCH_FEATURE_EXTENDED_TOUCH

  if (nullptr != touchHandler) {
    return touchHandler->nextButtonPage(event);
  }
  # endif // if TOUCH_FEATURE_EXTENDED_TOUCH
  return false;
}

/**
 * Decrement button group page (-10), if max. group > 0 then min. group = 1
 */
bool P123_data_struct::prevButtonPage(struct EventStruct *event) {
  # if TOUCH_FEATURE_EXTENDED_TOUCH

  if (nullptr != touchHandler) {
    return touchHandler->prevButtonPage(event);
  }
  # endif // if TOUCH_FEATURE_EXTENDED_TOUCH
  return false;
}

#endif // ifdef USES_P123

#include "../PluginStructs/P078_data_struct.h"

#include "../../_Plugin_Helper.h"

#ifdef USES_P078

# include "../Helpers/ESPEasy_UnitOfMeasure.h"

# include <limits>

# include <SDM.h> // Requires SDM library from Reaper7 - https://github.com/reaper7/SDM_Energy_Meter/

// Uncrustify may mess up this nice table, so turn uncrustify off for this table.
// *INDENT-OFF*
constexpr p078_register_description register_description_list[] = {
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
//      REGISTERS LIST FOR SDM DEVICES                                                                                                                                                                               |
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
//      REGISTER NAME                          REGISTER ADDRESS              UNIT                 | phase | dir | SDM630  | SDM320C | SDM230  | SDM220  | SDM120CT| SDM120  | SDM72D  | SDM72 V2 | DDM18SD | TAC2100 |
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
{ SDM_PHASE_1_VOLTAGE                           /* 0x0000 */              ,  SDM_UOM::V           ,   1   ,  0  ,    1    ,    1    ,    1    ,    1    ,    1    ,    1    ,    0    ,    1     ,    0    ,    1    },         
{ SDM_PHASE_2_VOLTAGE                           /* 0x0002 */              ,  SDM_UOM::V           ,   2   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1     ,    0    ,    0    },
{ SDM_PHASE_3_VOLTAGE                           /* 0x0004 */              ,  SDM_UOM::V           ,   3   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1     ,    0    ,    0    },
{ SDM_PHASE_1_CURRENT                           /* 0x0006 */              ,  SDM_UOM::A           ,   1   ,  0  ,    1    ,    1    ,    1    ,    1    ,    1    ,    1    ,    0    ,    1     ,    0    ,    1    },
{ SDM_PHASE_2_CURRENT                           /* 0x0008 */              ,  SDM_UOM::A           ,   2   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1     ,    0    ,    0    },
{ SDM_PHASE_3_CURRENT                           /* 0x000A */              ,  SDM_UOM::A           ,   3   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1     ,    0    ,    0    },
{ SDM_PHASE_1_POWER                             /* 0x000C */              ,  SDM_UOM::W           ,   1   ,  0  ,    1    ,    1    ,    1    ,    1    ,    1    ,    1    ,    0    ,    1     ,    0    ,    1    },
{ SDM_PHASE_2_POWER                             /* 0x000E */              ,  SDM_UOM::W           ,   2   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1     ,    0    ,    0    },
{ SDM_PHASE_3_POWER                             /* 0x0010 */              ,  SDM_UOM::W           ,   3   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1     ,    0    ,    0    },
{ SDM_PHASE_1_APPARENT_POWER                    /* 0x0012 */              ,  SDM_UOM::VA          ,   1   ,  0  ,    1    ,    1    ,    1    ,    1    ,    1    ,    1    ,    0    ,    1     ,    0    ,    1    },
{ SDM_PHASE_2_APPARENT_POWER                    /* 0x0014 */              ,  SDM_UOM::VA          ,   2   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1     ,    0    ,    0    },
{ SDM_PHASE_3_APPARENT_POWER                    /* 0x0016 */              ,  SDM_UOM::VA          ,   3   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1     ,    0    ,    0    },
{ SDM_PHASE_1_REACTIVE_POWER                    /* 0x0018 */              ,  SDM_UOM::VAr         ,   1   ,  0  ,    1    ,    1    ,    1    ,    1    ,    1    ,    1    ,    0    ,    1     ,    0    ,    1    },
{ SDM_PHASE_2_REACTIVE_POWER                    /* 0x001A */              ,  SDM_UOM::VAr         ,   2   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1     ,    0    ,    0    },
{ SDM_PHASE_3_REACTIVE_POWER                    /* 0x001C */              ,  SDM_UOM::VAr         ,   3   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1     ,    0    ,    0    },
{ SDM_PHASE_1_POWER_FACTOR                      /* 0x001E */              ,  SDM_UOM::cos_phi     ,   1   ,  0  ,    1    ,    1    ,    1    ,    1    ,    1    ,    1    ,    0    ,    1     ,    0    ,    1    },
{ SDM_PHASE_2_POWER_FACTOR                      /* 0x0020 */              ,  SDM_UOM::cos_phi     ,   2   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1     ,    0    ,    0    },
{ SDM_PHASE_3_POWER_FACTOR                      /* 0x0022 */              ,  SDM_UOM::cos_phi     ,   3   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1     ,    0    ,    0    },
{ SDM_PHASE_1_ANGLE                             /* 0x0024 */              ,  SDM_UOM::degrees     ,   1   ,  0  ,    1    ,    1    ,    1    ,    1    ,    1    ,    0    ,    0    ,    0     ,    0    ,    1    },
{ SDM_PHASE_2_ANGLE                             /* 0x0026 */              ,  SDM_UOM::degrees     ,   2   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_PHASE_3_ANGLE                             /* 0x0028 */              ,  SDM_UOM::degrees     ,   3   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_AVERAGE_L_TO_N_VOLTS                      /* 0x002A */              ,  SDM_UOM::V           ,   0   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1     ,    0    ,    0    },
{ SDM_AVERAGE_LINE_CURRENT                      /* 0x002E */              ,  SDM_UOM::A           ,   0   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1     ,    0    ,    0    },
{ SDM_SUM_LINE_CURRENT                          /* 0x0030 */              ,  SDM_UOM::A           ,   0   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1     ,    0    ,    0    },
{ SDM_TOTAL_SYSTEM_POWER                        /* 0x0034 */              ,  SDM_UOM::W           ,   0   ,  3  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1    ,    1     ,    0    ,    0    },
{ SDM_TOTAL_SYSTEM_APPARENT_POWER               /* 0x0038 */              ,  SDM_UOM::VA          ,   0   ,  3  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1     ,    0    ,    0    },
{ SDM_TOTAL_SYSTEM_REACTIVE_POWER               /* 0x003C */              ,  SDM_UOM::VAr         ,   0   ,  3  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1     ,    0    ,    0    },
{ SDM_TOTAL_SYSTEM_POWER_FACTOR                 /* 0x003E */              ,  SDM_UOM::cos_phi     ,   0   ,  3  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1     ,    0    ,    0    },
{ SDM_TOTAL_SYSTEM_PHASE_ANGLE                  /* 0x0042 */              ,  SDM_UOM::degrees     ,   0   ,  3  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_FREQUENCY                                 /* 0x0046 */              ,  SDM_UOM::Hz          ,   0   ,  0  ,    1    ,    1    ,    1    ,    1    ,    1    ,    1    ,    0    ,    1     ,    0    ,    1    },
{ SDM_IMPORT_ACTIVE_ENERGY                      /* 0x0048 */              ,  SDM_UOM::kWh         ,   0   ,  1  ,    1    ,    1    ,    1    ,    1    ,    1    ,    1    ,    1    ,    1     ,    0    ,    1    },
{ SDM_EXPORT_ACTIVE_ENERGY                      /* 0x004A */              ,  SDM_UOM::kWh         ,   0   ,  2  ,    1    ,    1    ,    1    ,    1    ,    1    ,    1    ,    1    ,    1     ,    0    ,    1    },
{ SDM_IMPORT_REACTIVE_ENERGY                    /* 0x004C */              ,  SDM_UOM::kVArh       ,   0   ,  1  ,    1    ,    1    ,    1    ,    1    ,    1    ,    1    ,    0    ,    0     ,    0    ,    1    },
{ SDM_EXPORT_REACTIVE_ENERGY                    /* 0x004E */              ,  SDM_UOM::kVArh       ,   0   ,  2  ,    1    ,    1    ,    1    ,    1    ,    1    ,    1    ,    0    ,    0     ,    0    ,    1    },
{ SDM_VAH_SINCE_LAST_RESET                      /* 0x0050 */              ,  SDM_UOM::kVAh        ,   0   ,  0  ,    1    ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_AH_SINCE_LAST_RESET                       /* 0x0052 */              ,  SDM_UOM::Ah          ,   0   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_TOTAL_SYSTEM_POWER_DEMAND                 /* 0x0054 */              ,  SDM_UOM::W           ,   0   ,  3  ,    1    ,    1    ,    1    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    1    },
{ SDM_MAXIMUM_TOTAL_SYSTEM_POWER_DEMAND         /* 0x0056 */              ,  SDM_UOM::W           ,   0   ,  0  ,    1    ,    1    ,    1    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    1    },
{ SDM_CURRENT_SYSTEM_POSITIVE_POWER_DEMAND      /* 0x0058 */              ,  SDM_UOM::W           ,   0   ,  1  ,    0    ,    0    ,    1    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    1    },
{ SDM_MAXIMUM_SYSTEM_POSITIVE_POWER_DEMAND      /* 0x005A */              ,  SDM_UOM::W           ,   0   ,  1  ,    0    ,    0    ,    1    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    1    },
{ SDM_CURRENT_SYSTEM_REVERSE_POWER_DEMAND       /* 0x005C */              ,  SDM_UOM::W           ,   0   ,  2  ,    0    ,    0    ,    1    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    1    },
{ SDM_MAXIMUM_SYSTEM_REVERSE_POWER_DEMAND       /* 0x005E */              ,  SDM_UOM::W           ,   0   ,  2  ,    0    ,    0    ,    1    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    1    },
{ SDM_TOTAL_SYSTEM_VA_DEMAND                    /* 0x0064 */              ,  SDM_UOM::VA          ,   0   ,  3  ,    1    ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_MAXIMUM_TOTAL_SYSTEM_VA_DEMAND            /* 0x0066 */              ,  SDM_UOM::VA          ,   0   ,  0  ,    1    ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_NEUTRAL_CURRENT_DEMAND                    /* 0x0068 */              ,  SDM_UOM::A           ,   0   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_MAXIMUM_NEUTRAL_CURRENT                   /* 0x006A */              ,  SDM_UOM::A           ,   0   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_REACTIVE_POWER_DEMAND                     /* 0x006C */              ,  SDM_UOM::VAr         ,   0   ,  0  ,    0    ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_MAXIMUM_REACTIVE_POWER_DEMAND             /* 0x006E */              ,  SDM_UOM::VAr         ,   0   ,  0  ,    0    ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_LINE_1_TO_LINE_2_VOLTS                    /* 0x00C8 */              ,  SDM_UOM::V           ,   0   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1     ,    0    ,    0    },
{ SDM_LINE_2_TO_LINE_3_VOLTS                    /* 0x00CA */              ,  SDM_UOM::V           ,   0   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1     ,    0    ,    0    },
{ SDM_LINE_3_TO_LINE_1_VOLTS                    /* 0x00CC */              ,  SDM_UOM::V           ,   0   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1     ,    0    ,    0    },
{ SDM_AVERAGE_LINE_TO_LINE_VOLTS                /* 0x00CE */              ,  SDM_UOM::V           ,   0   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1     ,    0    ,    0    },
{ SDM_NEUTRAL_CURRENT                           /* 0x00E0 */              ,  SDM_UOM::A           ,   0   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1     ,    0    ,    0    },
{ SDM_PHASE_1_LN_VOLTS_THD                      /* 0x00EA */              ,  SDM_UOM::percent     ,   1   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_PHASE_2_LN_VOLTS_THD                      /* 0x00EC */              ,  SDM_UOM::percent     ,   2   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_PHASE_3_LN_VOLTS_THD                      /* 0x00EE */              ,  SDM_UOM::percent     ,   3   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_PHASE_1_CURRENT_THD                       /* 0x00F0 */              ,  SDM_UOM::percent     ,   1   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_PHASE_2_CURRENT_THD                       /* 0x00F2 */              ,  SDM_UOM::percent     ,   2   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_PHASE_3_CURRENT_THD                       /* 0x00F4 */              ,  SDM_UOM::percent     ,   3   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_AVERAGE_LINE_TO_NEUTRAL_VOLTS_THD         /* 0x00F8 */              ,  SDM_UOM::percent     ,   0   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_AVERAGE_LINE_CURRENT_THD                  /* 0x00FA */              ,  SDM_UOM::percent     ,   0   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_TOTAL_SYSTEM_POWER_FACTOR_INV             /* 0x00FE */              ,  SDM_UOM::cos_phi     ,   0   ,  3  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_PHASE_1_CURRENT_DEMAND                    /* 0x0102 */              ,  SDM_UOM::A           ,   1   ,  0  ,    1    ,    1    ,    1    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    1    },
{ SDM_PHASE_2_CURRENT_DEMAND                    /* 0x0104 */              ,  SDM_UOM::A           ,   2   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_PHASE_3_CURRENT_DEMAND                    /* 0x0106 */              ,  SDM_UOM::A           ,   3   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_MAXIMUM_PHASE_1_CURRENT_DEMAND            /* 0x0108 */              ,  SDM_UOM::A           ,   1   ,  0  ,    1    ,    1    ,    1    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    1    },
{ SDM_MAXIMUM_PHASE_2_CURRENT_DEMAND            /* 0x010A */              ,  SDM_UOM::A           ,   2   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_MAXIMUM_PHASE_3_CURRENT_DEMAND            /* 0x010C */              ,  SDM_UOM::A           ,   3   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_LINE_1_TO_LINE_2_VOLTS_THD                /* 0x014E */              ,  SDM_UOM::percent     ,   0   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_LINE_2_TO_LINE_3_VOLTS_THD                /* 0x0150 */              ,  SDM_UOM::percent     ,   0   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_LINE_3_TO_LINE_1_VOLTS_THD                /* 0x0152 */              ,  SDM_UOM::percent     ,   0   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_AVERAGE_LINE_TO_LINE_VOLTS_THD            /* 0x0154 */              ,  SDM_UOM::percent     ,   0   ,  0  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_TOTAL_ACTIVE_ENERGY                       /* 0x0156 */              ,  SDM_UOM::kWh         ,   0   ,  3  ,    1    ,    1    ,    1    ,    1    ,    1    ,    1    ,    1    ,    1     ,    0    ,    1    },
{ SDM_TOTAL_REACTIVE_ENERGY                     /* 0x0158 */              ,  SDM_UOM::kVArh       ,   0   ,  3  ,    1    ,    1    ,    1    ,    1    ,    1    ,    1    ,    0    ,    1     ,    0    ,    1    },
{ SDM_L1_IMPORT_ACTIVE_ENERGY                   /* 0x015A */              ,  SDM_UOM::kWh         ,   1   ,  1  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_L2_IMPORT_ACTIVE_ENERGY                   /* 0x015C */              ,  SDM_UOM::kWh         ,   2   ,  1  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_L3_IMPORT_ACTIVE_ENERGY                   /* 0x015E */              ,  SDM_UOM::kWh         ,   3   ,  1  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_L1_EXPORT_ACTIVE_ENERGY                   /* 0x0160 */              ,  SDM_UOM::kWh         ,   1   ,  2  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_L2_EXPORT_ACTIVE_ENERGY                   /* 0x0162 */              ,  SDM_UOM::kWh         ,   2   ,  2  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_L3_EXPORT_ACTIVE_ENERGY                   /* 0x0164 */              ,  SDM_UOM::kWh         ,   3   ,  2  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_L1_TOTAL_ACTIVE_ENERGY                    /* 0x0166 */              ,  SDM_UOM::kWh         ,   1   ,  3  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_L2_TOTAL_ACTIVE_ENERGY                    /* 0x0168 */              ,  SDM_UOM::kWh         ,   2   ,  3  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_L3_TOTAL_ACTIVE_ENERGY                    /* 0x016a */              ,  SDM_UOM::kWh         ,   3   ,  3  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_L1_IMPORT_REACTIVE_ENERGY                 /* 0x016C */              ,  SDM_UOM::kVArh       ,   1   ,  1  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_L2_IMPORT_REACTIVE_ENERGY                 /* 0x016E */              ,  SDM_UOM::kVArh       ,   2   ,  1  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_L3_IMPORT_REACTIVE_ENERGY                 /* 0x0170 */              ,  SDM_UOM::kVArh       ,   3   ,  1  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_L1_EXPORT_REACTIVE_ENERGY                 /* 0x0172 */              ,  SDM_UOM::kVArh       ,   1   ,  2  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_L2_EXPORT_REACTIVE_ENERGY                 /* 0x0174 */              ,  SDM_UOM::kVArh       ,   2   ,  2  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_L3_EXPORT_REACTIVE_ENERGY                 /* 0x0176 */              ,  SDM_UOM::kVArh       ,   3   ,  2  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_L1_TOTAL_REACTIVE_ENERGY                  /* 0x0178 */              ,  SDM_UOM::kVArh       ,   1   ,  3  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_L2_TOTAL_REACTIVE_ENERGY                  /* 0x017A */              ,  SDM_UOM::kVArh       ,   2   ,  3  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_L3_TOTAL_REACTIVE_ENERGY                  /* 0x017C */              ,  SDM_UOM::kVArh       ,   3   ,  3  ,    1    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_CURRENT_RESETTABLE_TOTAL_ACTIVE_ENERGY    /* 0x0180 */              ,  SDM_UOM::kWh         ,   0   ,  3  ,    0    ,    0    ,    1    ,    0    ,    0    ,    0    ,    1    ,    1     ,    0    ,    0    },
{ SDM_CURRENT_RESETTABLE_TOTAL_REACTIVE_ENERGY  /* 0x0182 */              ,  SDM_UOM::kVArh       ,   0   ,  3  ,    0    ,    0    ,    1    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    0    },
{ SDM_CURRENT_RESETTABLE_IMPORT_ENERGY          /* 0x0184 */              ,  SDM_UOM::kWh         ,   0   ,  1  ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1    ,    1     ,    0    ,    0    },
{ SDM_CURRENT_RESETTABLE_EXPORT_ENERGY          /* 0x0186 */              ,  SDM_UOM::kWh         ,   0   ,  2  ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1    ,    1     ,    0    ,    0    },
{ SDM_CURRENT_RESETTABLE_IMPORT_REACTIVE_ENERGY /* 0x0188 */              ,  SDM_UOM::kVArh       ,   0   ,  1  ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1    ,    1     ,    0    ,    0    },
{ SDM_CURRENT_RESETTABLE_EXPORT_REACTIVE_ENERGY /* 0x018A */              ,  SDM_UOM::kVArh       ,   0   ,  2  ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1    ,    1     ,    0    ,    0    },
{ SDM_NET_KWH                                   /* 0x018C */              ,  SDM_UOM::kWh         ,   0   ,  0  ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1     ,    0    ,    0    },
{ SDM_NET_KVARH                                 /* 0x018E */              ,  SDM_UOM::kVArh       ,   0   ,  0  ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1     ,    0    ,    0    },
{ SDM_IMPORT_POWER                              /* 0x0500 */              ,  SDM_UOM::W           ,   0   ,  1  ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1    ,    1     ,    0    ,    0    },
{ SDM_EXPORT_POWER                              /* 0x0502 */              ,  SDM_UOM::W           ,   0   ,  2  ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    1    ,    1     ,    0    ,    0    },

{ DDM_PHASE_1_VOLTAGE                           /* 0x0000 */              ,  SDM_UOM::V           ,   1   ,  0  ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    1    ,    0    },
{ DDM_PHASE_1_CURRENT                           /* 0x0008 */              ,  SDM_UOM::A           ,   1   ,  0  ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    1    ,    0    },
{ DDM_PHASE_1_POWER                             /* 0x0012 */              ,  SDM_UOM::W           ,   1   ,  0  ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    1    ,    0    },
{ DDM_PHASE_1_REACTIVE_POWER                    /* 0x001A */              ,  SDM_UOM::VAr         ,   1   ,  0  ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    1    ,    0    },
{ DDM_PHASE_1_POWER_FACTOR                      /* 0x002A */              ,  SDM_UOM::cos_phi     ,   1   ,  0  ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    1    ,    0    },
{ DDM_FREQUENCY                                 /* 0x0036 */              ,  SDM_UOM::Hz          ,   0   ,  0  ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    1    ,    0    },
{ DDM_IMPORT_ACTIVE_ENERGY                      /* 0x0100 */              ,  SDM_UOM::kWh         ,   0   ,  1  ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    1    ,    0    },
{ DDM_IMPORT_REACTIVE_ENERGY                    /* 0x0400 */              ,  SDM_UOM::kVArh       ,   0   ,  1  ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    1    ,    0    },

{ TAC2100_REACTIVE_POWER_DEMAND                 /* 0x008E */              ,  SDM_UOM::VAr         ,   0   ,  0  ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    1    },
{ TAC2100_APPARENT_POWER_DEMAND                 /* 0x0090 */              ,  SDM_UOM::VA          ,   0   ,  0  ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    1    },
{ TAC2100_MAX_REACTIVE_POWER_DEMAND             /* 0x00A4 */              ,  SDM_UOM::VAr         ,   0   ,  0  ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    1    },
{ TAC2100_MAX_APPARENT_POWER_DEMAND             /* 0x00A6 */              ,  SDM_UOM::VA          ,   0   ,  0  ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    1    },
{ TAC2100_NATURE_OF_LOAD                        /* 0x004E */              ,  SDM_UOM::nat_load    ,   0   ,  0  ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0    ,    0     ,    0    ,    1    },
};
// *INDENT-ON*

constexpr int register_description_list_size = NR_ELEMENTS(register_description_list);

const __FlashStringHelper* SDM_UOMtoString(SDM_UOM uom, bool display) {
  const __FlashStringHelper *strings[] = {
    F("THD"),             F("%"),
    F("Voltage"),         F("V"),
    F("Current"),         F("A"),
    F("Active Power"),    F("W"),
    F("Active Energy"),   F("kWh"),
    F("Ah"),              F("Ah"),
    F("Frequency"),       F("Hz"),
    F("Phase Angle"),     F("Degrees"),
    F("Power Factor"),    F("cosphi"),
    F("Apparent Power"),  F("VA"),
    F("Reactive Power"),  F("VAr"),
    F("Apparent Energy"), F("kVAh"),
    F("Reactive Energy"), F("kVArh"),
    F("Nature of Load"),  F("nat_load"),
  };
  constexpr size_t nrStrings = NR_ELEMENTS(strings);
  size_t index               = 2 * static_cast<size_t>(uom);

  if (!display) { ++index; }

  if (index < nrStrings) { return strings[index]; }
  return F("");
}

const __FlashStringHelper* SDM_directionToString(SDM_DIRECTION dir) {
  if (dir == SDM_DIRECTION::Import) { return F("Import"); }

  if (dir == SDM_DIRECTION::Export) { return F("Export"); }

  if (dir == SDM_DIRECTION::Total) { return F("Total"); }
  return F("");
}

int SDM_getRegisterDescriptionIndexForModel(SDM_MODEL model, int x)
{
  int count = -1;

  for (int index = 0; index < register_description_list_size; ++index)
  {
    if (register_description_list[index].match_SDM_model(model)) {
      ++count;

      if (x == count) {
        return index;
      }
    }
  }
  return -1;
}

uint16_t SDM_getRegisterForModel(SDM_MODEL model, int choice)
{
  const int index = SDM_getRegisterDescriptionIndexForModel(model, choice);

  if (index >= 0) {
    return register_description_list[index].getRegister();
  }
  return std::numeric_limits<uint16_t>::max();
}

String SDM_getValueNameForModel(SDM_MODEL model, int choice)
{
  const int index = SDM_getRegisterDescriptionIndexForModel(model, choice);

  if (index >= 0) {
    const SDM_UOM uom = register_description_list[index].getUnitOfMeasure();

    return concat(
      SDM_UOMtoString(uom, false),
      register_description_list[index].getPhaseDescription(model, '_'));
  }
  return EMPTY_STRING;
}

# if FEATURE_MQTT_DISCOVER
Sensor_VType Plugin_078_QueryVType(SDM_MODEL model, int choice) {
  const int index = SDM_getRegisterDescriptionIndexForModel(model, choice);

  if (index >= 0) {
    const uint8_t value_nr      = static_cast<uint8_t>(register_description_list[index].getUnitOfMeasure());
    const Sensor_VType vtypes[] = {
      Sensor_VType::SENSOR_TYPE_NONE, // FIXME THD
      Sensor_VType::SENSOR_TYPE_VOLTAGE_ONLY,
      Sensor_VType::SENSOR_TYPE_CURRENT_ONLY,
      Sensor_VType::SENSOR_TYPE_POWER_USG_ONLY,
      Sensor_VType::SENSOR_TYPE_ENERGY,    // Active energy
      Sensor_VType::SENSOR_TYPE_NONE,      // FIXME Ah
      Sensor_VType::SENSOR_TYPE_FREQUENCY, // Frequency
      Sensor_VType::SENSOR_TYPE_NONE,      // FIXME Phase angle
      Sensor_VType::SENSOR_TYPE_POWER_FACT_ONLY,
      Sensor_VType::SENSOR_TYPE_APPRNT_POWER_USG_ONLY,
      Sensor_VType::SENSOR_TYPE_REACTIVE_POWER_ONLY,
      Sensor_VType::SENSOR_TYPE_NONE,            // FIXME Apparent energy
      Sensor_VType::SENSOR_TYPE_REACTIVE_ENERGY, // Reactive energy
      Sensor_VType::SENSOR_TYPE_NONE,            // FIXME Nature of load
    };
    constexpr uint8_t  valueCount = NR_ELEMENTS(vtypes);

    if (value_nr < valueCount) {
      return vtypes[value_nr];
    }
  }
  return Sensor_VType::SENSOR_TYPE_NONE;
}

# endif // if FEATURE_MQTT_DISCOVER

# if FEATURE_TASKVALUE_UNIT_OF_MEASURE
uint64_t Plugin_078_QueryUOMGroup(SDM_MODEL model, int choice) {
  const int index = SDM_getRegisterDescriptionIndexForModel(model, choice);

  if (index >= 0) {
    const uint8_t  value_nr = static_cast<uint8_t>(register_description_list[index].getUnitOfMeasure());
    const uint64_t vtypes[] = {
      UOM_GROUP_PERC_HUM_BAT_MOIST, // THD
      UOM_GROUP_VOLTAGE_CURRENT,    // Voltage
      UOM_GROUP_VOLTAGE_CURRENT,    // Current
      UOM_GROUP_POWER,              // Power
      UOM_GROUP_ENERGY_STORAGE,     // Active energy
      UOM_GROUP_REACTIVE_POWER,     // Ah
      UOM_GROUP_FREQUENCY,          // Frequency
      UOM_GROUP_WIND_DIRECTION,     // Phase angle
      UOM_GROUP_PERC_HUM_BAT_MOIST, // Power factor
      UOM_GROUP_APPARENT_POWER,     // Apparant power
      UOM_GROUP_REACTIVE_POWER,     // Reactive power
      UOM_GROUP_APPARENT_POWER,     // Apparent energy
      UOM_GROUP_REACTIVE_POWER,     // Reactive energy
      UOM_GROUP_NONE,               // Nature of load
    };
    constexpr uint8_t valueCount = NR_ELEMENTS(vtypes);

    if (value_nr < valueCount) {
      return vtypes[value_nr];
    }
  }
  return UOM_GROUP_ALL;
}

# endif // if FEATURE_TASKVALUE_UNIT_OF_MEASURE

void SDM_loadOutputSelector(struct EventStruct *event, uint8_t pconfigIndex, uint8_t valuenr)
{
  const SDM_MODEL model = static_cast<SDM_MODEL>(P078_MODEL);
  const String    label = concat(F("Value "), valuenr + 1);
  const String    id    = sensorTypeHelper_webformID(pconfigIndex);

  addRowLabel_tr_id(label, id);
  do_addSelector_Head(id, F("wide"), EMPTY_STRING, false);
  const int selectedIndex = PCONFIG(pconfigIndex);

  uint8_t x{};

  for (int index = 0; index < register_description_list_size; ++index)
  {
    if (register_description_list[index].match_SDM_model(model)) {
      const String option = register_description_list[index].getDescription(model);
      addSelector_Item(option, x, selectedIndex == x, false, EMPTY_STRING);
      ++x;
    }

    if (x % 10 == 0) { delay(0); }
  }
  addSelector_Foot();
}

uint16_t p078_register_description::getRegister() const
{
  return static_cast<uint16_t>((val >> 16) & 0xFFFF);
}

SDM_UOM p078_register_description::getUnitOfMeasure() const
{
  return static_cast<SDM_UOM>(val & 0xF);
}

uint8_t p078_register_description::getPhase() const
{
  return static_cast<uint8_t>((val >> 4) & 0x3);
}

SDM_DIRECTION p078_register_description::getDirection() const
{
  return static_cast<SDM_DIRECTION>((val >> 6) & 0x3);
}

bool p078_register_description::match_SDM_model(SDM_MODEL model) const
{
  switch (model) {
    case SDM_MODEL::SDM630: return bitRead(val, 8);
    case SDM_MODEL::SDM320C: return bitRead(val, 9);
    case SDM_MODEL::SDM230: return bitRead(val, 10);
    case SDM_MODEL::SDM220_SDM120CT_SDM120: return bitRead(val, 11);
    case SDM_MODEL::SDM72D: return bitRead(val, 12);
    case SDM_MODEL::SDM72_V2: return bitRead(val, 13);
    case SDM_MODEL::DDM18SD: return bitRead(val, 14);
    case SDM_MODEL::TAC2100: return bitRead(val, 15);
  }
  return false;
}

String p078_register_description::getDescription(SDM_MODEL model) const
{
  String res;
  const SDM_DIRECTION direction = getDirection();
  const SDM_UOM  uom            = getUnitOfMeasure();
  const uint16_t reg            = getRegister();
  bool showFullUnitOfMeasure    = true;

  // Check first for specific strings not generated using the description bitmap

  switch (reg)
  {
    case SDM_MAXIMUM_TOTAL_SYSTEM_POWER_DEMAND:
    case SDM_MAXIMUM_TOTAL_SYSTEM_VA_DEMAND:
    case SDM_MAXIMUM_NEUTRAL_CURRENT:
    case SDM_MAXIMUM_SYSTEM_POSITIVE_POWER_DEMAND:
    case SDM_MAXIMUM_SYSTEM_REVERSE_POWER_DEMAND:
    case SDM_MAXIMUM_PHASE_1_CURRENT_DEMAND:
    case SDM_MAXIMUM_PHASE_2_CURRENT_DEMAND:
    case SDM_MAXIMUM_PHASE_3_CURRENT_DEMAND:
    case SDM_MAXIMUM_REACTIVE_POWER_DEMAND:
    case TAC2100_MAX_REACTIVE_POWER_DEMAND:
    case TAC2100_MAX_APPARENT_POWER_DEMAND:

      res = F("Maximum ");
      break;

    case SDM_CURRENT_RESETTABLE_TOTAL_ACTIVE_ENERGY:
    case SDM_CURRENT_RESETTABLE_TOTAL_REACTIVE_ENERGY:
    case SDM_CURRENT_RESETTABLE_IMPORT_ENERGY:
    case SDM_CURRENT_RESETTABLE_EXPORT_ENERGY:
    case SDM_CURRENT_RESETTABLE_IMPORT_REACTIVE_ENERGY:
    case SDM_CURRENT_RESETTABLE_EXPORT_REACTIVE_ENERGY:

      res = F("Resettable ");
      break;

    case SDM_AVERAGE_LINE_TO_LINE_VOLTS:
    case SDM_AVERAGE_L_TO_N_VOLTS:
    case SDM_AVERAGE_LINE_CURRENT:
    case SDM_AVERAGE_LINE_TO_NEUTRAL_VOLTS_THD:
    case SDM_AVERAGE_LINE_CURRENT_THD:
    case SDM_AVERAGE_LINE_TO_LINE_VOLTS_THD:
      res = F("Average ");
      break;

    case SDM_SUM_LINE_CURRENT:
      res = F("Sum ");
      break;

    default:
      break;
  }

  switch (reg)
  {
    case SDM_NEUTRAL_CURRENT_DEMAND:
    case SDM_MAXIMUM_NEUTRAL_CURRENT:
    case SDM_NEUTRAL_CURRENT:

      res += F("Neutral ");
      break;

    case SDM_LINE_1_TO_LINE_2_VOLTS:
    case SDM_LINE_1_TO_LINE_2_VOLTS_THD:
      res += F("L1 to L2 ");
      break;
    case SDM_LINE_2_TO_LINE_3_VOLTS:
    case SDM_LINE_2_TO_LINE_3_VOLTS_THD:
      res += F("L2 to L3 ");
      break;
    case SDM_LINE_3_TO_LINE_1_VOLTS:
    case SDM_LINE_3_TO_LINE_1_VOLTS_THD:
      res += F("L3 to L1 ");
      break;
    case SDM_AVERAGE_LINE_TO_LINE_VOLTS:
    case SDM_AVERAGE_LINE_TO_LINE_VOLTS_THD:
      res += F("Line to Line ");
      break;
    case SDM_AVERAGE_L_TO_N_VOLTS:
    case SDM_AVERAGE_LINE_TO_NEUTRAL_VOLTS_THD:
    case SDM_PHASE_1_LN_VOLTS_THD:
    case SDM_PHASE_2_LN_VOLTS_THD:
    case SDM_PHASE_3_LN_VOLTS_THD:
      res += F("L to N ");
      break;
    case SDM_AVERAGE_LINE_CURRENT:
    case SDM_AVERAGE_LINE_CURRENT_THD:
    case SDM_SUM_LINE_CURRENT:
      res += F("Line ");
      break;
  }

  if (direction != SDM_DIRECTION::NotSpecified) {
    res += SDM_directionToString(direction);
    res += ' ';
  }

  if (showFullUnitOfMeasure) {
    res += SDM_UOMtoString(uom, true);
  }

  switch (reg)
  {
    case SDM_TOTAL_SYSTEM_POWER_DEMAND:
    case SDM_MAXIMUM_TOTAL_SYSTEM_POWER_DEMAND:
    case SDM_CURRENT_SYSTEM_POSITIVE_POWER_DEMAND:
    case SDM_MAXIMUM_SYSTEM_POSITIVE_POWER_DEMAND:
    case SDM_CURRENT_SYSTEM_REVERSE_POWER_DEMAND:
    case SDM_MAXIMUM_SYSTEM_REVERSE_POWER_DEMAND:
    case SDM_TOTAL_SYSTEM_VA_DEMAND:
    case SDM_MAXIMUM_TOTAL_SYSTEM_VA_DEMAND:
    case SDM_NEUTRAL_CURRENT_DEMAND:
    case SDM_MAXIMUM_NEUTRAL_CURRENT:
    case SDM_REACTIVE_POWER_DEMAND:
    case SDM_MAXIMUM_REACTIVE_POWER_DEMAND:
    case SDM_PHASE_1_CURRENT_DEMAND:
    case SDM_PHASE_2_CURRENT_DEMAND:
    case SDM_PHASE_3_CURRENT_DEMAND:
    case SDM_MAXIMUM_PHASE_1_CURRENT_DEMAND:
    case SDM_MAXIMUM_PHASE_2_CURRENT_DEMAND:
    case SDM_MAXIMUM_PHASE_3_CURRENT_DEMAND:
    case TAC2100_REACTIVE_POWER_DEMAND:
    case TAC2100_APPARENT_POWER_DEMAND:
    case TAC2100_MAX_REACTIVE_POWER_DEMAND:
    case TAC2100_MAX_APPARENT_POWER_DEMAND:

      res += F(" demand");
      break;
  }

  switch (reg)
  {
    case SDM_VAH_SINCE_LAST_RESET:
    case SDM_AH_SINCE_LAST_RESET:

      res += F(" since last reset");
      break;
  }

  res += concat(F(" ("), SDM_UOMtoString(uom, false));
  res += concat(F(")"), getPhaseDescription(model, ' '));

  res += concat(F(" ["), formatToHex(reg, 4)); // Append register for clarity
  res += ']';
  return res;
}

String p078_register_description::getPhaseDescription(SDM_MODEL model, char separator) const
{
  const int  phase     = getPhase();
  const bool showPhase = phase != 0 &&
                         (model == SDM_MODEL::SDM630 || model == SDM_MODEL::SDM72_V2);
  String res;

  if (showPhase) {
    if (separator != '\0') {
      res += separator;
    }
    res += 'L';
    res += phase;
  }
  return res;
}

SDM_RegisterReadQueue _SDM_RegisterReadQueue;

void SDM_removeRegisterReadQueueElement(taskIndex_t TaskIndex, taskVarIndex_t TaskVarIndex)
{
  if (validTaskIndex(TaskIndex) && validTaskVarIndex(TaskVarIndex)) {
    for (auto it = _SDM_RegisterReadQueue.begin(); it != _SDM_RegisterReadQueue.end();) {
      if ((it->taskIndex == TaskIndex) && (it->taskVarIndex == TaskVarIndex)) {
        it = _SDM_RegisterReadQueue.erase(it);
      } else {
        ++it;
      }
    }
  }
}

void SDM_addRegisterReadQueueElement(taskIndex_t TaskIndex, taskVarIndex_t TaskVarIndex, uint16_t reg, uint8_t dev_id)
{
  SDM_removeRegisterReadQueueElement(TaskIndex, TaskVarIndex);

  if ((reg != std::numeric_limits<uint16_t>::max()) &&
      validTaskIndex(TaskIndex) &&
      validTaskVarIndex(TaskVarIndex)) {
    _SDM_RegisterReadQueue.emplace_back(TaskIndex, TaskVarIndex, reg, dev_id);

    //    _SDM_RegisterReadQueue.sort(compare_SDM_RegisterReadQueueElement);
  }
}

void SDM_loopRegisterReadQueue(SDM *sdm)
{
  if (sdm == nullptr) { return; }
  auto it = _SDM_RegisterReadQueue.begin();

  if (it == _SDM_RegisterReadQueue.end()) { return; }

  if (it->_state == 1) {
    uint16_t readErr = sdm->readValReady(it->_dev_id);

    if (readErr == SDM_ERR_STILL_WAITING) { return; }

    if (readErr == SDM_ERR_NO_ERROR) {
      const float value = sdm->decodeFloatValue();
      UserVar.setFloat(it->taskIndex, it->taskVarIndex, value);

      # if FEATURE_PLUGIN_STATS
      PluginTaskData_base *taskdata = getPluginTaskDataBaseClassOnly(it->taskIndex);

      if (taskdata != nullptr) {
        PluginStats *stats = taskdata->getPluginStats(it->taskVarIndex);

        if (stats != nullptr) {
          stats->trackPeak(value);
        }
      }
      # endif // if FEATURE_PLUGIN_STATS
    } else {
      sdm->clearErrCode();
    }
    it->_state = 0;
    _SDM_RegisterReadQueue.emplace_back(*it);
    _SDM_RegisterReadQueue.pop_front();
    it = _SDM_RegisterReadQueue.begin();
  }

  if (it->_state == 0) {
    sdm->startReadVal(it->_reg, it->_dev_id);
    it->_state = 1;
  }
}

void SDM_pause_loopRegisterReadQueue()
{
  auto it = _SDM_RegisterReadQueue.begin();

  if (it != _SDM_RegisterReadQueue.end()) {
    it->_state = 2;
  }
}

void SDM_resume_loopRegisterReadQueue()
{
  auto it = _SDM_RegisterReadQueue.begin();

  if (it != _SDM_RegisterReadQueue.end()) {
    it->_state = 0;
  }
}

#endif // ifdef USES_P078

#include "../PluginStructs/P089_data_struct.h"

#ifdef USES_P089


# include "../Helpers/Networking.h"

# include "../Helpers/_Plugin_init.h"

# ifdef ESP32
P089_data_struct::P089_data_struct() {
  if (nullptr == P089_ping_service) {
    P089_ping_service = new (std::nothrow) P089_ping_service_struct();
    addLog(LOG_LEVEL_INFO, F("PING : Starting ping service."));
  }

  if ((nullptr != P089_ping_service) && P089_ping_service->isInitialized()) {
    addLog(LOG_LEVEL_INFO, F("PING : Increment task instance counter."));
    P089_ping_service->increment();
  }
}

P089_data_struct::~P089_data_struct() {
  if (nullptr != P089_ping_service) {
    addLog(LOG_LEVEL_INFO, F("PING : Decrement task instance counter."));

    if (0 == P089_ping_service->decrement()) {
      addLog(LOG_LEVEL_INFO, F("PING : Stopping ping service."));
      delete P089_ping_service;
      P089_ping_service = nullptr;
    }
  }
}

bool P089_data_struct::send_ping(struct EventStruct *event) {
  /* This ping lost for sure */
  if (!isInitialized() || !NetworkConnected()) {
    return true;
  }

  if ((nullptr == P089_ping_service) ||
      !P089_ping_service->isInitialized()) {
    return true; // Service not available
  }

  if (P089_ping_service->getPingResult(event->TaskIndex, _ping_request)) {
    // Get requested result

    if (_ping_request.status == P089_request_status::Result) {
      if (_ping_request.result) {
        addLog(LOG_LEVEL_INFO, strformat(F("PING : Successfully pinged %s (%s) count: %d avg: %.03f ms for task: %d"),
                                         _ping_request.hostname.c_str(),
                                         _ping_request.ip.toString().c_str(),
                                         _ping_request.count,
                                         _ping_request.avgTime,
                                         event->TaskIndex + 1));
      } else {
        addLog(LOG_LEVEL_ERROR, strformat(F("PING : Error pinging %s (%s) for task: %d"),
                                          _ping_request.hostname.c_str(),
                                          _ping_request.ip.toString().c_str(),
                                          event->TaskIndex + 1));
      }
      UserVar.setFloat(event->TaskIndex, 1, _ping_request.avgTime); // Set always, even when not specifically enabled

      _ping_request.status = P089_request_status::Finished;         // Ready for new work
      return !_ping_request.result;                                 // Inverted result to report back!
    }
  }

  if ((_ping_request.status > P089_request_status::Request) && (_ping_request.status < P089_request_status::Result)) {
    // addLog(LOG_LEVEL_INFO, F("PING : Still working."));
    return false; // Busy, but not a failure
  }

  // New request

  IPAddress ip;
  char hostname[PLUGIN_089_HOSTNAME_SIZE]{};

  LoadCustomTaskSettings(event->TaskIndex, (uint8_t *)&hostname, PLUGIN_089_HOSTNAME_SIZE);

  /* This one lost as well, DNS dead? */
  if (!resolveHostByName(hostname, ip)) {
    return true;
  }

  int16_t pingCount = P089_PING_COUNT;

  if ((pingCount < 1) || (pingCount > P089_MAX_PING_COUNT)) {
    pingCount = 5;
  }

  _ping_request.status   = P089_request_status::Request;
  _ping_request.count    = pingCount;
  _ping_request.ip       = ip;
  _ping_request.hostname = hostname; // Bring all data to the request
  const bool result = !P089_ping_service->addPingRequest(event->TaskIndex, _ping_request);
  P089_ping_service->loop();         // Kick-off if not already working
  return result;
}

bool P089_data_struct::loop() {
  if ((nullptr != P089_ping_service) && P089_ping_service->isInitialized()) {
    return P089_ping_service->loop();
  }
  return false; // We did nothing
}

# endif // ifdef ESP32

# ifdef ESP8266
P089_data_struct::P089_data_struct() {
  destIPAddress.addr = 0;
  idseq              = 0;

  if (nullptr == P089_data) {
    P089_data = new (std::nothrow) P089_icmp_pcb();

    if (P089_data != nullptr) {
      P089_data->m_IcmpPCB = raw_new(IP_PROTO_ICMP);
      raw_recv(P089_data->m_IcmpPCB, PingReceiver, nullptr);
      raw_bind(P089_data->m_IcmpPCB, IP_ADDR_ANY);
    }
  } else {
    P089_data->instances++;
  }
}

P089_data_struct::~P089_data_struct() {
  if (P089_data != nullptr) {
    P089_data->instances--;

    if (P089_data->instances == 0) {
      raw_remove(P089_data->m_IcmpPCB);
      delete P089_data;
      P089_data = nullptr;
    }
  }
}

bool P089_data_struct::send_ping(struct EventStruct *event) {
  bool is_failure = false;
  IPAddress ip;

  // Do we have unanswered pings? If we are sending new one, this means old one is lost
  if (destIPAddress.addr != 0) {
    is_failure = true;
  }

  /* This ping lost for sure */
  if (!NetworkConnected()) {
    return true;
  }

  char hostname[PLUGIN_089_HOSTNAME_SIZE];

  LoadCustomTaskSettings(event->TaskIndex, (uint8_t *)&hostname, PLUGIN_089_HOSTNAME_SIZE);

  /* This one lost as well, DNS dead? */
  if (!resolveHostByName(hostname, ip)) {
    return true;
  }
  destIPAddress.addr = ip;

  /* Generate random ID & seq */
  idseq = HwRandom();
  u16_t ping_len            = ICMP_PAYLOAD_LEN + sizeof(struct icmp_echo_hdr);
  struct pbuf *packetBuffer = pbuf_alloc(PBUF_IP, ping_len, PBUF_RAM);

  /* Lost for sure, TODO: Might be good to log such failures, this means we are short on ram? */
  if (packetBuffer == nullptr) {
    return true;
  }

  struct icmp_echo_hdr *echoRequestHeader = (struct icmp_echo_hdr *)packetBuffer->payload;

  ICMPH_TYPE_SET(echoRequestHeader, ICMP_ECHO);
  ICMPH_CODE_SET(echoRequestHeader, 0);
  echoRequestHeader->chksum = 0;
  echoRequestHeader->id     = (uint16_t)((idseq & 0xffff0000) >> 16);
  echoRequestHeader->seqno  = (uint16_t)(idseq & 0xffff);
  size_t icmpHeaderLen = sizeof(struct icmp_echo_hdr);
  size_t icmpDataLen   = ping_len - icmpHeaderLen;
  char   dataByte      = 0x61;

  for (size_t i = 0; i < icmpDataLen; i++) {
    ((char *)echoRequestHeader)[icmpHeaderLen + i] = dataByte;
    ++dataByte;

    if (dataByte > 0x77) // 'w' character
    {
      dataByte = 0x61;
    }
  }
  echoRequestHeader->chksum = inet_chksum(echoRequestHeader, ping_len);
  ip_addr_t destIPAddress;

  destIPAddress.addr = ip;
  raw_sendto(P089_data->m_IcmpPCB, packetBuffer, &destIPAddress);

  pbuf_free(packetBuffer);

  return is_failure;
}

uint8_t PingReceiver(void *origin, struct raw_pcb *pcb, struct pbuf *packetBuffer, const ip_addr_t *addr)
{
  if ((packetBuffer == nullptr) || (addr == nullptr)) {
    return 0;
  }

  if (packetBuffer->len < sizeof(struct ip_hdr) + sizeof(struct icmp_echo_hdr) + ICMP_PAYLOAD_LEN) {
    return 0;
  }

  // TODO: Check some ipv4 header values?
  // struct ip_hdr * ip = (struct ip_hdr *)packetBuffer->payload;

  if (pbuf_header(packetBuffer, -PBUF_IP_HLEN) != 0) {
    return 0;
  }

  // After the IPv4 header, we can access the icmp echo header
  struct icmp_echo_hdr *icmp_hdr = (struct icmp_echo_hdr *)packetBuffer->payload;

  // Is it echo reply?
  if (icmp_hdr->type != 0) {
    pbuf_header(packetBuffer, PBUF_IP_HLEN);
    return 0;
  }

  bool is_found = false;

  for (taskIndex_t index = 0; index < TASKS_MAX; index++) {
    deviceIndex_t deviceIndex = getDeviceIndex_from_TaskIndex(index);

    // Match all ping plugin instances and check them
    constexpr pluginID_t PLUGIN_ID_P089_PING(PLUGIN_ID_089);

    if (getPluginID_from_DeviceIndex(deviceIndex) == PLUGIN_ID_P089_PING) {
      P089_data_struct *P089_taskdata = static_cast<P089_data_struct *>(getPluginTaskData(index));

      if ((P089_taskdata != nullptr) && (icmp_hdr->id == (uint16_t)((P089_taskdata->idseq & 0xffff0000) >> 16)) &&
          (icmp_hdr->seqno == (uint16_t)(P089_taskdata->idseq & 0xffff))) {
        UserVar.setFloat(index, 0, 0); // Reset "fails", we got reply
        P089_taskdata->idseq              = 0;
        P089_taskdata->destIPAddress.addr = 0;
        is_found                          = true;
      }
    }
  }

  if (!is_found) {
    pbuf_header(packetBuffer, PBUF_IP_HLEN);
    return 0;
  }

  // Everything fine, release the kraken, ehm, buffer
  pbuf_free(packetBuffer);
  return 1;
}

# endif // ifdef ESP8266
#endif // ifdef USES_P089

#include "../PluginStructs/P103_data_struct.h"

#ifdef USES_P103

const __FlashStringHelper* toString(AtlasEZO_Sensors_e sensor) {
  switch (sensor) {
    case AtlasEZO_Sensors_e::PH: return F("pH (Potential of Hydrogen)");
    case AtlasEZO_Sensors_e::ORP: return F("ORP (Oxidation Reduction Potential)");
    case AtlasEZO_Sensors_e::EC: return F("EC (Electric conductivity)");
    case AtlasEZO_Sensors_e::DO: return F("DO (Dissolved Oxigen)");
    case AtlasEZO_Sensors_e::HUM: return F("HUM (Humidity)");
    # if P103_USE_RTD
    case AtlasEZO_Sensors_e::RTD: return F("RTD (Thermosensor)");
    # endif // if P103_USE_RTD
    # if P103_USE_FLOW
    case AtlasEZO_Sensors_e::FLOW: return F("FLOW (Flow meter)");
    # endif // if P103_USE_FLOW
    case AtlasEZO_Sensors_e::UNKNOWN: break;
  }
  return F("Unknown");
}

const __FlashStringHelper* P103_statusToString(char status) {
  switch (status) {
    case 'P':
      return F("powered off");
    case 'S':
      return F("software reset");
    case 'B':
      return F("brown out");
    case 'W':
      return F("watch dog");
    case 'U':
    default:
      break;
  }
  return F("unknown");
}

bool P103_send_I2C_command(uint8_t I2Caddress, const String& cmd, char *sensordata)
{
  sensordata[0] = '\0';

  uint16_t sensor_bytes_received = 0;

  uint8_t error;
  uint8_t i2c_response_code = 0;
  uint8_t in_char           = 0;

  # ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLogMove(LOG_LEVEL_DEBUG, concat(F("> cmd = "), cmd));
  }
  # endif // ifndef BUILD_NO_DEBUG
  Wire.beginTransmission(I2Caddress);

  for (size_t i = 0; i < cmd.length(); ++i)  {
    Wire.write(static_cast<uint8_t>(cmd[i]));
  }
  error = Wire.endTransmission();

  if (error != 0)
  {
    addLog(LOG_LEVEL_ERROR, F("Wire.endTransmission() returns error: Check Atlas shield, pH, ORP, EC, DO, HUM"
                              # if P103_USE_RTD
                              ", RTD"
                              # endif // if P103_USE_RTD
                              # if P103_USE_FLOW
                              ", FLOW"
                              # endif // if P103_USE_FLOW
                              " are supported."));
    return false;
  }

  // don't read answer if we want to go to sleep
  if (cmd.substring(0, 5).equalsIgnoreCase(F("Sleep")))
  {
    return true;
  }

  i2c_response_code = 254;

  while (i2c_response_code == 254)
  {
    Wire.requestFrom(I2Caddress, (uint8_t)(ATLAS_EZO_RETURN_ARRAY_SIZE - 1)); // call the circuit and request ATLAS_EZO_RETURN_ARRAY_SIZE -
                                                                              // 1 = 32 bytes (this is more then we need).
    i2c_response_code = Wire.read();                                          // read response code

    while (Wire.available())
    {                                                                         // read response
      in_char = Wire.read();

      if (in_char == 0)
      {   // if we receive a null character, we're done
        while (Wire.available())
        { // purge the data line if needed
          Wire.read();
        }

        break; // exit the while loop.
      }
      else
      {
        if (sensor_bytes_received >= ATLAS_EZO_RETURN_ARRAY_SIZE)
        {
          addLog(LOG_LEVEL_ERROR, F("< result array to short!"));
          return false;
        }
        sensordata[sensor_bytes_received] = in_char; // load this uint8_t into our array.
        sensor_bytes_received++;
      }
    }
    sensordata[sensor_bytes_received] = '\0';

    switch (i2c_response_code)
    {
      case 1:
        # ifndef BUILD_NO_DEBUG

        if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
          addLogMove(LOG_LEVEL_DEBUG, concat(F("< success, answer = "), String(sensordata)));
        }
        # endif // ifndef BUILD_NO_DEBUG
        break;

      case 2:
        # ifndef BUILD_NO_DEBUG
        addLog(LOG_LEVEL_DEBUG, F("< command failed"));
        # endif // ifndef BUILD_NO_DEBUG
        return false;

      case 254:
        # ifndef BUILD_NO_DEBUG
        addLog(LOG_LEVEL_DEBUG_MORE, F("< command pending"));
        # endif // ifndef BUILD_NO_DEBUG
        break;

      case 255:
        # ifndef BUILD_NO_DEBUG
        addLog(LOG_LEVEL_DEBUG, F("< no data"));
        # endif // ifndef BUILD_NO_DEBUG
        return false;
    }
  }

  return true;
}

int P103_getCalibrationPoints(uint8_t i2cAddress)
{
  int  nb_calibration_points = -1;
  char sensordata[ATLAS_EZO_RETURN_ARRAY_SIZE]{};

  if (P103_send_I2C_command(i2cAddress, F("Cal,?"), sensordata))
  {
    if (strncmp(sensordata, "?Cal,", 5))
    {
      char tmp[2];
      tmp[0]                = sensordata[5];
      tmp[1]                = '\0',
      nb_calibration_points = str2int(tmp);
    }
  }

  return nb_calibration_points;
}

void P103_html_color_message(const __FlashStringHelper *color, const String& message) {
  addHtml(F("&nbsp;<span style='color:"));
  addHtml(color);
  addHtml(F(";'>"));
  addHtml(message);
  addHtml(F("</span>"));
}

void P103_html_red(const String& message) {
  P103_html_color_message(F("red"), message);
}

void P103_html_orange(const String& message) {
  P103_html_color_message(F("orange"), message);
}

void P103_html_green(const String& message) {
  P103_html_color_message(F("green"), message);
}

void P103_addDisabler() {
  // disabler(): argument(s) on 'true' will set that checkbox to false! non-boolean ignores argument
  // addHtml(F("\n<script type='text/javascript'>"
  //           "function elId(e){return document.getElementById(e)}"
  //           "function chkd(e, y) { x = elId(e); if (x) x.checked = y }"
  //           "function disabler(clear,single,l,h,dry,nul,atm)"
  //           "{if (clear===true){chkd('en_cal_clear',false);}"
  //           "if (single===true){chkd('en_cal_single',false);}"
  //           "if(l===true){chkd('en_cal_L',false);}"
  //           "if(h===true){chkd('en_cal_H',false);}"
  //           "if(dry===true){chkd('en_cal_dry',false);}"
  //           "if(nul===true){chkd('en_cal_0',false);}"
  //           "if(atm===true){chkd('en_cal_atm',false);}};"
  //           "</script>"));
  // Minified:
  addHtml(F("\n<script type='text/javascript'>"
            "function elId(e){return document.getElementById(e)}"
            "function chkd(n,c){(x=elId(n))&&(x.checked=c)}"
            "function disabler(e,l,c,a,n,d,t)"
            "{!0===e&&(chkd('en_cal_clear',!1))"
            ",!0===l&&(chkd('en_cal_single',!1))"
            ",!0===c&&(chkd('en_cal_L',!1))"
            ",!0===a&&(chkd('en_cal_H',!1))"
            ",!0===n&&(chkd('en_cal_dry',!1))"
            ",!0===d&&(chkd('en_cal_0',!1))"
            ",!0===t&&(chkd('en_cal_atm',!1))};"
            "</script>"));
}

void P103_addClearCalibration()
{
  addRowLabel(F("<strong>Clear calibration</strong>"));
  addFormCheckBox(F("Clear"), F("en_cal_clear"), false);
  addHtml(F("\n<script type='text/javascript'>elId('en_cal_clear').onclick=function(){disabler(0,true,true,true,true,0,0)}</script>"));
  addFormNote(F("Attention! This will reset all calibrated data. New calibration will be needed!!!"));
}

int P103_addDOCalibration(uint8_t I2Cchoice)
{
  const int32_t nb_calibration_points = P103_getCalibrationPoints(I2Cchoice);

  addRowLabel("Calibrated Points");
  addHtmlInt(nb_calibration_points);

  if (nb_calibration_points < 1)
  {
    P103_html_red(F("   Calibration needed"));
  }

  addRowLabel(F("<strong>Calibrate to atmospheric oxygen levels</strong>"));
  addFormCheckBox(F("Enable"), F("en_cal_atm"), false);
  addHtml(F("\n<script type='text/javascript'>elId('en_cal_atm').onclick=function(){disabler(0,0,0,0,0,true,0)}</script>"));

  addRowLabel(F("<strong>Calibrate device to 0 dissolved oxygen</strong>"));
  addFormCheckBox(F("Enable"), F("en_cal_0"), false);
  addHtml(F("\n<script type='text/javascript'>elId('en_cal_0').onclick=function(){disabler(0,0,0,0,0,0,true)}</script>"));

  if (nb_calibration_points > 0)
  {
    P103_html_green(F("OK"));
  }
  else
  {
    P103_html_red(F("Not yet calibrated"));
  }

  return nb_calibration_points;
}

void P103_addCreateDryCalibration()
{
  addRowLabel(F("<strong>Dry calibration</strong>"));
  addFormCheckBox(F("Enable"), F("en_cal_dry"), false);
  addHtml(F("\n<script type='text/javascript'>elId('en_cal_dry').onclick=function(){disabler(true,true,true,true,0,0,0)}</script>"));
  addFormNote(F("Dry calibration must always be done first!"));
  addFormNote(F("Calibration for pH-Probe could be 1 (single) or 2 point (low, high)."));
}

int P103_addCreateSinglePointCalibration(AtlasEZO_Sensors_e  board_type,
                                         struct EventStruct *event,
                                         uint8_t             I2Cchoice,
                                         String              unit,
                                         float               min,
                                         float               max,
                                         uint8_t             nrDecimals,
                                         float               stepsize)
{
  const int32_t nb_calibration_points = P103_getCalibrationPoints(I2Cchoice);

  addRowLabel("Calibrated Points");
  addHtmlInt(nb_calibration_points);

  if (nb_calibration_points < 1)
  {
    P103_html_red(F("   Calibration needed"));
  }

  addRowLabel(F("<strong>Single point calibration</strong>"));
  addFormFloatNumberBox(F("Ref single point"), F("ref_cal_single'"), P103_CALIBRATION_SINGLE, min, max, nrDecimals, stepsize);
  addUnit(unit);

  if (((board_type != AtlasEZO_Sensors_e::EC) && (nb_calibration_points > 0)) ||
      ((board_type == AtlasEZO_Sensors_e::EC) && (nb_calibration_points == 1)))
  {
    P103_html_green(F("OK"));
  }
  else
  {
    if ((board_type == AtlasEZO_Sensors_e::EC) && (nb_calibration_points > 1))
    {
      P103_html_green(F("Not calibrated, because two point calibration is active."));
    }
    else
    {
      P103_html_red(F("Not yet calibrated"));
    }
  }
  addFormCheckBox(F("Enable"), F("en_cal_single"), false);
  addHtml(F("\n<script type='text/javascript'>elId('en_cal_single').onclick=function(){disabler(true,0,true,true,true,0,0)}</script>"));

  return nb_calibration_points;
}

int P103_addCreate3PointCalibration(AtlasEZO_Sensors_e  board_type,
                                    struct EventStruct *event,
                                    uint8_t             I2Cchoice,
                                    String              unit,
                                    float               min,
                                    float               max,
                                    uint8_t             nrDecimals,
                                    float               stepsize)
{
  int nb_calibration_points = P103_addCreateSinglePointCalibration(board_type, event, I2Cchoice, unit, min, max, nrDecimals, stepsize);

  addRowLabel(F("<strong>Low calibration</strong>"));
  addFormFloatNumberBox(F("Ref low point"), F("ref_cal_L"), P103_CALIBRATION_LOW, min, max, nrDecimals, stepsize);
  addUnit(unit);

  if (nb_calibration_points > 1)
  {
    P103_html_green(F("OK"));
  }
  else
  {
    P103_html_orange(F("Not yet calibrated"));
  }
  addFormCheckBox(F("Enable"), F("en_cal_L"), false);
  addHtml(F("\n<script type='text/javascript'>elId('en_cal_L').onclick=function(){disabler(true,true,0,true,true,0,0)}</script>"));

  addRowLabel(F("<strong>High calibration</strong>"));
  addFormFloatNumberBox(F("Ref high point"), F("ref_cal_H"), P103_CALIBRATION_HIGH, min, max, nrDecimals, stepsize);
  addUnit(unit);

  // pH: low, high OK with 3 calibration points (single is the first one); EC: low high OK with 2 calibration points
  if ((nb_calibration_points > 2) || ((board_type == AtlasEZO_Sensors_e::EC) && (nb_calibration_points > 1)))
  {
    P103_html_green(F("OK"));
  }
  else
  {
    P103_html_orange(F("Not yet calibrated"));
  }
  addFormCheckBox(F("Enable"), F("en_cal_H"), false);
  addHtml(F("\n<script type='text/javascript'>elId('en_cal_H').onclick=function(){disabler(true,true,true,0,true,0,0)}</script>"));

  return nb_calibration_points;
}

bool P103_getHUMOutputOptions(struct EventStruct *event,
                              bool              & _HUMhasHum,
                              bool              & _HUMhasTemp,
                              bool              & _HUMhasDew) {
  bool result = false;

  char boarddata[ATLAS_EZO_RETURN_ARRAY_SIZE]{};

  if ((result = P103_send_I2C_command(P103_I2C_ADDRESS, F("O,?"), boarddata))) {
    String outputs(boarddata);
    int    o      = 2;
    String outPar = parseString(outputs, o);

    while (!outPar.isEmpty()) {
      if (equals(outPar, F("hum"))) {
        _HUMhasHum = true;
      } else
      if (equals(outPar, F("t"))) {
        _HUMhasTemp = true;
      } else
      if (equals(outPar, F("dew"))) {
        _HUMhasDew = true;
      }
      o++;
      outPar = parseString(outputs, o);
    }
  }
  return result;
}

#endif  // ifdef USES_P103

#include "../PluginStructs/P019_data_struct.h"

#ifdef USES_P019

# include "../Helpers/_Plugin_Helper_webform.h"

# include "../ESPEasyCore/ESPEasyGPIO.h"

uint8_t P019_data_struct::getI2C_address(struct EventStruct *event)
{
  const uint8_t unit = (CONFIG_PORT - 1) / 8;

  //  const uint8_t port = CONFIG_PORT - (unit * 8);
  uint8_t address = 0x20 + unit;

  if (unit > 7) { address += 0x10; }

  return address;
}

P019_data_struct::P019_data_struct(struct EventStruct *event) :
  _data(
    PLUGIN_PCF,
    CONFIG_PORT,
    P019_DEBOUNCE,
    P019_DC_MAX_INT,
    P019_LP_MIN_INT,
    P019_DOUBLECLICK,
    P019_LONGPRESS,
    P019_SAFE_BTN != 0,
    P019_BOOTSTATE)
# if FEATURE_I2C_DEVICE_CHECK
  , _address(P019_data_struct::getI2C_address(event))
# endif // if FEATURE_I2C_DEVICE_CHECK
{
  // Turn on Pullup resistor
  setPCFInputMode(_data._pin);

  // read and store current state to prevent switching at boot time
  // "state" could be -1, 0 or 1
  const int8_t state = GPIO_PCF_Read(_data._pin);

  _data.init(
    event,
    state,
    (state == -1) ?  PIN_MODE_OFFLINE : PIN_MODE_INPUT_PULLUP);

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO,
           concat(F("PCF INIT="), state));
  }
}

void P019_data_struct::tenPerSecond(struct EventStruct *event)
{
# if FEATURE_I2C_DEVICE_CHECK

  if (!I2C_deviceCheck(_address, event->TaskIndex, 10, PLUGIN_I2C_GET_ADDRESS)) { // Generate stats
    return;                                                                       // Will return the default false for success
  }
# endif // if FEATURE_I2C_DEVICE_CHECK

  const int8_t state = GPIO_PCF_Read(_data._pin);

  _data.tenPerSecond(event, F("PCF"), state);
}

#endif // ifdef USES_P019

#include "../PluginStructs/P159_data_struct.h"

#ifdef USES_P159

const __FlashStringHelper* Plugin_159_valuename(uint8_t value_nr,
                                                bool    displayString) {
  const __FlashStringHelper *strings[] { /*** ATTENTION: Don't change order of values as these are stored as user-selected!!! ***/
    F("Presence"), F("Presence"),
    F("Stationary Presence"), F("StatPres"),
    F("Moving Presence"), F("MovPres"),
    F("Object distance"), F("Distance"),
    F("Stationary Object distance"), F("StatDist"),
    F("Moving Object distance"), F("MovDist"),
    F("Stationary Object energy"), F("StatEnergy"),
    F("Moving Object energy"), F("MovEnergy"),
    F("Ambient light sensor"), F("AmbLight"),
    F("Output pin state"), F("OutputPin"),
    F("Stationary Object energy gate "), F("StatEnergyGate"),
    F("Moving Object energy gate "), F("MovEnergyGate"),
    F("Stationary sensitivity gate "), F("StatSensGate"),
    F("Moving sensitivity gate "), F("MovSensGate"),
  };
  const size_t index         = (2 * value_nr) + (displayString ? 0 : 1);
  constexpr size_t nrStrings = NR_ELEMENTS(strings);

  if (index < nrStrings) {
    return strings[index];
  }
  return F("");
}

P159_data_struct::P159_data_struct(ESPEasySerialPort portType,
                                   int8_t            rxPin,
                                   int8_t            txPin,
                                   bool              engineeringMode) {
  _engineeringMode = engineeringMode;

  // Try to open the associated serial port
  easySerial = new (std::nothrow) ESPeasySerial(portType, rxPin, txPin);

  if (nullptr != easySerial) {
    easySerial->begin(256000);
    radar = new (std::nothrow) ld2410();

    if (nullptr != radar) {
      if (radar->begin(*easySerial, false)) {
        const bool rst = radar->requestRestart();

        // start initiated, now wait, next step: request configuration
        milestone = millis();
        state     = P159_state_e::Restarting;
        addLog(LOG_LEVEL_INFO, concat(F("LD2410: Restart sensor: "), rst ? 1 : 0));
      } else {
        delete radar;
        radar = nullptr;
      }
    }
  }
  addLog(LOG_LEVEL_INFO, concat(F("LD2410: Initialized: "), isValid() ? 1 : 0));
} // constructor

void P159_data_struct::disconnectSerial() {
  delete easySerial;
  easySerial = nullptr;

  delete radar;
  radar = nullptr;
} // disconnectSerial()

bool P159_data_struct::processSensor(struct EventStruct *event) {
  bool new_data = false;

  if (isValid()) {
    const uint32_t iStart = millis();
    P159_state_e   sState = state;

    switch (state) {
      case P159_state_e::Initializing:
        addLog(LOG_LEVEL_INFO, F("LD2410: Initializing..."));
        break;
      case P159_state_e::Restarting:

        if ((milestone > 0) && (timePassedSince(milestone) > P159_DELAY_RESTART)) {
          state     = P159_state_e::GetVersion;
          milestone = 0;
          radar->requestFirmwareVersion();
        }
        break;
      case P159_state_e::GetVersion:

        state = P159_state_e::GetConfiguration;

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          addLog(LOG_LEVEL_INFO, concat(F("LD2410: Firmware version: "), radar->cmdFirmwareVersion()));
        }
        _configurationRead = radar->requestCurrentConfiguration();
        break;
      case P159_state_e::GetConfiguration:

        state = P159_state_e::Engineering;

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          addLog(LOG_LEVEL_INFO, concat(F("LD2410: Fetch configuration: "), _configurationRead ? 1 : 0));

          if (_configurationRead) {
            const uint8_t mGate   = radar->cfgMaxGate();
            const uint8_t mMvGate = radar->cfgMaxMovingGate();
            const uint8_t mStGate = radar->cfgMaxStationaryGate();
            const uint8_t mMax    = std::max(mGate, std::max(mMvGate, mStGate));
            addLog(LOG_LEVEL_INFO, strformat(F("LD2410: Sensor idle time: %d sec."), radar->cfgSensorIdleTimeInSeconds()));
            addLog(LOG_LEVEL_INFO, strformat(F("LD2410: Max. gate: %d, max. moving gate: %d, max. stationary gate: %d"),
                                             mGate, mMvGate, mStGate));

            for (uint8_t i = 0; i <= mMax; ++i) {
              addLog(LOG_LEVEL_INFO, strformat(F("LD2410: Sensitivity, gate %d (%.2f - %.2f mtr): moving:%3d, stationary:%3d"),
                                               i, i * P159_GATE_DISTANCE_METERS, (i + 1) * P159_GATE_DISTANCE_METERS,
                                               i <= mMvGate ? radar->cfgMovingGateSensitivity(i) : 0,
                                               i <= mStGate ? radar->cfgStationaryGateSensitivity(i) : 0));
            }
          }
        }
        break;
      case P159_state_e::Engineering:
        state = P159_state_e::Running;

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          if (_engineeringMode) {
            addLog(LOG_LEVEL_INFO, concat(F("LD2410: Engineering mode: "), radar->requestStartEngineeringMode() ? 1 : 0));
          }
          addLog(LOG_LEVEL_INFO, F("LD2410: Enter Running state."));
        }
        break;
      case P159_state_e::Running:
        new_data = radar->ld2410_loop();

        if (new_data && radar->dataReady()) {
          const int8_t valueCount = P159_NR_OUTPUT_VALUES;
          bool result             = false;

          for (int8_t i = 0; i < valueCount; ++i) {
            const uint8_t pconfigIndex = i + P159_QUERY1_CONFIG_POS;
            bool isChanged             = false;
            UserVar.setFloat(event->TaskIndex, i, getRadarValue(PCONFIG(pconfigIndex), UserVar[event->BaseVarIndex + i], isChanged));

            result |= isChanged;
          }

          if (result && (timePassedSince(lastSent) >= P159_DELAY_SENDOUT)) {
            lastSent = millis(); // Sending data incorporated in timing, just like PLUGIN_READ
            sendData(event);
            const uint16_t err = radar->getErrorCountAndReset();

            if ((err != 0) && loglevelActiveFor(LOG_LEVEL_ERROR)) {
              addLog(LOG_LEVEL_ERROR, concat(F("LD2410: Frame Errorcounter: "), err));
            }
          }
        }
        break;
    }

    if (P159_state_e::Running != sState) { // FIXME Remove log
      addLog(LOG_LEVEL_INFO, strformat(F("LD2410: Starting state: %d duration: %d msec."),
                                       static_cast<uint8_t>(sState), timePassedSince(iStart)));
    }
  } // isValid()

  return new_data;
}   // processSensor()

bool P159_data_struct::plugin_read(struct EventStruct *event) {
  bool result = false;

  if (isValid() && radar->isConnected()) {
    const int8_t valueCount = P159_NR_OUTPUT_VALUES;
    result = !P159_GET_UPDATE_DIFF_ONLY;

    for (int8_t i = 0; i < valueCount; ++i) {
      const uint8_t pconfigIndex = i + P159_QUERY1_CONFIG_POS;
      bool isChanged             = false;
      UserVar.setFloat(event->TaskIndex, i, getRadarValue(PCONFIG(pconfigIndex), UserVar[event->BaseVarIndex + i], isChanged));

      result |= isChanged;
    }

    if (result && (timePassedSince(lastSent) < P159_DELAY_SENDOUT)) { // Avoid duplicate sendouts
      result = false;
    } else if (result) {
      lastSent = millis();
    }
  }
  return result;
} // plugin_read()

int P159_data_struct::getRadarValue(int16_t valueIndex,
                                    int     previousValue,
                                    bool  & isChanged) {
  int result = previousValue;

  if (isValid()) {
    switch (valueIndex) {
      case P159_OUTPUT_PRESENCE:            result = radar->presenceDetected() ? 1 : 0; break;
      case P159_OUTPUT_STATIONARY_PRESENCE: result = radar->isStationary() ? 1 : 0; break;
      case P159_OUTPUT_MOVING_PRESENCE:     result = radar->isMoving() ? 1 : 0; break;
      case P159_OUTPUT_DISTANCE:            result = radar->detectionDistance(); break;
      case P159_OUTPUT_STATIONARY_DISTANCE: result = radar->stationaryTargetDistance(); break;
      case P159_OUTPUT_MOVING_DISTANCE:     result = radar->movingTargetDistance(); break;
      case P159_OUTPUT_STATIONARY_ENERGY:   result = radar->stationaryTargetEnergy(); break;
      case P159_OUTPUT_MOVING_ENERGY:       result = radar->movingTargetEnergy(); break;
    }

    if ((valueIndex >= P159_OUTPUT_STATIC_SENSOR_ENERGY_GATE0) && (valueIndex <= P159_OUTPUT_STATIC_SENSOR_ENERGY_GATE8)) {
      result = radar->cfgStationaryGateSensitivity(valueIndex - P159_OUTPUT_STATIC_SENSOR_ENERGY_GATE0);
    }
    else
    if ((valueIndex >= P159_OUTPUT_MOVING_SENSOR_ENERGY_GATE0) && (valueIndex <= P159_OUTPUT_MOVING_SENSOR_ENERGY_GATE8)) {
      result = radar->cfgMovingGateSensitivity(valueIndex - P159_OUTPUT_MOVING_SENSOR_ENERGY_GATE0);
    }

    if (_engineeringMode) {
      if ((valueIndex >= P159_OUTPUT_STATIC_DISTANCE_ENERGY_GATE0) && (valueIndex <= P159_OUTPUT_STATIC_DISTANCE_ENERGY_GATE8)) {
        result = radar->engStaticDistanceGateEnergy(valueIndex - P159_OUTPUT_STATIC_DISTANCE_ENERGY_GATE0);
      }
      else
      if ((valueIndex >= P159_OUTPUT_MOVING_DISTANCE_ENERGY_GATE0) && (valueIndex <= P159_OUTPUT_MOVING_DISTANCE_ENERGY_GATE8)) {
        result = radar->engMovingDistanceGateEnergy(valueIndex - P159_OUTPUT_MOVING_DISTANCE_ENERGY_GATE0);
      }
      else
      if (P159_OUTPUT_LIGHT_SENSOR == valueIndex) {
        result = radar->engLightSensorValue();
      }
      else
      if (P159_OUTPUT_PIN_STATE == valueIndex) {
        result = radar->engOutputPinState();
      }
    }
    isChanged = result != previousValue;
  } // isValid()
  return result;
}   // getRadarValue()

void P159_data_struct::addJavascript() {
  addHtml(F("<script type='text/javascript'>"
            "function chDis(s,m) {"
            "for(i=1;i<=m;i++){" // "console.log('pc_arg'+i);" // For debugging the js code
            "document.getElementById('pc_arg'+i).disabled=!s"
            "}"
            "};</script>"
            ));
}

bool P159_data_struct::plugin_webform_load(struct EventStruct *event) {
  bool result = false;

  if (isValid()) {
    if (!_configurationRead) {
      _configurationRead = radar->requestCurrentConfiguration();
    }

    if (_configurationRead) {
      addRowLabel(F("Firmware version: "));
      addHtml(radar->cmdFirmwareVersion());

      const uint8_t  mGate    = radar->cfgMaxGate();
      const uint8_t  mMvGate  = radar->cfgMaxMovingGate();
      const uint8_t  mStGate  = radar->cfgMaxStationaryGate();
      const uint8_t  mMax     = std::max(mGate, std::max(mMvGate, mStGate));
      const uint16_t idleTime = radar->cfgSensorIdleTimeInSeconds();

      addJavascript();
      addFormCheckBox(F("Modify sensor settings"), F("saveSens"), false);

      int idx = 0;
      addFormNumericBox(F("Idle seconds"), getPluginCustomArgName(idx++), idleTime, 0, 65535,
                        # if FEATURE_TOOLTIPS
                        EMPTY_STRING,
                        # endif // if FEATURE_TOOLTIPS
                        true);
      addUnit(F("0..65535 sec."));
      addFormNumericBox(F("Max. Moving gates"), getPluginCustomArgName(idx++), mMvGate, 2, 8,
                        # if FEATURE_TOOLTIPS
                        EMPTY_STRING,
                        # endif // if FEATURE_TOOLTIPS
                        true);
      addUnit(F("2..8"));
      addFormNumericBox(F("Max. Stationary gates"), getPluginCustomArgName(idx++), mStGate, 2, 8,
                        # if FEATURE_TOOLTIPS
                        EMPTY_STRING,
                        # endif // if FEATURE_TOOLTIPS
                        true);
      addUnit(F("2..8"));

      addRowLabel(F("Sensitivity"));
      html_table(EMPTY_STRING, false); // Sub-table
      html_table_header(F("Gate"),       200);
      html_table_header(F("Moving"),     100);
      html_table_header(F("Stationary"), 100);

      for (uint8_t i = 0; i <= mMax; ++i) {
        html_TR_TD();
        addHtml(strformat(F("Gate %d (%.2f - %.2f mtr)"), i, i * P159_GATE_DISTANCE_METERS, (i + 1) * P159_GATE_DISTANCE_METERS));
        html_TD();
        addNumericBox(getPluginCustomArgName(idx++), radar->cfgMovingGateSensitivity(i), 0, P159_MAX_SENSITIVITY_VALUE, true);
        html_TD();
        addNumericBox(getPluginCustomArgName(idx++), radar->cfgStationaryGateSensitivity(i), 0, P159_MAX_SENSITIVITY_VALUE, true);
      }
      html_end_table();
      addHtml(strformat(F("\n<script type='text/javascript'>document.getElementById('saveSens')."
                          "onclick=function(){chDis(this.checked,%d)};</script>"),
                        idx));
      result = true;
    }
  }
  return result;
}

bool P159_data_struct::plugin_webform_save(struct EventStruct *event) {
  bool result       = false;
  const bool doSave = isFormItemChecked(F("saveSens"));

  if (isValid() && doSave) {
    int idx              = 0;
    const uint16_t idle  = getFormItemIntCustomArgName(idx++);
    const uint8_t  gMove = getFormItemIntCustomArgName(idx++);
    const uint8_t  gStat = getFormItemIntCustomArgName(idx++);
    addLog(LOG_LEVEL_INFO, F("LD2410: Save sensitivity settings to sensor, start..."));
    radar->requestConfigurationModeBegin();
    radar->setMaxValues(gMove, gStat, idle);
    const uint16_t maxGate = radar->cfgMaxGate();

    for (uint16_t gate = 0; gate <= maxGate; ++gate) {
      const uint16_t sMove = getFormItemIntCustomArgName(idx++);
      const uint16_t sStat = getFormItemIntCustomArgName(idx++);

      // Set sensitivity (level) to 100 to effectively disable sensitivity
      radar->setGateSensitivityThreshold(gate,
                                         gate <= gMove ? sMove : P159_MAX_SENSITIVITY_VALUE,
                                         gate <= gStat ? sStat : P159_MAX_SENSITIVITY_VALUE);
    }
    radar->requestConfigurationModeEnd();
    addLog(LOG_LEVEL_INFO, F("LD2410: Save sensitivity settings to sensor, done."));
  }
  return result;
}

bool P159_data_struct::plugin_write(struct EventStruct *event,
                                    String            & string) {
  bool result      = false;
  const String cmd = parseString(string, 1);

  if (isValid() && equals(cmd, F("ld2410"))) {
    const String subcmd = parseString(string, 2);

    if (equals(subcmd, F("factoryreset"))) {
      addLog(LOG_LEVEL_ERROR, F("LD2410: Performing unconditional sensor factory reset!"));
      result = radar->requestFactoryReset();
      radar->requestRestart();

      // start initiated, now wait, next step: request configuration
      milestone = millis();
      state     = P159_state_e::Restarting;
    } else if (equals(subcmd, F("logall"))) {
      result = plugin_get_config_value(event, string, true);
    }
  }
  return result;
}

bool P159_data_struct::plugin_get_config_value(struct EventStruct *event,
                                               String            & string,
                                               bool                logAll) {
  bool result = false;

  if (isValid()) {
    const String  cmd   = parseString(string, 1);
    const int16_t count = P159_NR_MAX_OUTPUT_OPTIONS;

    for (int16_t option = 0; option < count; ++option) {
      int16_t idx  = option;
      int16_t gate = -1;

      if ((option >= P159_OUTPUT_STATIC_SENSOR_ENERGY_GATE0) && (option <= P159_OUTPUT_STATIC_SENSOR_ENERGY_GATE8)) {
        idx  = P159_OUTPUT_STATIC_SENSOR_GATE_index;
        gate = option - P159_OUTPUT_STATIC_SENSOR_ENERGY_GATE0;
      } else
      if ((option >= P159_OUTPUT_MOVING_SENSOR_ENERGY_GATE0) && (option <= P159_OUTPUT_MOVING_SENSOR_ENERGY_GATE8)) {
        idx  = P159_OUTPUT_MOVING_SENSOR_GATE_index;
        gate = option - P159_OUTPUT_MOVING_SENSOR_ENERGY_GATE0;
      } else
      if ((option >= P159_OUTPUT_STATIC_DISTANCE_ENERGY_GATE0) && (option <= P159_OUTPUT_STATIC_DISTANCE_ENERGY_GATE8)) {
        if (_engineeringMode) {
          idx  = P159_OUTPUT_STATIC_DISTANCE_GATE_index;
          gate = option - P159_OUTPUT_STATIC_DISTANCE_ENERGY_GATE0;
        } else {
          idx = -1;
        }
      } else
      if ((option >= P159_OUTPUT_MOVING_DISTANCE_ENERGY_GATE0) && (option <= P159_OUTPUT_MOVING_DISTANCE_ENERGY_GATE8)) {
        if (_engineeringMode) {
          idx  = P159_OUTPUT_MOVING_DISTANCE_GATE_index;
          gate = option - P159_OUTPUT_MOVING_DISTANCE_ENERGY_GATE0;
        } else {
          idx = -1;
        }
      }

      if ((idx > -1) && (logAll ||
                         cmd.equalsIgnoreCase(concat(Plugin_159_valuename(idx, false), (gate > -1) ? String(gate) : EMPTY_STRING)))) {
        bool dummy;
        string = getRadarValue(option, -1, dummy);
        result = true;

        if (!logAll) {
          break;
        } else if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          addLog(LOG_LEVEL_INFO, strformat(F("LD2410: %s: %s"),
                                           concat(Plugin_159_valuename(idx, true), (gate > -1) ? String(gate) : EMPTY_STRING).c_str(),
                                           string.c_str()));
        }
      }
    }
  }
  return result;
}

#endif // USES_P159

#include "../PluginStructs/P003_data_struct.h"


#ifdef USES_P003
P003_data_struct::P003_data_struct(const Internal_GPIO_pulseHelper::pulseCounterConfig& config)
  : pulseHelper(config) {}

bool P003_data_struct::plugin_read(struct EventStruct *event) {
  bool success = true;
  unsigned long pulseCounter, pulseCounterTotal;
  float pulseTime_msec;

  pulseHelper.getPulseCounters(pulseCounter, pulseCounterTotal, pulseTime_msec);
  pulseHelper.resetPulseCounter();


  if (PCONFIG(P003_IDX_IGNORE_ZERO) && (0 == pulseCounter) && lastDeltaZero) {
    success = false;
  }
  lastDeltaZero = (0 == pulseCounter);

  // store the current counter values into UserVar (RTC-memory)
  // FIXME TD-er: Must check we're interacting with the raw values in this PulseCounter plugin
  // For backward compatibility, set all values
  UserVar.setFloat(event->TaskIndex, P003_IDX_pulseCounter,      pulseCounter);
  UserVar.setFloat(event->TaskIndex, P003_IDX_pulseTotalCounter, pulseCounterTotal);
  UserVar.setFloat(event->TaskIndex, P003_IDX_pulseTime,         pulseTime_msec);

  switch (PCONFIG(P003_IDX_COUNTERTYPE)) {
    case P003_CT_INDEX_COUNTER:            // No updates
    case P003_CT_INDEX_COUNTER_TOTAL:      // No updates
      break;
    case P003_CT_INDEX_TOTAL:              // Replace first value
      UserVar.setFloat(event->TaskIndex, P003_IDX_pulseCounter, pulseCounterTotal);
      break;
    case P003_CT_INDEX_COUNTER_TOTAL_TIME: // No updates
      break;
    # if P003_USE_EXTRA_COUNTERTYPES
    case P003_CT_INDEX_TIME:               // Replace first value
      UserVar.setFloat(event->TaskIndex, P003_IDX_pulseCounter,      pulseTime_msec);
      break;
    case P003_CT_INDEX_TOTAL_TIME:         // Replace first 2 values
      UserVar.setFloat(event->TaskIndex, P003_IDX_pulseCounter,      pulseCounterTotal);
      UserVar.setFloat(event->TaskIndex, P003_IDX_pulseTotalCounter, pulseTime_msec);
      break;
    case P003_CT_INDEX_TIME_COUNTER: // Replace first 2 values
      UserVar.setFloat(event->TaskIndex, P003_IDX_pulseCounter,      pulseTime_msec);
      UserVar.setFloat(event->TaskIndex, P003_IDX_pulseTotalCounter, pulseCounter);
      break;
    # endif // if P003_USE_EXTRA_COUNTERTYPES
  }

  // Store the raw value in the unused 4th position.
  // This is needed to restore the value from RTC as it may be converted into another output value using a formula.
  UserVar.setFloat(event->TaskIndex, P003_IDX_persistedTotalCounter, pulseCounterTotal);
  return success;
}

bool P003_data_struct::plugin_peek(struct EventStruct *event) {
  unsigned long pulseCounter, pulseCounterTotal;
  float pulseTime_msec;

  pulseHelper.getPulseCounters(pulseCounter, pulseCounterTotal, pulseTime_msec);

  const bool success = (PCONFIG(P003_IDX_IGNORE_ZERO) && (0 != pulseCounter));

  return success;
}

#endif // ifdef USES_P003

#include "../PluginStructs/P074_data_struct.h"

#ifdef USES_P074


P074_data_struct::P074_data_struct() {
  tsl = Adafruit_TSL2591(2591); // pass in a number for the sensor identifier
                                // (for your use later)
}

// Changing the integration time gives you a longer time over which to sense
// light
// longer timelines are slower, but are good in very low light situtations!
void P074_data_struct::setIntegrationTime(int time) {
  switch (time) {
    default:
    case 0: tsl.setTiming(TSL2591_INTEGRATIONTIME_100MS); break;
    case 1: tsl.setTiming(TSL2591_INTEGRATIONTIME_200MS); break;
    case 2: tsl.setTiming(TSL2591_INTEGRATIONTIME_300MS); break;
    case 3: tsl.setTiming(TSL2591_INTEGRATIONTIME_400MS); break;
    case 4: tsl.setTiming(TSL2591_INTEGRATIONTIME_500MS); break;
    case 5: tsl.setTiming(TSL2591_INTEGRATIONTIME_600MS); break;
  }
}

// You can change the gain on the fly, to adapt to brighter/dimmer light
// situations
void P074_data_struct::setGain(int gain) {
  switch (gain) {
    default:
    case 0: tsl.setGain(TSL2591_GAIN_LOW);  break; // 1x gain (bright light)
    case 1: tsl.setGain(TSL2591_GAIN_MED);  break; // 25x (Medium)
    case 2: tsl.setGain(TSL2591_GAIN_HIGH); break; // 428x (High)
    case 3: tsl.setGain(TSL2591_GAIN_MAX);  break; // 9876x (Max)
  }
}

// Return true when value is present.
bool P074_data_struct::getFullLuminosity(uint32_t& value) {
  value = 0;

  if (newValuePresent) {
    // don't try to read a new value until the last one was processed.
    return false;
  }

  if (!integrationActive) {
    if (startIntegrationNeeded) {
      // Fix to re-set the gain/timing before every read.
      // See https://github.com/letscontrolit/ESPEasy/issues/3347
      if (tsl.begin()) {
        tsl.enable();
        integrationStart       = millis();
        duration               = 0;
        integrationActive      = true;
        startIntegrationNeeded = false;
      }
    }
    return false; // Started integration, so no value possible yet.
  }
  bool finished = false;

  value    = tsl.getFullLuminosity(finished);
  duration = timePassedSince(integrationStart);

  if (finished) {
    integrationActive = false;
    integrationStart  = 0;
    newValuePresent   = true;
  } else {
    if (duration > 1000) {
      // Max integration time is 600 msec, so if we still have no value, reset the current state
      integrationStart       = 0;
      integrationActive      = false;
      newValuePresent        = false;
      startIntegrationNeeded = true; // Apparently a value was needed
    }
  }

  if (!integrationActive) {
    tsl.disable();
  }
  return finished;
}

#endif // ifdef USES_P074

#include "../PluginStructs/P012_data_struct.h"

#ifdef USES_P012

// Needed also here for PlatformIO's library finder as the .h file
// is in a directory which is excluded in the src_filter
# include <LiquidCrystal_I2C.h>


P012_data_struct::P012_data_struct(uint8_t addr,
                                   uint8_t lcd_size,
                                   uint8_t mode,
                                   uint8_t timer) :
  lcd(addr, 20, 4),
  Plugin_012_mode(mode),
  displayTimer(timer)
{
  switch (lcd_size)
  {
    case 1:
      Plugin_012_rows = 2;
      Plugin_012_cols = 16;
      break;
    case 2:
      Plugin_012_rows = 4;
      Plugin_012_cols = 20;
      break;

    default:
      Plugin_012_rows = 2;
      Plugin_012_cols = 16;
      break;
  }
}

void P012_data_struct::init() {
  // Setup LCD display
  lcd.init(); // initialize the lcd
  lcd.backlight();
  lcd.print(F("ESP Easy"));
  createCustomChars();
}

void P012_data_struct::setBacklightTimer(uint8_t timer) {
  displayTimer = timer;
  lcd.backlight();
}

void P012_data_struct::checkTimer() {
  if (displayTimer > 0)
  {
    displayTimer--;

    if (displayTimer == 0) {
      lcd.noBacklight();
    }
  }
}

void P012_data_struct::lcdWrite(const String& text, uint8_t col, uint8_t row) {
  // clear line before writing new string
  if (Plugin_012_mode == 2) {
    lcd.setCursor(col, row);

    for (uint8_t i = col; i < Plugin_012_cols; i++) {
      lcd.print(' ');
    }
  }

  if (row == 0) { splashState = P012_splashState_e::SplashCleared; } // Reset splashState
  lcd.setCursor(col, row);

  if ((Plugin_012_mode == 1) || (Plugin_012_mode == 2)) {
    for (uint8_t i = 0; i < Plugin_012_cols - col; i++) {
      if (text[i]) {
        lcd.print(text[i]);
      }
    }
  }

  // message exceeding cols will continue to next line
  else {
    // Fix Weird (native) lcd display behaviour that split long string into row 1,3,2,4, instead of 1,2,3,4
    bool stillProcessing = 1;
    uint8_t charCount    = 1;

    while (stillProcessing) {
      if (++col > Plugin_012_cols) { // have we printed 20 characters yet (+1 for the logic)
        row += 1;
        lcd.setCursor(0, row);       // move cursor down
        col = 1;
      }

      // dont print if "lower" than the lcd
      if (row < Plugin_012_rows) {
        lcd.print(text[charCount - 1]);
      }

      if (!text[charCount]) { // no more chars to process?
        stillProcessing = 0;
      }
      charCount += 1;
    }

    // lcd.print(text.c_str());
    // end fix
  }
}

// Perform some specific changes for LCD display
// https://www.letscontrolit.com/forum/viewtopic.php?t=2368
String P012_data_struct::P012_parseTemplate(String& tmpString, uint8_t lineSize) {
  String result            = parseTemplate_padded(tmpString, lineSize);
  const char degree[3]     = { 0xc2, 0xb0, 0 }; // Unicode degree symbol
  const char degree_lcd[2] = { 0xdf, 0 };       // P012_LCD degree symbol

  result.replace(degree, degree_lcd);

  char unicodePrefix = 0xc4;

# ifdef USES_P012_POLISH_CHARS

  if (result.indexOf(unicodePrefix) != -1) {
    const char znak_a_uni[3] = { 0xc4, 0x85, 0 }; // Unicode znak a
    const char znak_a_lcd[2] = { 0x05, 0 };       // P012_LCD znak a
    result.replace(znak_a_uni, znak_a_lcd);

    const char znak_A_uni[3] = { 0xc4, 0x84, 0 }; // Unicode znak A
    result.replace(znak_A_uni, znak_a_lcd);

    const char znak_c_uni[3] = { 0xc4, 0x87, 0 }; // Unicode znak c
    const char znak_c_lcd[2] = { 0x03, 0 };       // P012_LCD znak c
    result.replace(znak_c_uni, znak_c_lcd);

    const char znak_C_uni[3] = { 0xc4, 0x86, 0 }; // Unicode znak C
    result.replace(znak_C_uni, znak_c_lcd);

    const char znak_e_uni[3] = { 0xc4, 0x99, 0 }; // Unicode znak e
    const char znak_e_lcd[2] = { 0x02, 0 };       // P012_LCD znak e
    result.replace(znak_e_uni, znak_e_lcd);

    const char znak_E_uni[3] = { 0xc4, 0x98, 0 }; // Unicode znak E
    result.replace(znak_E_uni, znak_e_lcd);
  }

  unicodePrefix = 0xc5;

  if (result.indexOf(unicodePrefix) != -1) {
    const char znak_l_uni[3] = { 0xc5, 0x82, 0 };  // Unicode znak l
    const char znak_l_lcd[2] = { 0x01, 0 };        // P012_LCD znak l
    result.replace(znak_l_uni, znak_l_lcd);

    const char znak_L_uni[3] = { 0xc5, 0x81, 0 };  // Unicode znak L
    result.replace(znak_L_uni, znak_l_lcd);

    const char znak_n_uni[3] = { 0xc5, 0x84, 0 };  // Unicode znak n
    const char znak_n_lcd[2] = { 0x04, 0 };        // P012_LCD znak n
    result.replace(znak_n_uni, znak_n_lcd);

    const char znak_N_uni[3] = { 0xc5, 0x83, 0 };  // Unicode znak N
    result.replace(znak_N_uni, znak_n_lcd);

    const char znak_s_uni[3] = { 0xc5, 0x9b, 0 };  // Unicode znak s
    const char znak_s_lcd[2] = { 0x06, 0 };        // P012_LCD znak s
    result.replace(znak_s_uni, znak_s_lcd);

    const char znak_S_uni[3] = { 0xc5, 0x9a, 0 };  // Unicode znak S
    result.replace(znak_S_uni, znak_s_lcd);

    const char znak_z1_uni[3] = { 0xc5, 0xba, 0 }; // Unicode znak z z kreska
    const char znak_z1_lcd[2] = { 0x07, 0 };       // P012_LCD znak z z kropka
    result.replace(znak_z1_uni, znak_z1_lcd);

    const char znak_Z1_uni[3] = { 0xc5, 0xb9, 0 }; // Unicode znak Z z kreska
    result.replace(znak_Z1_uni, znak_z1_lcd);

    const char znak_z2_uni[3] = { 0xc5, 0xbc, 0 }; // Unicode znak z z kropka
    const char znak_z2_lcd[2] = { 0x07, 0 };       // P012_LCD znak z z kropka
    result.replace(znak_z2_uni, znak_z2_lcd);

    const char znak_Z2_uni[3] = { 0xc5, 0xbb, 0 }; // Unicode znak Z z kropka
    result.replace(znak_Z2_uni, znak_z2_lcd);
  }

  unicodePrefix = 0xc3;

  if (result.indexOf(unicodePrefix) != -1) {
    const char znak_o_uni[3] = { 0xc3, 0xB3, 0 }; // Unicode znak o
    const char znak_o_lcd[2] = { 0x08, 0 };       // P012_LCD znak o
    result.replace(znak_o_uni, znak_o_lcd);

    const char znak_O_uni[3] = { 0xc3, 0x93, 0 }; // Unicode znak O
    result.replace(znak_O_uni, znak_o_lcd);
  }
# endif // USES_P012_POLISH_CHARS

  unicodePrefix = 0xc3;

  if (result.indexOf(unicodePrefix) != -1) {
    // See: https://github.com/letscontrolit/ESPEasy/issues/2081

    const char umlautAE_uni[3] = { 0xc3, 0x84, 0 };  // Unicode Umlaute AE
    const char umlautAE_lcd[2] = { 0xe1, 0 };        // P012_LCD Umlaute
    result.replace(umlautAE_uni,  umlautAE_lcd);

    const char umlaut_ae_uni[3] = { 0xc3, 0xa4, 0 }; // Unicode Umlaute ae
    result.replace(umlaut_ae_uni, umlautAE_lcd);

    const char umlautOE_uni[3] = { 0xc3, 0x96, 0 };  // Unicode Umlaute OE
    const char umlautOE_lcd[2] = { 0xef, 0 };        // P012_LCD Umlaute
    result.replace(umlautOE_uni,  umlautOE_lcd);

    const char umlaut_oe_uni[3] = { 0xc3, 0xb6, 0 }; // Unicode Umlaute oe
    result.replace(umlaut_oe_uni, umlautOE_lcd);

    const char umlautUE_uni[3] = { 0xc3, 0x9c, 0 };  // Unicode Umlaute UE
    const char umlautUE_lcd[2] = { 0xf5, 0 };        // P012_LCD Umlaute
    result.replace(umlautUE_uni,  umlautUE_lcd);

    const char umlaut_ue_uni[3] = { 0xc3, 0xbc, 0 }; // Unicode Umlaute ue
    result.replace(umlaut_ue_uni, umlautUE_lcd);

    const char umlaut_sz_uni[3] = { 0xc3, 0x9f, 0 }; // Unicode Umlaute sz
    const char umlaut_sz_lcd[2] = { 0xe2, 0 };       // P012_LCD Umlaute
    result.replace(umlaut_sz_uni, umlaut_sz_lcd);
  }
  return result;
}

void P012_data_struct::createCustomChars() {
# ifdef USES_P012_POLISH_CHARS

  /*
     static const char LETTER_null[8] PROGMEM = { // spacja
      0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000
     };
   */
  static const char LETTER_a[8] PROGMEM = { // a
    0b00000, 0b00000, 0b01110, 0b00001, 0b01111, 0b10001, 0b01111, 0b00010
  };
  static const char LETTER_c[8] PROGMEM = { // c
    0b00010, 0b00100, 0b01110, 0b10000, 0b10000, 0b10001, 0b01110, 0b00000
  };
  static const char LETTER_e[8] PROGMEM = { // e
    0b00000, 0b00000, 0b01110, 0b10001, 0b11111, 0b10000, 0b01110, 0b00010
  };
  static const char LETTER_l[8] PROGMEM = { // l
    0b01100, 0b00100, 0b00101, 0b00110, 0b01100, 0b00100, 0b01110, 0b00000
  };
  static const char LETTER_n[8] PROGMEM = { // n
    0b00010, 0b00100, 0b10110, 0b11001, 0b10001, 0b10001, 0b10001, 0b00000
  };
  static const char LETTER_o[8] PROGMEM = { // o
    0b00010, 0b00100, 0b01110, 0b10001, 0b10001, 0b10001, 0b01110, 0b00000
  };
  static const char LETTER_s[8] PROGMEM = { // s
    0b00010, 0b00100, 0b01110, 0b10000, 0b01110, 0b00001, 0b11110, 0b00000
  };

  /*
     static const char LETTER_z1[8] PROGMEM = {   // z z kreska
     0b00010, 0b00100, 0b11111, 0b00010, 0b00100, 0b01000, 0b11111, 0b00000
     };
   */
  static const char LETTER_z2[8] PROGMEM = { // z z kropka
    0b00100, 0b00000, 0b11111, 0b00010, 0b00100, 0b01000, 0b11111, 0b00000
  };
  lcd.createChar(0, LETTER_o);               // probably defected memory cell
  lcd.createChar(1, LETTER_l);
  lcd.createChar(2, LETTER_e);
  lcd.createChar(3, LETTER_c);
  lcd.createChar(4, LETTER_n);
  lcd.createChar(5, LETTER_a);
  lcd.createChar(6, LETTER_s);
  lcd.createChar(7, LETTER_z2);
  lcd.createChar(8, LETTER_o);
# endif // ifdef USES_P012_POLISH_CHARS
}

#endif  // ifdef USES_P012

#include "../PluginStructs/P060_data_struct.h"


#ifdef USES_P060

P060_data_struct::P060_data_struct(uint8_t i2c_addr) : i2cAddress(i2c_addr) {}

void P060_data_struct::overSampleRead()
{
  Oversampling.add(readMCP3221());
}

float P060_data_struct::getValue()
{
  float value{};
  if (!Oversampling.get(value)) {
    value = readMCP3221();
  }
  return value;
}

uint16_t P060_data_struct::readMCP3221()
{
  uint16_t value;

  Wire.requestFrom(i2cAddress, (uint8_t)2);

  if (Wire.available() == 2)
  {
    value = (Wire.read() << 8) | Wire.read();
  }
  else {
    value = 9999;
  }

  return value;
}

#endif // ifdef USES_P060

#include "../PluginStructs/P027_data_struct.h"

#ifdef USES_P027


# define INA219_READ                            (0x01)
# define INA219_REG_CONFIG                      (0x00)
# define INA219_CONFIG_RESET                    (0x8000) // Reset Bit

# define INA219_CONFIG_BVOLTAGERANGE_MASK       (0x2000) // Bus Voltage Range Mask
# define INA219_CONFIG_BVOLTAGERANGE_16V        (0x0000) // 0-16V Range
# define INA219_CONFIG_BVOLTAGERANGE_32V        (0x2000) // 0-32V Range

# define INA219_CONFIG_GAIN_MASK                (0x1800) // Gain Mask
# define INA219_CONFIG_GAIN_1_40MV              (0x0000) // Gain 1, 40mV Range
# define INA219_CONFIG_GAIN_2_80MV              (0x0800) // Gain 2, 80mV Range
# define INA219_CONFIG_GAIN_4_160MV             (0x1000) // Gain 4, 160mV Range
# define INA219_CONFIG_GAIN_8_320MV             (0x1800) // Gain 8, 320mV Range

# define INA219_CONFIG_BADCRES_MASK             (0x0780) // Bus ADC Resolution Mask
# define INA219_CONFIG_BADCRES_9BIT             (0x0080) // 9-bit bus res = 0..511
# define INA219_CONFIG_BADCRES_10BIT            (0x0100) // 10-bit bus res = 0..1023
# define INA219_CONFIG_BADCRES_11BIT            (0x0200) // 11-bit bus res = 0..2047
# define INA219_CONFIG_BADCRES_12BIT            (0x0400) // 12-bit bus res = 0..4097

# define INA219_CONFIG_SADCRES_MASK             (0x0078) // Shunt ADC Resolution and Averaging Mask
# define INA219_CONFIG_SADCRES_9BIT_1S_84US     (0x0000) // 1 x 9-bit shunt sample
# define INA219_CONFIG_SADCRES_10BIT_1S_148US   (0x0008) // 1 x 10-bit shunt sample
# define INA219_CONFIG_SADCRES_11BIT_1S_276US   (0x0010) // 1 x 11-bit shunt sample
# define INA219_CONFIG_SADCRES_12BIT_1S_532US   (0x0018) // 1 x 12-bit shunt sample
# define INA219_CONFIG_SADCRES_12BIT_2S_1060US  (0x0048) // 2 x 12-bit shunt samples averaged together
# define INA219_CONFIG_SADCRES_12BIT_4S_2130US  (0x0050) // 4 x 12-bit shunt samples averaged together
# define INA219_CONFIG_SADCRES_12BIT_8S_4260US  (0x0058) // 8 x 12-bit shunt samples averaged together
# define INA219_CONFIG_SADCRES_12BIT_16S_8510US (0x0060) // 16 x 12-bit shunt samples averaged together
# define INA219_CONFIG_SADCRES_12BIT_32S_17MS   (0x0068) // 32 x 12-bit shunt samples averaged together
# define INA219_CONFIG_SADCRES_12BIT_64S_34MS   (0x0070) // 64 x 12-bit shunt samples averaged together
# define INA219_CONFIG_SADCRES_12BIT_128S_69MS  (0x0078) // 128 x 12-bit shunt samples averaged together

# define INA219_CONFIG_MODE_MASK                (0x0007) // Operating Mode Mask
# define INA219_CONFIG_MODE_POWERDOWN           (0x0000)
# define INA219_CONFIG_MODE_SVOLT_TRIGGERED     (0x0001)
# define INA219_CONFIG_MODE_BVOLT_TRIGGERED     (0x0002)
# define INA219_CONFIG_MODE_SANDBVOLT_TRIGGERED (0x0003)
# define INA219_CONFIG_MODE_ADCOFF              (0x0004)
# define INA219_CONFIG_MODE_SVOLT_CONTINUOUS    (0x0005)
# define INA219_CONFIG_MODE_BVOLT_CONTINUOUS    (0x0006)
# define INA219_CONFIG_MODE_SANDBVOLT_CONTINUOUS (0x0007)

# define INA219_REG_SHUNTVOLTAGE                (0x01)
# define INA219_REG_BUSVOLTAGE                  (0x02)
# define INA219_REG_POWER                       (0x03)
# define INA219_REG_CURRENT                     (0x04)
# define INA219_REG_CALIBRATION                 (0x05)


P027_data_struct::P027_data_struct(uint8_t i2c_addr) : i2caddr(i2c_addr) {}

void P027_data_struct::setCalibration_32V_2A() {
  calValue = 4027;

  // Set multipliers to convert raw current/power values
  currentDivider_mA = 10; // Current LSB = 100uA per bit (1000/100 = 10)

  // Set Calibration register to 'Cal' calculated above
  wireWriteRegister(INA219_REG_CALIBRATION, calValue);

  // Set Config register to take into account the settings above
  uint16_t config = INA219_CONFIG_BVOLTAGERANGE_32V |
                    INA219_CONFIG_GAIN_8_320MV |
                    INA219_CONFIG_BADCRES_12BIT |
                    INA219_CONFIG_SADCRES_12BIT_128S_69MS |
                    INA219_CONFIG_MODE_SANDBVOLT_CONTINUOUS;

  wireWriteRegister(INA219_REG_CONFIG, config);
}

void P027_data_struct::setCalibration_32V_1A() {
  calValue = 10240;

  // Set multipliers to convert raw current/power values
  currentDivider_mA = 25; // Current LSB = 40uA per bit (1000/40 = 25)

  // Set Calibration register to 'Cal' calculated above
  wireWriteRegister(INA219_REG_CALIBRATION, calValue);

  // Set Config register to take into account the settings above
  uint16_t config = INA219_CONFIG_BVOLTAGERANGE_32V |
                    INA219_CONFIG_GAIN_8_320MV |
                    INA219_CONFIG_BADCRES_12BIT |
                    INA219_CONFIG_SADCRES_12BIT_128S_69MS |
                    INA219_CONFIG_MODE_SANDBVOLT_CONTINUOUS;

  wireWriteRegister(INA219_REG_CONFIG, config);
}

void P027_data_struct::setCalibration_16V_400mA() {
  calValue = 8192;

  // Set multipliers to convert raw current/power values
  currentDivider_mA = 20; // Current LSB = 50uA per bit (1000/50 = 20)

  // Set Calibration register to 'Cal' calculated above
  wireWriteRegister(INA219_REG_CALIBRATION, calValue);

  // Set Config register to take into account the settings above
  uint16_t config = INA219_CONFIG_BVOLTAGERANGE_16V |
                    INA219_CONFIG_GAIN_1_40MV |
                    INA219_CONFIG_BADCRES_12BIT |
                    INA219_CONFIG_SADCRES_12BIT_128S_69MS |
                    INA219_CONFIG_MODE_SANDBVOLT_CONTINUOUS;

  wireWriteRegister(INA219_REG_CONFIG, config);
}

void P027_data_struct::setCalibration_26V_8A() {
  calValue = 4096;

  // Set multipliers to convert raw current/power values
  currentDivider_mA = 1;

  // Set Calibration register to 'Cal' calculated above
  wireWriteRegister(INA219_REG_CALIBRATION, calValue);

  // Set Config register to take into account the settings above
  uint16_t config = INA219_CONFIG_BVOLTAGERANGE_32V |
                    INA219_CONFIG_GAIN_8_320MV |
                    INA219_CONFIG_BADCRES_12BIT |
                    INA219_CONFIG_SADCRES_12BIT_128S_69MS |
                    INA219_CONFIG_MODE_SANDBVOLT_CONTINUOUS;

  wireWriteRegister(INA219_REG_CONFIG, config);
}

# if P027_FEATURE_POWERDOWN
void P027_data_struct::setPowerDown() {
  uint16_t value;

  wireReadRegister(INA219_REG_CONFIG, &value);

  // Set Config register to power down mode == lowest 3 bits 0
  value &= ~INA219_CONFIG_MODE_MASK;

  wireWriteRegister(INA219_REG_CONFIG, value);
}

void P027_data_struct::setActiveMode() {
  uint16_t value;

  wireReadRegister(INA219_REG_CONFIG, &value);

  // Set Config register in the default continuous reading mode
  value &= ~INA219_CONFIG_MODE_MASK;
  value |= INA219_CONFIG_MODE_SANDBVOLT_CONTINUOUS;

  wireWriteRegister(INA219_REG_CONFIG, value);
  delay(1); // Allow at least 40 microseconds to recover from powerdown mode
}

# endif // if P027_FEATURE_POWERDOWN

int16_t P027_data_struct::getBusVoltage_raw() {
  uint16_t value;

  wireReadRegister(INA219_REG_BUSVOLTAGE, &value);

  // Shift to the right 3 to drop CNVR and OVF and multiply by LSB
  return (int16_t)((value >> 3) * 4);
}

int16_t P027_data_struct::getShuntVoltage_raw() {
  uint16_t value;

  wireReadRegister(INA219_REG_SHUNTVOLTAGE, &value);
  return (int16_t)value;
}

int16_t P027_data_struct::getCurrent_raw() {
  uint16_t value;

  // Sometimes a sharp load will reset the INA219, which will
  // reset the cal register, meaning CURRENT and POWER will
  // not be available ... avoid this by always setting a cal
  // value even if it's an unfortunate extra step
  wireWriteRegister(INA219_REG_CALIBRATION, calValue);

  // Now we can safely read the CURRENT register!
  wireReadRegister(INA219_REG_CURRENT, &value);

  return (int16_t)value;
}

float P027_data_struct::getShuntVoltage_mV() {
  int16_t value;

  value = P027_data_struct::getShuntVoltage_raw();
  return value * 0.01f;
}

float P027_data_struct::getBusVoltage_V() {
  int16_t value = getBusVoltage_raw();

  return value * 0.001f;
}

float P027_data_struct::getCurrent_mA() {
  float valueDec = getCurrent_raw();

  valueDec /= currentDivider_mA;
  return valueDec;
}

void P027_data_struct::wireWriteRegister(uint8_t reg, uint16_t value)
{
  I2C_write16_reg(i2caddr, reg, value);
}

void P027_data_struct::wireReadRegister(uint8_t reg, uint16_t *value)
{
  // FIXME TD-er: Must add a function in I2C_access to allow for some delay between calls
  Wire.beginTransmission(i2caddr);
  Wire.write(reg); // Register
  Wire.endTransmission();

  delay(1);        // Max 12-bit conversion time is 586us per sample

  Wire.requestFrom(i2caddr, (uint8_t)2);

  // Shift values to create properly formed integer
  *value = ((Wire.read() << 8) | Wire.read());
}

#endif // ifdef USES_P027

#include "../PluginStructs/P177_data_struct.h"

#ifdef USES_P177


P177_data_struct::P177_data_struct(struct EventStruct *event) {
  _sendEvents     = P177_GENERATE_EVENTS == 1;
  _rawData        = P177_RAW_DATA == 1;
  _ignoreNegative = P177_IGNORE_NEGATIVE == 1;
}

P177_data_struct::~P177_data_struct() {}

bool P177_data_struct::plugin_ten_per_second(struct EventStruct *event) {
  bool is_ok;
  uint8_t byteData;

  if (P177_SensorMode_e::IdleMode == _sensorMode) {
    _cycles--;

    if (_cycles) { // Skip until counted down to 0
      return false;
    }

    // we're ignoring the 'bit 3' status info, as it's unclear if it's 0-based or 1-based bit 3...
    // a 100 msec 'delay' (10/sec) after the start of a read should be enough to stabilize the data

    # if P177_CHECK_READY_BIT
    byteData = I2C_read8_reg(P177_I2C_ADDR, P177_COMMAND_REG, &is_ok);

    if (is_ok && (0 == bitRead(byteData, 3))) // Not currently reading
    # endif // if P177_CHECK_READY_BIT
    {
      I2C_write8_reg(P177_I2C_ADDR, P177_COMMAND_REG, P177_START_READ);
      _sensorMode = P177_SensorMode_e::SensingMode;
      _cycles     = P177_SKIP_CYCLES; // Restart timer after a read is started
    }
    # if P177_LOG
    addLog(LOG_LEVEL_INFO, strformat(F("P177 : 1 Sensormode: %d, ok: %d, status: 0x%02x"),
                                     static_cast<uint8_t>(_sensorMode), is_ok, byteData));
    # endif // if P177_LOG
    return false;
  }

  if (P177_SensorMode_e::SensingMode == _sensorMode) {
    byteData = I2C_read8_reg(P177_I2C_ADDR, P177_COMMAND_REG, &is_ok);

    # if P177_LOG
    addLog(LOG_LEVEL_INFO, strformat(F("P177 : 2 Sensormode: %d, ok: %d, status: 0x%02x"),
                                     static_cast<uint8_t>(_sensorMode), is_ok, byteData));
    # endif // if P177_LOG

    // if (!is_ok || (1 == bitRead(byteData, 3))) { // Currently reading
    //   return false;
    // }
    _sensorMode = P177_SensorMode_e::ReportingMode;
  }

  # if P177_LOG
  addLog(LOG_LEVEL_INFO, concat(F("P177 : 3 Sensormode: "), static_cast<uint8_t>(_sensorMode)));
  # endif // if P177_LOG
  uint32_t prData{};
  uint32_t tmData{};

  # if P177_LOG
  String log = F("P177 : RAW data: ");
  # endif // if P177_LOG
  size_t i = 0;

  for (; i < 3; ++i) { // first 3 bytes pressure
    prData <<= 8;
    byteData = I2C_read8_reg(P177_I2C_ADDR, P177_DATA_REG_1 + i);
    prData  += byteData;
    # if P177_LOG
    log += strformat(F("0x%02X "), byteData);
    # endif // if P177_LOG
  }
  # if P177_LOG
  log += strformat(F("prData: 0x%06X, "), prData);
  # endif // if P177_LOG

  for (; i < P177_DATA_BYTES; ++i) { // 2 more bytes temperature
    tmData <<= 8;
    byteData = I2C_read8_reg(P177_I2C_ADDR, P177_DATA_REG_1 + i);
    tmData  += byteData;
    # if P177_LOG
    log += strformat(F("0x%02X "), byteData);
    # endif // if P177_LOG
  }

  # if P177_LOG
  log += strformat(F(" tmData: 0x%04X"), tmData);
  addLog(LOG_LEVEL_INFO, log);
  # endif // if P177_LOG

  _sensorMode = P177_SensorMode_e::IdleMode; // Start a new cycle
  const bool pressureChanged = (prData != _rawPressure);

  if (!pressureChanged && (tmData == _rawTemperature)) {
    return false;
  }
  _rawPressure    = prData;
  _rawTemperature = tmData;
  _updated        = true;

  if (_sendEvents && pressureChanged) {
    plugin_read(event);
    sendData(event);
    _updated = true; // Is reset in plugin_read()
  }
  return true;
}

bool P177_data_struct::plugin_read(struct EventStruct *event) {
  if (!_updated) {
    // Needed to get the 1st value, or can get the latest value when sampled too frequently.
    plugin_ten_per_second(event);
  }

  if (_updated) {
    int tmpPressure                       = _rawPressure; // Unsigned to signed
    int tmpTemperature                    = _rawTemperature;
    ESPEASY_RULES_FLOAT_TYPE _pressure    = tmpPressure;
    ESPEASY_RULES_FLOAT_TYPE _temperature = tmpTemperature;

    if (tmpPressure >= (1 << 23)) { // Negative value?
      if (_ignoreNegative) {
        _pressure = 0.0;
      } else {
        _pressure = tmpPressure - (1 << 24);
      }
    }

    if (tmpTemperature >= (1 << 15)) { // Negative value?
      _temperature = tmpTemperature - (1 << 16);
    }

    if (!essentiallyZero(_temperature)) {
      _temperature /= 256.0; // Move less significant bits
    }

    if (!_rawData) {
      _pressure *= (P177_PRESSURE_SCALE_FACTOR / static_cast<ESPEASY_RULES_FLOAT_TYPE>(1 << 23));

      if (P177_TEMPERATURE_OFFSET != 0) {
        _temperature += (P177_TEMPERATURE_OFFSET / 10.0); // In 0.1 degree steps
      }
    }

    UserVar.setFloat(event->TaskIndex, 0, _temperature);
    UserVar.setFloat(event->TaskIndex, 1, _pressure);
    _updated = false;
    return true;
  }
  return false;
}

#endif // ifdef USES_P177

#include "../PluginStructs/P118_data_struct.h"

#ifdef USES_P118

// **************************************************************************/
// Constructor
// **************************************************************************/
P118_data_struct::P118_data_struct(int8_t csPin,
                                   int8_t irqPin,
                                   bool   logData,
                                   bool   rfLog)
  : _csPin(csPin), _irqPin(irqPin), _log(logData), _rfLog(rfLog) {}

// **************************************************************************/
// Destructor
// **************************************************************************/
P118_data_struct::~P118_data_struct() {
  delete _rf;
  _rf = nullptr;
}

bool P118_data_struct::plugin_init(struct EventStruct *event) {
  bool success = false;

  LoadCustomTaskSettings(event->TaskIndex, (uint8_t *)&_ExtraSettings, sizeof(_ExtraSettings));
  # ifdef P118_DEBUG_LOG
  addLog(LOG_LEVEL_INFO, F("ITHO: Extra Settings PLUGIN_118 loaded"));
  # endif // ifdef P118_DEBUG_LOG

  int8_t   spi_pins[3];
  uint32_t startInit = 0;

  if (Settings.getSPI_pins(spi_pins) && validGpio(spi_pins[1])) {
    startInit = millis();
    _rf       = new (std::nothrow) IthoCC1101(_csPin, spi_pins[1]); // Pass CS and MISO
  } else {
    addLog(LOG_LEVEL_ERROR, F("ITHO: SPI configuration not correct!"));
  }

  if (nullptr != _rf) {
    success = true;
    # if P118_FEATURE_ORCON
    _rf->enableOrcon(P118_CONFIG_ORCON == 1); // Enabled?
    # endif // if P118_FEATURE_ORCON

    // DeviceID used to send commands, can also be changed on the fly for multi itho control, 10,87,81 corresponds with old library
    _rf->setDeviceID(P118_CONFIG_DEVID1, P118_CONFIG_DEVID2, P118_CONFIG_DEVID3);
    _rf->init();

    const long duration = timePassedSince(startInit);

    if (duration > P118_TIMEOUT_LIMIT) {
      if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
        addLogMove(LOG_LEVEL_ERROR, strformat(
                     F("ITHO: Init duration was: %d msec. suggesting that the CC1101 board is not (correctly) connected."),
                     duration));
      }
      success = false;
    }

    if (success) {
      if (validGpio(_irqPin)) {
        attachInterruptArg(digitalPinToInterrupt(_irqPin),
                           reinterpret_cast<void (*)(void *)>(ISR_ithoCheck),
                           this,
                           FALLING);
        # ifndef BUILD_NO_DEBUG
        addLog(LOG_LEVEL_INFO, F("ITHO: Interrupts enabled."));
        # endif // ifndef BUILD_NO_DEBUG
      } else {
        addLog(LOG_LEVEL_ERROR, F("ITHO: Interrupt pin disabled, sending is OK, not receiving data!"));
      }
      _rf->initReceive();
      _InitRunned = true;
    }
  }
  return success;
}

bool P118_data_struct::plugin_exit(struct EventStruct *event) {
  // remove interupt when plugin is removed
  if (validGpio(_irqPin)) {
    detachInterrupt(digitalPinToInterrupt(_irqPin));
  }

  return true;
}

bool P118_data_struct::plugin_once_a_second(struct EventStruct *event) {
  // decrement timer when timermode is running
  if ((_State >= 10) && (_Timer > 0)) {
    _Timer--;

    if (_Timer == 0) { _Timer--; }
  }

  // if timer has elapsed set Fan state to low
  if ((_State >= 10) && (_Timer < 0))
  {
    if (_State < 100) {
      _State = 1;   // Itho low
    } else {
      _State = 101; // Orcon low
    }
    _Timer = 0;     // Avoid doing this again
  }

  // Publish new data when vars are changed or init has runned or timer is running (update every 2 sec)
  if  ((_OldState != _State) || ((_Timer > 0) && (_Timer % 2 == 0)) ||
       (_OldLastIDindex != _LastIDindex) || _InitRunned)
  {
    # ifdef P118_DEBUG_LOG
    addLog(LOG_LEVEL_DEBUG, F("ITHO: UPDATE by PLUGIN_ONCE_A_SECOND"));
    # endif // ifdef P118_DEBUG_LOG
    PublishData(event);
    sendData(event);

    // reset flag set by init
    _InitRunned = false;
  }

  // Remeber current state for next cycle
  _OldState       = _State;
  _OldLastIDindex = _LastIDindex;
  return true;
}

bool P118_data_struct::plugin_fifty_per_second(struct EventStruct *event) {
  if (_Int) {
    ITHOcheck();
    _Int = false; // reset flag
  }

  return true;
}

bool P118_data_struct::plugin_read(struct EventStruct *event) {
  // This ensures that even when Values are not changing, data is send at the configured interval for aquisition
  # ifdef P118_DEBUG_LOG
  addLog(LOG_LEVEL_DEBUG, F("ITHO: UPDATE by PLUGIN_READ"));
  # endif // ifdef P118_DEBUG_LOG
  PublishData(event);

  return true;
}

bool P118_data_struct::plugin_write(struct EventStruct *event, const String& string) {
  bool success     = false;
  const String cmd = parseString(string, 1);

  const bool stateCmd = equals(cmd, F("state"));

  if (equals(cmd, F("itho")) || stateCmd) {
    # ifndef BUILD_NO_DEBUG

    if (stateCmd) { addLogMove(LOG_LEVEL_ERROR, F("ITHO: Command 'state' is deprecated, use 'itho' instead, see documentation.")); }
    # endif // ifndef BUILD_NO_DEBUG
    success = true;

    switch (event->Par1) {
      case 1111: // Join command
      {
        _rf->sendCommand(IthoJoin);
        _rf->initReceive();
        PluginWriteLog(F("join"));
        break;
      }
      case 9999: // Leave command
      {
        _rf->sendCommand(IthoLeave);
        _rf->initReceive();
        PluginWriteLog(F("leave"));
        break;
      }
      case 0: // Off command
      {
        _rf->sendCommand(IthoStandby);
        _State       = 0;
        _Timer       = 0;
        _LastIDindex = 0;
        _rf->initReceive();
        PluginWriteLog(F("standby"));
        break;
      }
      case 1: // Fan low
      {
        _rf->sendCommand(IthoLow);
        _State       = 1;
        _Timer       = 0;
        _LastIDindex = 0;
        _rf->initReceive();
        PluginWriteLog(F("low speed"));
        break;
      }
      case 2: // Fan medium
      {
        _rf->sendCommand(IthoMedium);
        _State       = 2;
        _Timer       = 0;
        _LastIDindex = 0;
        _rf->initReceive();
        PluginWriteLog(F("medium speed"));
        break;
      }
      case 3: // Fan high
      {
        _rf->sendCommand(IthoHigh);
        _State       = 3;
        _Timer       = 0;
        _LastIDindex = 0;
        _rf->initReceive();
        PluginWriteLog(F("high speed"));
        break;
      }
      case 4: // Fan full
      {
        _rf->sendCommand(IthoFull);
        _State       = 4;
        _Timer       = 0;
        _LastIDindex = 0;
        _rf->initReceive();
        PluginWriteLog(F("full speed"));
        break;
      }
      case 13: // Timer1 - 10 min
      {
        _rf->sendCommand(IthoTimer1);
        _State       = 13;
        _Timer       = PLUGIN_118_Time1;
        _LastIDindex = 0;
        _rf->initReceive();
        PluginWriteLog(F("timer 1"));
        break;
      }
      case 23: // Timer2 - 20 min
      {
        _rf->sendCommand(IthoTimer2);
        _State       = 23;
        _Timer       = PLUGIN_118_Time2;
        _LastIDindex = 0;
        _rf->initReceive();
        PluginWriteLog(F("timer 2"));
        break;
      }
      case 33: // Timer3 - 30 min
      {
        _rf->sendCommand(IthoTimer3);
        _State       = 33;
        _Timer       = PLUGIN_118_Time3;
        _LastIDindex = 0;
        _rf->initReceive();
        PluginWriteLog(F("timer 3"));
        break;
      }
      # if P118_FEATURE_ORCON
      case 100: // Fan StandBy
      {
        uint8_t srcID[3], destID[3];
        SetDestIDSrcID(event, srcID, destID, _ExtraSettings.ID1);
        _rf->sendCommand(OrconStandBy, srcID, destID);
        _State       = 100;
        _Timer       = 0;
        _LastIDindex = 0;
        _rf->initReceive();
        PluginWriteLog(F("Orcon standBy"));
        break;
      }
      case 101: // Fan low
      {
        uint8_t srcID[3], destID[3];
        SetDestIDSrcID(event, srcID, destID, _ExtraSettings.ID1);
        _rf->sendCommand(OrconLow, srcID, destID);
        _State       = 101;
        _Timer       = 0;
        _LastIDindex = 0;
        _rf->initReceive();
        PluginWriteLog(F("Orcon low speed"));
        break;
      }
      case 102: // Fan medium
      {
        uint8_t srcID[3], destID[3];
        SetDestIDSrcID(event, srcID, destID, _ExtraSettings.ID1);
        _rf->sendCommand(OrconMedium, srcID, destID);
        _State       = 102;
        _Timer       = 0;
        _LastIDindex = 0;
        _rf->initReceive();
        PluginWriteLog(F("Orcon medium speed"));
        break;
      }
      case 103: // Fan high
      {
        uint8_t srcID[3], destID[3];
        SetDestIDSrcID(event, srcID, destID, _ExtraSettings.ID1);
        _rf->sendCommand(OrconHigh, srcID, destID);
        _State       = 103;
        _Timer       = 0;
        _LastIDindex = 0;
        _rf->initReceive();
        PluginWriteLog(F("Orcon high speed"));
        break;
      }
      case 104: // Fan auto
      {
        uint8_t srcID[3], destID[3];
        SetDestIDSrcID(event, srcID, destID, _ExtraSettings.ID1);
        _rf->sendCommand(OrconAuto, srcID, destID);
        _State       = 104;
        _Timer       = 0;
        _LastIDindex = 0;
        _rf->initReceive();
        PluginWriteLog(F("Orcon auto speed"));
        break;
      }
      case 110: //  Timer 12*60 minutes @ speed 0
      {
        uint8_t srcID[3], destID[3];
        SetDestIDSrcID(event, srcID, destID, _ExtraSettings.ID1);
        _rf->sendCommand(OrconTimer0, srcID, destID);
        _State       = 110;
        _Timer       = PLUGIN_118_OrconTime0;
        _LastIDindex = 0;
        _rf->initReceive();
        PluginWriteLog(F("Orcon Timer 0"));
        break;
      }
      case 111: //  Timer 60 minutes @ speed 1
      {
        uint8_t srcID[3], destID[3];
        SetDestIDSrcID(event, srcID, destID, _ExtraSettings.ID1);
        _rf->sendCommand(OrconTimer1, srcID, destID);
        _State       = 111;
        _Timer       = PLUGIN_118_OrconTime1;
        _LastIDindex = 0;
        _rf->initReceive();
        PluginWriteLog(F("Orcon Timer 1"));
        break;
      }
      case 112: //  Timer 13*60 minutes @ speed 2
      {
        uint8_t srcID[3], destID[3];
        SetDestIDSrcID(event, srcID, destID, _ExtraSettings.ID1);
        _rf->sendCommand(OrconTimer2, srcID, destID);
        _State       = 112;
        _Timer       = PLUGIN_118_OrconTime2;
        _LastIDindex = 0;
        _rf->initReceive();
        PluginWriteLog(F("Orcon Timer 2"));
        break;
      }
      case 113: //  Timer 60 minutes @ speed 3
      {
        uint8_t srcID[3], destID[3];
        SetDestIDSrcID(event, srcID, destID, _ExtraSettings.ID1);
        _rf->sendCommand(OrconTimer3, srcID, destID);
        _State       = 113;
        _Timer       = PLUGIN_118_OrconTime3;
        _LastIDindex = 0;
        _rf->initReceive();
        PluginWriteLog(F("Orcon Timer 3"));
        break;
      }
      case 114:
      {
        uint8_t srcID[3], destID[3];
        SetDestIDSrcID(event, srcID, destID, _ExtraSettings.ID1);
        _rf->sendCommand(OrconAutoCO2, srcID, destID);
        _State       = 114;
        _Timer       = PLUGIN_118_OrconTime3;
        _LastIDindex = 0;
        _rf->initReceive();
        PluginWriteLog(F("Orcon Auto CO2"));
        break;
      }
      # else // if P118_FEATURE_ORCON
      case 100:
      case 101:
      case 102:
      case 103:
      case 104:
      case 110:
      case 111:
      case 112:
      case 113:
      case 114:
        PluginWriteLog(F("Orcon support not included!"));
        success = false;
        break;
      # endif // if P118_FEATURE_ORCON
      default:
      {
        PluginWriteLog(F("INVALID"));
        success = false;
        break;
      }
    }
  }
  return success;
}

void P118_data_struct::ITHOcheck() {
  bool _dbgLog = _log
  # ifndef BUILD_NO_DEBUG
                 && loglevelActiveFor(LOG_LEVEL_DEBUG)
  # endif // ifndef BUILD_NO_DEBUG
  ;

  # ifndef BUILD_NO_DEBUG

  if (_dbgLog) {
    addLog(LOG_LEVEL_DEBUG, "ITHO: RF signal received"); // All logs statements contain if-statement to disable logging to
  }                                                      // reduce log clutter when many RF sources are present
  # endif // ifndef BUILD_NO_DEBUG

  if (_rf->checkForNewPacket()) {
    const IthoCommand cmd = _rf->getLastCommand();
    const String Id       = _rf->getLastIDstr();

    if (_rfLog && loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, strformat(
                   F("ITHO: Received from ID: %s ; raw cmd: %d"),
                   Id.c_str(),  cmd));
    }

    // Move check here to prevent function calling within ISR
    byte index = 0;

    if (Id == _ExtraSettings.ID1) {
      index = 1;
    }
    else if (Id == _ExtraSettings.ID2) {
      index = 2;
    }
    else if (Id == _ExtraSettings.ID3) {
      index = 3;
    }

    String log;

    if (index > 0) {
      if (_dbgLog) {
        log += strformat(F("Command received from remote-ID: %s , command: "), Id.c_str());
      }

      switch (cmd) {
        case IthoUnknown:

          if (_dbgLog) { log += F("unknown"); }
          break;
        case IthoStandby:
        case DucoStandby:

          if (_dbgLog) { log += F("standby"); }
          _State       = 0;
          _Timer       = 0;
          _LastIDindex = index;
          break;
        case IthoLow:
        case DucoLow:

          if (_dbgLog) { log += F("low"); }
          _State       = 1;
          _Timer       = 0;
          _LastIDindex = index;
          break;
        case IthoMedium:
        case DucoMedium:

          if (_dbgLog) { log += F("medium"); }
          _State       = 2;
          _Timer       = 0;
          _LastIDindex = index;
          break;
        case IthoHigh:
        case DucoHigh:

          if (_dbgLog) { log += F("high"); }
          _State       = 3;
          _Timer       = 0;
          _LastIDindex = index;
          break;
        case IthoFull:

          if (_dbgLog) { log += F("full"); }
          _State       = 4;
          _Timer       = 0;
          _LastIDindex = index;
          break;
        case IthoTimer1:

          if (_dbgLog) { log += F("timer1"); }
          _State       = 13;
          _Timer       = PLUGIN_118_Time1;
          _LastIDindex = index;
          break;
        case IthoTimer2:

          if (_dbgLog) { log += F("timer2"); }
          _State       = 23;
          _Timer       = PLUGIN_118_Time2;
          _LastIDindex = index;
          break;
        case IthoTimer3:

          if (_dbgLog) { log += F("timer3"); }
          _State       = 33;
          _Timer       = PLUGIN_118_Time3;
          _LastIDindex = index;
          break;
        case IthoJoin:

          if (_dbgLog) { log += F("join"); }
          break;
        case IthoLeave:

          if (_dbgLog) { log += F("leave"); }
          break;
        # if P118_FEATURE_ORCON
        case OrconStandBy:

          if (_dbgLog) { log += F("Orcon standby"); }
          _State       = 100;
          _Timer       = 0;
          _LastIDindex = index;
          break;
        case OrconLow:

          if (_dbgLog) { log += F("Orcon low"); }
          _State       = 101;
          _Timer       = 0;
          _LastIDindex = index;
          break;
        case OrconMedium:

          if (_dbgLog) { log += F("Orcon medium"); }
          _State       = 102;
          _Timer       = 0;
          _LastIDindex = index;
          break;
        case OrconHigh:

          if (_dbgLog) { log += F("Orcon high"); }
          _State       = 103;
          _Timer       = 0;
          _LastIDindex = index;
          break;
        case OrconAuto:

          if (_dbgLog) { log += F("Orcon auto"); }
          _State       = 104;
          _Timer       = 0;
          _LastIDindex = index;
          break;
        case OrconTimer0:

          if (_dbgLog) { log += F("Orcon Timer0"); }
          _State       = 110;
          _Timer       = PLUGIN_118_OrconTime0;
          _LastIDindex = index;
          break;
        case OrconTimer1:

          if (_dbgLog) { log += F("Orcon Timer1"); }
          _State       = 111;
          _Timer       = PLUGIN_118_OrconTime1;
          _LastIDindex = index;
          break;
        case OrconTimer2:

          if (_dbgLog) { log += F("Orcon Timer2"); }
          _State       = 112;
          _Timer       = PLUGIN_118_OrconTime2;
          _LastIDindex = index;
          break;
        case OrconTimer3:

          if (_dbgLog) { log += F("Orcon Timer3"); }
          _State       = 113;
          _Timer       = PLUGIN_118_OrconTime3;
          _LastIDindex = index;
          break;
        case OrconAutoCO2:

          if (_dbgLog) { log += F("Orcon AutoCO2"); }
          _State       = 114;
          _Timer       = 0;
          _LastIDindex = index;
          break;
        # else // if P118_FEATURE_ORCON
        case OrconStandBy:
        case OrconLow:
        case OrconMedium:
        case OrconHigh:
        case OrconAuto:
        case OrconTimer0:
        case OrconTimer1:
        case OrconTimer2:
        case OrconTimer3:
        case OrconAutoCO2:
          break;
        # endif // if P118_FEATURE_ORCON
      }
    } else {
      if (_dbgLog) {
        log += strformat(F("Device-ID: %s IGNORED"), Id.c_str());
      }
    }

    # ifndef BUILD_NO_DEBUG

    if (_dbgLog) {
      addLogMove(LOG_LEVEL_DEBUG, log);
    }
    # endif // ifndef BUILD_NO_DEBUG
  }
}

void P118_data_struct::PublishData(struct EventStruct *event) {
  UserVar.setFloat(event->TaskIndex, 0, _State);
  UserVar.setFloat(event->TaskIndex, 1, _Timer);
  UserVar.setFloat(event->TaskIndex, 2, _LastIDindex);

  # ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLog(LOG_LEVEL_DEBUG, concat(F("State: "), formatUserVarNoCheck(event, 0)));
    addLog(LOG_LEVEL_DEBUG, concat(F("Timer: "), formatUserVarNoCheck(event, 1)));
    addLog(LOG_LEVEL_DEBUG, concat(F("LastIDindex: "), formatUserVarNoCheck(event, 2)));
  }
  # endif // ifndef BUILD_NO_DEBUG
}

void P118_data_struct::PluginWriteLog(const String& command) {
  String log = concat(F("Send Itho"
                        # if P118_FEATURE_ORCON
                        "/Orcon"
                        # endif // if P118_FEATURE_ORCON
                        " command for: "), command);

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, log);
  }
  printWebString += log;
}

// **************************************************************************/
// Interrupt handler
// **************************************************************************/
void P118_data_struct::ISR_ithoCheck(P118_data_struct *self) {
  self->_Int = true;
}

# if P118_FEATURE_ORCON

/**
 * Orcon specific: Set Destination ID
 */
void P118_data_struct::SetDestIDSrcID(struct EventStruct *event, uint8_t (& srcID)[3], uint8_t (& destID)[3], char (& tmpTmpID)[9])
{
  destID[0] = PCONFIG(1) - 0;
  destID[1] = PCONFIG(2) - 0;
  destID[2] = PCONFIG(3) - 0;

  const char *delimiter = ",";
  char *token;

  // char tmpID[9] = PLUGIN_118_ExtraSettings.ID1; // copy needed otherwise we modify PLUGIN_118_ExtraSettings.ID1 itself
  char tmpID[9];

  memcpy(tmpID, tmpTmpID, 9);
  token = strtok(tmpID, delimiter);     // select the first part

  if (token) {
    srcID[0] = strtol(token, NULL, 16); // convert first string part (hex) to int
  } else {
    srcID[0] = 0;
  }
  token = strtok(NULL, delimiter);

  if (token) {
    srcID[1] = strtol(token, NULL, 16); // convert first string part (hex) to int
  } else {
    srcID[1] = 0;
  }
  token = strtok(NULL, delimiter);

  if (token) {
    srcID[2] = strtol(token, NULL, 16); // convert first string part (hex) to int
  } else {
    srcID[2] = 0;
  }

  #  ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    String log = F("srcID: ");
    log += static_cast<int>(srcID[0]);
    log += ',';
    log += static_cast<int>(srcID[1]);
    log += ',';
    log += static_cast<int>(srcID[2]);
    log += F(" destID: ");
    log += static_cast<int>(destID[0]);
    log += ',';
    log += static_cast<int>(destID[1]);
    log += ',';
    log += static_cast<int>(destID[2]);
    addLogMove(LOG_LEVEL_DEBUG, log);
  }
  #  endif // ifndef BUILD_NO_DEBUG
}

# endif // if P118_FEATURE_ORCON

#endif // ifdef USES_P118

#include "../PluginStructs/P170_data_struct.h"

#ifdef USES_P170

/**************************************************************************
* Constructor
**************************************************************************/
P170_data_struct::P170_data_struct(uint8_t level,
                                   bool    log)
  : _level(level), _log(log) {}

/**************************************************************************
* Start the plugin
**************************************************************************/
bool P170_data_struct::init(struct EventStruct *event) {
  if (!Settings.CheckI2Cdevice() || (0 == I2C_wakeup(P170_I2C_ADDRESS_HIGH))) { // Also fail if second address isn't found
    initialized = true;

    if (0 == Settings.TaskDeviceTimer[event->TaskIndex]) {
      Scheduler.schedule_task_device_timer(event->TaskIndex, millis() + P170_DEFAULT_INTERVAL); // Schedule
    }
  } else {
    addLog(LOG_LEVEL_ERROR, F("LQLVL: Initialization error."));
  }
  return isInitialized();
}

/*****************************************************
* plugin_read
*****************************************************/
bool P170_data_struct::plugin_read(struct EventStruct *event)           {
  bool success = false;

  if (isInitialized()) {
    memset(data, 0, sizeof(data));

    if (readLowSteps() &&
        readHighSteps()) {
      steps = getSteps();
      level = steps * P170_MM_PER_STEP;

      UserVar.setFloat(event->TaskIndex, 0, level);
      UserVar.setFloat(event->TaskIndex, 1, steps);

      if ((P170_TRIGGER_LOW_LEVEL > 0) && (level < P170_TRIGGER_LOW_LEVEL)) {
        if (!P170_TRIGGER_ONCE || !lowlevel) {
          eventQueue.add(event->TaskIndex, F("LowLevel"), level);
          lowlevel = true;
        }

        if (level >= P170_TRIGGER_LOW_LEVEL) {
          lowlevel = false;
        }
      }

      if ((P170_TRIGGER_HIGH_LEVEL > 0) && (level > P170_TRIGGER_HIGH_LEVEL)) {
        if (!P170_TRIGGER_ONCE || !highlevel) {
          eventQueue.add(event->TaskIndex, F("HighLevel"), level);
          highlevel = true;
        }

        if (level <= P170_TRIGGER_HIGH_LEVEL) {
          highlevel = false;
        }
      }

      if (0 == Settings.TaskDeviceTimer[event->TaskIndex]) {
        Scheduler.schedule_task_device_timer(event->TaskIndex, millis() + P170_DEFAULT_INTERVAL); // Schedule again
      } else {
        success = true;
      }
    } else {
      addLog(LOG_LEVEL_ERROR, F("LQLVL: Read error."));
    }
  }

  return success;
}

/*****************************************************
* count the number of steps
*****************************************************/
uint8_t P170_data_struct::getSteps() {
  uint8_t result = 0;
  uint8_t max    = 0;

  for (int i = 0; i < P170_TOTAL_STEPS; ++i) {
    if (data[i] >= _level) {
      ++result;
    }

    if (data[i] > max) {
      max = data[i];
    }
  }

  if (_log) {
    String log;
    log.reserve(80);

    for (int i = 0; i < P170_TOTAL_STEPS; ++i) {
      log += data[i];
      log += ',';
    }
    addLog(LOG_LEVEL_INFO, strformat(F("LQLVL: Max level: %d, data: %s result: %d"), max, log.c_str(), result));
  }

  return result;
}

/*****************************************************
* read high part of the data (12 bytes)
*****************************************************/
bool P170_data_struct::readHighSteps() {
  const uint32_t start   = millis();
  const uint8_t  _addr   = P170_I2C_ADDRESS_HIGH;
  const uint8_t  _offset = P170_LOW_STEPS;
  const uint8_t  _size   = P170_HIGH_STEPS;
  bool success           = false;

  Wire.requestFrom(_addr, _size);

  while (_size != Wire.available() && 20 < timePassedSince(start)) {} // Limit waiting time when no device connected

  if (_size == Wire.available()) {
    success = true;

    for (uint8_t i = 0; i < _size; i++) {
      data[i + _offset] = Wire.read(); // receive a byte as character
    }
  }
  return success;
}

/*****************************************************
* read low part of the data (8 bytes)
*****************************************************/
bool P170_data_struct::readLowSteps() {
  const uint32_t start   = millis();
  const uint8_t  _addr   = P170_I2C_ADDRESS;
  const uint8_t  _offset = 0;
  const uint8_t  _size   = P170_LOW_STEPS;
  bool success           = false;

  Wire.requestFrom(_addr, _size);

  while (_size != Wire.available() && 20 < timePassedSince(start)) {} // Limit waiting time when no device connected

  if (_size == Wire.available()) {
    success = true;

    for (uint8_t i = 0; i < _size; i++) {
      data[i + _offset] = Wire.read(); // receive a byte as character
    }
  }
  return success;
}

#endif // ifdef USES_P170

#include "../PluginStructs/P121_data_struct.h"

#ifdef USES_P121

// Needed also here for PlatformIO's library finder as the .h file
// is in a directory which is excluded in the src_filter
# include <Adafruit_Sensor.h>
# include <Adafruit_HMC5883_U.h>

bool P121_data_struct::begin(int taskid)
{
  if (!initialized) {
    mag         = Adafruit_HMC5883_Unified(taskid);
    initialized = mag.begin();

    if (initialized) {
      // Set up oversampling and filter initialization
      sensor_t sensor;
      mag.getSensor(&sensor);
      # ifndef BUILD_NO_DEBUG

      if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
        addLogMove(LOG_LEVEL_DEBUG,
                   strformat(F("HMC5883_U: Sensor: %s, Driver Ver: %d, Unique ID: %d, Max Value: %.2f, Min Value: %.2f, Resolution: %.2f"),
                             sensor.name,
                             sensor.version,
                             sensor.sensor_id,
                             sensor.max_value,
                             sensor.min_value,
                             sensor.resolution));
      }
      # endif // ifndef BUILD_NO_DEBUG
    }
  }

  return initialized;
}

#endif // ifdef USES_P121

#include "../PluginStructs/P070_data_struct.h"

#ifdef USES_P070


P070_data_struct::~P070_data_struct() {
  delete Plugin_070_pixels;
  Plugin_070_pixels = nullptr;
}

void P070_data_struct::reset() {
  delete Plugin_070_pixels;
  Plugin_070_pixels = nullptr;
}

void P070_data_struct::init(struct EventStruct *event) {
  if (nullptr == Plugin_070_pixels)
  {
    Plugin_070_pixels = new (std::nothrow) NeoPixelBus_wrapper(NUMBER_LEDS, CONFIG_PIN1, NEO_GRB + NEO_KHZ800);

    if (Plugin_070_pixels == nullptr) {
      return;
    }
    Plugin_070_pixels->begin(); // This initializes the NeoPixel library.
  }
  set(event);
}

void P070_data_struct::set(struct EventStruct *event) {
  display_enabled       = PCONFIG(0);
  brightness            = PCONFIG(1);
  brightness_hour_marks = PCONFIG(2);
  offset_12h_mark       = PCONFIG(3);
  thick_12_mark         = PCONFIG(4);
}

void P070_data_struct::Clock_update()
{
  clearClock();              // turn off the LEDs

  if (display_enabled > 0) { // if the display is enabled, calculate the LEDs to turn on
    const int Hours   = node_time.hour();
    const int Minutes = node_time.minute();
    const int Seconds = node_time.second();
    timeToStrip(Hours, Minutes, Seconds);
  }
  Plugin_070_pixels->show(); // This sends the updated pixel color to the hardware.
}

void P070_data_struct::calculateMarks()
{ // generate a list of the LEDs that have hour marks
  for (int i = 0; i < 12; ++i) {
    marks[i] = 5 * i + (offset_12h_mark % 5);
  }

  if (thick_12_mark) {
    if (offset_12h_mark == 0) {
      marks[12] = 1;
      marks[13] = 59;
    }
    else if (offset_12h_mark == 59) {
      marks[12] = 0;
      marks[13] = 58;
    }
    else {
      marks[12] = offset_12h_mark + 1;
      marks[13] = offset_12h_mark - 1;
    }
  }
  else {
    marks[12] = 255;
    marks[13] = 255;
  }
}

void P070_data_struct::clearClock() {
  for (int i = 0; i < NUMBER_LEDS; ++i) {
    Plugin_070_pixels->setPixelColor(i, Plugin_070_pixels->Color(0, 0, 0));
  }
}

void P070_data_struct::timeToStrip(int hours, int minutes, int seconds) {
  if (hours > 11) { hours = hours - 12; }
  hours = (hours * 5) + (minutes / 12) + offset_12h_mark; // make the hour hand move each 12 minutes and apply the offset

  if (hours > 59) { hours = hours - 60; }
  minutes = minutes + offset_12h_mark;                    // apply offset to minutes

  if (minutes > 59) { minutes = minutes - 60; }
  seconds = seconds + offset_12h_mark;                    // apply offset to seconds

  if (seconds > 59) { seconds = seconds - 60; }

  for (int i = 0; i < 14; ++i) {                                                                      // set the hour marks as white;
    if ((marks[i] != hours) && (marks[i] != minutes) && (marks[i] != seconds) && (marks[i] != 255)) { // do not draw a mark there is a clock
                                                                                                      // hand in that position
      Plugin_070_pixels->setPixelColor(marks[i],
                                       Plugin_070_pixels->Color(brightness_hour_marks, brightness_hour_marks, brightness_hour_marks));
    }
  }
  uint32_t currentColor;
  uint8_t  r_val, g_val;                  // , b_val;

  for (int i = 0; i < NUMBER_LEDS; ++i) { // draw the clock hands, adding the colors together
    if (i == hours) {                     // hours hand is RED
      Plugin_070_pixels->setPixelColor(i, Plugin_070_pixels->Color(brightness, 0, 0));
    }

    if (i == minutes) { // minutes hand is GREEN
      currentColor = Plugin_070_pixels->getPixelColor(i);
      r_val        = (uint8_t)(currentColor >> 16);
      Plugin_070_pixels->setPixelColor(i, Plugin_070_pixels->Color(r_val, brightness, 0));
    }

    if (i == seconds) { // seconds hand is BLUE
      currentColor = Plugin_070_pixels->getPixelColor(i);
      r_val        = (uint8_t)(currentColor >> 16);
      g_val        = (uint8_t)(currentColor >>  8);
      Plugin_070_pixels->setPixelColor(i, Plugin_070_pixels->Color(r_val, g_val, brightness));
    }
  }
}

#endif // ifdef USES_P070

#include "../PluginStructs/P047_data_struct.h"


#ifdef USES_P047

// **************************************************************************/
// Constructor
// **************************************************************************/
P047_data_struct::P047_data_struct(uint8_t address,
                                   uint8_t model) :
  _address(address), _model(static_cast<P047_SensorModels>(model)) {
  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO,
           strformat(F("SoilMoisture: Initializing sensor: %s, version: 0x%x"), String(toString(_model)).c_str(), getVersion()));
  }
}

const __FlashStringHelper* toString(P047_SensorModels sensor) {
  switch (sensor) {
    case P047_SensorModels::CatnipMiceuz: return F("Catnip electronics/miceuz");
    case P047_SensorModels::BeFlE: return F("BeFlE v2.2");
    # if P047_FEATURE_ADAFRUIT
    case P047_SensorModels::Adafruit: return F("Adafruit (4026)");
    # endif // if P047_FEATURE_ADAFRUIT
    # if P047_FEATURE_BEFLE_V3
    case P047_SensorModels::BeFlEv3: return F("BeFlE v3.x");
    # endif // if P047_FEATURE_BEFLE_V3
  }
  return F("");
}

// **************************************************************************/
// PLUGIN_READ
// **************************************************************************/
bool P047_data_struct::plugin_read(struct EventStruct *event) {
  bool success = false;

  if (P047_SENSOR_SLEEP && (P047_ReadMode::ReadStarted != _readMode)) {
    // wake sensor when not reading
    setToSleep(false);   // Wake sensor
    delayBackground(20); // Seems acceptable to have this relatively short delay
    # ifndef BUILD_NO_DEBUG
    addLog(LOG_LEVEL_DEBUG, F("SoilMoisture->wake"));
    # endif // ifndef BUILD_NO_DEBUG
  }

  switch (_readMode) {
    case P047_ReadMode::NotReading:
    {
      if (P047_CHECK_VERSION) {
        // get sensor version to check if sensor is present
        _sensorVersion = getVersion();

        if (!((_sensorVersion == 0x22) || (_sensorVersion == 0x23) || (_sensorVersion == 0x24) || (_sensorVersion == 0x25) ||
              (_sensorVersion == 0x26))) {
          // invalid sensor
          addLog(LOG_LEVEL_ERROR, F("SoilMoisture: Bad Version, no Sensor?"));
          resetSensor();
        }
      }

      // check if we want to change the sensor address
      if (P047_CHANGE_ADDR && (P047_I2C_ADDR != P047_NEW_ADDR) && (0 != P047_NEW_ADDR)) {
        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          addLog(LOG_LEVEL_INFO, strformat(F("SoilMoisture: Change Address: 0x%02x -> 0x%02x"), P047_I2C_ADDR, P047_NEW_ADDR));
        }

        if (changeAddress(P047_NEW_ADDR)) {
          P047_I2C_ADDR = P047_NEW_ADDR;
        }
        P047_CHANGE_ADDR = false;
      }

      // start light measurement
      startMeasure();

      _readMode = P047_ReadMode::ReadStarted;

      // 2 s delay ...we need this delay, otherwise we get only the last reading...
      // delayBackground(2000);
      // Schedule the next task-run 2 seconds from now
      Scheduler.schedule_task_device_timer(event->TaskIndex, millis() + 2000);
      break;
    }

    case P047_ReadMode::ReadStarted:
    {
      if (measurementReady()) {
        # if P047_FEATURE_ADAFRUIT

        // Define range
        float moisture_min = 1.0f;
        float moisture_max = 800.0f;

        if (P047_MODEL_ADAFRUIT == _model) {
          moisture_min = 200.0f;
          moisture_max = 2000.0f;
        }
        # else // if P047_FEATURE_ADAFRUIT

        // Define range
        const float moisture_min = 1.0f;
        const float moisture_max = 800.0f;
        # endif // if P047_FEATURE_ADAFRUIT

        // Get the values
        const float temperature = readTemperature();
        const float moisture    = readMoisture();
        const float light       = readLight();

        if ((temperature > 100.0f) || (temperature < -40.0f) ||
            (moisture > moisture_max) || (moisture < moisture_min) ||
            (light > 65535.0f) || (light < 0.0f)) {
          addLog(LOG_LEVEL_INFO, F("SoilMoisture: Bad Reading, resetting Sensor..."));
          resetSensor();
        }
        else
        {
          UserVar.setFloat(event->TaskIndex, 0, temperature);
          UserVar.setFloat(event->TaskIndex, 1, moisture);
          UserVar.setFloat(event->TaskIndex, 2, light);

          if (loglevelActiveFor(LOG_LEVEL_INFO)) {
            String log = strformat(F("SoilMoisture: Address: 0x%02x"), P047_I2C_ADDR);

            if (P047_CHECK_VERSION) {
              log += strformat(F(" Version: 0x%02x"), _sensorVersion);
            }
            addLogMove(LOG_LEVEL_INFO, log);
            addLogMove(LOG_LEVEL_INFO, concat(F("SoilMoisture: Temperature: "), formatUserVarNoCheck(event, 0)));
            addLogMove(LOG_LEVEL_INFO, strformat(F("SoilMoisture: Moisture: %.0f"), moisture));

            if (P047_MODEL_CATNIP == _model) {
              addLogMove(LOG_LEVEL_INFO, concat(F("SoilMoisture: Light: "), formatUserVarNoCheck(event, 2)));
            }
          }

          if (P047_SENSOR_SLEEP) {
            // send sensor to sleep
            setToSleep(true);
            # ifndef BUILD_NO_DEBUG
            addLog(LOG_LEVEL_DEBUG, F("SoilMoisture->sleep"));
            # endif // ifndef BUILD_NO_DEBUG
          }
          success = true;
        }
        _readMode = P047_ReadMode::NotReading;
      }

      break;
    }
  }
  return success;
}

// **************************************************************************/
// Read temperature
// **************************************************************************/
float P047_data_struct::readTemperature() {
  if (P047_MODEL_CATNIP == _model) {
    return I2C_readS16_reg(_address, P047_CATNIP_GET_TEMPERATURE) / 10.0f;
  } else if (P047_MODEL_BEFLE == _model
             # if P047_FEATURE_BEFLE_V3
             || P047_MODEL_BEFLE_V3 == _model
             # endif // if P047_FEATURE_BEFLE_V3
             ) {
    return static_cast<float>(I2C_read8_reg(_address, P047_BEFLE_GET_TEMPERATURE));
    # if P047_FEATURE_ADAFRUIT
  } else if (P047_MODEL_ADAFRUIT == _model) {
    uint8_t buf[4]{};
    bool    isOk;
    const uint8_t toRead = 4;

    I2C_write8_reg(_address, P047_ADAFRUIT_STATUS_BASE, P047_ADAFRUIT_GET_TEMPERATURE);
    delayMicroseconds(1000);

    if (Wire.requestFrom(_address, toRead) == toRead) {
      for (uint8_t b = 0; b < toRead; ++b) {
        buf[b] = Wire.read();
      }
    }
    const int32_t rTemp = ((uint32_t)buf[0] << 24) | ((uint32_t)buf[1] << 16) |
                          ((uint32_t)buf[2] << 8) | (uint32_t)buf[3];

    return (1.0 / (1UL << 16)) * rTemp;
    # endif // if P047_FEATURE_ADAFRUIT
  }
  return -273.15f; // 0 K = error
}

// **************************************************************************/
// Read light
// **************************************************************************/
float P047_data_struct::readLight() {
  if (P047_MODEL_CATNIP == _model) {
    return I2C_read16_reg(_address, P047_CATNIP_GET_LIGHT);
  }

  // Not supported by BeFlE or Adafruit sensors
  return 0.0f;
}

// **************************************************************************/
// Read moisture
// **************************************************************************/
unsigned int P047_data_struct::readMoisture() {
  if (P047_MODEL_CATNIP == _model) {
    return I2C_read16_reg(_address, P047_CATNIP_GET_CAPACITANCE);
  } else if (P047_MODEL_BEFLE == _model
             # if P047_FEATURE_BEFLE_V3
             || P047_MODEL_BEFLE_V3 == _model
             # endif // if P047_FEATURE_BEFLE_V3
             ) {
    return I2C_read16_reg(_address, P047_BEFLE_GET_CAPACITANCE) >> 8; // Get averaged value
    # if P047_FEATURE_ADAFRUIT
  } else if (P047_MODEL_ADAFRUIT == _model) {
    uint8_t  p    = 0;
    uint16_t ret  = 65535;
    bool     isOk = false;

    for (uint8_t retry = 0; retry < 5; ++retry) {
      I2C_write8_reg(_address, P047_ADAFRUIT_TOUCH_BASE, P047_ADAFRUIT_GET_CAPACITANCE + p);
      delayMicroseconds(3000 + retry * 1000);

      ret = I2C_read16(_address, &isOk);

      if (isOk) {
        break;
      }
    }
    return ret;

    # endif // if P047_FEATURE_ADAFRUIT
  }
  return 0;
}

// Read Sensor Version
uint32_t P047_data_struct::getVersion() {
  if (P047_MODEL_CATNIP == _model) {
    return I2C_read8_reg(_address, P047_CATNIP_GET_VERSION);
    # if P047_FEATURE_ADAFRUIT
  } else if (P047_MODEL_ADAFRUIT == _model) {
    uint8_t buf[4]{};
    bool    isOk         = false;
    const uint8_t toRead = 4;

    I2C_write8_reg(_address, P047_ADAFRUIT_STATUS_BASE, P047_ADAFRUIT_GET_VERSION);

    if (Wire.requestFrom(_address, toRead) == toRead) {
      for (uint8_t b = 0; b < toRead; ++b) {
        buf[b] = Wire.read();
      }
    }
    uint32_t ret = ((uint32_t)buf[0] << 24) | ((uint32_t)buf[1] << 16) |
                   ((uint32_t)buf[2] << 8) | (uint32_t)buf[3];
    return ret;
    # endif // if P047_FEATURE_ADAFRUIT
    # if P047_FEATURE_BEFLE_V3
  } else if (P047_MODEL_BEFLE_V3 == _model) {
    uint8_t buf[4]{};

    I2C_write8(_address, P047_BEFLE_V3_GET_VERSION);
    delay(1);

    if (Wire.requestFrom(_address, 4) == 4) {
      for (int b = 0; b < 4; ++b) {
        buf[b] = Wire.read();
      }
    }

    if ((buf[0] == 0x76) /*'v'*/ && (buf[2] == 0x2E) /*'.'*/) {                      // Returns "v3.4" for version 3.4
      uint32_t ret = (((uint32_t)buf[1] - 0x30) * 0x10) | ((uint32_t)buf[3] - 0x30); // Return 0x34 for 3.4
      return ret;
    } else {
      return 0x22;                                                                   // Fallback to HW version 2.2
    }
    # endif // if P047_FEATURE_ADAFRUIT
  }

  // Not supported by BeFlE v2 sensor
  return 0;
}

/*----------------------------------------------------------------------*
* Change I2C address of the sensor to the provided address (1..127)    *
* and do a reset after it in order for the new address to become       *
* effective if second parameter is true.                               *
* Method returns true if the new address is set successfully on sensor.*
*----------------------------------------------------------------------*/
bool P047_data_struct::changeAddress(uint8_t new_i2cAddr) {
  uint8_t command = 0;

  if (P047_MODEL_CATNIP == _model) {
    command = P047_CATNIP_SET_ADDRESS;
  } else
  if (P047_MODEL_BEFLE == _model) {
    command     = P047_BEFLE_SET_ADDRESS;
    new_i2cAddr = (new_i2cAddr << 1) & 0xFE;
    # if P047_FEATURE_ADAFRUIT
  } else
  if (P047_MODEL_ADAFRUIT == _model) {
    return true; // Is set in hardware
    # endif // if P047_FEATURE_ADAFRUIT
    # if P047_FEATURE_BEFLE_V3
  } else
  if (P047_MODEL_BEFLE_V3 == _model) {
    command     = P047_BEFLE_SET_ADDRESS;
    new_i2cAddr = (new_i2cAddr << 1) & 0xFE;
    # endif // if P047_FEATURE_BEFLE_V3
  }
  I2C_write8_reg(_address, command, new_i2cAddr);
  I2C_write8_reg(_address, command, new_i2cAddr);

  if (resetSensor()) {
    delayBackground(1000); // FIXME TD-er: Why is this blocking call needed?
  }
  _address = new_i2cAddr;
  return checkAddress(new_i2cAddr);
}

bool P047_data_struct::checkAddress(uint8_t new_i2cAddr) {
  if (P047_MODEL_CATNIP == _model) {
    return I2C_read8_reg(_address, P047_CATNIP_GET_ADDRESS) == new_i2cAddr;
  }

  // Not supported by BeFlE or Adafruit sensors
  return true;
}

// **************************************************************************/
// Reset sensor
// **************************************************************************/
bool P047_data_struct::resetSensor() {
  if (P047_MODEL_CATNIP == _model) {
    I2C_write8(_address, P047_CATNIP_RESET);
    return true;
  }

  // Not supported by BeFlE or Adafruit sensors
  return false;
}

void P047_data_struct::setToSleep(bool sleep) {
  if ((P047_MODEL_CATNIP == _model)) {
    if (sleep) {
      I2C_write8(_address, P047_CATNIP_SLEEP);
    } else {
      getVersion(); // Standard method to wake the sensor
    }
    # if P047_FEATURE_BEFLE_V3
  } else
  if (P047_MODEL_BEFLE_V3 == _model) {
    I2C_write8_reg(_address, P047_BEFLE_V3_SLEEP, sleep); // Set low-power mode
    # endif // if P047_FEATURE_BEFLE_V3
  }

  // Not supported by BeFlE v2 or Adafruit sensors
}

void P047_data_struct::startMeasure() {
  if (P047_MODEL_CATNIP == _model) {
    I2C_write8(_address, P047_CATNIP_MEASURE_LIGHT);
    # if P047_FEATURE_BEFLE_V3
  } else
  if (P047_MODEL_BEFLE_V3 == _model) {
    I2C_write8_reg(_address, P047_BEFLE_V3_START_MEASURE, 100); // Read 100 samples
    # endif // if P047_FEATURE_BEFLE_V3
  }

  // Not supported by BeFlE v2 or Adafruit sensors
}

/**
 * Check if a measurement is available
 */
bool P047_data_struct::measurementReady() {
  if (P047_MODEL_CATNIP == _model) {
    const uint8_t status = I2C_read8_reg(_address, P047_BEFLE_GET_BUSY);
    return status == 0;
    # if P047_FEATURE_BEFLE_V3
  } else
  if (P047_MODEL_BEFLE_V3 == _model) {
    const uint8_t status = I2C_read8_reg(_address, P047_BEFLE_GET_BUSY);
    return (status & 0x01) == 0;
    # endif // if P047_FEATURE_BEFLE_V3
  }

  // Not supported by BeFlE v2 or Adafruit sensors
  return true;
}

#endif // ifdef USES_P047

#include "../PluginStructs/P137_data_struct.h"

#ifdef USES_P137

# ifdef ESP32

int16_t P137_settingToValue(uint16_t data,
                            uint16_t threshold) {
  if (data > threshold) {
    return threshold - data;
  }
  return data;
}

uint16_t P137_valueToSetting(int      data,
                             uint16_t threshold) {
  if (data <= -1) {
    return threshold - data;
  }
  return static_cast<uint16_t>(data);
}

/**
 * Apply default parameters based on P137_CONFIG_PREDEFINED setting.
 * This will be 0 by default, so nothing is changed.
 * P137_CONFIG_DISABLEBITS: (set to 1 to disable that setting)
 * Bit = Feature
 * 0   = LDO2
 * 1   = LDO3
 * 2   = LDOIO
 * 3   = GPIO0
 * 4   = GPIO1
 * 5   = GPIO2
 * 6   = GPIO3
 * 7   = GPIO4
 * 8   = DCDC2
 * 9   = DCDC3
 */
void P137_CheckPredefinedParameters(struct EventStruct *event) {
  if (P137_CONFIG_PREDEFINED > 0) {
    P137_CURRENT_PREDEFINED = P137_CONFIG_PREDEFINED;

    // Set defaults
    for (int i = 0; i < 5; ++i) { // GPI0..4
      P137_SET_GPIO_FLAGS(i, static_cast<uint8_t>(P137_GPIOBootState_e::Default));
    }

    switch (static_cast<P137_PredefinedDevices_e>(P137_CONFIG_PREDEFINED)) {
      case P137_PredefinedDevices_e::M5Stack_StickC: // M5Stack StickC
      {
        P137_REG_DCDC2_LDO2     = (P137_valueToSetting(-1, P137_CONST_MAX_DCDC2) << 16) | P137_valueToSetting(3000, P137_CONST_MAX_LDO);
        P137_REG_DCDC3_LDO3     = (P137_valueToSetting(-1, P137_CONST_MAX_DCDC) << 16) | P137_valueToSetting(3000, P137_CONST_MAX_LDO);
        P137_REG_LDOIO          =  P137_valueToSetting(2800, P137_CONST_MAX_LDOIO);
        P137_CONFIG_DISABLEBITS = 0b1111110000;     // NC pins disabled
        break;
      }
      case P137_PredefinedDevices_e::M5Stack_Core2: // M5Stack Core2
      {
        P137_REG_DCDC2_LDO2     = (P137_valueToSetting(-1, P137_CONST_MAX_DCDC2) << 16) | P137_valueToSetting(3300, P137_CONST_MAX_LDO);
        P137_REG_DCDC3_LDO3     = (P137_valueToSetting(0, P137_CONST_MAX_DCDC) << 16) | P137_valueToSetting(0, P137_CONST_MAX_LDO);
        P137_REG_LDOIO          =  P137_valueToSetting(3300, P137_CONST_MAX_LDOIO);
        P137_CONFIG_DISABLEBITS = 0b0101000000; // NC pins disabled
        // Specials: GPIO1 High = LED off, GPIO4 High = Enable TFT
        P137_SET_GPIO_FLAGS(1, static_cast<uint8_t>(P137_GPIOBootState_e::Output_high));
        P137_SET_GPIO_FLAGS(4, static_cast<uint8_t>(P137_GPIOBootState_e::Output_high));
        break;
      }
      case P137_PredefinedDevices_e::LilyGO_TBeam: // LilyGO T-Beam
      {
        P137_REG_DCDC2_LDO2     = (P137_valueToSetting(-1, P137_CONST_MAX_DCDC2) << 16) | P137_valueToSetting(3300, P137_CONST_MAX_LDO);
        P137_REG_DCDC3_LDO3     = (P137_valueToSetting(3300, P137_CONST_MAX_DCDC) << 16) | P137_valueToSetting(3300, P137_CONST_MAX_LDO);
        P137_REG_LDOIO          =  P137_valueToSetting(3300, P137_CONST_MAX_LDOIO);
        P137_CONFIG_DISABLEBITS = 0b1111111000;          // NC pins disabled
        break;
      }
      case P137_PredefinedDevices_e::M5Stack_StickCPlus: // M5Stack StickC Plus
      {
        P137_REG_DCDC2_LDO2     = (P137_valueToSetting(-1, P137_CONST_MAX_DCDC2) << 16) | P137_valueToSetting(2800, P137_CONST_MAX_LDO);
        P137_REG_DCDC3_LDO3     = (P137_valueToSetting(-1, P137_CONST_MAX_DCDC) << 16) | P137_valueToSetting(3000, P137_CONST_MAX_LDO);
        P137_REG_LDOIO          =  P137_valueToSetting(2800, P137_CONST_MAX_LDOIO);
        P137_CONFIG_DISABLEBITS = 0b1111110000;   // NC pins disabled
        break;
      }
      case P137_PredefinedDevices_e::UserDefined: // User defined
      {
        P137_REG_DCDC2_LDO2     = (P137_valueToSetting(-1, P137_CONST_MAX_DCDC2) << 16) | P137_valueToSetting(3300, P137_CONST_MAX_LDO);
        P137_REG_DCDC3_LDO3     = (P137_valueToSetting(-1, P137_CONST_MAX_DCDC) << 16) | P137_valueToSetting(-1, P137_CONST_MAX_LDO);
        P137_REG_LDOIO          = P137_valueToSetting(-1, P137_CONST_MAX_LDOIO);
        P137_CONFIG_DISABLEBITS = 0b0000000000; // All pins enabled
        break;
      }
      case P137_PredefinedDevices_e::Unselected:
        // Fall through
        break;
    }
  }
}

// **************************************************************************/
// toString: convert P137_valueOptions_e enum to value-name or display-name
// **************************************************************************/
const __FlashStringHelper* toString(const P137_valueOptions_e value,
                                    bool                      displayString) {
  switch (value) {
    case P137_valueOptions_e::None: return displayString ? F("None") : F("none");
    case P137_valueOptions_e::BatteryVoltage: return displayString ? F("Battery voltage") : F("batteryvoltage");
    case P137_valueOptions_e::BatteryDischargeCurrent: return displayString ? F("Battery discharge current") : F("batterydischargecurrent");
    case P137_valueOptions_e::BatteryChargeCurrent: return displayString ? F("Battery charge current") : F("batterychargecurrent");
    case P137_valueOptions_e::BatteryPower: return displayString ? F("Battery power") : F("batterypower");
    case P137_valueOptions_e::AcinVoltage: return displayString ? F("Input voltage") : F("inputvoltage");
    case P137_valueOptions_e::AcinCurrent: return displayString ? F("Input current") : F("inputcurrent");
    case P137_valueOptions_e::VbusVoltage: return displayString ? F("VBus voltage") : F("vbusvoltage");
    case P137_valueOptions_e::VbusCurrent: return displayString ? F("VBus current") : F("vbuscurrent");
    case P137_valueOptions_e::InternalTemperature: return displayString ? F("Internal temperature") : F("internaltemperature");
    case P137_valueOptions_e::ApsVoltage: return displayString ? F("APS Voltage") : F("apsvoltage");
    case P137_valueOptions_e::LDO2: return displayString ? F("LDO2 Voltage") : F("ldo2voltage");
    case P137_valueOptions_e::LDO3: return displayString ? F("LDO3 Voltage") : F("ldo3voltage");
    case P137_valueOptions_e::LDOIO: return displayString ? F("LDOIO Voltage") : F("gpiovoltage");
    case P137_valueOptions_e::DCDC2: return displayString ? F("DCDC2 Voltage") : F("dcdc2voltage");
    case P137_valueOptions_e::DCDC3: return displayString ? F("DCDC3 Voltage") : F("dcdc3voltage");
  }
  return F("Undefined");
}

// **************************************************************************/
// toString: convert P137_GPIOBootState_e enum to string
// **************************************************************************/
const __FlashStringHelper* toString(const P137_GPIOBootState_e value) {
  switch (value) {
    case P137_GPIOBootState_e::Default: return F("Default");
    case P137_GPIOBootState_e::Output_low: return F("Output, low");
    case P137_GPIOBootState_e::Output_high: return F("Output, high");
    case P137_GPIOBootState_e::Input: return F("Input");
    case P137_GPIOBootState_e::PWM: return F("PWM");
  }
  return F("Undefined");
}

// **************************************************************************/
// toString: convert P137_PredefinedDevices_e enum to string
// **************************************************************************/
const __FlashStringHelper* toString(const P137_PredefinedDevices_e device) {
  switch (device) {
    case P137_PredefinedDevices_e::Unselected: return F("Select an option to set default values");
    case P137_PredefinedDevices_e::M5Stack_StickC: return F("M5Stack StickC");
    case P137_PredefinedDevices_e::M5Stack_Core2: return F("M5Stack Core2 (Default)");
    case P137_PredefinedDevices_e::LilyGO_TBeam: return F("LilyGO T-Beam");
    case P137_PredefinedDevices_e::M5Stack_StickCPlus: return F("M5Stack StickC Plus");
    case P137_PredefinedDevices_e::UserDefined: return F("User defined");
  }
  return F("Undefined");
}

#  if FEATURE_MQTT_DISCOVER
int Plugin_137_QueryVType(uint8_t value_nr) {
  const P137_valueOptions_e value = static_cast<P137_valueOptions_e>(value_nr);

  switch (value) {
    case P137_valueOptions_e::None: break;
    case P137_valueOptions_e::BatteryDischargeCurrent:
    case P137_valueOptions_e::BatteryChargeCurrent:
    case P137_valueOptions_e::AcinCurrent:
    case P137_valueOptions_e::VbusCurrent:
      return static_cast<int>(Sensor_VType::SENSOR_TYPE_CURRENT_ONLY);
    case P137_valueOptions_e::BatteryPower:
      return static_cast<int>(Sensor_VType::SENSOR_TYPE_POWER_USG_ONLY);
    case P137_valueOptions_e::InternalTemperature:
      return static_cast<int>(Sensor_VType::SENSOR_TYPE_TEMP_ONLY);
    case P137_valueOptions_e::BatteryVoltage:
    case P137_valueOptions_e::AcinVoltage:
    case P137_valueOptions_e::VbusVoltage:
    case P137_valueOptions_e::ApsVoltage:
    case P137_valueOptions_e::LDO2:
    case P137_valueOptions_e::LDO3:
    case P137_valueOptions_e::LDOIO:
    case P137_valueOptions_e::DCDC2:
    case P137_valueOptions_e::DCDC3:
      return static_cast<int>(Sensor_VType::SENSOR_TYPE_VOLTAGE_ONLY);
  }
  return static_cast<int>(Sensor_VType::SENSOR_TYPE_NONE);
}

#  endif // if FEATURE_MQTT_DISCOVER

// **************************************************************************/
// Constructor
// **************************************************************************/
P137_data_struct::P137_data_struct(struct EventStruct *event) {
  axp192 = new (std::nothrow) I2C_AXP192();                // Default address and I2C Wire object

  if (isInitialized()) {                                   // Functions based on:
    I2C_AXP192_InitDef initDef = {                         // M5Stack StickC / M5Stack Core2 / LilyGO T-Beam
      .EXTEN  = true,                                      // Enable ESP Power
      .BACKUP = true,                                      // Enable RTC power
      .DCDC1  = 3300,                                      // ESP Power      / ESP Power (Fixed)  / OLed
      .DCDC2  = P137_GET_CONFIG_DCDC2,                     // Unused         / Unused             / Unused
      .DCDC3  = P137_GET_CONFIG_DCDC3,                     // Unused         / LCD Backlight      / ESP Power
      .LDO2   = P137_GET_CONFIG_LDO2,                      // Backlight power (3000 mV) / Periferal VDD   / LoRa
      .LDO3   = P137_GET_CONFIG_LDO3,                      // TFT Power (3000 mV)       / Vibration motor / GPS
      .LDOIO  = P137_GET_CONFIG_LDOIO,                     // LDOIO voltage (2800 mV)
      .GPIO0  = static_cast<int>(P137_GET_FLAG_GPIO0 - 1), // Microphone power / Bus pwr enable / Unused
      .GPIO1  = static_cast<int>(P137_GET_FLAG_GPIO1 - 1), // Unused / Sys Led (green)          / Unused
      .GPIO2  = static_cast<int>(P137_GET_FLAG_GPIO2 - 1), // Unused / Speaker enable           / Unused
      .GPIO3  = static_cast<int>(P137_GET_FLAG_GPIO3 - 1), // Unused / Unused                   / Unused
      .GPIO4  = static_cast<int>(P137_GET_FLAG_GPIO4 - 1), // Unused / TFT Reset                / Unused
    };
    ldo2_value  = P137_GET_CONFIG_LDO2;
    ldo3_value  = P137_GET_CONFIG_LDO3;
    ldoio_value = P137_GET_CONFIG_LDOIO;
    dcdc2_value = P137_GET_CONFIG_DCDC2;
    dcdc3_value = P137_GET_CONFIG_DCDC3;

    axp192->begin(initDef);
  }
}

// **************************************************************************/
// Destructor
// **************************************************************************/
P137_data_struct::~P137_data_struct() {
  delete axp192;
}

// **************************************************************************/
// plugin_read: Read the values and send to controller(s)
// **************************************************************************/
bool P137_data_struct::plugin_read(struct EventStruct *event) {
  bool success = true;

  const uint8_t valueCount = P137_NR_OUTPUT_VALUES;

  for (uint8_t i = 0; i < valueCount; ++i) {
    UserVar.setFloat(event->TaskIndex, i, read_value(static_cast<P137_valueOptions_e>(PCONFIG(P137_CONFIG_BASE + i))));
  }

  return success;
}

// **************************************************************************/
// read_value: Read the requested value
// **************************************************************************/
float P137_data_struct::read_value(P137_valueOptions_e value) {
  if (isInitialized()) {
    switch (value) {
      case P137_valueOptions_e::None: return 0.0f;
      case P137_valueOptions_e::BatteryVoltage: return axp192->getBatteryVoltage();
      case P137_valueOptions_e::BatteryDischargeCurrent: return axp192->getBatteryDischargeCurrent();
      case P137_valueOptions_e::BatteryChargeCurrent: return axp192->getBatteryChargeCurrent();
      case P137_valueOptions_e::BatteryPower: return axp192->getBatteryPower();
      case P137_valueOptions_e::AcinVoltage: return axp192->getAcinVolatge();
      case P137_valueOptions_e::AcinCurrent: return axp192->getAcinCurrent();
      case P137_valueOptions_e::VbusVoltage: return axp192->getVbusVoltage();
      case P137_valueOptions_e::VbusCurrent: return axp192->getVbusCurrent();
      case P137_valueOptions_e::InternalTemperature: return axp192->getInternalTemperature();
      case P137_valueOptions_e::ApsVoltage: return axp192->getApsVoltage();
      case P137_valueOptions_e::LDO2: return static_cast<float>(ldo2_value);
      case P137_valueOptions_e::LDO3: return static_cast<float>(ldo3_value);
      case P137_valueOptions_e::LDOIO: return static_cast<float>(ldoio_value);
      case P137_valueOptions_e::DCDC2: return static_cast<float>(dcdc2_value);
      case P137_valueOptions_e::DCDC3: return static_cast<float>(dcdc3_value);
    }
  }
  return 0.0f;
}

// **************************************************************************/
// plugin_write: Process commands
// **************************************************************************/
const char P137_subcommands[] PROGMEM = "ldo2|ldo3|ldoio|gpio0|gpio1|gpio2|gpio3|gpio4|dcdc2|dcdc3|ldo2perc|lco3perc|ldoioperc|"
                                        "dcdc2perc|dcdc3pers|ldo2map|lco3map|ldoiomap|dcdc2map|dcdc3map|";
enum class P137_subcommands_e : int8_t {
  invalid = -1,
  ldo2    = 0,
  ldo3,
  ldoio,
  gpio0,
  gpio1,
  gpio2,
  gpio3,
  gpio4,
  dcdc2,
  dcdc3,
  ldo2perc,
  ldo3perc,
  ldoioperc,
  dcdc2perc,
  dcdc3perc,
  ldo2map,
  ldo3map,
  ldoiomap,
  dcdc2map,
  dcdc3map,
};

bool P137_data_struct::plugin_write(struct EventStruct *event,
                                    const String      & string) {
  bool   success = false;
  String cmd     = parseString(string, 1);

  if (isInitialized() && equals(cmd, F("axp"))) { // Command trigger
    cmd = parseString(string, 2);                 // sub command
    const int subcommand_i = GetCommandCode(cmd.c_str(), P137_subcommands);

    if (subcommand_i < 0) { return success; } // fail fast

    const P137_subcommands_e subcmd = static_cast<P137_subcommands_e>(subcommand_i);

    const String var3   = parseString(string, 3);
    const bool   empty3 = var3.isEmpty();
    const bool   empty4 = parseString(string, 4).isEmpty();
    const bool   state3 = !empty3 && (event->Par2 == 0 || event->Par2 == 1);
    success = true;

    if ((event->Par2 >= 0) && (event->Par2 <= P137_CONST_100_PERCENT) && !empty3 && empty4 &&
        (subcmd >= P137_subcommands_e::ldo2perc) && (subcmd <= P137_subcommands_e::dcdc3perc)) {
      // percentage 0..100, 0 turns off

      switch (subcmd) {
        case P137_subcommands_e::ldo2perc:

          if (!bitRead(P137_CONFIG_DISABLEBITS, 0)) { // axp,ldo2perc,<percentage>
            ldo2_value = event->Par2 > 0 ? map(event->Par2,
                                               P137_CONST_1_PERCENT, P137_CONST_100_PERCENT,
                                               ldo2_range[0], ldo2_range[1]) : 0;
            axp192->setLDO2(ldo2_value);
          }
          break;
        case P137_subcommands_e::ldo3perc:

          if (!bitRead(P137_CONFIG_DISABLEBITS, 1)) { // axp,ldo3perc,<percentage>
            ldo3_value = event->Par2 > 0 ? map(event->Par2,
                                               P137_CONST_1_PERCENT, P137_CONST_100_PERCENT,
                                               ldo3_range[0], ldo3_range[1]) : 0;
            axp192->setLDO3(ldo3_value);
          }
          break;
        case P137_subcommands_e::ldoioperc:

          if ((event->Par2 > 0) &&
              !bitRead(P137_CONFIG_DISABLEBITS, 2)) { // axp,ldoioperc,<percentage>
            ldoio_value = map(event->Par2,
                              P137_CONST_1_PERCENT, P137_CONST_100_PERCENT,
                              ldoio_range[0], ldoio_range[1]);
            axp192->setLDOIO(ldoio_value);
          }
          break;
        case P137_subcommands_e::dcdc2perc:

          if (!bitRead(P137_CONFIG_DISABLEBITS, 8)) { // axp,dcdc2perc,<percentage>
            dcdc2_value = event->Par2 > 0 ? map(event->Par2,
                                                P137_CONST_1_PERCENT, P137_CONST_100_PERCENT,
                                                dcdc2_range[0], dcdc2_range[1]) : 0;
            axp192->setDCDC2(dcdc2_value);
          }
          break;
        case P137_subcommands_e::dcdc3perc:

          if (!bitRead(P137_CONFIG_DISABLEBITS, 9)) { // axp,dcdc3perc,<percentage>
            dcdc3_value = event->Par2 > 0 ? map(event->Par2,
                                                P137_CONST_1_PERCENT, P137_CONST_100_PERCENT,
                                                dcdc3_range[0], dcdc3_range[1]) : 0;
            axp192->setDCDC3(dcdc3_value);
          }
          break;
        default:
          success = false;
          break;
      }
    } else
    if ((event->Par2 >= 0) && (event->Par3 <= P137_CONST_MAX_LDO) && (event->Par2 < event->Par3) && !empty3 && !empty4 &&
        (subcmd >= P137_subcommands_e::ldo2map) && (subcmd <= P137_subcommands_e::dcdc3map)) {
      // map range <low>,<high>

      switch (subcmd) {
        case P137_subcommands_e::ldo2map:

          if (!bitRead(P137_CONFIG_DISABLEBITS, 0)) { // axp,ldo2map,<low>,<high>
            ldo2_range[0] = event->Par2;
            ldo2_range[1] = event->Par3;
          }
          break;
        case P137_subcommands_e::ldo3map:

          if (!bitRead(P137_CONFIG_DISABLEBITS, 1)) { // axp,ldo3map,<low>,<high>
            ldo3_range[0] = event->Par2;
            ldo3_range[1] = event->Par3;
          }
          break;
        case P137_subcommands_e::ldoiomap:

          if ((event->Par2 >= P137_CONST_MIN_LDOIO) &&
              !bitRead(P137_CONFIG_DISABLEBITS, 2)) { // axp,ldoiomap,<low>,<high>
            ldoio_range[0] = event->Par2;
            ldoio_range[1] = event->Par3;
          }
          break;
        case P137_subcommands_e::dcdc2map:

          if ((event->Par3 <= P137_CONST_MAX_DCDC2) &&
              !bitRead(P137_CONFIG_DISABLEBITS, 8)) { // axp,dcdc2map,<low>,<high>
            dcdc2_range[0] = event->Par2;
            dcdc2_range[1] = event->Par3;
          }
          break;
        case P137_subcommands_e::dcdc3map:

          if (!bitRead(P137_CONFIG_DISABLEBITS, 9)) { // axp,dcdc3map,<low>,<high>
            dcdc3_range[0] = event->Par2;
            dcdc3_range[1] = event->Par3;
          }
          break;
        default:
          success = false;
          break;
      }
    } else {
      if (bitRead(P137_CONFIG_DISABLEBITS, static_cast<uint8_t>(subcmd))) {
        success = false;
      } else {
        switch (subcmd) {
          case P137_subcommands_e::ldo2:

            if (!empty3) {                  // axp,ldo2,<voltage>
              axp192->setLDO2(event->Par2); // Range checking done by function
              ldo2_value = event->Par2;
            }
            break;
          case P137_subcommands_e::ldo3:

            if (!empty3) {                  // axp,ldo3,<voltage>
              axp192->setLDO3(event->Par2); // Range checking done by function
              ldo3_value = event->Par2;
            }
            break;
          case P137_subcommands_e::ldoio:

            if (!empty3) {                   // axp,ldoio,<voltage>
              axp192->setLDOIO(event->Par2); // Range checking done by function
              ldoio_value = event->Par2;
            }
            break;
          case P137_subcommands_e::gpio0:

            if (state3) {                    // axp,gpio0,state
              axp192->setGPIO0(event->Par2); // Range checking done before
            }
            break;
          case P137_subcommands_e::gpio1:

            if (state3) {                    // axp,gpio1,state
              axp192->setGPIO1(event->Par2); // Range checking done before
            }
            break;
          case P137_subcommands_e::gpio2:

            if (state3) {                    // axp,gpio2,state
              axp192->setGPIO2(event->Par2); // Range checking done before
            }
            break;
          case P137_subcommands_e::gpio3:

            if (state3) {                    // axp,gpio3,state
              axp192->setGPIO3(event->Par2); // Range checking done before
            }
            break;
          case P137_subcommands_e::gpio4:

            if (state3) {                    // axp,gpio4,state
              axp192->setGPIO4(event->Par2); // Range checking done before
            }
            break;
          case P137_subcommands_e::dcdc2:

            if (!empty3) {                   // axp,dcdc2,<voltage>
              axp192->setDCDC2(event->Par2); // Range checking done by function
              dcdc2_value = event->Par2;
            }
            break;
          case P137_subcommands_e::dcdc3:

            if (!empty3) {                   // axp,dcdc3,<voltage>
              axp192->setDCDC3(event->Par2); // Range checking done by function
              dcdc3_value = event->Par2;
            }
            break;
          default:
            success = false;
            break;
        }
      }
    }
  }

  return success;
}

/****************************************************************************
 * plugin_get_config_value: Retrieve values like [<taskname>#<valuename>]
 ***************************************************************************/
const char P137_getvalues[] PROGMEM =
  "batvoltage"    //  BatteryVoltage          = 0x01,
  "|batdischarge" //  BatteryDischargeCurrent = 0x02,
  "|batcharge"    //  BatteryChargeCurrent    = 0x03,
  "|batpower"     //  BatteryPower            = 0x04,
  "|inpvoltage"   //  AcinVoltage             = 0x05,
  "|inpcurrent"   //  AcinCurrent             = 0x06,
  "|vbusvolt"     //  VbusVoltage             = 0x07,
  "|vbuscurr"     //  VbusCurrent             = 0x08,
  "|inttemp"      //  InternalTemperature     = 0x09,
  "|apsvolt"      //  ApsVoltage              = 0x0A,
  "|ldo2volt"     //  LDO2                    = 0x0B,
  "|ldo3volt"     //  LDO3                    = 0x0C,
  "|ldoiovolt"    //  LDOIO                   = 0x0D,
  "|dcdc2volt"    //  DCDC2                   = 0x12,
  "|dcdc3volt"    //  DCDC3                   = 0x13,
;


bool P137_data_struct::plugin_get_config_value(struct EventStruct *event,
                                               String            & string)
{
  const String command = parseString(string, 1);
  int getvalue_i       = GetCommandCode(command.c_str(), P137_getvalues);

  if (getvalue_i == -1) { return false; }

  // P137_valueOptions_e do not enumerate without a gap, so we need to 'patch' the index
  if (getvalue_i == 13) {
    getvalue_i = static_cast<int>(P137_valueOptions_e::DCDC2);
  }  else if (getvalue_i == 14) {
    getvalue_i = static_cast<int>(P137_valueOptions_e::DCDC3);
  }  else {
    ++getvalue_i; // Don't match with "None"
  }
  const P137_valueOptions_e getval = static_cast<P137_valueOptions_e>(getvalue_i);

  string = toString(read_value(getval), static_cast<unsigned int>(P137_CONFIG_DECIMALS));
  return true;
}

# endif // ifdef ESP32
#endif  // ifdef USES_P137

#include "../PluginStructs/P044_data_struct.h"

#ifdef USES_P044_ORG

# include "../ESPEasyCore/Serial.h"
# include "../ESPEasyCore/ESPEasyNetwork.h"

# include "../Globals/EventQueue.h"

# include "../Helpers/ESPEasy_Storage.h"
# include "../Helpers/Misc.h"


P044_Task::P044_Task(struct EventStruct *event) {
  clearBuffer();

  if (P044_LED_ENABLED & 0x80) {
    _ledPin = P044_LED_PIN;                      // Default pin (12) is already initialized in P044_Task
  }
  _ledEnabled  = (P044_LED_ENABLED & 0x7f) == 0; // Inverted setting and strip off new-settings bit
  _ledInverted = P044_LED_INVERTED == 1;
}

P044_Task::~P044_Task() {
  if (P1GatewayServer != nullptr) {
    delete P1GatewayServer;
    P1GatewayServer = nullptr;
  }
  if (P1EasySerial != nullptr) {
    delete P1EasySerial;
    P1EasySerial = nullptr;
  }
}

bool P044_Task::serverActive(WiFiServer *server) {
  # if defined(ESP8266)
  return nullptr != server && server->status() != CLOSED;
  # endif // if defined(ESP8266)
  # if defined(ESP32)
  return nullptr != server && *server;
  # endif // if defined(ESP32)
}

void P044_Task::startServer(uint16_t portnumber) {
  if ((gatewayPort == portnumber) && serverActive(P1GatewayServer)) {
    // server is already listening on this port
    return;
  }
  stopServer();
  gatewayPort     = portnumber;
  P1GatewayServer = new (std::nothrow) WiFiServer(portnumber);

  if ((nullptr != P1GatewayServer) && NetworkConnected()) {
    P1GatewayServer->begin();

    if (serverActive(P1GatewayServer)) {
      # ifndef LIMIT_BUILD_SIZE
      addLog(LOG_LEVEL_INFO, concat(F("P1   : WiFi server started at port "), static_cast<int>(portnumber)));
      # endif // ifndef LIMIT_BUILD_SIZE
    } else {
      addLog(LOG_LEVEL_ERROR, concat(F("P1   : WiFi server start failed at port "), static_cast<int>(portnumber)) + F(", retrying..."));
    }
  }
}

void P044_Task::checkServer() {
  if ((nullptr != P1GatewayServer) && !serverActive(P1GatewayServer) && NetworkConnected()) {
    P1GatewayServer->close();
    P1GatewayServer->begin();

    if (serverActive(P1GatewayServer)) {
      # ifndef LIMIT_BUILD_SIZE
      addLog(LOG_LEVEL_INFO, F("P1   : WiFi server started"));
      # endif // ifndef LIMIT_BUILD_SIZE
    }
  }
}

void P044_Task::stopServer() {
  if (nullptr != P1GatewayServer) {
    if (P1GatewayClient) { P1GatewayClient.stop(); }
    clientConnected = false;
    P1GatewayServer->close();
    # ifndef LIMIT_BUILD_SIZE
    addLog(LOG_LEVEL_INFO, F("P1   : WiFi server closed"));
    # endif // ifndef LIMIT_BUILD_SIZE
    delete P1GatewayServer;
    P1GatewayServer = nullptr;
  }
}

bool P044_Task::hasClientConnected() {
  if ((nullptr != P1GatewayServer) && P1GatewayServer->hasClient())
  {
    if (P1GatewayClient) { P1GatewayClient.stop(); }
    P1GatewayClient = P1GatewayServer->available();

    # ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS

    // See: https://github.com/espressif/arduino-esp32/pull/6676
    P1GatewayClient.setTimeout((CONTROLLER_CLIENTTIMEOUT_DFLT + 500) / 1000); // in seconds!!!!
    Client *pClient = &P1GatewayClient;
    pClient->setTimeout(CONTROLLER_CLIENTTIMEOUT_DFLT);
    # else // ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS
    P1GatewayClient.setTimeout(CONTROLLER_CLIENTTIMEOUT_DFLT); // in msec as it should be!
    # endif // ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS

    # ifndef LIMIT_BUILD_SIZE
    addLog(LOG_LEVEL_INFO, F("P1   : Client connected!"));
    # endif // ifndef LIMIT_BUILD_SIZE
  }

  if (P1GatewayClient.connected())
  {
    clientConnected = true;
  }
  else
  {
    if (clientConnected) // there was a client connected before...
    {
      clientConnected = false;
      # ifndef LIMIT_BUILD_SIZE
      addLog(LOG_LEVEL_INFO, F("P1   : Client disconnected!"));
      # endif // ifndef LIMIT_BUILD_SIZE
    }
  }
  return clientConnected;
}

void P044_Task::discardClientIn() {
  // flush all data received from the WiFi gateway
  // as a P1 meter does not receive data
  while (P1GatewayClient.available()) {
    P1GatewayClient.read();
  }
}

void P044_Task::blinkLED() {
  if (_ledEnabled) {
    blinkLEDStartTime = millis();
    digitalWrite(_ledPin, _ledInverted ? 0 : 1);
  }
}

void P044_Task::checkBlinkLED() {
  if (_ledEnabled && (blinkLEDStartTime > 0) && (timePassedSince(blinkLEDStartTime) >= 500)) {
    digitalWrite(_ledPin, _ledInverted ? 1 : 0);
    blinkLEDStartTime = 0;
  }
}

void P044_Task::clearBuffer() {
  if (serial_buffer.length() > maxMessageSize) {
    maxMessageSize = _min(serial_buffer.length(), P044_DATAGRAM_MAX_SIZE);
  }

  free_string(serial_buffer);
  serial_buffer.reserve(maxMessageSize);
}

void P044_Task::addChar(char ch) {
  serial_buffer += ch;
}

/*  checkDatagram
    checks whether the P044_CHECKSUM of the data received from P1 matches the P044_CHECKSUM
    attached to the telegram
 */
bool P044_Task::checkDatagram() const {
  int endChar = serial_buffer.length() - 1;

  if (CRCcheck) {
    endChar -= P044_CHECKSUM_LENGTH;
  }

  if ((endChar < 0) || (serial_buffer[0] != P044_DATAGRAM_START_CHAR) ||
      (serial_buffer[endChar] != P044_DATAGRAM_END_CHAR)) { return false; }

  if (!CRCcheck) { return true; }

  const int checksumStartIndex = endChar + 1;

  # ifdef PLUGIN_044_DEBUG

  for (unsigned int cnt = 0; cnt < serial_buffer.length(); ++cnt) {
    serialPrint(serial_buffer.substring(cnt, 1));
  }
  # endif // ifdef PLUGIN_044_DEBUG

  // calculate the CRC and check if it equals the hexadecimal one attached to the datagram
  unsigned int crc = CRC16(serial_buffer, checksumStartIndex);
  return strtoul(serial_buffer.substring(checksumStartIndex).c_str(), nullptr, 16) == crc;
}

/*
   CRC16
      based on code written by Jan ten Hove
     https://github.com/jantenhove/P1-Meter-ESP8266
 */
unsigned int P044_Task::CRC16(const String& buf, int len)
{
  unsigned int crc = 0;

  for (int pos = 0; pos < len; pos++)
  {
    crc ^= static_cast<const unsigned int>(buf[pos]); // XOR byte into least sig. byte of crc

    for (int i = 8; i != 0; i--) {                    // Loop over each bit
      if ((crc & 0x0001) != 0) {                      // If the LSB is set
        crc >>= 1;                                    // Shift right and XOR 0xA001
        crc  ^= 0xA001;
      }
      else {                                          // Else LSB is not set
        crc >>= 1;                                    // Just shift right
      }
    }
  }

  return crc;
}

/*
   validP1char
       Checks if the character is valid as part of the P1 datagram contents and/or checksum.
       Returns false on a datagram start ('/'), end ('!') or invalid character
 */
bool P044_Task::validP1char(char ch) {
  return
    isAlphaNumeric(ch) ||
    ch == '.' ||
    ch == ' ' ||
    ch == '\\' || // Single backslash, but escaped in C++
    ch == '\r' ||
    ch == '\n' ||
    ch == '(' ||
    ch == ')' ||
    ch == '-' ||
    ch == '*' ||
    ch == ':' ||
    ch == '_';
}

void P044_Task::serialBegin(const ESPEasySerialPort port, int16_t rxPin, int16_t txPin,
                            unsigned long baud, uint8_t config) {
  serialEnd();

  if (rxPin >= 0) {
    P1EasySerial = new (std::nothrow) ESPeasySerial(port, rxPin, txPin);

    if (nullptr != P1EasySerial) {
      # if defined(ESP8266)
      P1EasySerial->begin(baud, (SerialConfig)config);
      # elif defined(ESP32)
      P1EasySerial->begin(baud, config);
      # endif // if defined(ESP8266)
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_DEBUG, F("P1   : Serial opened"));
      # endif // ifndef BUILD_NO_DEBUG
    }
  }
  state = ParserState::WAITING;
}

void P044_Task::serialEnd() {
  if (nullptr != P1EasySerial) {
    delete P1EasySerial;
    P1EasySerial = nullptr;
    # ifndef BUILD_NO_DEBUG
    addLog(LOG_LEVEL_DEBUG, F("P1   : Serial closed"));
    # endif // ifndef BUILD_NO_DEBUG
  }
}

void P044_Task::handleSerialIn(struct EventStruct *event) {
  if (nullptr == P1EasySerial) { return; }
  int  RXWait  = P044_RX_WAIT;
  bool done    = false;
  int  timeOut = RXWait;

  do {
    if (P1EasySerial->available()) {
      if (_ledEnabled) {
        digitalWrite(_ledPin, _ledInverted ? 0 : 1);
      }
      done = handleChar(P1EasySerial->read());

      if (_ledEnabled) {
        digitalWrite(_ledPin, _ledInverted ? 1 : 0);
      }

      if (done) { break; }
      timeOut = RXWait; // if serial received, reset timeout counter
    } else {
      if (timeOut <= 0) { break; }
      delay(1);
      --timeOut;
    }
  } while (true);

  if (done) {
    P1GatewayClient.print(serial_buffer);
    P1GatewayClient.PR_9453_FLUSH_TO_CLEAR();
    # ifndef BUILD_NO_DEBUG
    addLog(LOG_LEVEL_DEBUG, F("P1   : data send!"));
    # endif // ifndef BUILD_NO_DEBUG
    blinkLED();

    eventQueue.add(event->TaskIndex, F("Data"), EMPTY_STRING);
  } // done
}

bool P044_Task::handleChar(char ch) {
  if (serial_buffer.length() >= P044_DATAGRAM_MAX_SIZE - 2) { // room for cr/lf
    # ifndef BUILD_NO_DEBUG
    addLog(LOG_LEVEL_DEBUG, F("P1   : Error: Buffer overflow, discarded input."));
    # endif // ifndef BUILD_NO_DEBUG
    state = ParserState::WAITING; // reset
  }

  bool done    = false;
  bool invalid = false;

  switch (state) {
    case ParserState::WAITING:

      if (ch == P044_DATAGRAM_START_CHAR)  {
        clearBuffer();
        addChar(ch);
        state = ParserState::READING;
      } // else ignore data
      break;
    case ParserState::READING:

      if (validP1char(ch)) {
        addChar(ch);
      } else if (ch == P044_DATAGRAM_END_CHAR) {
        addChar(ch);

        if (CRCcheck) {
          checkI = 0;
          state  = ParserState::CHECKSUM;
        } else {
          done = true;
        }
      } else if (ch == P044_DATAGRAM_START_CHAR) {
        # ifndef BUILD_NO_DEBUG
        addLog(LOG_LEVEL_DEBUG, F("P1   : Error: Start detected, discarded input."));
        # endif // ifndef BUILD_NO_DEBUG
        state = ParserState::WAITING; // reset
        return handleChar(ch);
      } else {
        invalid = true;
      }
      break;
    case ParserState::CHECKSUM:

      if (validP1char(ch)) {
        addChar(ch);
        ++checkI;

        if (checkI == P044_CHECKSUM_LENGTH) {
          done = true;
        }
      } else {
        invalid = true;
      }
      break;
  } // switch

  if (invalid) {
    // input is not a datagram char
    # ifndef BUILD_NO_DEBUG
    addLog(LOG_LEVEL_DEBUG, F("P1   : Error: DATA corrupt, discarded input."));
    # endif // ifndef BUILD_NO_DEBUG

    # ifdef PLUGIN_044_DEBUG
    serialPrint(F("faulty char>"));
    serialPrint(String(ch));
    serialPrintln("<");
    # endif // ifdef PLUGIN_044_DEBUG
    state = ParserState::WAITING; // reset
  }

  if (done) {
    done = checkDatagram();

    if (done) {
      // add the cr/lf pair to the datagram ahead of reading both
      // from serial as the datagram has already been validated
      addChar('\r');
      addChar('\n');
    } else if (CRCcheck) {
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_DEBUG, F("P1   : Error: Invalid CRC, dropped data"));
      # endif // ifndef BUILD_NO_DEBUG
    } else {
      # ifndef BUILD_NO_DEBUG
      addLog(LOG_LEVEL_DEBUG, F("P1   : Error: Invalid datagram, dropped data"));
      # endif // ifndef BUILD_NO_DEBUG
    }
    state = ParserState::WAITING; // prepare for next one
  }

  return done;
}

void P044_Task::discardSerialIn() {
  if (nullptr != P1EasySerial) {
    while (P1EasySerial->available()) {
      P1EasySerial->read();
    }
  }
  state = ParserState::WAITING;
}

bool P044_Task::isInit() const {
  return nullptr != P1GatewayServer && nullptr != P1EasySerial;
}

#endif // ifdef USES_P044

#include "../PluginStructs/P166_data_struct.h"

#ifdef USES_P166

/**************************************************************************
* Constructor
**************************************************************************/
P166_data_struct::P166_data_struct(uint8_t                        address,
                                   DFRobot_GP8403::eOutPutRange_t range) :
  _address(address), _range(range)
{}

P166_data_struct::~P166_data_struct() {
  delete gp8403;
}

bool P166_data_struct::init(struct EventStruct *event) {
  //
  gp8403 = new (std::nothrow) DFRobot_GP8403(&Wire, _address);

  if (nullptr != gp8403) {
    LoadCustomTaskSettings(event->TaskIndex, presets, P166_PresetEntries, 0);
    maxPreset = 0;

    for (uint8_t i = 0; i < P166_PresetEntries; ++i) {
      if (!presets[i].isEmpty()) {
        ++maxPreset;
      } else {
        break; // done
      }
    }
    initialized = true;
    gp8403->setDACOutRange(_range); // Set the output voltage range

    const bool restoreValues = !essentiallyZero(UserVar.getFloat(event->TaskIndex, 3)) && (1 == P166_RESTORE_VALUES);

    for (uint8_t i = 0; i < P166_MAX_OUTPUTS; ++i) { // Set the initial output values
      float fValue;

      if (restoreValues) {
        fValue = UserVar.getFloat(event->TaskIndex, i);
      } else {
        fValue = P166_PRESET_OUTPUT(i);
      }
      const int iValue = static_cast<int>(roundf(fValue * P166_FACTOR_mV));

      if ((iValue >= 0) && (iValue <= ((_range == DFRobot_GP8403::eOutPutRange_t::eOutputRange5V) ? P166_RANGE_5V : P166_RANGE_10V))) {
        gp8403->setDACOutVoltage(iValue, i);
        setUserVarAndLog(event, i, true, fValue, restoreValues ? F("restore") : F("init"));
      }
    }
  }
  addLog(LOG_LEVEL_ERROR, concat(F("GP8403: Initialization "), isInitialized() ? F("succeeded") : F("failed")));
  return isInitialized();
}

/*****************************************************
* plugin_read
*****************************************************/
bool P166_data_struct::plugin_read(struct EventStruct *event)           {
  return isInitialized();
}

const char P166_subcommands[] PROGMEM = "volt|mvolt|range|preset|init";

enum class P166_subcmd_e : int8_t {
  invalid = -1,
  volt    = 0,
  mvolt,
  range,
  preset,
  init,
};

/*****************************************************
* plugin_write
*****************************************************/
bool P166_data_struct::plugin_write(struct EventStruct *event,
                                    String            & string) {
  bool success = false;

  const String command = parseString(string, 1);

  if (isInitialized() && equals(command, F("gp8403"))) {
    const String subcommand   = parseString(string, 2);
    const String sValue       = parseString(string, 4);
    const int    subcommand_i = GetCommandCode(subcommand.c_str(), P166_subcommands);

    if (subcommand_i < 0) { return false; } // Fail fast

    const P166_subcmd_e subcmd = static_cast<P166_subcmd_e>(subcommand_i);
    uint32_t   nChannel{};
    const bool hasChannel = validUIntFromString(parseString(string, 3), nChannel);

    switch (subcmd) {
      case P166_subcmd_e::invalid:
        break;
      case P166_subcmd_e::volt:
      case P166_subcmd_e::mvolt:
      case P166_subcmd_e::preset:

        if (hasChannel && (nChannel <= 2)) { // Channel range check
          float fValue{};
          bool  isValid = false;

          if (P166_subcmd_e::preset == subcmd) {
            isValid = validPresetValue(sValue, fValue);
          } else {
            isValid = validFloatFromString(sValue, fValue);
          }

          if (isValid) { // Value valid check
            const bool voltValue = (P166_subcmd_e::volt == subcmd || P166_subcmd_e::preset == subcmd);

            // Calculate mV from requested voltage
            const int iValue = static_cast<int>(roundf(voltValue ? fValue * P166_FACTOR_mV : fValue));

            if ((iValue >= 0) && (iValue <= ((_range == DFRobot_GP8403::eOutPutRange_t::eOutputRange5V) ? P166_RANGE_5V : P166_RANGE_10V))) {
              gp8403->setDACOutVoltage(static_cast<uint16_t>(iValue), static_cast<uint8_t>(nChannel));

              if ((0 == nChannel) || (2 == nChannel)) {
                setUserVarAndLog(event, 0, voltValue, fValue, subcommand);
              }

              if ((1 == nChannel) || (2 == nChannel)) {
                setUserVarAndLog(event, 1, voltValue, fValue, subcommand);
              }

              // Send out data for events and to controllers by scheduling a TaskRun/PLUGIN_READ
              Scheduler.schedule_task_device_timer(event->TaskIndex, millis() + 10);

              success = true;
            }
          }
        }
        break;

      case P166_subcmd_e::init:

        if (hasChannel && (nChannel <= 2)) { // Channel range check
          for (uint8_t i = 0; i < P166_MAX_OUTPUTS; ++i) {
            const int iValue = static_cast<int>(roundf(P166_PRESET_OUTPUT(i) * P166_FACTOR_mV));

            if ((iValue >= 0) && (iValue <= ((_range == DFRobot_GP8403::eOutPutRange_t::eOutputRange5V) ? P166_RANGE_5V : P166_RANGE_10V))) {
              if ((0 == i) && ((0 == nChannel) || (2 == nChannel))) {
                gp8403->setDACOutVoltage(static_cast<uint16_t>(iValue), static_cast<uint8_t>(i));
                setUserVarAndLog(event, 0, true, P166_PRESET_OUTPUT(i), subcommand);
              }

              if ((1 == i) && ((1 == nChannel) || (2 == nChannel))) {
                gp8403->setDACOutVoltage(static_cast<uint16_t>(iValue), static_cast<uint8_t>(i));
                setUserVarAndLog(event, 1, true, P166_PRESET_OUTPUT(i), subcommand);
              }
              success = true;
            }
          }

          if (success) {
            // Send out data for events and to controllers by scheduling a TaskRun/PLUGIN_READ
            Scheduler.schedule_task_device_timer(event->TaskIndex, millis() + 10);
          }
        }
        break;

      case P166_subcmd_e::range:

        if ((event->Par2 == 5) || (event->Par2 == 10)) { // Value options check
          _range = (event->Par2 == 5
                    ? DFRobot_GP8403::eOutPutRange_t::eOutputRange5V
                    : DFRobot_GP8403::eOutPutRange_t::eOutputRange10V);
          P166_MAX_VOLTAGE = static_cast<int>(_range); // Save manually to store new setting...
          gp8403->setDACOutRange(_range);              // Device output will be updated immediately!

          if (loglevelActiveFor(LOG_LEVEL_INFO)) {
            addLog(LOG_LEVEL_INFO, strformat(F("%s: 'range', value: 0-%dV"), F("GP8403"), event->Par2));
          }
          success = true;
        }
        break;
    }
  }
  return success;
}

/*****************************************************
* setUserVarAndLog
*****************************************************/
void P166_data_struct::setUserVarAndLog(struct EventStruct *event,
                                        taskVarIndex_t      varNr,
                                        const bool          voltValue,
                                        const float         fValue,
                                        const String      & subcommand) {
  UserVar.setFloat(event->TaskIndex, varNr, voltValue ? fValue : fValue / P166_FACTOR_mV); // V
  UserVar.setFloat(event->TaskIndex, 3,     1.0f);                                         // Mark as set, will be reset on cold boot

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO, strformat(F("%s: '%s' Output %d, value: %.3fV"), F("GP8403"),
                                     subcommand.c_str(),
                                     varNr,
                                     voltValue ? fValue : fValue / P166_FACTOR_mV));
  }
}

/*****************************************************
* validPresetValue
*****************************************************/
bool P166_data_struct::validPresetValue(const String& name,
                                        float       & value) {
  bool isValid = false;

  if (!name.isEmpty()) {
    for (uint8_t i = 0; i < maxPreset; ++i) {
      if (parseString(presets[i], 1).equals(name)) {
        isValid = validFloatFromString(parseString(presets[i], 2), value);

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          addLog(LOG_LEVEL_INFO, strformat(F("%s: Preset '%s' found, value: %.3fV"), F("GP8403"),
                                           name.c_str(),
                                           value));
        }
        break; // Found it
      }
    }
  }
  return isValid;
}

/*****************************************************
* plugin_get_config_value
*****************************************************/
bool P166_data_struct::plugin_get_config_value(struct EventStruct *event,
                                               String            & string) {
  bool success = false;

  const String var = parseString(string, 1);

  if (var.startsWith(F("preset")) && (var.length() > 6)) { // [<taskname>#presetX] = the configured preset X value
    uint32_t preIdx = 0;

    if (validUIntFromString(var.substring(6), preIdx) && (preIdx > 0) && (preIdx <= maxPreset)) {
      string  = parseString(presets[preIdx - 1], 2);
      success = true;
    }
  } else if (equals(var, F("initial0"))) { // [<taskname>#initial0] = the configured initial output 0 value
    string  = toString(P166_PRESET_OUTPUT(0), 3);
    success = true;
  } else if (equals(var, F("initial1"))) { // [<taskname>#initial1] = the configured initial output 1 value
    string  = toString(P166_PRESET_OUTPUT(1), 3);
    success = true;
  } else if (equals(var, F("range"))) {    // [<taskname>#range] = the configured range setting 5 or 10
    string  = static_cast<DFRobot_GP8403::eOutPutRange_t>(P166_MAX_VOLTAGE) == DFRobot_GP8403::eOutPutRange_t::eOutputRange5V ? 5 : 10;
    success = true;
  }
  return success;
}

#endif // ifdef USES_P166

#include "../PluginStructs/P028_data_struct.h"

#ifdef USES_P028

# include "../Helpers/Convert.h"

// It takes at least 1.587 sec for valid measurements to complete.
// The datasheet names this the "T63" moment.
// 1 second = 63% of the time needed to perform a measurement.
# define P028_MEASUREMENT_TIMEOUT 1.587f

P028_data_struct::P028_data_struct(uint8_t addr, float tempOffset) :
  i2cAddress(addr), temp_offset(tempOffset) {}


uint8_t P028_data_struct::get_config_settings() const {
  return sensorID == Unknown_DEVICE ? 0u : 0x28; // Tstandby 62.5ms, filter 4, 3-wire SPI Disable
}

uint8_t P028_data_struct::get_control_settings() const {
  return sensorID == Unknown_DEVICE ? 0u : 0x93; // Oversampling: 8x P, 8x T, normal mode
}

const __FlashStringHelper * P028_data_struct::getDeviceName() const {
  return getDeviceName(sensorID);
}

const __FlashStringHelper * P028_data_struct::getDeviceName(BMx_ChipId id) {
  switch (id) {
    case BMP280_DEVICE_SAMPLE1:
    case BMP280_DEVICE_SAMPLE2: return F("sample BMP280");
    case BMP280_DEVICE:         return F("BMP280");
    case BME280_DEVICE:         return F("BME280");
    default: return F("Unknown");
  }
}

bool P028_data_struct::hasHumidity() const {
  return sensorID == BME280_DEVICE;
}

bool P028_data_struct::initialized() const {
  return state != BMx_Uninitialized;
}

void P028_data_struct::setUninitialized() {
  state = BMx_Uninitialized;
}

bool P028_data_struct::measurementInProgress() const {
  if ((state != BMx_Wait_for_samples) || (last_measurement == 0)) { return false; }

  return !timeOutReached(last_measurement + P028_MEASUREMENT_TIMEOUT);
}

void P028_data_struct::startMeasurement() {
  if (measurementInProgress()) { return; }

  if (!initialized()) {
    if (begin()) {
      state            = BMx_Initialized;
      last_measurement = 0;
    }
  }
  check(); // Check id device is present

  if (state != BMx_Error) {
    // Set the Sensor in sleep to be make sure that the following configs will be stored
    I2C_write8_reg(i2cAddress, BMx280_REGISTER_CONTROL, 0x00);

    if (hasHumidity()) {
      I2C_write8_reg(i2cAddress, BMx280_REGISTER_CONTROLHUMID, BME280_CONTROL_SETTING_HUMIDITY);
    }
    I2C_write8_reg(i2cAddress, BMx280_REGISTER_CONFIG,  get_config_settings());
    I2C_write8_reg(i2cAddress, BMx280_REGISTER_CONTROL, get_control_settings());
    state            = BMx_Wait_for_samples;
    last_measurement = millis();
  } else {
    lastMeasurementError = true;
  }
}

// Only perform the measurements with big interval to prevent the sensor from warming up.
bool P028_data_struct::updateMeasurements(taskIndex_t task_index) {
  if ((state != BMx_Wait_for_samples) || measurementInProgress()) {
    // Nothing to do in processing the measurement
    return false;
  }

  if (!readUncompensatedData()) {
    return false;
  }

  // Set to sleep mode again to prevent the sensor from heating up.
  I2C_write8_reg(i2cAddress, BMx280_REGISTER_CONTROL, 0x00);

  lastMeasurementError = false;
  state                = BMx_New_values;
  last_temp_val        = readTemperature();
  last_press_val       = readPressure();
  last_hum_val         = readHumidity();


# ifndef LIMIT_BUILD_SIZE
  String log;

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    reserve_special(log, 120); // Prevent re-allocation
    log  = getDeviceName(sensorID);
    log += ':';
  }
  bool logAdded = false;
# endif // ifndef LIMIT_BUILD_SIZE

  if (hasHumidity()) {
    // Apply half of the temp offset, to correct the dew point offset.
    // The sensor is warmer than the surrounding air, which has effect on the perceived humidity.
    last_dew_temp_val = compute_dew_point_temp(last_temp_val + (temp_offset / 2.0f), last_hum_val);
  } else {
    // No humidity measurement, thus set dew point equal to air temperature.
    last_dew_temp_val = last_temp_val;
  }

  if (!approximatelyEqual(temp_offset, 0.0f)) {
    # ifndef LIMIT_BUILD_SIZE

    // There is some offset to apply.
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      log += strformat(F(" Apply temp offset: %.2fC"), temp_offset);
    }
    # endif // ifndef LIMIT_BUILD_SIZE

    if (hasHumidity()) {
      # ifndef LIMIT_BUILD_SIZE

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        log += concat(F(" humidity: "), last_hum_val);
      }
      # endif // ifndef LIMIT_BUILD_SIZE
      last_hum_val = compute_humidity_from_dewpoint(last_temp_val + temp_offset, last_dew_temp_val);

      # ifndef LIMIT_BUILD_SIZE

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        log += strformat(F("%% => %.2f%%"), last_hum_val);
      }
      # endif // ifndef LIMIT_BUILD_SIZE
    } else {
      last_hum_val = 0.0f;
    }

# ifndef LIMIT_BUILD_SIZE

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      log += concat(F(" temperature: "), last_temp_val);
    }
# endif // ifndef LIMIT_BUILD_SIZE
    last_temp_val = last_temp_val + temp_offset;

# ifndef LIMIT_BUILD_SIZE

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      log     += strformat(F("C => %.2fC"), last_temp_val);
      logAdded = true;
    }
# endif // ifndef LIMIT_BUILD_SIZE
  }

# ifndef LIMIT_BUILD_SIZE

  if (hasHumidity()) {
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      log     += strformat(F(" dew point: %.2fC"), last_dew_temp_val);
      logAdded = true;
    }
  }

  if (logAdded && loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO, log);
  }
# endif // ifndef LIMIT_BUILD_SIZE
  return true;
}

// **************************************************************************/
// Check BME280 presence
// **************************************************************************/
bool P028_data_struct::check() {
  bool wire_status      = false;
  const uint8_t chip_id = I2C_read8_reg(i2cAddress, BMx280_REGISTER_CHIPID, &wire_status);

  if (!wire_status) { setUninitialized(); }

  switch (chip_id) {
    case BMP280_DEVICE_SAMPLE1:
    case BMP280_DEVICE_SAMPLE2:
    case BMP280_DEVICE:
    case BME280_DEVICE: {
      if (wire_status) {
        // Store detected chip ID when chip found.
        if (sensorID != chip_id) {
          sensorID = static_cast<BMx_ChipId>(chip_id);
          setUninitialized();

          if (loglevelActiveFor(LOG_LEVEL_INFO)) {
            addLog(LOG_LEVEL_INFO, concat(F("BMx280: Detected "), getDeviceName(sensorID)));
          }
        }
      } else {
        sensorID = Unknown_DEVICE;
      }
      break;
    }
    default:
      sensorID = Unknown_DEVICE;
      break;
  }

  if (sensorID == Unknown_DEVICE) {
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      String log = concat(F("BMx280: Unable to detect chip ID ("), chip_id);

      if (!wire_status) {
        log += F(", failed");
      }
      log += ')';
      addLogMove(LOG_LEVEL_INFO, log);
    }

    state = BMx_Error;

    return false;
  }
  return wire_status;
}

bool P028_data_struct::begin() {
  if (!check()) {
    return false;
  }

  // Perform soft reset
  I2C_write8_reg(i2cAddress, BMx280_REGISTER_SOFTRESET, 0xB6);
  delay(2); // Startup time is 2 ms (datasheet)
  readCoefficients();

  //  delay(65); //May be needed here as well to fix first wrong measurement?
  return true;
}

void P028_data_struct::readCoefficients()
{
  calib.dig_T1 = I2C_read16_LE_reg(i2cAddress, BMx280_REGISTER_DIG_T1);
  calib.dig_T2 = I2C_readS16_LE_reg(i2cAddress, BMx280_REGISTER_DIG_T2);
  calib.dig_T3 = I2C_readS16_LE_reg(i2cAddress, BMx280_REGISTER_DIG_T3);

  calib.dig_P1 = I2C_read16_LE_reg(i2cAddress, BMx280_REGISTER_DIG_P1);
  calib.dig_P2 = I2C_readS16_LE_reg(i2cAddress, BMx280_REGISTER_DIG_P2);
  calib.dig_P3 = I2C_readS16_LE_reg(i2cAddress, BMx280_REGISTER_DIG_P3);
  calib.dig_P4 = I2C_readS16_LE_reg(i2cAddress, BMx280_REGISTER_DIG_P4);
  calib.dig_P5 = I2C_readS16_LE_reg(i2cAddress, BMx280_REGISTER_DIG_P5);
  calib.dig_P6 = I2C_readS16_LE_reg(i2cAddress, BMx280_REGISTER_DIG_P6);
  calib.dig_P7 = I2C_readS16_LE_reg(i2cAddress, BMx280_REGISTER_DIG_P7);
  calib.dig_P8 = I2C_readS16_LE_reg(i2cAddress, BMx280_REGISTER_DIG_P8);
  calib.dig_P9 = I2C_readS16_LE_reg(i2cAddress, BMx280_REGISTER_DIG_P9);

  if (hasHumidity()) {
    calib.dig_H1 = I2C_read8_reg(i2cAddress, BMx280_REGISTER_DIG_H1);
    calib.dig_H2 = I2C_readS16_LE_reg(i2cAddress, BMx280_REGISTER_DIG_H2);
    calib.dig_H3 = I2C_read8_reg(i2cAddress, BMx280_REGISTER_DIG_H3);
    calib.dig_H4 = (I2C_read8_reg(i2cAddress, BMx280_REGISTER_DIG_H4) << 4) | (I2C_read8_reg(i2cAddress, BMx280_REGISTER_DIG_H4 + 1) & 0xF);
    calib.dig_H5 = (I2C_read8_reg(i2cAddress, BMx280_REGISTER_DIG_H5 + 1) << 4) | (I2C_read8_reg(i2cAddress, BMx280_REGISTER_DIG_H5) >> 4);
    calib.dig_H6 = (int8_t)I2C_read8_reg(i2cAddress, BMx280_REGISTER_DIG_H6);
  }
}

bool P028_data_struct::readUncompensatedData() {
  // wait until measurement has been completed, otherwise we would read
  // the values from the last measurement
  if (I2C_read8_reg(i2cAddress, BMx280_REGISTER_STATUS) & 0x08) {
    return false;
  }

  I2Cdata_bytes BME280_data(BME280_P_T_H_DATA_LEN, BME280_DATA_ADDR);
  const bool    allDataRead = I2C_read_bytes(i2cAddress, BME280_data);

  if (!allDataRead) {
    return false;
  }

  /* Variables to store the sensor data */
  uint32_t data_xlsb;
  uint32_t data_lsb;
  uint32_t data_msb;

  /* Store the parsed register values for pressure data */
  data_msb               = (uint32_t)BME280_data[BME280_DATA_ADDR + 0] << 12;
  data_lsb               = (uint32_t)BME280_data[BME280_DATA_ADDR + 1] << 4;
  data_xlsb              = (uint32_t)BME280_data[BME280_DATA_ADDR + 2] >> 4;
  uncompensated.pressure = data_msb | data_lsb | data_xlsb;

  /* Store the parsed register values for temperature data */
  data_msb                  = (uint32_t)BME280_data[BME280_DATA_ADDR + 3] << 12;
  data_lsb                  = (uint32_t)BME280_data[BME280_DATA_ADDR + 4] << 4;
  data_xlsb                 = (uint32_t)BME280_data[BME280_DATA_ADDR + 5] >> 4;
  uncompensated.temperature = data_msb | data_lsb | data_xlsb;

  /* Store the parsed register values for temperature data */
  data_lsb               = (uint32_t)BME280_data[BME280_DATA_ADDR + 6] << 8;
  data_msb               = (uint32_t)BME280_data[BME280_DATA_ADDR + 7];
  uncompensated.humidity = data_msb | data_lsb;
  return true;
}

float P028_data_struct::readTemperature()
{
  const int32_t adc_T = uncompensated.temperature;

  const int32_t var1 = ((((adc_T >> 3) - ((int32_t)calib.dig_T1 << 1))) *
                        ((int32_t)calib.dig_T2)) >> 11;

  const int32_t var2 = (((((adc_T >> 4) - ((int32_t)calib.dig_T1)) *
                          ((adc_T >> 4) - ((int32_t)calib.dig_T1))) >> 12) *
                        ((int32_t)calib.dig_T3)) >> 14;

  calib.t_fine = var1 + var2;

  return static_cast<float>((calib.t_fine * 5 + 128) >> 8) / 100.0f;
}

float P028_data_struct::readPressure() const
{
  int64_t var1, var2, p;
  int32_t adc_P = uncompensated.pressure;

  var1 = ((int64_t)calib.t_fine) - 128000;
  var2 = var1 * var1 * (int64_t)calib.dig_P6;
  var2 = var2 + ((var1 * (int64_t)calib.dig_P5) << 17);
  var2 = var2 + (((int64_t)calib.dig_P4) << 35);
  var1 = ((var1 * var1 * (int64_t)calib.dig_P3) >> 8) +
         ((var1 * (int64_t)calib.dig_P2) << 12);
  var1 = ((((((int64_t)1) << 47) + var1)) * ((int64_t)calib.dig_P1)) >> 33;

  if (var1 == 0) {
    return 0.0f; // avoid exception caused by division by zero
  }
  p    = 1048576 - adc_P;
  p    = (((p << 31) - var2) * 3125) / var1;
  var1 = (((int64_t)calib.dig_P9) * (p >> 13) * (p >> 13)) >> 25;
  var2 = (((int64_t)calib.dig_P8) * p) >> 19;

  p = ((p + var1 + var2) >> 8) + (((int64_t)calib.dig_P7) << 4);
  return static_cast<float>(p) / 25600.0f;
}

float P028_data_struct::readHumidity() const
{
  if (!hasHumidity()) {
    // No support for humidity
    return 0.0f;
  }
  const int32_t adc_H = uncompensated.humidity;

  int32_t v_x1_u32r;

  v_x1_u32r = (calib.t_fine - ((int32_t)76800));

  v_x1_u32r = (((((adc_H << 14) - (((int32_t)calib.dig_H4) << 20) -
                  (((int32_t)calib.dig_H5) * v_x1_u32r)) + ((int32_t)16384)) >> 15) *
               (((((((v_x1_u32r * ((int32_t)calib.dig_H6)) >> 10) *
                    (((v_x1_u32r * ((int32_t)calib.dig_H3)) >> 11) + ((int32_t)32768))) >> 10) +
                  ((int32_t)2097152)) * ((int32_t)calib.dig_H2) + 8192) >> 14));

  v_x1_u32r = (v_x1_u32r - (((((v_x1_u32r >> 15) * (v_x1_u32r >> 15)) >> 7) *
                             ((int32_t)calib.dig_H1)) >> 4));

  v_x1_u32r = (v_x1_u32r < 0) ? 0 : v_x1_u32r;
  v_x1_u32r = (v_x1_u32r > 419430400) ? 419430400 : v_x1_u32r;
  return static_cast<float>(v_x1_u32r >> 12) / 1024.0f;
}

#endif // ifdef USES_P028

#include "../PluginStructs/P005_data_struct.h"

#ifdef USES_P005


// DEBUG code using logic analyzer for timings
// #define DEBUG_LOGIC_ANALYZER_PIN  27


// Macros to perform direct access on GPIOs
// Macros written by Paul Stoffregen
// See: https://github.com/PaulStoffregen/OneWire/blob/master/util/
# include <GPIO_Direct_Access.h>

enum struct P005_logNr {
  P005_error_no_reading,
  P005_error_protocol_timeout,
  P005_error_checksum_error,
  P005_error_invalid_NAN_reading,
  P005_info_temperature,
  P005_info_humidity
};

const __FlashStringHelper* P005_logString(P005_logNr logNr) {
  switch (logNr) {
    case P005_logNr::P005_error_no_reading:          return F("No Reading");
    case P005_logNr::P005_error_protocol_timeout:    return F("Protocol Timeout");
    case P005_logNr::P005_error_checksum_error:      return F("Checksum Error");
    case P005_logNr::P005_error_invalid_NAN_reading: return F("Invalid NAN reading");
    case P005_logNr::P005_info_temperature:          return F("Temperature: ");
    case P005_logNr::P005_info_humidity:             return F("Humidity: ");
  }
  return F("");
}

/*********************************************************************************************\
* DHT sub to log an error
\*********************************************************************************************/
void P005_log(struct EventStruct *event, P005_logNr logNr)
{
  bool isError = true;

  switch (logNr) {
    case P005_logNr::P005_info_temperature:
    case P005_logNr::P005_info_humidity:
      isError = false;
      break;

    default:
      UserVar.setFloat(event->TaskIndex, 0, NAN);
      UserVar.setFloat(event->TaskIndex, 1, NAN);
      break;
  }

  if (loglevelActiveFor(isError ? LOG_LEVEL_ERROR : LOG_LEVEL_INFO)) {
    String text = concat(F("DHT  : "),
                         P005_logString(logNr));

    if (logNr == P005_logNr::P005_info_temperature) {
      text += formatUserVarNoCheck(event, 0);
    }
    else if (logNr == P005_logNr::P005_info_humidity) {
      text += formatUserVarNoCheck(event, 1);
    }
    addLogMove(isError ? LOG_LEVEL_ERROR : LOG_LEVEL_INFO, text);
  }
}

P005_data_struct::P005_data_struct(struct EventStruct *event) {
  SensorModel = PCONFIG(0);
  DHT_pin     = CONFIG_PIN1;
}

/*********************************************************************************************\
* DHT sub to wait until a pin is in a certain state
\*********************************************************************************************/
bool P005_data_struct::waitState(uint32_t state)
{
  const uint32_t start = micros();

# ifdef DEBUG_LOGIC_ANALYZER_PIN

  // DEBUG code using logic analyzer for timings
  DIRECT_pinWrite(DEBUG_LOGIC_ANALYZER_PIN, 0);
# endif // ifdef DEBUG_LOGIC_ANALYZER_PIN

  while (DIRECT_pinRead(DHT_pin) != state)
  {
    if (usecPassedSince_fast(start) >= 100) { return false; }
  }

# ifdef DEBUG_LOGIC_ANALYZER_PIN
  DIRECT_pinWrite(DEBUG_LOGIC_ANALYZER_PIN, 1);
# endif // ifdef DEBUG_LOGIC_ANALYZER_PIN
  return true;
}

/*********************************************************************************************\
* Perform the actual reading + interpreting of data.
\*********************************************************************************************/
bool P005_data_struct::readDHT(struct EventStruct *event) {
  // Call the "slow" function to make sure the pin is in a defined state.
  // Apparently the pull-up state may not always be in a well known state
  // With the direct pinmode calls we don't set the pull-up or -down resistors.
  pinMode(DHT_pin, INPUT_PULLUP);

# ifdef DEBUG_LOGIC_ANALYZER_PIN

  // DEBUG code using logic analyzer for timings
  DIRECT_PINMODE_OUTPUT(DEBUG_LOGIC_ANALYZER_PIN);
  DIRECT_pinWrite(DEBUG_LOGIC_ANALYZER_PIN, 0);
# endif // ifdef DEBUG_LOGIC_ANALYZER_PIN

  // To begin asking the DHT22 for humidity and temperature data,
  // Start sequence to get data from a DHTxx sensor:
  // Pin must be a logic 0 (low) for at least 500 microseconds (DHT22, others may need different timing)
  // followed by a logic 1 (high).
  DIRECT_PINMODE_OUTPUT(DHT_pin);
  DIRECT_pinWrite(DHT_pin, 0);           // Pull low

  switch (SensorModel) {
    case P005_DHT11:  delay(19); break;  // minimum 18ms
    case P005_DHT22:  delay(2);  break;  // minimum 1ms
    case P005_DHT12:  delay(200); break; // minimum 200ms
    case P005_AM2301: delayMicroseconds(900); break;
    case P005_SI7021: delayMicroseconds(500); break;
    case P005_MS01:   delayMicroseconds(450); break;
  }

  {
# ifdef DEBUG_LOGIC_ANALYZER_PIN
    DIRECT_pinWrite(DEBUG_LOGIC_ANALYZER_PIN, 1);
# endif // ifdef DEBUG_LOGIC_ANALYZER_PIN

    DIRECT_PINMODE_INPUT(DHT_pin);

    // pinMode(DHT_pin, INPUT_PULLUP);  // Way too slow, takes upto 227 usec

# ifdef DEBUG_LOGIC_ANALYZER_PIN
    DIRECT_pinWrite(DEBUG_LOGIC_ANALYZER_PIN, 0);
# endif // ifdef DEBUG_LOGIC_ANALYZER_PIN
  }

  bool readingAborted = false;
  uint8_t dht_dat[5]  = { 0 };

  uint8_t  dht_byte      = 0;
  uint32_t avg_low_total = 0;


  // Response from DHTxx: (N = 80 usec for DHT22)
  // Low for N usec
  // Hight for N usec
  // Low for 50 usec
  bool receive_start;

  uint8_t timings[16] = { 0 };


  ISR_noInterrupts();
  receive_start = waitState(0) && waitState(1) && waitState(0);

# ifdef DEBUG_LOGIC_ANALYZER_PIN
  DIRECT_pinWrite(DEBUG_LOGIC_ANALYZER_PIN, 0);
# endif // ifdef DEBUG_LOGIC_ANALYZER_PIN

  if (receive_start) {
    // We know we're now at a "low" state.
    uint32_t last_micros = micros();
    uint32_t prev_edge   = last_micros;

    for (dht_byte = 0; dht_byte < 5 && !readingAborted; ++dht_byte)
    {
      // Start reading next byte
# ifdef DEBUG_LOGIC_ANALYZER_PIN
      DIRECT_pinWrite(DEBUG_LOGIC_ANALYZER_PIN, 1);
# endif // ifdef DEBUG_LOGIC_ANALYZER_PIN

      for (uint8_t t = 0; t < 16 && !readingAborted; ++t) {
        // "even" index = "low" duration
        // "odd"  index = "high" duration
        const uint32_t current_state = (t & 1);

        // Wait till pin state has changed, or timeout.
        while (DIRECT_pinRead(DHT_pin) == current_state && !readingAborted)
        {
          // Keep track of last microsecond the state had not yet changed.
          // This way we are less dependent on any jitter caused by
          // the delay call or rise times of the voltage on the pin.
          last_micros = micros();

          if (timeDiff(prev_edge, last_micros) > 100) {
            readingAborted = true;
          }
        }

        if (!readingAborted) {
          // We know it is less than 100 usec, so it does fit in the uint8_t timings array.
          timings[t] = usecPassedSince_fast(prev_edge);
          prev_edge  = last_micros;
        } else {
          timings[t] = 255;
        }
      }
# ifdef DEBUG_LOGIC_ANALYZER_PIN
      DIRECT_pinWrite(DEBUG_LOGIC_ANALYZER_PIN, 0);
# endif // ifdef DEBUG_LOGIC_ANALYZER_PIN

      if (!readingAborted) {
        // Evaluate the timings
        // timings on even indices represent "duration low"
        // timings on odd  indices represent "duration high"
        //
        // Timing for a single bit:
        // Logic "1":  50 usec low, 70 usec high
        // Logic "0":  50 usec low, 26 usec high
        // There is a significant difference between the "high" state durations
        // Thus "high duration" > "avg_low duration" means it is an "1".
        //
        // By taking the average low duration, we get rid of
        // critical timing differences among modules and
        // environmental effects which may change these timings.
        // It is all about the relative timings.
        uint32_t avg_low = 0;

        // Don't take the 1st "low" period into account for computing avg_low
        // as there might be an extra wait between bytes.
        // Just to be sure as it is not clear from the documentation if all models act the same.
        for (uint8_t t = 2; t < 16; t += 2) {
          avg_low += timings[t];
        }
        avg_low       /= 7;
        avg_low_total += avg_low;

        dht_dat[dht_byte] = 0;

        for (uint8_t bit = 0; bit < 8; ++bit) {
          if (timings[2 * bit + 1] > avg_low) {
            dht_dat[dht_byte] |= (1 << (7 - bit));
          }
        }
      }
    }
  }
  ISR_interrupts();


  if (!receive_start) {
    P005_log(event, P005_logNr::P005_error_no_reading);
    return false;
  }

  # ifndef BUILD_NO_DEBUG

  if (dht_byte != 0) {
    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      String log = strformat(F("DHT  : Avg Low: %.2f usec %d bytes:"), static_cast<float>(avg_low_total) / dht_byte, dht_byte);

      for (int i = 0; i < dht_byte; ++i) {
        log += ' ';
        log += formatToHex_no_prefix(dht_dat[i], 2);
      }
      log += F(" timings:");

      for (int i = 0; i < 16; ++i) {
        log += ' ';
        log += timings[i];
      }
      addLogMove(LOG_LEVEL_DEBUG, log);
    }
  }
  # endif // ifndef BUILD_NO_DEBUG


  if (readingAborted) {
    P005_log(event, P005_logNr::P005_error_protocol_timeout);
    return false;
  }

  // Checksum calculation is a Rollover Checksum by design!
  uint8_t dht_check_sum = (dht_dat[0] + dht_dat[1] + dht_dat[2] + dht_dat[3]) & 0xFF; // check check_sum

  if (dht_dat[4] != dht_check_sum)
  {
    P005_log(event, P005_logNr::P005_error_checksum_error);
    return false;
  }

  float temperature = NAN;
  float humidity    = NAN;

  switch (SensorModel) {
    case P005_DHT11:
    case P005_DHT12:
      temperature = float(dht_dat[2] * 10 + (dht_dat[3] & 0x7f)) / 10.0f; // Temperature

      if (dht_dat[3] & 0x80) { temperature = -temperature; } // Negative temperature
      humidity = float(dht_dat[0] * 10 + dht_dat[1]) / 10.0f;             // Humidity
      break;
    case P005_DHT22:
    case P005_AM2301:
    case P005_SI7021:

      if (dht_dat[2] & 0x80) { // negative temperature
        temperature = -0.1f * word(dht_dat[2] & 0x7F, dht_dat[3]);
      }
      else {
        temperature = 0.1f * word(dht_dat[2], dht_dat[3]);
      }
      humidity = 0.1f * word(dht_dat[0], dht_dat[1]); // Humidity
      break;

    case P005_MS01:
    {
      // Conversion from Tasmota:
      // https://github.com/arendst/Tasmota/blob/0ea36d996c2b8b519ae5aa127f1a5fea354706af/tasmota/tasmota_xsns_sensor/xsns_06_dht_v7.ino#L297


      const int16_t voltage = ((dht_dat[0] << 8) | dht_dat[1]);

      // Rough approximate of soil moisture % (based on values observed in the eWeLink app)
      // Observed values are available here: https://gist.github.com/minovap/654cdcd8bc37bb0d2ff338f8d144a509


      // Info on capacitive soil moisture sensors:
      // https://makersportal.com/blog/2020/5/26/capacitive-soil-moisture-calibration-with-arduino

      if (voltage < 15037) {
        const float x = voltage - 15200;
        humidity = -powf(0.0024f * x, 3) - 0.0004f * x + 20.1f;
      }
      else if (voltage < 22300) {
        humidity = -0.00069f * voltage + 30.6f;
      }
      else {
        const float x = voltage - 22800;
        humidity = -powf(0.00046f * x, 3) - 0.0004f * x + 15;
      }

      if (definitelyLessThan(humidity, 0.0f)) {
        humidity = 0.0f;
      }

      temperature = voltage;
      break;
    }
  }

  if (isnan(temperature) || isnan(humidity)) {
    P005_log(event, P005_logNr::P005_error_invalid_NAN_reading);
    return false;
  }

  UserVar.setFloat(event->TaskIndex, 0, temperature);
  UserVar.setFloat(event->TaskIndex, 1, humidity);
  P005_log(event, P005_logNr::P005_info_temperature);
  P005_log(event, P005_logNr::P005_info_humidity);
  return true;
}

#endif // ifdef USES_P005

#include "../PluginStructs/P009_data_struct.h"

#ifdef USES_P009

# include "../Helpers/_Plugin_Helper_webform.h"

# include "../ESPEasyCore/ESPEasyGPIO.h"


uint8_t P009_data_struct::getI2C_address(struct EventStruct *event)
{
  const uint8_t unit = (CONFIG_PORT - 1) / 16;

  return 0x20 + unit;
}

P009_data_struct::P009_data_struct(struct EventStruct *event) :
  _data(
    PLUGIN_MCP,
    CONFIG_PORT,
    P009_DEBOUNCE,
    P009_DC_MAX_INT,
    P009_LP_MIN_INT,
    P009_DOUBLECLICK,
    P009_LONGPRESS,
    P009_SAFE_BTN != 0,
    P009_BOOTSTATE)
# if FEATURE_I2C_DEVICE_CHECK
  , _address(P009_data_struct::getI2C_address(event))
# endif // if FEATURE_I2C_DEVICE_CHECK
{
  // Turn on Pullup resistor
  setMCPInputAndPullupMode(_data._pin, true);

  // read and store current state to prevent switching at boot time
  // "state" could be -1, 0 or 1
  const int8_t state = GPIO_MCP_Read(_data._pin);

  _data.init(
    event,
    state,
    (state == -1) ?  PIN_MODE_OFFLINE : PIN_MODE_INPUT_PULLUP);

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLog(LOG_LEVEL_INFO,
           concat(F("MCP INIT="), state));
  }
}

void P009_data_struct::tenPerSecond(struct EventStruct *event)
{
# if FEATURE_I2C_DEVICE_CHECK

  if (!I2C_deviceCheck(_address, event->TaskIndex, 10, PLUGIN_I2C_GET_ADDRESS)) { // Generate stats
    return;                                                                       // Will return the default false for success
  }
# endif // if FEATURE_I2C_DEVICE_CHECK

  const int8_t state = GPIO_MCP_Read(_data._pin);

  _data.tenPerSecond(event, F("MCP"), state);
}

#endif // ifdef USES_P009

#include "../PluginStructs/P124_data_struct.h"

#ifdef USES_P124

// **************************************************************************/
// Constructor
// **************************************************************************/
P124_data_struct::P124_data_struct(int8_t  i2c_address,
                                   uint8_t relayCount,
                                   bool    changeAddress)
  : _i2c_address(i2c_address), _relayCount(relayCount), _changeAddress(changeAddress)
{}

// **************************************************************************/
// Destructor
// **************************************************************************/
P124_data_struct::~P124_data_struct() {
  if (isInitialized()) {
    delete relay;
    relay = nullptr;
  }
}

bool P124_data_struct::init() {
  relay = new (std::nothrow) Multi_Channel_Relay();

  if (isInitialized()) {
    relay->begin(_i2c_address);

    if (_changeAddress) {
      // This increment shpould match with the range of addresses in _P124_MultiRelay.ino PLUGIN_I2C_HAS_ADDRESS
      const uint8_t _new_address = _i2c_address == 0x18 ? 0x11 : _i2c_address + 1; // Set to next address
      relay->changeI2CAddress(_new_address, _i2c_address);
      # ifndef BUILD_NO_DEBUG

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLogMove(LOG_LEVEL_INFO, strformat(F("MultiRelay: Change I2C address 0x%02x to 0x%02x"), _i2c_address, _new_address));
      }
      # endif // ifndef BUILD_NO_DEBUG
    }
  }
  return isInitialized();
}

uint8_t P124_data_struct::getChannelState() {
  if (isInitialized()) {
    return relay->getChannelState();
  }
  return 0u;
}

uint8_t P124_data_struct::getFirmwareVersion() {
  if (isInitialized()) {
    return relay->getFirmwareVersion();
  }
  return 0u;
}

bool P124_data_struct::channelCtrl(uint8_t state) {
  if (isInitialized()) {
    relay->channelCtrl(state);
    return true;
  }
  return false;
}

bool P124_data_struct::turn_on_channel(uint8_t channel) {
  if (isInitialized() && (validChannel(channel))) {
    relay->turn_on_channel(channel);
    return true;
  }
  return false;
}

bool P124_data_struct::turn_off_channel(uint8_t channel) {
  if (isInitialized() && (validChannel(channel))) {
    relay->turn_off_channel(channel);
    return true;
  }
  return false;
}

uint8_t P124_data_struct::getNextLoop() {
  if (_loopEnabled) {
    _getLoop++;

    if (_getLoop > _relayCount) {
      _getLoop = 1;
    }
    return _getLoop;
  }
  return 0;
}

#endif // ifdef USES_P124

#include "../PluginStructs/P108_data_struct.h"

#ifdef USES_P108


P108_data_struct::~P108_data_struct() {
  modbus.reset();
}

void P108_data_struct::reset() {
  modbus.reset();
}

bool P108_data_struct::init(ESPEasySerialPort port, const int16_t serial_rx, const int16_t serial_tx, int8_t dere_pin,
                            unsigned int baudrate, uint8_t modbusAddress) {
  return modbus.init(port, serial_rx, serial_tx, baudrate, modbusAddress, dere_pin);
}

const __FlashStringHelper* Plugin_108_valuename(uint8_t value_nr, bool displayString) {
  switch (value_nr) {
    case P108_QUERY_V: return displayString ? F("Voltage (V)") : F("V");
    case P108_QUERY_A: return displayString ? F("Current (A)") : F("A");
    case P108_QUERY_W: return displayString ? F("Active Power (W)") : F("W");
    case P108_QUERY_VA: return displayString ? F("Reactive Power (VA)") : F("VA");
    case P108_QUERY_PF: return displayString ? F("Power Factor (Pf)") : F("Pf");
    case P108_QUERY_F: return displayString ? F("Frequency (Hz)") : F("Hz");
    case P108_QUERY_Wh_imp: return displayString ? F("Import Energy (Wh)") : F("Wh_imp");
    case P108_QUERY_Wh_exp: return displayString ? F("Export Energy (Wh)") : F("Wh_exp");
    case P108_QUERY_Wh_tot: return displayString ? F("Total Energy (Wh)") : F("Wh_tot");
  }
  return F("");
}

# if FEATURE_MQTT_DISCOVER
int Plugin_108_QueryVType(uint8_t value_nr) {
  const Sensor_VType vtypes[] = {
    Sensor_VType::SENSOR_TYPE_VOLTAGE_ONLY,
    Sensor_VType::SENSOR_TYPE_CURRENT_ONLY,
    Sensor_VType::SENSOR_TYPE_POWER_USG_ONLY,
    Sensor_VType::SENSOR_TYPE_REACTIVE_POWER_ONLY,
    Sensor_VType::SENSOR_TYPE_POWER_FACT_ONLY,
    Sensor_VType::SENSOR_TYPE_FREQUENCY,
    Sensor_VType::SENSOR_TYPE_ENERGY,
    Sensor_VType::SENSOR_TYPE_ENERGY,
    Sensor_VType::SENSOR_TYPE_ENERGY,
  };
  constexpr uint8_t  valueCount = NR_ELEMENTS(vtypes);
  Sensor_VType result           = Sensor_VType::SENSOR_TYPE_NONE;

  if (value_nr < valueCount) {
    result = vtypes[value_nr];
  }
  return static_cast<int>(result);
}

# endif // if FEATURE_MQTT_DISCOVER

int p108_storageValueToBaudrate(uint8_t baudrate_setting) {
  switch (baudrate_setting) {
    case 0:
      return 1200;
    case 1:
      return 2400;
    case 2:
      return 4800;
    case 3:
      return 9600;
  }
  return 9600;
}

float p108_readValue(uint8_t query, struct EventStruct *event) {
  uint8_t errorcode           = -1;   // DF - not present in P085
  float   value               = 0.0f; // DF - not present in P085
  P108_data_struct *P108_data =
    static_cast<P108_data_struct *>(getPluginTaskData(event->TaskIndex));

  if ((nullptr != P108_data) && P108_data->isInitialized()) {
    switch (query) {
      case P108_QUERY_V:
        value = P108_data->modbus.readHoldingRegister(0x0C, errorcode) / 10.0f;  // 0.1 V => V
        break;
      case P108_QUERY_A:
        value = P108_data->modbus.readHoldingRegister(0x0D, errorcode) / 100.0f; // 0.01 A => A
        break;
      case P108_QUERY_W:
        value =  P108_data->modbus.readHoldingRegister(0x0E, errorcode);

        if (value > 32767) { value -= 65535; }
        break;
      case P108_QUERY_VA:
        value = P108_data->modbus.readHoldingRegister(0x0F, errorcode);

        if (value > 32767) { value -= 65535; }
        break;
      case P108_QUERY_PF:
        value = P108_data->modbus.readHoldingRegister(0x10, errorcode) / 1000.0f; // 0.001 Pf => Pf
        break;
      case P108_QUERY_F:
        value = P108_data->modbus.readHoldingRegister(0x11, errorcode) / 100.0f;  // 0.01 Hz => Hz
        break;
      case P108_QUERY_Wh_imp:
        return P108_data->modbus.read_32b_HoldingRegister(0x0A) * 10.0f;          // 0.01 kWh => Wh
        break;
      case P108_QUERY_Wh_exp:
        return P108_data->modbus.read_32b_HoldingRegister(0x08) * 10.0f;          // 0.01 kWh => Wh
        break;
      case P108_QUERY_Wh_tot:
        return P108_data->modbus.read_32b_HoldingRegister(0x00) * 10.0f;          // 0.01 kWh => Wh
        break;
    }
  }

  if (errorcode == 0) { return value; // DF - not present in P085
  }
  return 0.0f;
}

void p108_showValueLoadPage(uint8_t query, struct EventStruct *event) {
  addRowLabel(Plugin_108_valuename(query, true));
  addHtml(String(p108_readValue(query, event)));
}

#endif // ifdef USES_P108

#include "../PluginStructs/P079_data_struct.h"

#ifdef USES_P079


WemosMotor::WemosMotor(uint8_t address, uint8_t motor, uint32_t freq)
  : _address(address), _freq(freq), _use_STBY_IO(false)
{
  if (motor == P079_MOTOR_A) {
    _motor = P079_MOTOR_A;
  }
  else {
    _motor = P079_MOTOR_B;
  }
}

WemosMotor::WemosMotor(uint8_t address, uint8_t motor, uint32_t freq, uint8_t STBY_IO)
  : _address(address), _freq(freq), _use_STBY_IO(true), _STBY_IO(STBY_IO)
{
  if (motor == P079_MOTOR_A) {
    _motor = P079_MOTOR_A;
  }
  else {
    _motor = P079_MOTOR_B;
  }
}

void WemosMotor::init() {
  setfreq(_freq);

  if (_use_STBY_IO) {
    pinMode(_STBY_IO, OUTPUT);
    digitalWrite(_STBY_IO, LOW);
  }
}

/* setfreq() -- set PWM's frequency
   freq: PWM's frequency

   total 4bytes
 |0.5byte CMD     | 3.5byte Parm|
 |CMD             | parm        |
 |0x0X  set freq  | uint32  freq|
 */
void WemosMotor::setfreq(uint32_t freq)
{
  Wire.beginTransmission(_address);
  Wire.write(((uint8_t)(freq >> 24)) & (uint8_t)0x0f);
  Wire.write((uint8_t)(freq >> 16));
  Wire.write((uint8_t)(freq >> 8));
  Wire.write((uint8_t)freq);
  Wire.endTransmission(); // stop transmitting
  delay(0);
}

/* setmotor() -- set motor
   motor:
        P079_MOTOR_A    0   Motor A
        P079_MOTOR_B    1   Motor B

   dir:
        P079_SHORT_BRAKE  0
        P079_CCW          1
        P079_CW               2
        P079_STOP         3
        P079_STANDBY      4

   pwm_val:
        0.00 - 100.00  (%)

   total 4bytes
 |0.5byte CMD      | 3.5byte Parm         |
 |CMD              | parm                 |
 |0x10  set motorA | uint8 dir  uint16 pwm|
 |0x11  set motorB | uint8 dir  uint16 pwm|
 */
void WemosMotor::setmotor(uint8_t dir, float pwm_val)
{
  uint16_t _pwm_val;

  if (_use_STBY_IO == true) {
    if (dir == P079_STANDBY) {
      digitalWrite(_STBY_IO, LOW);
      return;
    } else {
      digitalWrite(_STBY_IO, HIGH);
    }
  }

  Wire.beginTransmission(_address);
  Wire.write(_motor | (uint8_t)0x10); // CMD either 0x10 or 0x11
  Wire.write(dir);

  // PWM in %
  _pwm_val = uint16_t(pwm_val * 100);

  if (_pwm_val > 10000) { // _pwm_val > 100.00
    _pwm_val = 10000;
  }

  Wire.write((uint8_t)(_pwm_val >> 8));
  Wire.write((uint8_t)_pwm_val);
  Wire.endTransmission(); // stop transmitting

  delay(0);
}

LOLIN_I2C_MOTOR::LOLIN_I2C_MOTOR(uint8_t address) : _address(address) {}

/*
    Change Motor Status.
    ch: Motor Channel
        MOTOR_CH_A
        MOTOR_CH_B
        MOTOR_CH_BOTH

    sta: Motor Status
        MOTOR_STATUS_STOP
        MOTOR_STATUS_CCW
        MOTOR_STATUS_CW
        MOTOR_STATUS_SHORT_BRAKE
        MOTOR_STATUS_STANDBY
 */
unsigned char LOLIN_I2C_MOTOR::changeStatus(unsigned char ch, unsigned char sta)
{
  send_data[0] = CHANGE_STATUS;
  send_data[1] = ch;
  send_data[2] = sta;
  unsigned char result = sendData(send_data, 3);

  return result;
}

/*
    Change Motor Frequency
        ch: Motor Channel
            MOTOR_CH_A
            MOTOR_CH_B
            MOTOR_CH_BOTH

        freq: PWM frequency (Hz)
            1 - 80KHz, typically 1000Hz
 */
unsigned char LOLIN_I2C_MOTOR::changeFreq(unsigned char ch, uint32_t freq)
{
  send_data[0] = CHANGE_FREQ;
  send_data[1] = ch;

  send_data[2] = (uint8_t)(freq & 0xff);
  send_data[3] = (uint8_t)((freq >> 8) & 0xff);
  send_data[4] = (uint8_t)((freq >> 16) & 0xff);
  unsigned char result = sendData(send_data, 5);

  return result;
}

/*
    Change Motor Duty.
    ch: Motor Channel
        MOTOR_CH_A
        MOTOR_CH_B
        MOTOR_CH_BOTH

    duty: PWM Duty (%)
        0.01 - 100.00 (%)
 */
unsigned char LOLIN_I2C_MOTOR::changeDuty(unsigned char ch, float duty)
{
  uint16_t _duty;

  _duty = (uint16_t)(duty * 100);

  send_data[0] = CHANGE_DUTY;
  send_data[1] = ch;

  send_data[2] = (uint8_t)(_duty & 0xff);
  send_data[3] = (uint8_t)((_duty >> 8) & 0xff);
  unsigned char result = sendData(send_data, 4);

  return result;
}

/*
    Reset Device.
 */
unsigned char LOLIN_I2C_MOTOR::reset()
{
  send_data[0] = RESET_SLAVE;
  unsigned char result = sendData(send_data, 1);

  return result;
}

/*
    Change Device I2C address
    address: when address=0, address>=127, will change address to default I2C address 0x31
 */
unsigned char LOLIN_I2C_MOTOR::changeAddress(unsigned char address)
{
  send_data[0] = CHANGE_I2C_ADDRESS;
  send_data[1] = address;
  unsigned char result = sendData(send_data, 2);

  return result;
}

/*
    Get PRODUCT_ID and Firmwave VERSION
 */
unsigned char LOLIN_I2C_MOTOR::getInfo(void)
{
  send_data[0] = GET_SLAVE_STATUS;
  unsigned char result = sendData(send_data, 1);

  if (result == 0)
  {
    PRODUCT_ID = get_data[0];
    VERSION_ID = get_data[1];
  }
  else
  {
    PRODUCT_ID = 0;
    VERSION_ID = 0;
  }

  return result;
}

/*
    Send and Get I2C Data
 */
unsigned char LOLIN_I2C_MOTOR::sendData(unsigned char *data, unsigned char len)
{
  unsigned char i;

  if ((_address == 0) || (_address >= 127))
  {
    return 1;
  }
  else
  {
    Wire.beginTransmission(_address);

    for (i = 0; i < len; i++) {
      Wire.write(data[i]);
    }
    Wire.endTransmission();
    delay(50);

    if (data[0] == GET_SLAVE_STATUS) {
      Wire.requestFrom(static_cast<int>(_address), 2);
    }
    else {
      Wire.requestFrom(static_cast<int>(_address), 1);
    }

    i = 0;

    while (Wire.available())
    {
      get_data[i] = Wire.read();
      i++;
    }

    return 0;
  }
}

#endif // ifdef USES_P079

#include "../PluginStructs/P057_data_struct.h"

#ifdef USES_P057

// Needed also here for PlatformIO's library finder as the .h file 
// is in a directory which is excluded in the src_filter
# include <HT16K33.h>


P057_data_struct::P057_data_struct(uint8_t i2c_addr) : i2cAddress(i2c_addr) {
  ledMatrix.Init(i2cAddress);
}

#endif // ifdef USES_P057

//////////////////////////////////////////////////////////////////////////////////////////////////
// P122 device class for SHT2x temperature & humidity sensors
// See datasheet https://www.sensirion.com/products/catalog/SHT21/
// Based upon code from Rob Tillaart, Viktor Balint, https://github.com/RobTillaart/SHT2x
// Rewritten and adapted for ESPeasy by Flashmark
// 2023-04-01 Initial version by Flashmark
//////////////////////////////////////////////////////////////////////////////////////////////////

#include "../PluginStructs/P122_data_struct.h"
#ifdef USES_P122

//  SUPPORTED SHT2x COMMANDS
# define SHT2x_GET_TEMPERATURE_NO_HOLD   0xF3 // Read temperature without holding I2C
# define SHT2x_GET_HUMIDITY_NO_HOLD      0xF5 // Read humidity without holding I2C
# define SHT2x_GET_TEMPERATURE_HOLD      0xE3 // Read temperature holding I2C during measurement
# define SHT2x_GET_HUMIDITY_HOLD         0xE5 // Read humidity holding I2C during measurement
# define SHT2x_SOFT_RESET                0xFE // Software reset
# define SHT2x_WRITE_USER_REGISTER       0xE6 // Write the user register
# define SHT2x_READ_USER_REGISTER        0xE7 // Read the user register
# define SHT2x_GET_EIDA                  0xFA // Reverse engineering
# define SHT2x_EIDA_ADDRESS              0x0F // 2nd command byte to read EIDA
# define SHT2x_GET_EIDB                  0xFC // Reverse engineering
# define SHT2x_EIDB_ADDRESS              0xC9 // 2nd commnad byte to read EIDB
# define SHT2x_GET_FIRMWARE              0x84 // Reverse engineering
# define SHT2x_FIRMWARE_ADDRESS          0xB8 // 2nd command byte to read firmware version

// Bitflags for SHT2x User Register
# define SHT2x_USRREG_RESOLUTION         0x81 // Resolution split into bits 7 and 0
# define SHT2x_USRREG_BATTERY            0x40 // Battery status flag, bit 6
# define SHT2x_USRREG_RESERVED           0x38 // Reserved bits, do not modify, bit 3,4,5
# define SHT2x_USRREG_HEATER             0x04 // Heater enable, bit 2
# define SHT2x_USRREG_OTP                0x02 // Disable OTP reload, bit 1

# define SHT2x_READ_VAL_TIME                5 // Timeout value for reading the termperature/humidity values [ms]
# define SHT2x_READ_REG_TIME               10 // Timeout value for reading a register sequence [ms]
# define SHT2x_READ_USR_TIME                5 // Timeout value for reading user register [ms]
# define P122_RESET_DELAY                  15 // delay in miliseconds for the reset to settle down [ms]
# define P122_MAX_RETRY                   250 // Give up after amount of retries befoe going to error

//////////////////////////////////////////////////////////////////////////////////////////////////
//
//  PUBLIC
//
P122_data_struct::P122_data_struct() {}

// Initialize/setup device properties
// Must be called at least once before oP122::Wairperating the device
bool P122_data_struct::setupDevice(uint8_t i2caddr, uint8_t resolution)
{
  _i2caddr    = i2caddr;
  _resolution = resolution;

# ifdef PLUGIN_122_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO))
  {
    addLog(LOG_LEVEL_INFO,
           strformat(F("SHT2x : Setup Device with address= %x resolution= %d"),
                     _i2caddr,
                     resolution));
  }
# endif // ifdef PLUGIN_122_DEBUG
  return true;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Evaluate FSM for data acquisition
// This is a state machine that is evaluated step by step by calling update() repetatively
// NOTE: Function is expected to run as critical section w.r.t. other provided functions
//       This is typically met in ESPeasy plugin context when called from within the plugin
bool P122_data_struct::update()
{
  bool stable = false; // signals when a stable state is reached

# ifdef PLUGIN_122_DEBUG
  P122_state oldState = _state;
# endif // ifdef PLUGIN_122_DEBUG

  switch (_state)
  {
    case P122_state::Uninitialized:

      // we have to stop trying after a while
      if (_errCount > P122_MAX_RETRY) {
        _state = P122_state::Error;
        stable = true;
      }
      else if (I2C_wakeup(_i2caddr) != 0) // Try to access the I2C device
      {
        if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
          addLog(LOG_LEVEL_ERROR, strformat(F("SHT2x : Not found at I2C address: %x"), _i2caddr));
        }
        _errCount++;
      }
      else if (writeCmd(SHT2x_SOFT_RESET))                 // Issue a reset command
      {
        _state               = P122_state::Wait_for_reset; // Will take <15ms according to datasheet
        _last_action_started = millis();
      }
      break;

    case P122_state::Wait_for_reset:

      if (timeOutReached(_last_action_started + P122_RESET_DELAY)) // we need to wait for the chip to reset
      {
        if (I2C_wakeup(_i2caddr) != 0)
        {
          _errCount++;
          _state = P122_state::Uninitialized; // Retry
        }
        else
        {
          _errCount = 0; // Device is reachable and initialized, reset error counter
          _state    = P122_state::Read_eid;
        }
      }
      break;

    case P122_state::Read_eid:
# ifndef LIMIT_BUILD_SIZE
      _eida     = getEIDA();
      _eidb     = getEIDB();
      _firmware = getFirmwareVersion();
# endif // ifndef LIMIT_BUILD_SIZE
      _state = P122_state::Write_user_reg;
      break;

    case P122_state::Write_user_reg:
      writeUserReg();
      _state = P122_state::Initialized;
      break;

    case P122_state::Initialized:
      // For now trigger the first read cycle automatically
      _state = P122_state::Ready;
      break;

    case P122_state::Ready:

      // Ready to execute a measurement cycle
      // Start measuring temperature
      if (!I2C_write8(_i2caddr, SHT2x_GET_TEMPERATURE_NO_HOLD))
      {
        _errCount++;
        _state = P122_state::Uninitialized; // Retry
      }
      else
      {
        _last_action_started = millis();
        _state               = P122_state::Wait_for_temperature_samples;
      }
      break;

    case P122_state::Wait_for_temperature_samples:

      if (timeOutReached(_last_action_started + getTempDuration()))
      {
        if (!readValue(_rawTemperature)) {    // Read the previously measured temperature
          _errCount++;
          _state = P122_state::Uninitialized; // Lost connection
        }
        else if (!requestHumidity())          // Start humidity measurement
        {
          _errCount++;
          _state = P122_state::Uninitialized; // Lost connection
        }
        else
        {
          _last_action_started = millis();
          _state               = P122_state::Wait_for_humidity_samples;
        }
      }
      break;

    case P122_state::Wait_for_humidity_samples:

      // make sure we wait for the measurement to complete
      if (timeOutReached(_last_action_started + getHumDuration()))
      {
        if (!readValue(_rawHumidity))
        {
          _errCount++;
          _state = P122_state::Uninitialized; // Lost connection
        }
        else
        {
          _last_action_started = millis();
          _state               = P122_state::New_Values_Available;
          stable               = true;
        }
      }
      break;

    case P122_state::Error:
    case P122_state::New_Values_Available:
      // this state is used outside so all we need is to stay here
      stable = true;
      break;

      // Missing states (enum values) to be checked by the compiler
  } // switch

# ifdef PLUGIN_122_DEBUG

  if (_state != oldState)
  {
    if (loglevelActiveFor(LOG_LEVEL_INFO))
    {
      addLog(LOG_LEVEL_INFO, strformat(F("SHT2x : ***state transition %d-->%d"), static_cast<int>(oldState), static_cast<int>(_state)));
    }
  }
# endif // ifdef PLUGIN_122_DEBUG
  return stable;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Returns the I2C connection state
// Note: based upon the FSM state without actual accessing the device
bool P122_data_struct::isConnected() const
{
  switch (_state)
  {
    case  P122_state::Initialized:
    case  P122_state::Ready:
    case  P122_state::Wait_for_temperature_samples:
    case  P122_state::Wait_for_humidity_samples:
    case  P122_state::New_Values_Available:
    case  P122_state::Read_eid:
    case  P122_state::Write_user_reg:
      return true;
      break;
    case  P122_state::Uninitialized:
    case  P122_state::Error:
    case  P122_state::Wait_for_reset:
      return false;
      break;

      // Missing states (enum values) to be checked by the compiler
  }
  return false;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Returns if the device communication is in error
// Note: based upon the FSM state without actual accessing the device
bool P122_data_struct::inError() const
{
  return _state == P122_state::Error;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Returns if new acquired values are available
bool P122_data_struct::newValues() const
{
  return _state == P122_state::New_Values_Available;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Restart the FSM used to access the device
bool P122_data_struct::reset()
{
  _state = P122_state::Uninitialized;
  return true;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Start a new measurement cycle
bool P122_data_struct::startMeasurements()
{
  if ((_state == P122_state::New_Values_Available) || (_state == P122_state::Initialized))
  {
    _state = P122_state::Ready;
  }
  return true;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Get the electronic idenfification data store in the device
// Note: The data is read from the device during initialization
bool P122_data_struct::getEID(uint32_t& eida, uint32_t& eidb, uint8_t& firmware) const
{
  eida     = _eida;
  eidb     = _eidb;
  firmware = _firmware;
  return true;
}

uint8_t P122_data_struct::getUserReg() const
{
  return _userreg;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Return the previously measured temperature in [C]
float P122_data_struct::getTemperature() const
{
  return -46.85f + (175.72f / 65536.0f) * _rawTemperature; // Datasheet par 6.2
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Return the previously measured humidity in [%RH]
float P122_data_struct::getHumidity() const
{
  return -6.0f + (125.0f / 65536.0f) * _rawHumidity; // Datasheet par  6.1
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Return the previously measured raw temperature data [bits]
uint16_t P122_data_struct::getRawTemperature() const
{
  return _rawTemperature;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Return the previously measured raw humidity data [bits]
uint16_t P122_data_struct::getRawHumidity() const
{
  return _rawHumidity;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Set the resolution selection
// Resolution is one of 4 predefined settings
// Note: This will only set a new value in the plugin data struct. Reset device to load the new value
bool P122_data_struct::setResolution(uint8_t res)
{
  _resolution = res;
  return true;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Return the last set resolution
uint8_t P122_data_struct::getResolution() const
{
  return _resolution;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Retrieve the battery status (power supply level)
// Note: This will bypass the FSM and directly accesses the user register of the SHT2x
bool P122_data_struct::batteryOK()
{
  uint8_t userReg = 0x00;

  writeCmd(SHT2x_READ_USER_REGISTER);

  if (!readBytes(1, (uint8_t *)&userReg, SHT2x_READ_USR_TIME))
  {
    return false;
  }
  return (userReg & SHT2x_USRREG_BATTERY) == 0;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
//
//  PROTECTED
//
//////////////////////////////////////////////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////////////////////////////////////////////
uint8_t P122_data_struct::crc8(const uint8_t *data, uint8_t len)
{
  // CRC-8 formula from page 14 of SHT spec pdf
  // Sensirion_Humidity_Sensors_SHT2x_CRC_Calculation.pdf
  const uint8_t POLY = 0x31;
  uint8_t crc        = 0x00;

  for (uint8_t j = len; j; --j)
  {
    crc ^= *data++;

    for (uint8_t i = 8; i; --i)
    {
      crc = (crc & 0x80) ? (crc << 1) ^ POLY : (crc << 1);
    }
  }
  return crc;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
bool P122_data_struct::writeCmd(uint8_t cmd)
{
  return I2C_write8(_i2caddr, cmd);
}

//////////////////////////////////////////////////////////////////////////////////////////////////
bool P122_data_struct::writeCmd(uint8_t cmd, uint8_t value)
{
  return I2C_write8_reg(_i2caddr, cmd, value);
}

//////////////////////////////////////////////////////////////////////////////////////////////////
bool P122_data_struct::readBytes(uint8_t n, uint8_t *val, uint8_t maxDuration)
{
  // TODO check if part can be delegated to the I2C_access libraray from ESPeasy
  Wire.requestFrom(_i2caddr, (uint8_t)n);
  uint32_t start = millis();

  while (Wire.available() < n)
  {
    if (timePassedSince(start) > maxDuration)
    {
      return false;
    }
    yield();
  }

  for (uint8_t i = 0; i < n; i++)
  {
    val[i] = Wire.read();
  }
  return true;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Send Temperature measurement command to device
bool P122_data_struct::requestTemperature()
{
  return writeCmd(SHT2x_GET_TEMPERATURE_NO_HOLD);
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Sent Humidity measurement command to device
bool P122_data_struct::requestHumidity()
{
  return writeCmd(SHT2x_GET_HUMIDITY_NO_HOLD);
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Read temperature/humidity measurement results from device
bool P122_data_struct::readValue(uint16_t& value)
{
  uint8_t buffer[3];

  if (!readBytes(3, (uint8_t *)&buffer[0], SHT2x_READ_VAL_TIME))
  {
    return false;
  }

  if (crc8(buffer, 2) == buffer[2])
  {
    value  = buffer[0] << 8;
    value += buffer[1];
    value &= 0xFFFC;
  }
  return true;
}

# ifndef LIMIT_BUILD_SIZE

//////////////////////////////////////////////////////////////////////////////////////////////////
//  Retrieve SHT2x identification code part I
//  Sensirion_Humidity_SHT2x_Electronic_Identification_Code_V1.1.pdf
uint32_t P122_data_struct::getEIDA()
{
  uint32_t id = 0;
  uint8_t  buffer[8];

  writeCmd(SHT2x_GET_EIDA, SHT2x_EIDA_ADDRESS);

  if (readBytes(8, (uint8_t *)buffer, SHT2x_READ_REG_TIME))
  {
    for (uint8_t i = 0; i < 4; i++)
    {
      id <<= 8;
      id  |= buffer[i * 2]; //  SNB, skip CRC's use only uneven bytes
    }
  }
  return id;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
//  Retrieve SHT2x identification code part II
//  Sensirion_Humidity_SHT2x_Electronic_Identification_Code_V1.1.pdf
uint32_t P122_data_struct::getEIDB()
{
  uint32_t id = 0;
  uint8_t  buffer[8];

  writeCmd(SHT2x_GET_EIDB, SHT2x_EIDB_ADDRESS);

  if (readBytes(8, (uint8_t *)buffer, SHT2x_READ_REG_TIME))
  {
    id   = buffer[0]; //  SNC_1
    id <<= 8;
    id  |= buffer[1]; //  SNC_0
    id <<= 8;
    id  |= buffer[3]; //  SNA_1
    id <<= 8;
    id  |= buffer[4]; //  SNA_0
  }
  return id;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Retrieve SHT2x Firmware version from device
uint8_t P122_data_struct::getFirmwareVersion()
{
  uint8_t version = 0;

  writeCmd(SHT2x_GET_FIRMWARE, SHT2x_FIRMWARE_ADDRESS);

  if (!readBytes(1, (uint8_t *)&version, SHT2x_READ_REG_TIME))
  {
    version = 0;
  }
  return version;
}

# endif // ifndef LIMIT_BUILD_SIZE

//////////////////////////////////////////////////////////////////////////////////////////////////
// Write the user register with data stored in the plugin data struct
bool P122_data_struct::writeUserReg()
{
  uint8_t userReg = 0;

  // First fetch the current userReg value
  writeCmd(SHT2x_READ_USER_REGISTER);

  if (!readBytes(1, (uint8_t *)&userReg, SHT2x_READ_USR_TIME))
  {
    return false;
  }

  // Set selected resolution which is split into bit 7 and bit 0
  userReg &= ~SHT2x_USRREG_RESOLUTION; //  clear old resolution and set new
  userReg &= ~SHT2x_USRREG_HEATER;     // Reset heater control bit
  userReg |= ((_resolution & 0x02) << 6);
  userReg |= (_resolution & 0x01);

  if (!writeCmd(SHT2x_WRITE_USER_REGISTER, userReg))
  {
    return false;
  }

# ifdef PLUGIN_122_DEBUG

  // Read back the register for debugging purpose only
  writeCmd(SHT2x_READ_USER_REGISTER);

  if (!readBytes(1, (uint8_t *)&userReg, SHT2x_READ_USR_TIME))
  {
    return false;
  }
  _userreg = userReg;
# endif // ifdef PLUGIN_122_DEBUG
  return true;
}

//////////////////////////////////////////////////////////////////////////////////////////////////

unsigned long P122_data_struct::getTempDuration()
{
  //  Datasheet table 7
  switch (_resolution)
  {
    case 0: return 85;  // 14 bit
    case 1: return 22;  // 12 bit
    case 2: return 43;  // 13 bit
    case 3: return 11;  // 11 bit
    default: return 85; // Save value?
  }
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// Calculate the minimum delay for humidity measurement (depends on the resolution)
unsigned long P122_data_struct::getHumDuration()
{
  // Datasheet table 7
  switch (_resolution)
  {
    case 0: return 29;  // 12 bit
    case 1: return 4;   //  8 bit
    case 2: return 9;   // 10 bit
    case 3: return 15;  // 11 bit
    default: return 29; // Save value?
  }
}

#endif // USES_P122

#include "../PluginStructs/P032_data_struct.h"

#ifdef USES_P032


enum
{
  MS5xxx_CMD_RESET    = 0x1E, // perform reset
  MS5xxx_CMD_ADC_READ = 0x00, // initiate read sequence
  MS5xxx_CMD_ADC_CONV = 0x40, // start conversion
  MS5xxx_CMD_ADC_D1   = 0x00, // read ADC 1
  MS5xxx_CMD_ADC_D2   = 0x10, // read ADC 2
  MS5xxx_CMD_ADC_256  = 0x00, // set ADC oversampling ratio to 256
  MS5xxx_CMD_ADC_512  = 0x02, // set ADC oversampling ratio to 512
  MS5xxx_CMD_ADC_1024 = 0x04, // set ADC oversampling ratio to 1024
  MS5xxx_CMD_ADC_2048 = 0x06, // set ADC oversampling ratio to 2048
  MS5xxx_CMD_ADC_4096 = 0x08, // set ADC oversampling ratio to 4096
  MS5xxx_CMD_PROM_RD  = 0xA0  // initiate readout of PROM registers
};


P032_data_struct::P032_data_struct(uint8_t i2c_addr) : i2cAddress(i2c_addr) {}


// **************************************************************************/
// Initialize MS5611
// **************************************************************************/
bool P032_data_struct::begin() {
  return 0 == I2C_wakeup(i2cAddress);
}

// **************************************************************************/
// Reads the PROM of MS5611
// There are in total 8 addresses resulting in a total memory of 128 bit.
// Address 0 contains factory data and the setup, addresses 1-6 calibration
// coefficients and address 7 contains the serial code and CRC.
// The command sequence is 8 bits long with a 16 bit result which is
// clocked with the MSB first.
// **************************************************************************/
void P032_data_struct::read_prom() {
  I2C_write8(i2cAddress, MS5xxx_CMD_RESET);
  delay(3);

  for (uint8_t i = 0; i < 8; i++)
  {
    ms5611_prom[i] = I2C_read16_reg(i2cAddress, MS5xxx_CMD_PROM_RD + 2 * i);
  }
}

// **************************************************************************/
// Read analog/digital converter
// **************************************************************************/
unsigned long P032_data_struct::read_adc(unsigned char aCMD)
{
  I2C_write8(i2cAddress, MS5xxx_CMD_ADC_CONV + aCMD); // start DAQ and conversion of ADC data

  switch (aCMD & 0x0f)
  {
    case MS5xxx_CMD_ADC_256: delayMicroseconds(900);
      break;
    case MS5xxx_CMD_ADC_512: delay(3);
      break;
    case MS5xxx_CMD_ADC_1024: delay(4);
      break;
    case MS5xxx_CMD_ADC_2048: delay(6);
      break;
    case MS5xxx_CMD_ADC_4096: delay(10);
      break;
  }

  // read out values
  return I2C_read24_reg(i2cAddress, MS5xxx_CMD_ADC_READ);
}

// **************************************************************************/
// Readout
// **************************************************************************/
void P032_data_struct::readout() {
  unsigned long D1 = 0, D2 = 0;

  ESPEASY_RULES_FLOAT_TYPE dT;
  ESPEASY_RULES_FLOAT_TYPE Offset;
  ESPEASY_RULES_FLOAT_TYPE SENS;

  D2 = read_adc(MS5xxx_CMD_ADC_D2 + MS5xxx_CMD_ADC_4096);
  D1 = read_adc(MS5xxx_CMD_ADC_D1 + MS5xxx_CMD_ADC_4096);

  // calculate 1st order pressure and temperature (MS5611 1st order algorithm)
  dT     = D2 - ms5611_prom[5] * static_cast<ESPEASY_RULES_FLOAT_TYPE>(1 << 8);
  Offset = ms5611_prom[2] *
           static_cast<ESPEASY_RULES_FLOAT_TYPE>(1 << 16) + dT * ms5611_prom[4] / static_cast<ESPEASY_RULES_FLOAT_TYPE>(1 << 7);
  SENS = ms5611_prom[1] *
         static_cast<ESPEASY_RULES_FLOAT_TYPE>(1 << 15) + dT * ms5611_prom[3] / static_cast<ESPEASY_RULES_FLOAT_TYPE>(1 << 8);
  ms5611_temperature = (2000 + (dT * ms5611_prom[6]) / static_cast<ESPEASY_RULES_FLOAT_TYPE>(1 << 23));

  // perform higher order corrections
  ESPEASY_RULES_FLOAT_TYPE T2 = 0., OFF2 = 0., SENS2 = 0.;

  if (ms5611_temperature < 2000) {
    T2 = dT * dT / static_cast<ESPEASY_RULES_FLOAT_TYPE>(1 << 31);
    const ESPEASY_RULES_FLOAT_TYPE temp_20deg = ms5611_temperature - 2000;
    OFF2  = 5.0 * temp_20deg * temp_20deg / static_cast<ESPEASY_RULES_FLOAT_TYPE>(1 << 1);
    SENS2 = 5.0 * temp_20deg * temp_20deg / static_cast<ESPEASY_RULES_FLOAT_TYPE>(1 << 2);

    if (ms5611_temperature < -1500) {
      const ESPEASY_RULES_FLOAT_TYPE temp_min15deg = ms5611_temperature + 1500;
      OFF2  += 7.0 * temp_min15deg * temp_min15deg;
      SENS2 += 11.0 * temp_min15deg * temp_min15deg / static_cast<ESPEASY_RULES_FLOAT_TYPE>(1 << 1);
    }
  }

  ms5611_temperature -= T2;
  Offset             -= OFF2;
  SENS               -= SENS2;
  ms5611_pressure     =
    (((D1 * SENS) / static_cast<ESPEASY_RULES_FLOAT_TYPE>(1 << 21) - Offset) / static_cast<ESPEASY_RULES_FLOAT_TYPE>(1 << 15));
}

#endif // ifdef USES_P032

#include "../PluginStructs/P115_data_struct.h"

#ifdef USES_P115

P115_data_struct::P115_data_struct(
  sfe_max1704x_devices_e device,
  int                    threshold)
  : _device(device), lipo(device), _threshold(threshold), initialized(false)
{
  // Call begin() immediately, or else _i2cPort in lipo object is still a nullptr
  lipo.begin();
  _threshold = constrain(_threshold, 1, 32);
}

bool P115_data_struct::begin()
{
  // quickStart() - Restarts the MAX1704x to allow it to re-"guess" the
  // parameters that go into its SoC algorithms. Calling this in your setup()
  // usually results in more accurate SoC readings.
  // Output: 0 on success, positive integer on fail.

  // FIXME TD-er: Looks like begin() and/or quickStart() may not return expected values.
  // const bool success = lipo.begin() && (lipo.quickStart() == 0);
  lipo.begin();
  lipo.quickStart();
  const bool success = true;

  if (success) {
    switch (_device) {
      case MAX1704X_MAX17043:
      case MAX1704X_MAX17044:
        break;
      case MAX1704X_MAX17048:
      case MAX1704X_MAX17049:
        lipo.enableSOCAlert();
        break;
    }
    lipo.setThreshold(_threshold);
    initialized = true;
    return true;
  }
  initialized = false;
  return false;
}

bool P115_data_struct::read(bool clearAlert)
{
  voltage    = lipo.getVoltage();
  soc        = lipo.getSOC();
  changeRate = lipo.getChangeRate();
  return lipo.getAlert(clearAlert);
}

void P115_data_struct::clearAlert() {
  alert = false;
  lipo.clearAlert();
}

#endif // ifdef USES_P115

#include "../PluginStructs/P068_data_struct.h"

#ifdef USES_P068

# include "../Helpers/CRC_functions.h"

// ==============================================
// P068_SHT3X LIBRARY - SHT3X.cpp
// =============================================
P068_SHT3X::P068_SHT3X(uint8_t addr) : _i2c_device_address(addr)
{
  // Set to periodic mode
  I2C_write8_reg(
    _i2c_device_address,
    0x20, // periodic 0.5mps
    0x32  // repeatability high
    );
}

void P068_SHT3X::readFromSensor()
{
  I2C_write8_reg(
    _i2c_device_address,
    0xE0, // fetch data command
    0x00
    );

  // FIXME TD-er: Currently the I2Cdev::readBytes does not support writing 2 bytes before reading.
  Wire.requestFrom(_i2c_device_address, (uint8_t)6);

  if (Wire.available() == 6)
  {
    uint16_t data[6];

    for (uint8_t i = 0; i < 6u; ++i) {
      data[i] = Wire.read();
    }

    // TODO: check CRC (data[2] and data[5])
    if (calc_CRC8(data[0], data[1], data[2]) &&
        calc_CRC8(data[3], data[4], data[5]))
    {
      tmp = ((((data[0] << 8) | data[1]) * 175.0f) / 65535.0f) - 45.0f;
      hum = ((((data[3] << 8) | data[4]) * 100.0f) / 65535.0f);

      // Humidity temperature compensation borrowed from P028 BME280
      if (!essentiallyZero(tmpOff)) {
        const float last_dew_temp_val = compute_dew_point_temp(tmp + (tmpOff / 2.0f), hum);
        tmp += tmpOff;
        hum  = compute_humidity_from_dewpoint(tmp, last_dew_temp_val);
      }
    }
  }
  else
  {
    tmp = NAN;
    hum = NAN;

    // Set to periodic mode
    I2C_write8_reg(
      _i2c_device_address,
      0x20, // periodic 0.5mps
      0x32  // repeatability high
      );
  }
}

#endif // ifdef USES_P068

#include "../PluginStructs/P119_data_struct.h"

#ifdef USES_P119

// **************************************************************************/
// Constructor
// **************************************************************************/
P119_data_struct::P119_data_struct(uint8_t i2c_addr, bool rawData, uint8_t aSize)
  : _i2cAddress(i2c_addr), _rawData(rawData), _aSize(aSize) {
  if (_aSize == 0) { _aSize = 1; }
  _XA.resize(_aSize, 0);
  _YA.resize(_aSize, 0);
  _ZA.resize(_aSize, 0);
  _aUsed = 0;
  _aMax  = 0;
}

// **************************************************************************/
// Destructor
// **************************************************************************/
P119_data_struct::~P119_data_struct() {
  if (initialized()) {
    delete itg3205;
    itg3205 = nullptr;
  }
}

// **************************************************************************/
// Initialize sensor and read data from ITG3205
// **************************************************************************/
bool P119_data_struct::read_sensor() {
  # ifdef PLUGIN_119_DEBUG
  String log;
  # endif // if PLUGIN_119_DEBUG

  if (!initialized()) {
    init_sensor();

    # ifdef PLUGIN_119_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      addLogMove(LOG_LEVEL_DEBUG,
                 strformat(F("ITG3205: i2caddress: 0x%02x, initialized: %d, ID=0x%02x"),
                           _i2cAddress, initialized(), itg3205->readWhoAmI()));
    }
    # endif // if PLUGIN_119_DEBUG
  }

  if (initialized()) {
    if (_rawData) {
      itg3205->readGyroRaw();
    } else {
      itg3205->readGyro();
    }
    _XA[_aUsed] = itg3205->g.x;
    _YA[_aUsed] = itg3205->g.y;
    _ZA[_aUsed] = itg3205->g.z;

    _aUsed++;

    if ((_aMax < _aUsed) && (_aUsed < _aSize)) {
      _aMax = _aUsed;
    }

    if (_aUsed == _aSize) {
      _aUsed = 0;
    }

    # ifdef PLUGIN_119_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      addLogMove(LOG_LEVEL_DEBUG, strformat(F("ITG3205: %s, X: %d, Y: %d, Z: %d"),
                                            String(_rawData ? F("raw ") : F("")).c_str(),
                                            itg3205->g.x, itg3205->g.y, itg3205->g.z));
    }
    # endif // if PLUGIN_119_DEBUG
    return true;
  }
  return false;
}

// **************************************************************************/
// Average the measurements and return the results
// **************************************************************************/
bool P119_data_struct::read_data(int& X, int& Y, int& Z) {
  X = 0;
  Y = 0;
  Z = 0;

  if (initialized()) {
    for (uint8_t n = 0; n <= _aMax; ++n) {
      X += _XA[n];
      Y += _YA[n];
      Z += _ZA[n];
    }

    X /= _aMax; // Average available measurements
    Y /= _aMax;
    Z /= _aMax;

    # ifdef PLUGIN_119_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      addLogMove(LOG_LEVEL_DEBUG, strformat(F("ITG3205: averages, X: %d, Y: %d, Z: %d"), X, Y, Z));
    }
    # endif // if PLUGIN_119_DEBUG
  }
  return initialized();
}

// **************************************************************************/
// Initialize ITG3205
// **************************************************************************/
bool P119_data_struct::init_sensor() {
  itg3205 = new (std::nothrow) ITG3205(_i2cAddress);

  if (initialized()) {
    addLog(LOG_LEVEL_INFO, F("ITG3205: Initializing Gyro..."));
    itg3205->initGyro();
    addLog(LOG_LEVEL_INFO, F("ITG3205: Calibrating Gyro..."));
    itg3205->calibrate();
    addLog(LOG_LEVEL_INFO, F("ITG3205: Calibration done."));
  } else {
    addLog(LOG_LEVEL_ERROR, F("ITG3205: Initialization of Gyro failed."));
    return false;
  }

  # ifdef PLUGIN_119_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLogMove(LOG_LEVEL_DEBUG, strformat(F("ITG3205: Address: 0x%02x"), _i2cAddress));
  }
  # endif // ifdef PLUGIN_119_DEBUG

  return true;
}

#endif // ifdef USES_P119

#include "../PluginStructs/P151_data_struct.h"

#ifdef USES_P151


P151_data_struct::~P151_data_struct() {}

bool P151_data_struct::plugin_read(struct EventStruct *event) {
  // FIXME TD-er: When used on ESP8266, this conversion union may not work
  // It might work as it is 32-bit in size.
  union Honeywell_struct {
    struct {
      uint32_t dummy       : 5;
      uint32_t temperature : 11;
      uint32_t bridgeData  : 14;
      uint32_t status      : 2;
    };
    struct {
      uint8_t bytes[4];
    };
    uint32_t raw;
  } conv;

  I2Cdata_bytes data(4);

  if (!I2C_read_bytes(P151_I2C_ADDR, data)) {
    addLog(LOG_LEVEL_ERROR, concat(F("P151 : cannot read I2C, address "), formatToHex(P151_I2C_ADDR)));
    return false;
  }

  //  String log = F("P151 : RAW data: ");
  for (size_t i = 0; i < 4; ++i) {
    conv.bytes[3 - i] = data[i];

    //    log += formatToHex(data[i]);
    //    log += ' ';
  }

  /*
     log += formatToHex(conv.raw);
     log += concat(F(" st:"), conv.status);
     log += concat(F(" br:"), conv.bridgeData);
     log += concat(F(" temp:"), conv.temperature);
     log += concat(F(" dummy:"), conv.dummy);
     log += concat(F(" expected status:"), (data[0] >> 6));
     log += concat(F(" expected dummy:"), (data[3] & 0x1F));

     addLog(LOG_LEVEL_INFO, log);
   */


  if (conv.status != 0) {
    // status == 2: stale data, indicating the sensordata was already read, or the sensor is performing a measurement
    if ((conv.status != 2) || // Not stale data, or value not changed
        ((conv.bridgeData == _rawPressure) && (conv.temperature == _rawTemperature))) {
      return false;
    }
  }
  _rawPressure    = conv.bridgeData;
  _rawTemperature = conv.temperature;
  _updated        = true;
  return true;
}

bool P151_data_struct::plugin_ten_per_second(struct EventStruct *event) {
  return true;
}

bool P151_data_struct::plugin_fifty_per_second(struct EventStruct *event) {
  return true;
}

bool P151_data_struct::fetch_last_sample(struct EventStruct *event) {
  if (!_updated) { 
    // Needed to get the 1st value, or can get the latest value when sampled too frequently.
    plugin_read(event); 
  }
  if (_updated) {
    float _pressure =
      (_rawPressure - P151_OUTPUT_MIN) * (P151_PRESSURE_MAX - P151_PRESSURE_MIN);

    if (P151_OUTPUT_MAX != P151_OUTPUT_MIN) {
      _pressure /= (P151_OUTPUT_MAX - P151_OUTPUT_MIN);
    }
    _pressure += P151_PRESSURE_MIN;

    UserVar.setFloat(event->TaskIndex, 0, _pressure);
    UserVar.setFloat(event->TaskIndex, 1, ((_rawTemperature * 200.0f) / 2047.0f) - 50.0f);
    _updated                         = false;
    return true;
  }
  return false;
}

#endif // ifdef USES_P151

#include "../PluginStructs/P004_data_struct.h"

#ifdef USES_P004

P004_data_struct::P004_data_struct(
  taskIndex_t taskIndex,
  int8_t      pin_rx,
  int8_t      pin_tx,
  uint8_t     res,
  bool        scanOnInit) :
  _taskIndex(taskIndex),
  _gpio_rx(pin_rx), _gpio_tx(pin_tx),
  _res(res), _scanOnInit(scanOnInit)
{
  _timer            = millis();
  _measurementStart = _timer;

  for (uint8_t i = 0; i < VARS_PER_TASK; ++i) {
    _sensors[i].clear();
  }

  if ((_res < 9) || (_res > 12)) {
    _res = 12;
  }
}

bool P004_data_struct::sensorAddressSet() const
{
  for (uint8_t i = 0; i < VARS_PER_TASK; ++i) {
    if (_sensors[i].addr != 0) { return true; }
  }
  return false;
}

void P004_data_struct::init()
{
  // Explicitly set the pinMode using the "slow" pinMode function
  // This way we know for sure the state of any pull-up or -down resistor is known.
  pinMode(_gpio_rx, INPUT);

  // The Shelly 1 temp. addon uses separate
  // input and output pins, and therefore
  // doesn't switch between input and output
  // when running.
  if (_gpio_rx != _gpio_tx) {
    pinMode(_gpio_tx, OUTPUT);
  }
  set_measurement_inactive();

  if (_scanOnInit) {
    // Scan for any sensor and use it.
    // This can only be used when we use only 1 sensor.
    uint8_t addr[8] = { 0 };
    Dallas_reset_search();

    if (!Dallas_search(addr, _gpio_rx, _gpio_tx)) {
      // Nothing found, for now restore from the settings (if anything is set)
      Dallas_plugin_get_addr(addr, _taskIndex);
    }

    if (addr[0] != 0) {
      add_addr(addr, 0);
    }
  }
}

void P004_data_struct::add_addr(const uint8_t addr[], uint8_t index) {
  if (index < VARS_PER_TASK) {
    const uint64_t new_addr = Dallas_addr_to_uint64(addr);

    if (new_addr == 0) { return; }

    if (new_addr != _sensors[index].addr) {
      _sensors[index].clear();
      _sensors[index].addr = new_addr;
    }

    // If the address already exists, set it to 0 to avoid duplicates
    for (uint8_t i = 0; i < VARS_PER_TASK; ++i) {
      if (index != i) {
        if (new_addr == _sensors[i].addr) {
          _sensors[index].clear();
          return;
        }
      }
    }
    _sensors[index].check_sensor(_gpio_rx, _gpio_tx, _res);
  }
}

bool P004_data_struct::initiate_read() {
  _measurementStart = millis();

  bool mustInit   = false;
  uint8_t use_res = 9;

  for (uint8_t i = 0; i < VARS_PER_TASK; ++i) { // Determine the resolution to use
    use_res = max(use_res, _sensors[i].actual_res);
  }

  for (uint8_t i = 0; i < VARS_PER_TASK; ++i) {
    if (_sensors[i].initiate_read(_gpio_rx, _gpio_tx, _res)) {
      if (!measurement_active()) {
        // Set the timer right after initiating the first sensor


        /*********************************************************************************************\
        *  Dallas Start Temperature Conversion, expected max duration:
        *    9 bits resolution ->  93.75 ms
        *   10 bits resolution -> 187.5 ms
        *   11 bits resolution -> 375 ms
        *   12 bits resolution -> 750 ms
        \*********************************************************************************************/
        _timer = millis() + (800 / (1 << (12 - use_res))); // Use actual sensor resolution
      }
      _sensors[i].measurementActive = true;
    } else {
      if (_scanOnInit && (_sensors[i].start_read_failed > (_sensors[i].reinit_count * 10))) {
        _sensors[i].reinit_count++;
        mustInit = true;
      }
    }
  }

  if (mustInit) {
    init();
    return false;
  }

  return measurement_active();
}

bool P004_data_struct::collect_values() {
  bool success = false;

  for (uint8_t i = 0; i < VARS_PER_TASK; ++i) {
    if (_sensors[i].collect_value(_gpio_rx, _gpio_tx)) {
      success = true;
    }
  }
  return success;
}

bool P004_data_struct::read_temp(float& value, uint8_t index) const {
  if (index >= VARS_PER_TASK) { return false; }

  if ((_sensors[index].addr == 0) || !_sensors[index].valueRead) { return false; }

  value = _sensors[index].value;
  return true;
}

String P004_data_struct::get_formatted_address(uint8_t index) const {
  if (index < VARS_PER_TASK) {
    return _sensors[index].get_formatted_address();
  }
  return EMPTY_STRING;
}

bool P004_data_struct::measurement_active() const {
  for (uint8_t i = 0; i < VARS_PER_TASK; ++i) {
    if (_sensors[i].measurementActive) { return true; }
  }

  return false;
}

bool P004_data_struct::measurement_active(uint8_t index) const {
  if (index < VARS_PER_TASK) {
    return _sensors[index].measurementActive;
  }
  return false;
}

void P004_data_struct::set_measurement_inactive() {
  for (uint8_t i = 0; i < VARS_PER_TASK; ++i) {
    _sensors[i].set_measurement_inactive();
  }
}

Dallas_SensorData P004_data_struct::get_sensor_data(uint8_t index) const {
  if (index < VARS_PER_TASK) { return _sensors[index]; }
  Dallas_SensorData res;

  return res;
}

int P004_data_struct::measure_rise_time() const {
  return Dallas_measure_rise_time(_gpio_rx, _gpio_tx);
}


#endif // ifdef USES_P004

#include "../PluginStructs/P026_data_struct.h"

#ifdef USES_P026

# include "../DataStructs/ESPEasy_packed_raw_data.h"
# include "../ESPEasyCore/ESPEasyNetwork.h"
# include "../Globals/ESPEasyWiFiEvent.h"
# include "../Helpers/ESPEasy_UnitOfMeasure.h"
# include "../Helpers/Memory.h"
# include "../Helpers/Hardware_temperature_sensor.h"
# ifdef ESP32
#  include "../Helpers/Hardware_device_info.h"

# endif // ifdef ESP32

# include "ESPEasy-Globals.h"

// Do not change assigned values as they are stored
// Shown selection and its order can be set in P026_value_option_indices
// These P026_VALUETYPE_xxx values should represent the index in the p026_valuenames array
# define P026_VALUETYPE_uptime       0
# define P026_VALUETYPE_freeheap     1
# define P026_VALUETYPE_rssi         2
# define P026_VALUETYPE_vcc          3
# define P026_VALUETYPE_load         4
# define P026_VALUETYPE_ip1          5
# define P026_VALUETYPE_ip2          6
# define P026_VALUETYPE_ip3          7
# define P026_VALUETYPE_ip4          8
# define P026_VALUETYPE_web          9
# define P026_VALUETYPE_freestack    10
# define P026_VALUETYPE_none         11
# define P026_VALUETYPE_txpwr        12
# define P026_VALUETYPE_free2ndheap  13
# define P026_VALUETYPE_internaltemp 14
# define P026_VALUETYPE_freepsram    15


const __FlashStringHelper* Plugin_026_valuename(uint8_t value_nr, bool displayString) {
  const __FlashStringHelper *p026_valuenames[] {
    F("Uptime"), F("uptime"),
    F("Free RAM"), F("freeheap"),
    F("Wifi RSSI"), F("rssi"),
    F("Input VCC"), F("vcc"),
    F("System load"), F("load"),
    F("IP 1.Octet"), F("ip1"),
    F("IP 2.Octet"), F("ip2"),
    F("IP 3.Octet"), F("ip3"),
    F("IP 4.Octet"), F("ip4"),
    F("Web activity"), F("web"),
    F("Free Stack"), F("freestack"),
    F("None"), F(""),
    F("WiFi TX pwr"), F("txpwr"),
# ifdef USE_SECOND_HEAP
    F("Free 2nd Heap"), F("free2ndheap"),
# else // ifdef USE_SECOND_HEAP
    F(""), F(""), // Must keep the same indexes
# endif // ifdef USE_SECOND_HEAP
# if FEATURE_INTERNAL_TEMPERATURE
    F("Internal temperature (ESP32)"), F("internaltemp"),
# else // if FEATURE_INTERNAL_TEMPERATURE
    F(""), F(""), // Must keep the same indexes
# endif // if FEATURE_INTERNAL_TEMPERATURE

# if defined(ESP32) && defined(BOARD_HAS_PSRAM)
    F("Free PSRAM"), F("freepsram"),
# else // if defined(ESP32) && defined(BOARD_HAS_PSRAM)
    F(""), F(""), // Must keep the same indexes
# endif // if defined(ESP32) && defined(BOARD_HAS_PSRAM)
  };

  const size_t index         = (2 * value_nr) + (displayString ? 0 : 1);
  constexpr size_t nrStrings = NR_ELEMENTS(p026_valuenames);

  if (index < nrStrings) {
    return p026_valuenames[index];
  }
  return F("");
}

// List of options in the order how they will be shown in the plugin selector.
const int P026_value_option_indices[] = {
  P026_VALUETYPE_none, // Have the "none" option as first option

  P026_VALUETYPE_uptime,
  P026_VALUETYPE_load,
  P026_VALUETYPE_freeheap,
# if defined(ESP32) && defined(BOARD_HAS_PSRAM)
  P026_VALUETYPE_freepsram,
# endif // if defined(ESP32) && defined(BOARD_HAS_PSRAM)
# ifdef USE_SECOND_HEAP
  P026_VALUETYPE_free2ndheap,
# endif // ifdef USE_SECOND_HEAP
  P026_VALUETYPE_freestack,
  P026_VALUETYPE_rssi,
  P026_VALUETYPE_txpwr,
# if FEATURE_ADC_VCC
  P026_VALUETYPE_vcc,
# endif // if FEATURE_ADC_VCC
# if FEATURE_INTERNAL_TEMPERATURE
  P026_VALUETYPE_internaltemp,
# endif // if FEATURE_INTERNAL_TEMPERATURE
  P026_VALUETYPE_ip1,
  P026_VALUETYPE_ip2,
  P026_VALUETYPE_ip3,
  P026_VALUETYPE_ip4,
  P026_VALUETYPE_web,
};


float P026_get_value(uint8_t type)
{
  float res{};

  switch (type)
  {
    case P026_VALUETYPE_uptime:   res = getUptimeMinutes(); break;
    case P026_VALUETYPE_freeheap: res = FreeMem(); break;
    case P026_VALUETYPE_rssi:     res = WiFi.RSSI(); break;
    case P026_VALUETYPE_vcc:
# if FEATURE_ADC_VCC
      res = vcc;
# else // if FEATURE_ADC_VCC
      res = -1.0f;
# endif // if FEATURE_ADC_VCC
      break;
    case P026_VALUETYPE_load: res = getCPUload(); break;
    case P026_VALUETYPE_ip1:
    case P026_VALUETYPE_ip2:
    case P026_VALUETYPE_ip3:
    case P026_VALUETYPE_ip4:
      res = NetworkLocalIP()[type - P026_VALUETYPE_ip1];
      break;
    case P026_VALUETYPE_web:
      res = timePassedSince(lastWeb) / 1000.0f;
      break; // respond in seconds
    case P026_VALUETYPE_freestack: res = getCurrentFreeStack(); break;
    case P026_VALUETYPE_txpwr:     res = WiFiEventData.wifi_TX_pwr; break;
# ifdef USE_SECOND_HEAP
    case P026_VALUETYPE_free2ndheap:
      res = FreeMem2ndHeap();
      break;
# endif // ifdef USE_SECOND_HEAP
# if FEATURE_INTERNAL_TEMPERATURE
    case P026_VALUETYPE_internaltemp:
      res = getInternalTemperature();
      break;
# endif // if FEATURE_INTERNAL_TEMPERATURE
# if defined(ESP32) && defined(BOARD_HAS_PSRAM)
    case P026_VALUETYPE_freepsram:

      if (UsePSRAM()) {
        res = ESP.getFreePsram();
      }
      break;
# endif // if defined(ESP32) && defined(BOARD_HAS_PSRAM)
  }
  return res;
}

# if FEATURE_MQTT_DISCOVER || FEATURE_CUSTOM_TASKVAR_VTYPE
Sensor_VType P026_get_valueVType(uint8_t type) {
  switch (type)
  {
    case P026_VALUETYPE_uptime:   return Sensor_VType::SENSOR_TYPE_DURATION;
    case P026_VALUETYPE_freeheap: return Sensor_VType::SENSOR_TYPE_DATA_SIZE;
    case P026_VALUETYPE_rssi:     return Sensor_VType::SENSOR_TYPE_SIGNAL_STRENGTH;
    case P026_VALUETYPE_vcc:      return Sensor_VType::SENSOR_TYPE_VOLTAGE_ONLY;
    case P026_VALUETYPE_load:     return Sensor_VType::SENSOR_TYPE_POWER_FACT_ONLY;
    case P026_VALUETYPE_ip1:
    case P026_VALUETYPE_ip2:
    case P026_VALUETYPE_ip3:
    case P026_VALUETYPE_ip4:       return Sensor_VType::SENSOR_TYPE_NONE;
    case P026_VALUETYPE_web:       return Sensor_VType::SENSOR_TYPE_DURATION;
    case P026_VALUETYPE_freestack: return Sensor_VType::SENSOR_TYPE_DATA_SIZE;
    case P026_VALUETYPE_txpwr:     return Sensor_VType::SENSOR_TYPE_SOUND_PRESSURE;
#  ifdef USE_SECOND_HEAP
    case P026_VALUETYPE_free2ndheap:  return Sensor_VType::SENSOR_TYPE_DATA_SIZE;
#  endif // ifdef USE_SECOND_HEAP
#  if FEATURE_INTERNAL_TEMPERATURE
    case P026_VALUETYPE_internaltemp: return Sensor_VType::SENSOR_TYPE_TEMP_ONLY;
#  endif // if FEATURE_INTERNAL_TEMPERATURE
#  if defined(ESP32) && defined(BOARD_HAS_PSRAM)
    case P026_VALUETYPE_freepsram:    return Sensor_VType::SENSOR_TYPE_DATA_SIZE;
#  endif // if defined(ESP32) && defined(BOARD_HAS_PSRAM)
  }

  return Sensor_VType::SENSOR_TYPE_NONE;
}

# endif // if FEATURE_MQTT_DISCOVER || FEATURE_CUSTOM_TASKVAR_VTYPE

bool P026_data_struct::GetDeviceValueNames(struct EventStruct *event)
{
  const int valueCount = P026_NR_OUTPUT_VALUES;

  for (uint8_t i = 0; i < VARS_PER_TASK; ++i) {
    if (i < valueCount) {
      const uint8_t pconfigIndex = i + P026_QUERY1_CONFIG_POS;
      ExtraTaskSettings.setTaskDeviceValueName(i, Plugin_026_valuename(PCONFIG(pconfigIndex), false));
    } else {
      ExtraTaskSettings.clearTaskDeviceValueName(i);
    }
  }
  return true;
}

bool P026_data_struct::WebformLoadOutputSelector(struct EventStruct *event)
{
  constexpr size_t NrOptions = NR_ELEMENTS(P026_value_option_indices);

  const __FlashStringHelper *options[NrOptions];

  for (uint8_t index = 0; index < NrOptions; ++index) {
    options[index] = Plugin_026_valuename(P026_value_option_indices[index], true);
  }

  const int valueCount = P026_NR_OUTPUT_VALUES;

  for (uint8_t i = 0; i < valueCount; ++i) {
    const uint8_t pconfigIndex = i + P026_QUERY1_CONFIG_POS;
    sensorTypeHelper_loadOutputSelector(event, pconfigIndex, i, NrOptions, options, P026_value_option_indices);
  }
  return true;
}

bool P026_data_struct::WebformSave(struct EventStruct *event)
{
  // Save output selector parameters.
  const int valueCount = P026_NR_OUTPUT_VALUES;

  for (uint8_t i = 0; i < valueCount; ++i) {
    const uint8_t pconfigIndex = i + P026_QUERY1_CONFIG_POS;
    const uint8_t choice       = PCONFIG(pconfigIndex);
    sensorTypeHelper_saveOutputSelector(event, pconfigIndex, i, Plugin_026_valuename(choice, false));
    # if FEATURE_MQTT_DISCOVER && FEATURE_CUSTOM_TASKVAR_VTYPE && FEATURE_TASKVALUE_UNIT_OF_MEASURE

    if (choice != PCONFIG(pconfigIndex)) {
      const Sensor_VType vtype = P026_get_valueVType(PCONFIG(pconfigIndex));
      ExtraTaskSettings.setTaskVarCustomVType(i, static_cast<uint8_t>(vtype));
      const String uom = getValueType2DefaultHAUoM(vtype);
      ExtraTaskSettings.setTaskVarUnitOfMeasure(i, 0);

      if (!uom.isEmpty()) {
        const int uomIdx = getUnitOfMeasureIndex(uom);

        if (uomIdx > 0) {
          ExtraTaskSettings.setTaskVarUnitOfMeasure(i, uomIdx);
        }
      }
    }
    # endif // if FEATURE_MQTT_DISCOVER && FEATURE_CUSTOM_TASKVAR_VTYPE && FEATURE_TASKVALUE_UNIT_OF_MEASURE
  }
  return true;
}

bool P026_data_struct::Plugin_Read(struct EventStruct *event)
{
  const int valueCount = P026_NR_OUTPUT_VALUES;

  for (int i = 0; i < valueCount; ++i) {
    UserVar.setFloat(event->TaskIndex, i, P026_get_value(PCONFIG(i)));
  }
      # ifndef LIMIT_BUILD_SIZE

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    String log;

    if (log.reserve(7 * (valueCount + 1)))
    {
      log += F("SYS  : ");

      for (int i = 0; i < valueCount; ++i) {
        if (i != 0) {
          log += ',';
        }
        log += formatUserVarNoCheck(event, i);
      }
      addLogMove(LOG_LEVEL_INFO, log);
    }
  }
      # endif // ifndef LIMIT_BUILD_SIZE
  return true;
}

# ifndef PLUGIN_BUILD_MINIMAL_OTA
bool P026_data_struct::Plugin_GetConfigValue(struct EventStruct *event, String& string)
{
  bool success     = false;
  const String cmd = parseString(string, 1);

  constexpr size_t P026_NR_OUTPUT_OPTIONS = NR_ELEMENTS(P026_value_option_indices);

  for (uint8_t option = 0; option < P026_NR_OUTPUT_OPTIONS; ++option) {
    if ((P026_value_option_indices[option] != P026_VALUETYPE_none) &&
        equals(cmd, Plugin_026_valuename(P026_value_option_indices[option], false))) {     // Use default valuename
      string  = floatToString(P026_get_value(P026_value_option_indices[option]), 2, true); // Trim trailing zeroes
      success = true;
      break;
    }
  }

  return success;
}

# endif // ifndef PLUGIN_BUILD_MINIMAL_OTA


# if FEATURE_PACKED_RAW_DATA
bool P026_data_struct::Plugin_GetPackedRawData(struct EventStruct *event, String& string)
{
  // Matching JS code:
  // return decode(bytes,
  //  [header, uint24, uint24, int8, vcc, pct_8, uint8, uint8, uint8, uint8, uint24, uint16],
  //  ['header', 'uptime', 'freeheap', 'rssi', 'vcc', 'load', 'ip1', 'ip2', 'ip3', 'ip4', 'web', 'freestack']);
  // on ESP32 you can add 'internaltemperature' of type int16 (1e2) to the list (disabled for now, so not available)
  uint8_t index = 0;

  string += LoRa_addInt(P026_get_value(index++), PackedData_uint24);  // uptime
  string += LoRa_addInt(P026_get_value(index++), PackedData_uint24);  // freeheap
  string += LoRa_addFloat(P026_get_value(index++), PackedData_int8);  // rssi
  string += LoRa_addFloat(P026_get_value(index++), PackedData_vcc);   // vcc
  string += LoRa_addFloat(P026_get_value(index++), PackedData_pct_8); // load
  string += LoRa_addInt(P026_get_value(index++), PackedData_uint8);   // ip1
  string += LoRa_addInt(P026_get_value(index++), PackedData_uint8);   // ip2
  string += LoRa_addInt(P026_get_value(index++), PackedData_uint8);   // ip3
  string += LoRa_addInt(P026_get_value(index++), PackedData_uint8);   // ip4
  string += LoRa_addInt(P026_get_value(index++), PackedData_uint24);  // web
  string += LoRa_addInt(P026_get_value(index++), PackedData_uint16);  // freestack
  // #  if FEATURE_INTERNAL_TEMPERATURE
  // string += LoRa_addInt(P026_get_value(index++) * 100.0f, PackedData_int16_1e2); // internal temperature in 0.01 degrees
  // #  endif // if FEATURE_INTERNAL_TEMPERATURE
  event->Par1 = index; // valuecount
  return true;
}

# endif // if FEATURE_PACKED_RAW_DATA


#endif  // ifdef USES_P026

#ifdef USES_P023
#include "../PluginStructs/P023_data_struct.h"

#include "../Helpers/Misc.h"
#include "../Helpers/StringParser.h"

#define P023_DATA_MODE_REG      0x40
#define P023_COMMAND_MODE_REG   0x80


const char Plugin_023_myFont_Size[] PROGMEM = {
  0x05, // SPACE
  0x05, // !
  0x07, // "
  0x08, // #
  0x08, // $
  0x08, // %
  0x08, // &
  0x06, // '
  0x06, // (
  0x06, // )
  0x08, // *
  0x08, // +
  0x05, // ,
  0x08, // -
  0x05, // .
  0x08, // /
  0x08, // 0
  0x07, // 1
  0x08, // 2
  0x08, // 3
  0x08, // 4
  0x08, // 5
  0x08, // 6
  0x08, // 7
  0x08, // 8
  0x08, // 9
  0x06, // :
  0x06, // ;
  0x07, // <
  0x08, // =
  0x07, // >
  0x08, // ?
  0x08, // @
  0x08, // A
  0x08, // B
  0x08, // C
  0x08, // D
  0x08, // E
  0x08, // F
  0x08, // G
  0x08, // H
  0x06, // I
  0x08, // J
  0x08, // K
  0x08, // L
  0x08, // M
  0x08, // N
  0x08, // O
  0x08, // P
  0x08, // Q
  0x08, // R
  0x08, // S
  0x08, // T
  0x08, // U
  0x08, // V
  0x08, // W
  0x08, // X
  0x08, // Y
  0x08, // Z
  0x06, // [
  0x08, // BACKSLASH
  0x06, // ]
  0x08, // ^
  0x08, // _
  0x06, // `
  0x08, // a
  0x08, // b
  0x07, // c
  0x08, // d
  0x08, // e
  0x07, // f
  0x08, // g
  0x08, // h
  0x05, // i
  0x06, // j
  0x07, // k
  0x06, // l
  0x08, // m
  0x07, // n
  0x07, // o
  0x07, // p
  0x07, // q
  0x07, // r
  0x07, // s
  0x06, // t
  0x07, // u
  0x08, // v
  0x08, // w
  0x08, // x
  0x07, // y
  0x08, // z
  0x06, // {
  0x05, // |
  0x06, // }
  0x08, // ~
  0x08  // DEL
};

const char Plugin_023_myFont[][5] PROGMEM = {
  { 0x00, 0x00, 0x00, 0x00, 0x00 }, // SPACE
  { 0x00, 0x5F, 0x00, 0x00, 0x00 }, // !
  { 0x00, 0x07, 0x00, 0x07, 0x00 }, // "
  { 0x14, 0x7F, 0x14, 0x7F, 0x14 }, // #
  { 0x24, 0x2A, 0x7F, 0x2A, 0x12 }, // $
  { 0x23, 0x13, 0x08, 0x64, 0x62 }, // %
  { 0x36, 0x49, 0x55, 0x22, 0x50 }, // &
  { 0x00, 0x05, 0x03, 0x00, 0x00 }, // '
  { 0x1C, 0x22, 0x41, 0x00, 0x00 }, // (
  { 0x41, 0x22, 0x1C, 0x00, 0x00 }, // )
  { 0x08, 0x2A, 0x1C, 0x2A, 0x08 }, // *
  { 0x08, 0x08, 0x3E, 0x08, 0x08 }, // +
  { 0xA0, 0x60, 0x00, 0x00, 0x00 }, // ,
  { 0x08, 0x08, 0x08, 0x08, 0x08 }, // -
  { 0x60, 0x60, 0x00, 0x00, 0x00 }, // .
  { 0x20, 0x10, 0x08, 0x04, 0x02 }, // /
  { 0x3E, 0x51, 0x49, 0x45, 0x3E }, // 0
  { 0x00, 0x42, 0x7F, 0x40, 0x00 }, // 1
  { 0x62, 0x51, 0x49, 0x49, 0x46 }, // 2
  { 0x22, 0x41, 0x49, 0x49, 0x36 }, // 3
  { 0x18, 0x14, 0x12, 0x7F, 0x10 }, // 4
  { 0x27, 0x45, 0x45, 0x45, 0x39 }, // 5
  { 0x3C, 0x4A, 0x49, 0x49, 0x30 }, // 6
  { 0x01, 0x71, 0x09, 0x05, 0x03 }, // 7
  { 0x36, 0x49, 0x49, 0x49, 0x36 }, // 8
  { 0x06, 0x49, 0x49, 0x29, 0x1E }, // 9
  { 0x00, 0x36, 0x36, 0x00, 0x00 }, // :
  { 0x00, 0xAC, 0x6C, 0x00, 0x00 }, // ;
  { 0x08, 0x14, 0x22, 0x41, 0x00 }, // <
  { 0x14, 0x14, 0x14, 0x14, 0x14 }, // =
  { 0x41, 0x22, 0x14, 0x08, 0x00 }, // >
  { 0x02, 0x01, 0x51, 0x09, 0x06 }, // ?
  { 0x32, 0x49, 0x79, 0x41, 0x3E }, // @
  { 0x7E, 0x09, 0x09, 0x09, 0x7E }, // A
  { 0x7F, 0x49, 0x49, 0x49, 0x36 }, // B
  { 0x3E, 0x41, 0x41, 0x41, 0x22 }, // C
  { 0x7F, 0x41, 0x41, 0x22, 0x1C }, // D
  { 0x7F, 0x49, 0x49, 0x49, 0x41 }, // E
  { 0x7F, 0x09, 0x09, 0x09, 0x01 }, // F
  { 0x3E, 0x41, 0x41, 0x51, 0x72 }, // G
  { 0x7F, 0x08, 0x08, 0x08, 0x7F }, // H
  { 0x41, 0x7F, 0x41, 0x00, 0x00 }, // I
  { 0x20, 0x40, 0x41, 0x3F, 0x01 }, // J
  { 0x7F, 0x08, 0x14, 0x22, 0x41 }, // K
  { 0x7F, 0x40, 0x40, 0x40, 0x40 }, // L
  { 0x7F, 0x02, 0x0C, 0x02, 0x7F }, // M
  { 0x7F, 0x04, 0x08, 0x10, 0x7F }, // N
  { 0x3E, 0x41, 0x41, 0x41, 0x3E }, // O
  { 0x7F, 0x09, 0x09, 0x09, 0x06 }, // P
  { 0x3E, 0x41, 0x51, 0x21, 0x5E }, // Q
  { 0x7F, 0x09, 0x19, 0x29, 0x46 }, // R
  { 0x26, 0x49, 0x49, 0x49, 0x32 }, // S
  { 0x01, 0x01, 0x7F, 0x01, 0x01 }, // T
  { 0x3F, 0x40, 0x40, 0x40, 0x3F }, // U
  { 0x1F, 0x20, 0x40, 0x20, 0x1F }, // V
  { 0x3F, 0x40, 0x38, 0x40, 0x3F }, // W
  { 0x63, 0x14, 0x08, 0x14, 0x63 }, // X
  { 0x03, 0x04, 0x78, 0x04, 0x03 }, // Y
  { 0x61, 0x51, 0x49, 0x45, 0x43 }, // Z
  { 0x7F, 0x41, 0x41, 0x00, 0x00 }, // [
  { 0x02, 0x04, 0x08, 0x10, 0x20 }, // BACKSLASH
  { 0x41, 0x41, 0x7F, 0x00, 0x00 }, // ]
  { 0x04, 0x02, 0x01, 0x02, 0x04 }, // ^
  { 0x80, 0x80, 0x80, 0x80, 0x80 }, // _
  { 0x01, 0x02, 0x04, 0x00, 0x00 }, // `
  { 0x20, 0x54, 0x54, 0x54, 0x78 }, // a
  { 0x7F, 0x48, 0x44, 0x44, 0x38 }, // b
  { 0x38, 0x44, 0x44, 0x28, 0x00 }, // c
  { 0x38, 0x44, 0x44, 0x48, 0x7F }, // d
  { 0x38, 0x54, 0x54, 0x54, 0x18 }, // e
  { 0x08, 0x7E, 0x09, 0x02, 0x00 }, // f
  { 0x18, 0xA4, 0xA4, 0xA4, 0x7C }, // g
  { 0x7F, 0x08, 0x04, 0x04, 0x78 }, // h
  { 0x00, 0x7D, 0x00, 0x00, 0x00 }, // i
  { 0x80, 0x84, 0x7D, 0x00, 0x00 }, // j
  { 0x7F, 0x10, 0x28, 0x44, 0x00 }, // k
  { 0x41, 0x7F, 0x40, 0x00, 0x00 }, // l
  { 0x7C, 0x04, 0x18, 0x04, 0x78 }, // m
  { 0x7C, 0x08, 0x04, 0x7C, 0x00 }, // n
  { 0x38, 0x44, 0x44, 0x38, 0x00 }, // o
  { 0xFC, 0x24, 0x24, 0x18, 0x00 }, // p
  { 0x18, 0x24, 0x24, 0xFC, 0x00 }, // q
  { 0x00, 0x7C, 0x08, 0x04, 0x00 }, // r
  { 0x48, 0x54, 0x54, 0x24, 0x00 }, // s
  { 0x04, 0x7F, 0x44, 0x00, 0x00 }, // t
  { 0x3C, 0x40, 0x40, 0x7C, 0x00 }, // u
  { 0x1C, 0x20, 0x40, 0x20, 0x1C }, // v
  { 0x3C, 0x40, 0x30, 0x40, 0x3C }, // w
  { 0x44, 0x28, 0x10, 0x28, 0x44 }, // x
  { 0x1C, 0xA0, 0xA0, 0x7C, 0x00 }, // y
  { 0x44, 0x64, 0x54, 0x4C, 0x44 }, // z
  { 0x08, 0x36, 0x41, 0x00, 0x00 }, // {
  { 0x00, 0x7F, 0x00, 0x00, 0x00 }, // |
  { 0x41, 0x36, 0x08, 0x00, 0x00 }, // }
  { 0x02, 0x01, 0x01, 0x02, 0x01 }, // ~
  { 0x02, 0x05, 0x05, 0x02, 0x00 }  // DEL
};


P023_data_struct::P023_data_struct(uint8_t                   _address,
                                   uint8_t                   _type,
                                   P023_data_struct::Spacing _font_spacing,
                                   uint8_t                   _displayTimer,
                                   uint8_t                   _use_sh1106)
  :  address(_address), type(_type),  font_spacing(_font_spacing),  displayTimer(_displayTimer), use_sh1106(_use_sh1106)
{}

void P023_data_struct::setDisplayTimer(uint8_t _displayTimer) {
  displayOn();
  displayTimer = _displayTimer;
}

void P023_data_struct::checkDisplayTimer() {
  if (displayTimer > 0) {
    displayTimer--;

    if (displayTimer == 0) {
      displayOff();
    }
  }
}

// Perform some specific changes for OLED display
String P023_data_struct::parseTemplate(String& tmpString, uint8_t lineSize) {
  String result             = parseTemplate_padded(tmpString, lineSize);
  const char degree[3]      = { 0xc2, 0xb0, 0 }; // Unicode degree symbol
  const char degree_oled[2] = { 0x7F, 0 };       // P023_OLED degree symbol

  result.replace(degree, degree_oled);
  return result;
}

void P023_data_struct::resetDisplay() {
  displayOff();
  clearDisplay();
  displayOn();
}

void P023_data_struct::StartUp_OLED(struct EventStruct *event) {
  init_OLED();
  resetDisplay();

  // displayOff();
  setXY(0, 0);

  // clearDisplay(); // Why clear twice?
  // displayOn();

  LoadCustomTaskSettings(event->TaskIndex, strings, P23_Nlines, P23_Nchars);
}

bool P023_data_struct::plugin_read(struct EventStruct *event) {
  for (uint8_t x = 0; x < 8; ++x) {
    if (strings[x].length()) {
      String tmp             = strings[x];
      const String newString = parseTemplate(tmp, 16);

      sendStrXY(newString.c_str(), x, 0);
      #if P023_FEATURE_DISPLAY_PREVIEW
      currentLines[x] = newString;
      #endif // if P023_FEATURE_DISPLAY_PREVIEW
    }
  }
  return true;
}

bool P023_data_struct::plugin_write(struct EventStruct *event,
                                    String            & string) {
  bool success     = false;
  const String cmd = parseString(string, 1); // Changes to lowercase

  if (equals(cmd, F("oledcmd"))) {
    const String param = parseString(string, 2);

    if (equals(param, F("off"))) {
      displayOff();
      success = true;
    }
    else if (equals(param, F("on"))) {
      displayOn();
      success = true;
    }
    else if (equals(param, F("clear"))) {
      clearDisplay();
      success = true;
    }
  }
  else if (equals(cmd, F("oled"))) {
    success = true;
    String text = parseStringToEndKeepCase(string, 4);
    text = parseTemplate(text, 16);
    sendStrXY(text.c_str(), event->Par1 - 1, event->Par2 - 1);
    #if P023_FEATURE_DISPLAY_PREVIEW
    setCurrentText(text, event->Par1 - 1, event->Par2 - 1);
    #endif // if P023_FEATURE_DISPLAY_PREVIEW
  }
  return success;
}

#if P023_FEATURE_DISPLAY_PREVIEW

/**
 * Update the buffer that is used for PLUGIN_WEB_SHOW_VALUES function
 */
void P023_data_struct::setCurrentText(const String& string, int X, int Y) {
  if ((X < 0) || (Y < 0) || (X >= P23_Nlines)) { return; } // Sanity check

  if (Y > 0) {
    if (currentLines[X].length() >= static_cast<size_t>(Y)) {
      currentLines[X] = currentLines[X].substring(0, Y + 1) + string;
    } else {
      for (size_t i = currentLines[X].length(); i < static_cast<size_t>(Y); ++i) {
        currentLines[X] += ' ';
      }
      currentLines[X] += string;
    }
  } else {
    currentLines[X] = string;
  }
}

bool P023_data_struct::web_show_values() {
  bool result     = true;
  uint8_t maxLine = P23_Nlines;

  for (; maxLine > 0; --maxLine) { // Don't show trailing empty lines
    String tmp = currentLines[maxLine - 1];
    tmp.trim();

    if (!tmp.isEmpty()) { break; }
  }

  addHtml(F("<pre>")); // To keep spaces etc. in the shown output

  for (uint8_t i = 0; i < maxLine; ++i) {
    addHtmlDiv(F("div_l"), currentLines[i], EMPTY_STRING, F("style='font-size:75%;'"));

    if (i != maxLine - 1) {
      addHtmlDiv(F("div_br"));
    }
  }
  addHtml(F("</pre>"));
  return result;
}

#endif // if P023_FEATURE_DISPLAY_PREVIEW

void P023_data_struct::displayOn() {
  sendCommand(0xaf); // display on
}

void P023_data_struct::displayOff() {
  sendCommand(0xae); // display off
}

void P023_data_struct::clearDisplay() {
  unsigned char i, k;

  for (k = 0; k < 8; ++k) {
    setXY(k, 0);

    for (i = 0; i < 128; ++i) { // clear all COL
      sendChar(0);              // clear all COL
    }
  }
}

// Actually this sends a byte, not a char to draw in the display.
void P023_data_struct::sendChar(unsigned char data) {
  I2C_write8_reg(address, P023_DATA_MODE_REG, data);
}

void P023_data_struct::sendCommand(unsigned char com) {
  I2C_write8_reg(address, P023_COMMAND_MODE_REG, com);
}

// Set the cursor position in a 16 COL * 8 ROW map (128x64 pixels)
// or 8 COL * 5 ROW map (64x48 pixels)
void P023_data_struct::setXY(unsigned char row, unsigned char col) {
  unsigned char col_offset = 0;

  if (use_sh1106) {
    col_offset = 0x02; // offset of 2 when using SSH1106 controller
  }

  if (type == OLED_64x48) {
    col += 4;
  } else if (type == (OLED_64x48 | OLED_rotated)) {
    col += 4;
    row += 2;
  }

  sendCommand(0xb0 + row);                             // set page address
  sendCommand(0x00 + ((8 * col + col_offset) & 0x0f)); // set low col address
  sendCommand(0x10 + (((8 * col) >> 4) & 0x0f));       // set high col address
}

// Prints a string in coordinates X Y, being multiples of 8.
// This means we have 16 COLS (0-15) and 8 ROWS (0-7).
void P023_data_struct::sendStrXY(const char *string, int X, int Y) {
  setXY(X, Y);
  uint16_t i             = 0;
  uint16_t char_width    = 8;
  uint16_t maxPixels     = 128;   // Assumed default display width
  uint16_t currentPixels = Y * 8; // setXY always uses char_width = 8, Y = 0-based

  if ((type == OLED_64x48) ||     // Cater for that 1 smaller size display
      (type == (OLED_64x48 | OLED_rotated))) {
    maxPixels = 64;
  }

  constexpr int myFont_nrChars = NR_ELEMENTS(Plugin_023_myFont_Size);

  while (*string && currentPixels < maxPixels) { // Prevent display overflow on the character level
    auto index = *string - 0x20;
    if (index < 0 || index >= myFont_nrChars) {
      // Char not in font, just print a space
      index = 0;
    }
    if (font_spacing == Spacing::optimized) {
      char_width = pgm_read_byte(&(Plugin_023_myFont_Size[index]));
    }

    const char * baddr = Plugin_023_myFont[index];
    for (i = 0; i < char_width && currentPixels < maxPixels; ++i,++currentPixels) { // Prevent display overflow on the pixel-level
      sendChar((i == 0 || i > 5) ? 0x00 : pgm_read_byte(baddr + i - 1)); 
    }
    string++;
  }
}

void P023_data_struct::init_OLED() {
  unsigned char multiplex;
  unsigned char compins;

  if (type == OLED_128x32) {
    multiplex = 0x1F;
    compins   = 0x02;
  } else {
    multiplex = 0x3F;
    compins   = 0x12;
  }

  sendCommand(0xAE);       // display off
  sendCommand(0xD5);       // SETDISPLAYCLOCKDIV
  sendCommand(0x80);       // the suggested ratio 0x80
  sendCommand(0xA8);       // SSD1306_SETMULTIPLEX
  sendCommand(multiplex);  // 0x1F if 128x32, 0x3F if others (e.g. 128x64)
  sendCommand(0xD3);       // SETDISPLAYOFFSET
  sendCommand(0x00);       // no offset
  sendCommand(0x40 | 0x0); // SETSTARTLINE

  if (use_sh1106) {
    sendCommand(0xAD);     // CHARGEPUMP mode SH1106
    sendCommand(0x8B);     // CHARGEPUMP On SH1106
    sendCommand(0x32);     // CHARGEPUMP voltage 8V SH1106
    sendCommand(0x81);     // SETCONTRAS
    sendCommand(0x80);     // SH1106
  } else {
    sendCommand(0x8D);     // CHARGEPUMP
    sendCommand(0x14);
    sendCommand(0x81);     // SETCONTRAS
    sendCommand(0xCF);
  }
  sendCommand(0x20);       // MEMORYMODE
  sendCommand(0x00);       // 0x0 act like ks0108
  sendCommand(0xA0);       // 128x32 ???
  sendCommand(0xC0);       // 128x32 ???
  sendCommand(0xDA);       // COMPINS
  sendCommand(compins);    // 0x02 if 128x32, 0x12 if others (e.g. 128x64)
  sendCommand(0xD9);       // SETPRECHARGE
  sendCommand(0xF1);
  sendCommand(0xDB);       // SETVCOMDETECT
  sendCommand(0x40);
  sendCommand(0xA4);       // DISPLAYALLON_RESUME
  sendCommand(0xA6);       // NORMALDISPLAY

  clearDisplay();
  sendCommand(0x2E);       // stop scroll
  sendCommand(0x20);       // Set Memory Addressing Mode
  sendCommand(0x00);       // Set Memory Addressing Mode ab Horizontal addressing mode
}

#endif // ifdef USES_P023

#include "../PluginStructs/P116_data_struct.h"

#ifdef USES_P116

/****************************************************************************
 * ST77xx_type_toString: Display-value for the device selected
 ***************************************************************************/
const __FlashStringHelper* ST77xx_type_toString(const ST77xx_type_e& device) {
  switch (device) {
    case ST77xx_type_e::ST7735s_128x128: return F("ST7735 128 x 128px");
    case ST77xx_type_e::ST7735s_128x160: return F("ST7735 128 x 160px");
    case ST77xx_type_e::ST7735s_80x160: return F("ST7735 80 x 160px");
    case ST77xx_type_e::ST7735s_80x160_M5: return F("ST7735 80 x 160px (Color inverted)");
    # if P116_EXTRA_ST7735
    case ST77xx_type_e::ST7735s_135x240: return F("ST7735 135 x 240px");
    case ST77xx_type_e::ST7735s_172x320: return F("ST7735 172 x 320px");
    case ST77xx_type_e::ST77xxs_170x320: return F("ST77xx 170 x 320px");
    case ST77xx_type_e::ST77xxs_240x320: return F("ST77xx 240 x 320px");
    case ST77xx_type_e::ST77xxs_240x280: return F("ST77xx 240 x 280px");
    # endif // if P116_EXTRA_ST7735
    case ST77xx_type_e::ST7789vw_240x320: return F("ST7789 240 x 320px");
    case ST77xx_type_e::ST7789vw_240x240: return F("ST7789 240 x 240px");
    case ST77xx_type_e::ST7789vw_240x280: return F("ST7789 240 x 280px");
    case ST77xx_type_e::ST7789vw_135x240: return F("ST7789 135 x 240px");
    # if P116_EXTRA_ST7789
    case ST77xx_type_e::ST7789vw1_135x240: return F("ST7789 135 x 240px (alt1)");
    case ST77xx_type_e::ST7789vw2_135x240: return F("ST7789 135 x 240px (alt2)");
    case ST77xx_type_e::ST7789vw3_135x240: return F("ST7789 135 x 240px (alt3)");
    # endif // if P116_EXTRA_ST7789
    case ST77xx_type_e::ST7796s_320x480: return F("ST7796 320 x 480px");
  }
  return F("Unsupported type!");
}

/****************************************************************************
 * ST77xx_type_toResolution: X and Y resolution for the selected type
 ***************************************************************************/
void ST77xx_type_toResolution(const ST77xx_type_e& device,
                              uint16_t           & x,
                              uint16_t           & y) {
  switch (device) {
    case ST77xx_type_e::ST7735s_128x128:
      x = 128;
      y = 128;
      break;
    case ST77xx_type_e::ST7735s_128x160:
      x = 128;
      y = 160;
      break;
    case ST77xx_type_e::ST7735s_80x160_M5:
    case ST77xx_type_e::ST7735s_80x160:
      x = 80;
      y = 160;
      break;
    case ST77xx_type_e::ST7789vw_240x320:
      x = 240;
      y = 320;
      break;
    case ST77xx_type_e::ST7789vw_240x240:
      x = 240;
      y = 240;
      break;
    case ST77xx_type_e::ST7789vw_240x280:
      x = 240;
      y = 280;
      break;
    case ST77xx_type_e::ST7789vw_135x240:
    # if P116_EXTRA_ST7789
    case ST77xx_type_e::ST7789vw1_135x240:
    case ST77xx_type_e::ST7789vw2_135x240:
    case ST77xx_type_e::ST7789vw3_135x240:
    # endif // if P116_EXTRA_ST7789
    # if P116_EXTRA_ST7735
    case ST77xx_type_e::ST7735s_135x240:
    # endif // if P116_EXTRA_ST7735
      x = 135;
      y = 240;
      break;
    # if P116_EXTRA_ST7735
    case ST77xx_type_e::ST7735s_172x320:
      x = 172;
      y = 320;
      break;
    case ST77xx_type_e::ST77xxs_170x320:
      x = 170;
      y = 320;
      break;
    case ST77xx_type_e::ST77xxs_240x320:
      x = 240;
      y = 320;
      break;
    case ST77xx_type_e::ST77xxs_240x280:
      x = 240;
      y = 280;
      break;
    # endif // if P116_EXTRA_ST7735
    case ST77xx_type_e::ST7796s_320x480:
      x = 320;
      y = 480;
      break;
  }
}

/****************************************************************************
 * P116_CommandTrigger_toString: return the command string selected
 ***************************************************************************/
const __FlashStringHelper* P116_CommandTrigger_toString(const P116_CommandTrigger& cmd) {
  switch (cmd) {
    case P116_CommandTrigger::tft: return F("tft");
    case P116_CommandTrigger::st7735: return F("st7735");
    case P116_CommandTrigger::st7789: return F("st7789");
    case P116_CommandTrigger::st7796: return F("st7796");
    case P116_CommandTrigger::st77xx: break;
  }
  return F("st77xx"); // Default command trigger
}

/****************************************************************************
 * Constructor
 ***************************************************************************/
P116_data_struct::P116_data_struct(ST77xx_type_e       device,
                                   uint8_t             rotation,
                                   uint8_t             fontscaling,
                                   AdaGFXTextPrintMode textmode,
                                   int8_t              backlightPin,
                                   uint8_t             backlightPercentage,
                                   uint32_t            displayTimer,
                                   String              commandTrigger,
                                   uint16_t            fgcolor,
                                   uint16_t            bgcolor,
                                   bool                textBackFill
                                   # if                ADAGFX_FONTS_INCLUDED
                                   ,
                                   const uint8_t       defaultFontId
                                   # endif // if ADAGFX_FONTS_INCLUDED
                                   )
  : _device(device), _rotation(rotation), _fontscaling(fontscaling), _textmode(textmode), _backlightPin(backlightPin),
  _backlightPercentage(backlightPercentage), _displayTimer(displayTimer), _displayTimeout(displayTimer),
  _commandTrigger(commandTrigger), _fgcolor(fgcolor), _bgcolor(bgcolor), _textBackFill(textBackFill)
  # if ADAGFX_FONTS_INCLUDED
  , _defaultFontId(defaultFontId)
  # endif // if ADAGFX_FONTS_INCLUDED
{
  _commandTrigger.toLowerCase();
  _commandTriggerCmd = concat(_commandTrigger, F("cmd"));
}

/****************************************************************************
 * Destructor
 ***************************************************************************/
P116_data_struct::~P116_data_struct() {
  cleanup();
}

/****************************************************************************
 * plugin_init: Initialize display
 ***************************************************************************/
bool P116_data_struct::plugin_init(struct EventStruct *event) {
  ST77xx_type_toResolution(_device, _xpix, _ypix);

  updateFontMetrics();

  bool success = false;

  ButtonState     = false; // button not touched
  ButtonLastState = 0xFF;  // Last state checked (debouncing in progress)
  DebounceCounter = 0;     // debounce counter

  if (nullptr == st77xx) {
    addLog(LOG_LEVEL_INFO, F("ST77xx: Init start."));
    uint8_t initRoptions = 0xFF;

    switch (_device) {
      case ST77xx_type_e::ST7735s_128x128:

        initRoptions = INITR_144GREENTAB; // 128x128px

      // fall through
      case ST77xx_type_e::ST7735s_128x160:

        if (initRoptions == 0xFF) {
          initRoptions = INITR_BLACKTAB; // 128x160px
        }

      // fall through
      case ST77xx_type_e::ST7735s_80x160_M5:

        if (initRoptions == 0xFF) {
          initRoptions = INITR_GREENTAB160x80; // 80x160px ST7735sv, inverted (M5Stack StickC)
        }

        // fall through
      # if P116_EXTRA_ST7735
      case ST77xx_type_e::ST7735s_135x240:

        if (initRoptions == 0xFF) {
          initRoptions = INITR_BLACKTAB135x240; // 135x240px
        }

        // fall through
      case ST77xx_type_e::ST7735s_172x320:

        if (initRoptions == 0xFF) {
          initRoptions = INITR_BLACKTAB172x320; // 172x320px
        }

        // fall through
      case ST77xx_type_e::ST77xxs_170x320:

        if (initRoptions == 0xFF) {
          initRoptions = INITR_BLACKTAB170x320; // 170x320px
        }

        // fall through
      case ST77xx_type_e::ST77xxs_240x320:

        if (initRoptions == 0xFF) {
          initRoptions = INITR_BLACKTAB240x320; // 240x320px
        }

        // fall through
      case ST77xx_type_e::ST77xxs_240x280:

        if (initRoptions == 0xFF) {
          initRoptions = INITR_BLACKTAB240x280; // 240x280px
        }

        // fall through
      # endif // if P116_EXTRA_ST7735
      case ST77xx_type_e::ST7735s_80x160:
      {
        if (initRoptions == 0xFF) {
          initRoptions = INITR_MINI160x80; // 80x160px
        }

        st7735 = new (std::nothrow) Adafruit_ST7735(PIN(0), PIN(1), PIN(2));

        if (nullptr != st7735) {
          st7735->initR(initRoptions); // initialize a ST7735s chip
          st77xx = st7735;             // pass pointer after initialization
        }
        break;
      }
      case ST77xx_type_e::ST7789vw_240x320: // Fall through
      case ST77xx_type_e::ST7789vw_240x240:
      case ST77xx_type_e::ST7789vw_240x280:
      case ST77xx_type_e::ST7789vw_135x240:
      # if P116_EXTRA_ST7789
      case ST77xx_type_e::ST7789vw1_135x240:
      case ST77xx_type_e::ST7789vw2_135x240:
      case ST77xx_type_e::ST7789vw3_135x240:
      # endif // if P116_EXTRA_ST7789
      {
        st7789 = new (std::nothrow) Adafruit_ST7789(PIN(0), PIN(1), PIN(2));

        if (nullptr != st7789) {
          uint8_t init_seq = 0; // Default/original initialisation

          # if P116_EXTRA_ST7789

          if (ST77xx_type_e::ST7789vw1_135x240 == _device) {
            init_seq = 1;
          } else if (ST77xx_type_e::ST7789vw2_135x240 == _device) {
            init_seq = 2;
          } else if (ST77xx_type_e::ST7789vw3_135x240 == _device) {
            init_seq = 3;
          }
          # endif // if P116_EXTRA_ST7789
          st7789->init(_xpix, _ypix, SPI_MODE2, init_seq);
          st77xx = st7789;
        }
        break;
      }
      case ST77xx_type_e::ST7796s_320x480:
      {
        st7796 = new (std::nothrow) Adafruit_ST7796S_kbv(PIN(0), PIN(1), PIN(2));

        if (nullptr != st7796) {
          st7796->begin();
          st77xx = st7796;
        }
        break;
      }
    }

    # ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      String log;
      log.reserve(90);
      log += strformat(F("ST77xx: Init done, address: 0x%x "), reinterpret_cast<ulong>(st77xx));

      if (nullptr == st77xx) {
        log += F("in");
      }
      log += strformat(F("valid, commands: %s, display: "), _commandTrigger.c_str());
      log += ST77xx_type_toString(_device);
      addLogMove(LOG_LEVEL_INFO, log);
    }
    # endif // ifndef BUILD_NO_DEBUG
  } else {
    addLog(LOG_LEVEL_INFO, F("ST77xx: No init?"));
  }

  if (nullptr != st77xx) {
    gfxHelper = new (std::nothrow) AdafruitGFX_helper(st77xx,
                                                      _commandTrigger,
                                                      _xpix,
                                                      _ypix,
                                                      AdaGFXColorDepth::FullColor,
                                                      _textmode,
                                                      _fontscaling,
                                                      _fgcolor,
                                                      _bgcolor,
                                                      true,
                                                      _textBackFill
                                                      # if ADAGFX_FONTS_INCLUDED
                                                      , _defaultFontId
                                                      # endif // if ADAGFX_FONTS_INCLUDED
                                                      );

    if (nullptr != gfxHelper) {
      displayOnOff(true);

      gfxHelper->initialize();
      gfxHelper->setRotation(_rotation);
      st77xx->fillScreen(_bgcolor);             // fill screen with black color
      st77xx->setTextColor(_fgcolor, _bgcolor); // set text color to white and black background

      # ifdef P116_SHOW_SPLASH
      uint16_t yPos = 0;
      gfxHelper->printText(String(F("ESPEasy")).c_str(), 0, yPos, 3, ST77XX_WHITE, ST77XX_BLUE);
      yPos += (3 * _fontheight);
      gfxHelper->printText(String(F("ST77xx")).c_str(),  0, yPos, 2, ST77XX_BLUE,  ST77XX_WHITE);
      delay(100); // Splash
      # endif // ifdef P116_SHOW_SPLASH

      gfxHelper->setColumnRowMode(bitRead(P116_CONFIG_FLAGS, P116_CONFIG_FLAG_USE_COL_ROW));
      st77xx->setTextSize(_fontscaling); // Handles 0 properly, text size, default 1 = very small
      st77xx->setCursor(0, 0);           // move cursor to position (0, 0) pixel
      updateFontMetrics();


      if (P116_CONFIG_BUTTON_PIN != -1) {
        pinMode(P116_CONFIG_BUTTON_PIN, INPUT_PULLUP);
      }

      if (!stringsLoaded) {
        LoadCustomTaskSettings(event->TaskIndex, strings, P116_Nlines, 0);
        stringsLoaded = true;

        for (uint8_t x = 0; x < P116_Nlines && !stringsHasContent; ++x) {
          stringsHasContent = !strings[x].isEmpty();
        }
      }
      success = true;
    }
  }
  return success;
}

/****************************************************************************
 * updateFontMetrics: recalculate x and y columns, based on font size and font scale
 ***************************************************************************/
void P116_data_struct::updateFontMetrics() {
  if (nullptr != gfxHelper) {
    gfxHelper->getTextMetrics(_textcols, _textrows, _fontwidth, _fontheight, _fontscaling, _heightOffset, _xpix, _ypix);
    gfxHelper->getColors(_fgcolor, _bgcolor);
  } else {
    _textcols = _xpix / (_fontwidth * _fontscaling);
    _textrows = _ypix / (_fontheight * _fontscaling);
  }
}

/****************************************************************************
 * plugin_exit: De-initialize before destruction
 ***************************************************************************/
bool P116_data_struct::plugin_exit(struct EventStruct *event) {
  # ifndef BUILD_NO_DEBUG
  addLog(LOG_LEVEL_INFO, F("ST77xx: Exit."));
  # endif // ifndef BUILD_NO_DEBUG

  if ((nullptr != st77xx) && bitRead(P116_CONFIG_FLAGS, P116_CONFIG_FLAG_CLEAR_ON_EXIT)) {
    st77xx->fillScreen(ADAGFX_BLACK); // fill screen with black color
    displayOnOff(false);
  }
  cleanup();
  return true;
}

/****************************************************************************
 * cleanup: De-initialize pointers
 ***************************************************************************/
void P116_data_struct::cleanup() {
  delete gfxHelper;
  gfxHelper = nullptr;

  delete st7735;
  st7735 = nullptr;

  delete st7789;
  st7789 = nullptr;
  st77xx = nullptr; // Only used as a proxy
}

/****************************************************************************
 * plugin_read: Re-draw the default content
 ***************************************************************************/
bool P116_data_struct::plugin_read(struct EventStruct *event) {
  if (nullptr != st77xx) {
    if (stringsHasContent) {
      gfxHelper->setColumnRowMode(false); // Turn off column mode

      int yPos = 0;

      for (uint8_t x = 0; x < P116_Nlines; ++x) {
        const String newString = AdaGFXparseTemplate(strings[x], _textcols, gfxHelper);

        # if ADAGFX_PARSE_SUBCOMMAND
        updateFontMetrics();
        # endif // if ADAGFX_PARSE_SUBCOMMAND

        if (yPos < _ypix) {
          gfxHelper->printText(newString.c_str(), 0, yPos, _fontscaling, _fgcolor, _bgcolor);
        }
        delay(0);
        yPos += (_fontheight * _fontscaling);
      }
      gfxHelper->setColumnRowMode(bitRead(P116_CONFIG_FLAGS, P116_CONFIG_FLAG_USE_COL_ROW)); // Restore column mode
      int16_t curX, curY;
      gfxHelper->getCursorXY(curX, curY);                                                    // Get current X and Y coordinates,
      UserVar.setFloat(event->TaskIndex, 0, curX);                                           // and put into Values
      UserVar.setFloat(event->TaskIndex, 1, curY);
    }
  }
  return false; // Always return false, so no attempt to send to
                // Controllers or generate events is started
}

/****************************************************************************
 * plugin_ten_per_second: check button, if any, that wakes up the display
 ***************************************************************************/
bool P116_data_struct::plugin_ten_per_second(struct EventStruct *event) {
  if ((P116_CONFIG_BUTTON_PIN != -1) && getButtonState() && (nullptr != st77xx)) {
    displayOnOff(true);
    markButtonStateProcessed();
  }
  return true;
}

/****************************************************************************
 * plugin_once_a_second: Count down display timer, if any, and turn display off if countdown reached
 ***************************************************************************/
bool P116_data_struct::plugin_once_a_second(struct EventStruct *event) {
  if (_displayTimer > 0) {
    _displayTimer--;

    if ((nullptr != st77xx) && (_displayTimer == 0)) {
      displayOnOff(false);
    }
  }
  return true;
}

/****************************************************************************
 * plugin_write: Handle commands
 ***************************************************************************/
bool P116_data_struct::plugin_write(struct EventStruct *event,
                                    const String      & string) {
  bool success     = false;
  const String cmd = parseString(string, 1);

  if ((nullptr != st77xx) && cmd.equals(_commandTriggerCmd)) {
    const String arg1 = parseString(string, 2);
    success = true;

    if (equals(arg1, F("off"))) {
      displayOnOff(false);
    }
    else if (equals(arg1, F("on"))) {
      displayOnOff(true);
    }
    else if (equals(arg1, F("clear"))) {
      st77xx->fillScreen(_bgcolor);
    }
    else if (equals(arg1, F("backlight"))) {
      if ((P116_CONFIG_BACKLIGHT_PIN != -1) &&       // All is valid?
          (event->Par2 >= 0) &&
          (event->Par2 <= 100)) {
        P116_CONFIG_BACKLIGHT_PERCENT = event->Par2; // Set but don't store
        _backlightPercentage          = event->Par2; // Also set to current
        displayOnOff(true);
      } else {
        success = false;
      }
    } else {
      success = false;
    }
  }
  else if (st77xx && (cmd.equals(_commandTrigger) ||
                      (gfxHelper && gfxHelper->isAdaGFXTrigger(cmd)))) {
    success = true;

    if (!bitRead(P116_CONFIG_FLAGS, P116_CONFIG_FLAG_NO_WAKE)) { // Wake display?
      displayOnOff(true);
    }

    if (nullptr != gfxHelper) {
      String tmp = string;

      // Hand it over after replacing variables
      success = gfxHelper->processCommand(AdaGFXparseTemplate(tmp, _textcols, gfxHelper));

      updateFontMetrics(); // Font or color may have changed

      if (success) {
        int16_t curX, curY;
        gfxHelper->getCursorXY(curX, curY); // Get current X and Y coordinates, and put into Values
        UserVar.setFloat(event->TaskIndex, 0, curX);
        UserVar.setFloat(event->TaskIndex, 1, curY);
      }
    }
  }
  return success;
}

# if ADAGFX_ENABLE_GET_CONFIG_VALUE

/****************************************************************************
 * plugin_get_config_value: Retrieve values like [<taskname>#<valuename>]
 ***************************************************************************/
bool P116_data_struct::plugin_get_config_value(struct EventStruct *event,
                                               String            & string) {
  bool success = false;

  if (gfxHelper != nullptr) {
    success = gfxHelper->pluginGetConfigValue(string);
  }
  return success;
}

# endif // if ADAGFX_ENABLE_GET_CONFIG_VALUE

/****************************************************************************
 * displayOnOff: Turn display on or off
 ***************************************************************************/
void P116_data_struct::displayOnOff(bool state) {
  if (_backlightPin != -1) {
    # if defined(ESP8266)
    analogWrite(_backlightPin, state ? ((1024 / 100) * _backlightPercentage) : 0);
    # endif // if defined(ESP8266)
    # if defined(ESP32)
    analogWriteESP32(_backlightPin, state ? ((1024 / 100) * _backlightPercentage) : 0, 0);
    # endif // if defined(ESP32)
  }
  st77xx->enableDisplay(state); // Display on
  _displayTimer = (state ? _displayTimeout : 0);
}

/****************************************************************************
 * registerButtonState: the button has been pressed, apply some debouncing
 ***************************************************************************/
void P116_data_struct::registerButtonState(uint8_t newButtonState,
                                           bool    bPin3Invers) {
  if ((ButtonLastState == 0xFF) || (bPin3Invers != (!!newButtonState))) {
    ButtonLastState = newButtonState;
    DebounceCounter++;
  } else {
    ButtonLastState = 0xFF; // Reset
    DebounceCounter = 0;
    ButtonState     = false;
  }

  if ((ButtonLastState == newButtonState) &&
      (DebounceCounter >= P116_DebounceTreshold)) {
    ButtonState = true;
  }
}

/****************************************************************************
 * markButtonStateProcessed: reset the button state
 ***************************************************************************/
void P116_data_struct::markButtonStateProcessed() {
  ButtonState     = false;
  DebounceCounter = 0;
}

#endif // ifdef USES_P116

#include "../PluginStructs/P133_data_struct.h"

#ifdef USES_P133

/**************************************************************************
* Constructor
**************************************************************************/
P133_data_struct::P133_data_struct(P133_selectMode_e   selectMode,
                                   ltr390_gain_t       uvGain,
                                   ltr390_resolution_t uvResolution,
                                   ltr390_gain_t       alsGain,
                                   ltr390_resolution_t alsResolution,
                                   bool                initReset)
  : _selectMode(selectMode), _uvGain(uvGain), _uvResolution(uvResolution),
  _alsGain(alsGain), _alsResolution(alsResolution), _initReset(initReset)
{}

/*****************************************************
* Destructor
*****************************************************/
P133_data_struct::~P133_data_struct() {
  if (nullptr != ltr390) { delete ltr390; }
  ltr390 = nullptr;
}

/*****************************************************
* plugin_read
*****************************************************/
bool P133_data_struct::plugin_read(struct EventStruct *event)           {
  if (initialised) {
    // Last obtained values
    UserVar.setFloat(event->TaskIndex, 0, uvValue);
    UserVar.setFloat(event->TaskIndex, 1, uviValue);
    UserVar.setFloat(event->TaskIndex, 2, alsValue);
    UserVar.setFloat(event->TaskIndex, 3, luxValue);
    sensorRead = false;
    return true;
  }
  return false;
}

/*****************************************************
* plugin_ten_per_second
*****************************************************/
bool P133_data_struct::plugin_ten_per_second(struct EventStruct *event) {
  if (initialised && ltr390->newDataAvailable()) {
    if (mode == LTR390_MODE_UVS) {
      uvValue  = ltr390->readUVS();
      uviValue = ltr390->getUVI();
    } else {
      alsValue = ltr390->readALS();
      luxValue = ltr390->getLUX();
    }

    // Dual Mode: Switch mode after measuring a value for a few loop cycles, so the data is stable
    if (_selectMode == P133_selectMode_e::DualMode) {
      if ((loopCounter == 0) || (loopCounter > P133_LOOP_INTERVAL)) {
        if (mode == LTR390_MODE_UVS) {
          mode = LTR390_MODE_ALS;
          ltr390->setGain(_alsGain);
          ltr390->setResolution(_alsResolution);
        } else {
          mode = LTR390_MODE_UVS;
          ltr390->setGain(_uvGain);
          ltr390->setResolution(_uvResolution);
        }
        ltr390->setMode(mode);

        loopCounter = P133_LOOP_INTERVAL;
      } else {
        loopCounter--;
      }
    }

    # if PLUGIN_133_DEBUG

    if (!sensorRead && loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO,
                 strformat(F("LTR390: data read. Mode: %d, UV: %d, UVindex: %.2f, ALS: %d, Lux: %.2f"),
                           mode, uvValue, uviValue, alsValue, luxValue));
    }
    # endif // if PLUGIN_133_DEBUG
    sensorRead = true;
    return true;
  }
  return false;
}

/**************************************************************************
* plugin_init Initialize sensor and prepare for reading
**************************************************************************/
bool P133_data_struct::plugin_init(struct EventStruct *event) {
  if (!initialised) {
    initialised = init_sensor(); // Check if device is present
  }

  if (initialised) {
    switch (_selectMode) {
      case P133_selectMode_e::DualMode:
      case P133_selectMode_e::UVMode:
        mode = LTR390_MODE_UVS;
        ltr390->setGain(_uvGain);
        ltr390->setResolution(_uvResolution);
        break;
      case P133_selectMode_e::ALSMode:
        mode = LTR390_MODE_ALS;
        ltr390->setGain(_alsGain);
        ltr390->setResolution(_alsResolution);
        break;
    }
    ltr390->setMode(mode);

    # if PLUGIN_133_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO,
                 strformat(F("LTR390: Configured, mode: %d, UV gain: %d, UV resolution: %d, ALS gain: %d, ALS resolution: %d"),
                           mode, _uvGain, _uvResolution, _alsGain, _alsResolution));
    }
    # endif // if PLUGIN_133_DEBUG
  }

  return initialised;
}

/**************************************************************************
* Check LTR390 presence and initialize
**************************************************************************/
bool P133_data_struct::init_sensor() {
  if (!initialised) {
    ltr390 = new (std::nothrow) UVlight_LTR390();

    if (nullptr != ltr390) {
      initialised = ltr390->init(_initReset);
    }

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      String log = concat(F("LTR390: Initialized: "), initialised ? F("OK") : F("ERROR"));
      log += strformat(F(", chip ID: 0x%02x"), ltr390->getChipID());
      addLogMove(LOG_LEVEL_INFO, log);
    }
  }

  return initialised;
}

#endif // ifdef USES_P133

#include "../PluginStructs/P081_data_struct.h"

#ifdef USES_P081

P081_data_struct::P081_data_struct(const String& expression)
{
  const char *error;

  memset(&_expr, 0, sizeof(_expr));
  cron_parse_expr(expression.c_str(), &_expr, &error);

  if (!error) {
    _initialized = true;
  } else {
    _error = String(error);
  }
}

bool P081_data_struct::hasError(String& error) const {
  if (_initialized) { return false; }
  error = _error;
  return true;
}

time_t P081_data_struct::get_cron_next(time_t date) const {
  if (!_initialized) { return CRON_INVALID_INSTANT; }
  return cron_next((cron_expr *)&_expr, date);
}

time_t P081_data_struct::get_cron_prev(time_t date) const {
  if (!_initialized) { return CRON_INVALID_INSTANT; }
  return cron_prev((cron_expr *)&_expr, date);
}

String P081_getCronExpr(taskIndex_t taskIndex)
{
  char expression[PLUGIN_081_EXPRESSION_SIZE + 1];

  ZERO_FILL(expression);
  LoadCustomTaskSettings(taskIndex, reinterpret_cast<uint8_t *>(&expression), PLUGIN_081_EXPRESSION_SIZE);
  String res(expression);

  res.trim();
  return res;
}

time_t P081_computeNextCronTime(taskIndex_t taskIndex, time_t last)
{
  P081_data_struct *P081_data =
    static_cast<P081_data_struct *>(getPluginTaskData(taskIndex));

  if ((nullptr != P081_data) && P081_data->isInitialized()) {
    //    int32_t freeHeapStart = ESP.getFreeHeap();

    time_t res = P081_data->get_cron_next(last);

    /*
        int32_t freeHeapEnd = ESP.getFreeHeap();

        if (freeHeapEnd < freeHeapStart) {
          String log = F("Cron: Free Heap Decreased: ");
          log += String(freeHeapStart - freeHeapEnd);
          log += F(" (");
          log += freeHeapStart;
          log += F(" -> ");
          log += freeHeapEnd;
          addLog(LOG_LEVEL_INFO, log);
        }
     */
    return res;
  }
  return CRON_INVALID_INSTANT;
}

time_t P081_getCronExecTime(taskIndex_t taskIndex, uint8_t varNr)
{
  return static_cast<time_t>(UserVar.getUint32(taskIndex, varNr));
}

void P081_setCronExecTimes(struct EventStruct *event, time_t lastExecTime, time_t nextExecTime) {
  UserVar.setUint32(event->TaskIndex, LASTEXECUTION, static_cast<uint32_t>(lastExecTime));
  UserVar.setUint32(event->TaskIndex, NEXTEXECUTION, static_cast<uint32_t>(nextExecTime));
}

time_t P081_getCurrentTime()
{
  node_time.now_();

  // FIXME TD-er: Why work on a deepcopy of tm?
  struct tm current = node_time.local_tm;

  return mktime((struct tm *)&current);
}

void P081_check_or_init(struct EventStruct *event)
{
  if (node_time.systemTimePresent()) {
    const time_t current_time = P081_getCurrentTime();
    time_t last_exec_time     = P081_getCronExecTime(event->TaskIndex, LASTEXECUTION);
    time_t next_exec_time     = P081_getCronExecTime(event->TaskIndex, NEXTEXECUTION);

    // Must check if the values of LASTEXECUTION and NEXTEXECUTION make sense.
    // These can be invalid values from a reboot, or simply contain uninitialized values.
    if ((last_exec_time > current_time) || (last_exec_time == CRON_INVALID_INSTANT) || (next_exec_time == CRON_INVALID_INSTANT)) {
      // Last execution time cannot be correct.
      last_exec_time = CRON_INVALID_INSTANT;
      const time_t tmp_next = P081_computeNextCronTime(event->TaskIndex, current_time);

      if ((tmp_next < next_exec_time) || (next_exec_time == CRON_INVALID_INSTANT)) {
        next_exec_time = tmp_next;
      }
      P081_setCronExecTimes(event, CRON_INVALID_INSTANT, next_exec_time);
    }
  }
}

# if PLUGIN_081_DEBUG
void PrintCronExp(struct cron_expr_t e) {
  serialPrintln(F("===DUMP Cron Expression==="));
  serialPrint(F("Seconds:"));

  for (int i = 0; i < 8; i++)
  {
    serialPrint(e.seconds[i]);
    serialPrint(",");
  }
  serialPrintln();
  serialPrint(F("Minutes:"));

  for (int i = 0; i < 8; i++)
  {
    serialPrint(e.minutes[i]);
    serialPrint(",");
  }
  serialPrintln();
  serialPrint(F("hours:"));

  for (int i = 0; i < 3; i++)
  {
    serialPrint(e.hours[i]);
    serialPrint(",");
  }
  serialPrintln();
  serialPrint(F("months:"));

  for (int i = 0; i < 2; i++)
  {
    serialPrint(e.months[i]);
    serialPrint(",");
  }
  serialPrintln();
  serialPrint(F("days_of_week:"));

  for (int i = 0; i < 1; i++)
  {
    serialPrint(e.days_of_week[i]);
    serialPrint(",");
  }
  serialPrintln();
  serialPrint(F("days_of_month:"));

  for (int i = 0; i < 4; i++)
  {
    serialPrint(e.days_of_month[i]);
    serialPrint(",");
  }
  serialPrintln();
  serialPrintln(F("END=DUMP Cron Expression==="));
}

# endif // if PLUGIN_081_DEBUG


String P081_formatExecTime(taskIndex_t taskIndex, uint8_t varNr) {
  time_t exec_time = P081_getCronExecTime(taskIndex, varNr);

  if (exec_time != CRON_INVALID_INSTANT) {
    return formatDateTimeString(*gmtime(&exec_time));
  }
  return F("-");
}

void P081_html_show_cron_expr(struct EventStruct *event) {
  P081_data_struct *P081_data =
    static_cast<P081_data_struct *>(getPluginTaskData(event->TaskIndex));

  if ((nullptr != P081_data) && P081_data->isInitialized()) {
    String error;

    if (P081_data->hasError(error)) {
      addRowLabel(F("Error"));
      addHtml(error);
    } else {
      addRowLabel(F("Last Exec Time"));
      addHtml(P081_formatExecTime(event->TaskIndex, LASTEXECUTION));
      addRowLabel(F("Next Exec Time"));
      addHtml(P081_formatExecTime(event->TaskIndex, NEXTEXECUTION));
    }
  }
}

#endif // ifdef USES_P081

#include "../PluginStructs/P002_data_struct.h"

#ifdef USES_P002
#if SOC_ADC_SUPPORTED || defined(ESP8266)

# include "../Globals/RulesCalculate.h"

# include "../Helpers/Hardware_ADC_cali.h"

# ifndef DEFAULT_VREF
#  define DEFAULT_VREF 1100
# endif // ifndef DEFAULT_VREF

# ifndef P002_ADC_ATTEN_MAX
#  if ESP_IDF_VERSION_MAJOR < 5
#   define P002_ADC_ATTEN_MAX ADC_ATTEN_MAX
#  else // if ESP_IDF_VERSION_MAJOR < 5
#   define P002_ADC_ATTEN_MAX ADC_ATTENDB_MAX
#  endif // if ESP_IDF_VERSION_MAJOR < 5
# endif // ifndef P002_ADC_ATTEN_MAX


void P002_data_struct::init(struct EventStruct *event)
{
  _sampleMode = P002_OVERSAMPLING;

  # ifdef ESP8266
  _pin_analogRead = A0;
  # endif // ifdef ESP8266
  # ifdef ESP32
  _pin_analogRead        = CONFIG_PIN1;
  _useFactoryCalibration = useFactoryCalibration(event);
  _attenuation           = getAttenuation(event);

  // Initialize attenuation and perform read
  // This way there is less chance of a big difference between 1st read and any next reads
  analog_read();
  # endif // ifdef ESP32

  if (P002_CALIBRATION_ENABLED) {
    _use2pointCalibration = true;
    _calib_adc1           = P002_CALIBRATION_POINT1;
    _calib_adc2           = P002_CALIBRATION_POINT2;
    _calib_out1           = P002_CALIBRATION_VALUE1;
    _calib_out2           = P002_CALIBRATION_VALUE2;
  }
  _nrDecimals = Cache.getTaskDeviceValueDecimals(event->TaskIndex, 0);
# ifndef LIMIT_BUILD_SIZE
  _nrMultiPointItems = P002_NR_MULTIPOINT_ITEMS;
  _useMultipoint     = P002_MULTIPOINT_ENABLED;

  load(event);
# endif // ifndef LIMIT_BUILD_SIZE
}

# ifndef LIMIT_BUILD_SIZE
void P002_data_struct::load(struct EventStruct *event)
{
  const size_t nr_lines = P002_Nlines;

  {
    String lines[nr_lines];
    LoadCustomTaskSettings(event->TaskIndex, lines, nr_lines, 0);
    const int stored_nr_lines = lines[P002_SAVED_NR_LINES].toInt();
    move_special(_formula,              std::move(lines[P002_LINE_INDEX_FORMULA]));
    move_special(_formula_preprocessed, RulesCalculate_t::preProces(_formula));

    for (size_t i = P002_LINE_IDX_FIRST_MP; i < nr_lines && static_cast<int>(i) < stored_nr_lines; i += P002_STRINGS_PER_MP) {
      float adc, value = 0.0f;

      if (validFloatFromString(lines[i], adc) && validFloatFromString(lines[i + 1], value)) {
        // sizeof() multipoint item is multiple of 4 bytes, so should work just fine on 2nd heap
        #  ifdef USE_SECOND_HEAP
        HeapSelectIram ephemeral;
        #  endif // ifdef USE_SECOND_HEAP

        _multipoint.emplace_back(adc, value);
      }
    }
  }
  std::sort(_multipoint.begin(), _multipoint.end());
  {
    #  ifdef USE_SECOND_HEAP
    HeapSelectIram ephemeral;
    #  endif // ifdef USE_SECOND_HEAP

    _binning.resize(_multipoint.size(), 0);
    _binningRange.resize(_multipoint.size());
  }
}

# endif // ifndef LIMIT_BUILD_SIZE

void P002_data_struct::webformLoad_2p_calibPoint(
  const __FlashStringHelper *label,
  const __FlashStringHelper *id_point,
  const __FlashStringHelper *id_value,
  int                        point,
  float                      value) const
{
  addRowLabel_tr_id(label, id_point);
  addTextBox(id_point, String(point), 10, F("number"));

# ifdef ESP32

  if (_useFactoryCalibration) {
    addUnit(F("mV"));
  }
# endif // ifdef ESP32

  html_add_estimate_symbol();
  const unsigned int display_nrDecimals = _nrDecimals > 3 ? _nrDecimals : 3;

  addTextBox(id_value, toString(value, display_nrDecimals), 10, F("number"));
}

void P002_data_struct::webformLoad(struct EventStruct *event)
{
  // Output the statistics for the current settings.
  int raw_value            = 0;
  const float currentValue = P002_data_struct::getCurrentValue(event, raw_value);

# if FEATURE_PLUGIN_STATS
  PluginStats *stats = getPluginStats(0);

  if (stats != nullptr) {
    stats->trackPeak(raw_value);
  }
# endif // if FEATURE_PLUGIN_STATS

# ifdef ESP32
  addRowLabel(F("Analog Pin"));
  #  if HAS_HALL_EFFECT_SENSOR
  addADC_PinSelect(AdcPinSelectPurpose::ADC_Touch_HallEffect, F("taskdevicepin1"), CONFIG_PIN1);
  #  else // if HAS_HALL_EFFECT_SENSOR
  addADC_PinSelect(AdcPinSelectPurpose::ADC_Touch,            F("taskdevicepin1"), CONFIG_PIN1);
  #  endif // if HAS_HALL_EFFECT_SENSOR

  {
    const __FlashStringHelper *outputOptions[] = {
      F("12 dB"),
      F("6 dB"),
      F("2.5 dB"),
      F("0 dB")
    };
    const int outputOptionValues[] = {
      P002_ADC_11db,
      P002_ADC_6db,
      P002_ADC_2_5db,
      P002_ADC_0db
    };
    constexpr int nrOptions = NR_ELEMENTS(outputOptionValues);
    const FormSelectorOptions selector(nrOptions, outputOptions, outputOptionValues);
    selector.addFormSelector(F("Attenuation"), F("attn"), P002_ATTENUATION);
  }

# endif // ifdef ESP32

  {
    const __FlashStringHelper *outputOptions[] = {
      F("Use Current Sample"),
      F("Oversampling")
# ifndef LIMIT_BUILD_SIZE
      , F("Binning")
# endif // ifndef LIMIT_BUILD_SIZE
    };
    const int outputOptionValues[] = {
      P002_USE_CURENT_SAMPLE,
      P002_USE_OVERSAMPLING
# ifndef LIMIT_BUILD_SIZE
      , P002_USE_BINNING
# endif // ifndef LIMIT_BUILD_SIZE
    };
    constexpr int nrOptions = NR_ELEMENTS(outputOptionValues);
    const FormSelectorOptions selector(nrOptions, outputOptions, outputOptionValues);
    selector.addFormSelector(F("Oversampling"), F("oversampling"), P002_OVERSAMPLING);
  }

# ifdef ESP32
  addFormSubHeader(F("Factory Calibration"));
  addFormCheckBox(F("Apply Factory Calibration"), F("fac_cal"), P002_APPLY_FACTORY_CALIB, !hasADC_factory_calibration());
  addFormNote(F("When checked, reading is in mV"));

  if (hasADC_factory_calibration()) {
    addRowLabel(F("Factory Calibration Type"));
    addHtml(getADC_factory_calibration_type());
    #  if FEATURE_CHART_JS
    webformLoad_calibrationCurve(event);
    #  endif // if FEATURE_CHART_JS
    # ifdef ESP32
    if (_useFactoryCalibration) {
      formatADC_statistics(F("Current Voltage"), raw_value);
    } else {
      formatADC_statistics(F("Current ADC raw value"), raw_value);
    }
    #else
    formatADC_statistics(F("Current ADC raw value"), raw_value);
    #endif

    for (size_t att = 0; att < P002_ADC_ATTEN_MAX; ++att) {
      const adc_atten_t attenuation = static_cast<adc_atten_t>(att);
      const int   low               = getADC_factory_calibrated_min(attenuation);
      const int   high              = getADC_factory_calibrated_max(attenuation);
      const float step              = static_cast<float>(high - low) / MAX_ADC_VALUE;

      addRowLabel(concat(F("Attenuation @"), AttenuationToString(attenuation)));
      addHtml(F("Range / Step: "));
      addHtmlInt(low);
      addHtml(F(" ... "));
      addHtmlInt(high);
      addUnit(F("mV"));
      addHtml(F(" / "));
      addHtmlFloat(step, 3); // calibration output is int value in mV, so doesn't really matter how many decimals
      addUnit(F("mV"));
    }
  }
# endif // ifdef ESP32

  addFormSubHeader(F("Two Point Calibration"));

  addFormCheckBox(F("Calibration Enabled"), F("cal"), P002_CALIBRATION_ENABLED);

# ifdef ESP8266
#  if FEATURE_ADC_VCC
  addFormNote(F("Measuring ESP VCC, not A0. Unit is 1/1024 V. See documentation."));
#  endif // if FEATURE_ADC_VCC
# endif  // ifdef ESP8266


  webformLoad_2p_calibPoint(
    F("Point 1"),
    F("adc1"),
    F("out1"),
    P002_CALIBRATION_POINT1,
    P002_CALIBRATION_VALUE1);
  webformLoad_2p_calibPoint(
    F("Point 2"),
    F("adc2"),
    F("out2"),
    P002_CALIBRATION_POINT2,
    P002_CALIBRATION_VALUE2);

  addFormNote(F("Input float values will be stored as int, calibration values will be adjusted accordingly"));

  {
    // Output the statistics for the current settings.
    if (P002_CALIBRATION_ENABLED) {
      # if FEATURE_CHART_JS
      webformLoad_2pt_calibrationCurve(event);
      # endif // if FEATURE_CHART_JS

      int minInputValue, maxInputValue;
      getInputRange(event, minInputValue, maxInputValue);

      const float minY_value         = P002_data_struct::applyCalibration(event, minInputValue);
      const float maxY_value         = P002_data_struct::applyCalibration(event, maxInputValue);
      const float current_calibrated = P002_data_struct::applyCalibration(event, currentValue);

      format_2point_calib_statistics(F("Current"), currentValue,  current_calibrated);
      format_2point_calib_statistics(F("Minimum"), minInputValue, minY_value);
      format_2point_calib_statistics(F("Maximum"), maxInputValue, maxY_value);

      const float stepsize = (maxY_value - minY_value) / (MAX_ADC_VALUE + 1);
      addRowLabel(F("Step Size"));
      addHtmlFloat(stepsize, _nrDecimals);
    } else {
      addRowLabel(F("Current"));
      addHtmlFloat(currentValue, _nrDecimals);
    }
  }
# ifndef LIMIT_BUILD_SIZE
  const bool useBinning = P002_OVERSAMPLING == P002_USE_BINNING;
  addFormSubHeader(useBinning ? F("Binning Processing") : F("Multipoint Processing"));
  addFormCheckBox(useBinning ? F("Binning Processing Enabled") : F("Multipoint Processing Enabled"),
                  F("multi_en"),
                  P002_MULTIPOINT_ENABLED);

  if (useBinning) {
    addFormTextBox(F("Binning Formula"), getPluginCustomArgName(P002_LINE_INDEX_FORMULA), _formula, P002_MAX_FORMULA_LENGTH);
  }

  addFormNumericBox(useBinning ? F("Nr of Bins") : F("Nr Multipoint Fields"),
                    F("nr_mp"),
                    P002_NR_MULTIPOINT_ITEMS,
                    0,
                    P002_MAX_NR_MP_ITEMS);

  // Checkbox needed to explicitly allow to split-paste over each field
  addFormCheckBox(useBinning ? F("Split-Paste Binning Fields") : F("Split-Paste Multipoint Fields"), F("splitpaste"), false);
  addFormNote(F("When checked, a set of tab, space or newline separated values can be pasted at once."));

  size_t line_nr = 0;

  for (int varNr = P002_LINE_IDX_FIRST_MP; varNr < P002_Nlines; varNr += P002_STRINGS_PER_MP)
  {
    const String label = String(useBinning ? F("Bin ") : F("Point ")) + String(line_nr + 1);
    addFormTextBox(F("query-input widenumber"),
                   label,
                   getPluginCustomArgName(varNr),

                   _multipoint.size() > line_nr ?
#  if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
                   doubleToString
#  else // if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
                   floatToString
#  endif // if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
                     (static_cast<ESPEASY_RULES_FLOAT_TYPE>(_multipoint[line_nr]._adc),
                     _nrDecimals,
                     true) : EMPTY_STRING,
                   0);
    html_add_estimate_symbol();
    addTextBox(getPluginCustomArgName(varNr + 1),
               _multipoint.size() > line_nr ?
#  if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
               doubleToString
#  else // if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
               floatToString
#  endif // if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
                 (static_cast<ESPEASY_RULES_FLOAT_TYPE>(_multipoint[line_nr]._value),
                 _nrDecimals,
                 true) : EMPTY_STRING,
               0,
               F("query-input widenumber"));

    ++line_nr;
  }
  #  if FEATURE_CHART_JS
  webformLoad_multipointCurve(event);
  #  endif // if FEATURE_CHART_JS
# endif    // ifndef LIMIT_BUILD_SIZE
}

# if FEATURE_PLUGIN_STATS
bool P002_data_struct::webformLoad_show_stats(struct EventStruct *event)
{
  bool somethingAdded = false;

  if (_plugin_stats_array != nullptr) {
    somethingAdded = _plugin_stats_array->webformLoad_show_stats(event, false);
  }

  const PluginStats *stats = getPluginStats(0);

  if (stats != nullptr) {
    if (stats->webformLoad_show_avg(event)) { somethingAdded = true; }

    if (stats->webformLoad_show_stdev(event)) { somethingAdded = true; }

    if (stats->hasPeaks()) {
      float floatvalue_low, floatvalue_high;

      if (stats->webformLoad_show_peaks(
            event,
            stats->getLabel(),
            formatADC_statistics_to_str(stats->getPeakLow(),  floatvalue_low,  true),
            formatADC_statistics_to_str(stats->getPeakHigh(), floatvalue_high, true),
            false))
      {
        addRowLabel(concat(stats->getLabel(),  F(" Peak-to-peak")));
        addHtmlFloat(floatvalue_high - floatvalue_low, _nrDecimals);
        somethingAdded = true;
      }
    }
  }
  return somethingAdded;
}

# endif // if FEATURE_PLUGIN_STATS


# ifdef ESP32
#  if FEATURE_CHART_JS
void P002_data_struct::webformLoad_calibrationCurve(struct EventStruct *event)
{
  if (!hasADC_factory_calibration()) { return; }

  addRowLabel(F("Calibration Curve"));

  const int valueCount = 33;
  int xAxisValues[valueCount];

  getChartRange(event, xAxisValues, valueCount, true);

  String axisOptions;

  {
    ChartJS_options_scales scales;
    scales.add({ F("x"), F("ADC Value") });
    scales.add({ F("y"), F("Input Voltage (mV)") });
    axisOptions = scales.toString();
  }
  add_ChartJS_chart_header(
    F("line"),
    F("fact_cal"),
    { F("Factory Calibration per Attenuation") },
    500,
    500,
    axisOptions);

  add_ChartJS_chart_labels(
    valueCount,
    xAxisValues);

  const __FlashStringHelper *colors[] = { F("#A52422"), F("#BEA57D"), F("#0F4C5C"), F("#A4BAB7") };

  size_t current_attenuation = getAttenuation(event);

  if (current_attenuation >= P002_ADC_ATTEN_MAX) {
#   if ESP_IDF_VERSION_MAJOR >= 5
    current_attenuation = ADC_ATTEN_DB_12;
#   else // if ESP_IDF_VERSION_MAJOR >= 5
    current_attenuation = ADC_ATTEN_DB_11;
#   endif // if ESP_IDF_VERSION_MAJOR >= 5
  }

  for (size_t att = 0; att < P002_ADC_ATTEN_MAX; ++att)
  {
    float values[valueCount];

    for (int i = 0; i < valueCount; ++i) {
      values[i] = applyADCFactoryCalibration(xAxisValues[i], static_cast<adc_atten_t>(att));
    }

    ChartJS_dataset_config config(
      AttenuationToString(static_cast<adc_atten_t>(att)),
      colors[att]);
    config.hidden = att != current_attenuation;

    if (att != 0) {
      addHtml(',');
    }

    add_ChartJS_dataset(
      config,
      values,
      valueCount,
      Cache.getTaskDeviceValueDecimals(event->TaskIndex, 0));
  }
  add_ChartJS_chart_footer();
}

#  endif // if FEATURE_CHART_JS
# endif  // ifdef ESP32

# if FEATURE_CHART_JS
const __FlashStringHelper * P002_data_struct::getChartXaxisLabel(struct EventStruct *event)
{
  #  ifdef ESP32

  if (useFactoryCalibration(event)) {
    // reading in mVolt, not ADC
    return F("Input Voltage (mV)");
  }
  #  endif // ifdef ESP32
  return F("ADC Value");
}

# endif // if FEATURE_CHART_JS

void P002_data_struct::getInputRange(struct EventStruct *event, int& minInputValue, int& maxInputValue, bool ignoreCalibration)
{
  minInputValue = 0;
  maxInputValue = MAX_ADC_VALUE;
# ifdef ESP32

  if (useFactoryCalibration(event) && !ignoreCalibration) {
    // reading in mVolt, not ADC
    const adc_atten_t attenuation = getAttenuation(event);

    minInputValue = getADC_factory_calibrated_min(attenuation);
    maxInputValue = getADC_factory_calibrated_max(attenuation);
  }
# endif // ifdef ESP32
}

# if FEATURE_CHART_JS

void P002_data_struct::getChartRange(struct EventStruct *event, int values[], int count, bool ignoreCalibration)
{
  int minInputValue, maxInputValue;

  getInputRange(event, minInputValue, maxInputValue, ignoreCalibration);

  const float stepSize = static_cast<float>(maxInputValue + 1 - minInputValue) / (count - 1);

  for (int i = 0; i < count; ++i) {
    values[i] = minInputValue + i * stepSize;
  }
}

void P002_data_struct::webformLoad_2pt_calibrationCurve(struct EventStruct *event)
{
  addRowLabel(F("Two Point Calibration"));

  const int valueCount = 33;
  int xAxisValues[valueCount];

  getChartRange(event, xAxisValues, valueCount);

  String axisOptions;

  {
    ChartJS_options_scales scales;
    scales.add({ F("x"), getChartXaxisLabel(event) });
    scales.add({ F("y"), F("Calibrated Output") });
    axisOptions = scales.toString();
  }


  add_ChartJS_chart_header(
    F("line"),
    F("twoPointCurve"),
    { F("Two Point Calibration Curve") },
    500,
    500,
    axisOptions);

  add_ChartJS_chart_labels(
    valueCount,
    xAxisValues);

  {
    float values[valueCount];

    for (int i = 0; i < valueCount; ++i) {
      values[i] = P002_data_struct::applyCalibration(event, xAxisValues[i]);
    }

    const ChartJS_dataset_config config(
      F("2 Point Calibration"),
      F("rgb(255, 99, 132)"));


    add_ChartJS_dataset(
      config,
      values,
      valueCount,
      Cache.getTaskDeviceValueDecimals(event->TaskIndex, 0));
  }
  add_ChartJS_chart_footer();
}

# endif // if FEATURE_CHART_JS

void P002_data_struct::formatADC_statistics(const __FlashStringHelper *label, int raw, bool includeOutputValue) const
{
  addRowLabel(label);
  float float_value{};

  addHtml(formatADC_statistics_to_str(raw, float_value, includeOutputValue));
}

String P002_data_struct::formatADC_statistics_to_str(
  int    raw,
  float& float_value,
  bool   includeOutputValue) const
{
  String res;

  float_value = raw;

# ifdef ESP32

  if (_useFactoryCalibration) {
    float_value = applyADCFactoryCalibration(raw, _attenuation);
    res = strformat(
      F("%s [mV]  &#8793; %d [ADC]"),
      toString(float_value, _nrDecimals).c_str(),
      raw);
  } else {
    res += raw;
  }
#else
  res += raw;
# endif // ifdef ESP32

  if (includeOutputValue) {
    res        += F(" &rarr; ");
    float_value =  applyCalibration(float_value);

# ifndef LIMIT_BUILD_SIZE

    switch (_sampleMode) {
      case P002_USE_OVERSAMPLING:
        float_value = applyMultiPointInterpolation(float_value);
        break;
      case P002_USE_BINNING:
      {
        const int index = computeADC_to_bin(raw);

        if ((index >= 0) && (static_cast<int>(_binning.size()) > index)) {
          float_value = _multipoint[index]._value;
        }

        break;
      }
    }
# endif // ifndef LIMIT_BUILD_SIZE
    res += toString(float_value, _nrDecimals);
  }

  return res;
}

void P002_data_struct::format_2point_calib_statistics(const __FlashStringHelper *label, int raw, float float_value) const
{
  addRowLabel(label);
  addHtmlInt(raw);
  # ifdef ESP32
  addUnit(_useFactoryCalibration ? F("mV") : F("raw"));
  # else // ifdef ESP32
  addUnit(F("raw"));
  # endif // ifdef ESP32
  html_add_estimate_symbol();
  addHtmlFloat(float_value, _nrDecimals);
}

# ifdef ESP32
const __FlashStringHelper * P002_data_struct::AttenuationToString(adc_atten_t attenuation) {
  const __FlashStringHelper *datalabels[] = { F("0 dB"), F("2.5 dB"), F("6 dB"), F("12 dB") };

  if (attenuation < 4) { return datalabels[attenuation]; }
  return F("Unknown");
}

adc_atten_t P002_data_struct::getAttenuation(struct EventStruct *event) {
  if ((P002_ATTENUATION >= P002_ADC_0db) && (P002_ATTENUATION <= P002_ADC_11db)) {
    // Make sure the attenuation is only set to correct values or else it may damage the board
    return static_cast<adc_atten_t>(P002_ATTENUATION - 10);
  }
  P002_ATTENUATION = P002_ADC_11db;

#  if ESP_IDF_VERSION_MAJOR >= 5
  return ADC_ATTEN_DB_12;
#  else // if ESP_IDF_VERSION_MAJOR >= 5
  return ADC_ATTEN_DB_11;
#  endif // if ESP_IDF_VERSION_MAJOR >= 5
}

# endif // ifdef ESP32

# if FEATURE_CHART_JS
void P002_data_struct::webformLoad_multipointCurve(struct EventStruct *event) const
{
  if (P002_MULTIPOINT_ENABLED)
  {
    const bool useBinning = P002_OVERSAMPLING == P002_USE_BINNING;
    addRowLabel(useBinning ? F("Binning Curve") : F("Multipoint Curve"));

    String axisOptions;

    {
      ChartJS_options_scales scales;
      scales.add({ F("x"), useBinning ? F("Bin Center Value") : F("Input") });
      scales.add({ F("y"), useBinning ? F("Bin Output Value") : F("Output") });
      axisOptions = scales.toString();
    }

    add_ChartJS_chart_header(
      useBinning ? F("bar") : F("line"),
      F("mpcurve"),
      { useBinning ? F("Bin Values") : F("Multipoint Curve") },
      500,
      500,
      axisOptions);

    // Add labels
    addHtml(F("\"labels\":["));

    for (size_t i = 0; i < _multipoint.size(); ++i) {
      if (i != 0) {
        addHtml(',');
      }
      addHtmlFloat(_multipoint[i]._adc, _nrDecimals);
    }
    addHtml(F("],\n\"datasets\":["));

    add_ChartJS_dataset_header(
    {
      useBinning ? F("Bins") : F("Multipoint Values"),
      F("rgb(255, 99, 132)") });

    for (size_t i = 0; i < _multipoint.size(); ++i) {
      if (i != 0) {
        addHtml(',');
      }
      addHtmlFloat(_multipoint[i]._value, _nrDecimals);
    }
    add_ChartJS_dataset_footer();
    add_ChartJS_chart_footer();

    if (!useBinning) {
      // Try to compute the expected mapping from ADC to multipoint values
      addRowLabel(F("Input to Output Curve"));
      const int valueCount = 33;
      int xAxisValues[valueCount];
      getChartRange(event, xAxisValues, valueCount);

      String axisOptions;

      {
        ChartJS_options_scales scales;
        scales.add({ F("x"), getChartXaxisLabel(event) });
        scales.add({ F("y"), F("Output") });
        axisOptions = scales.toString();
      }
      add_ChartJS_chart_header(
        F("line"),
        F("mpCurveSimulated"),
        { F("Simulated Input to Output Curve") },
        500,
        500,
        axisOptions);

      add_ChartJS_chart_labels(
        valueCount,
        xAxisValues);

      const __FlashStringHelper *label = F("Multipoint");
      const __FlashStringHelper *color = F("rgb(255, 99, 132)");

      for (int step = 0; step < 3; ++step)
      {
        float values[valueCount];
        bool  use2PointCalib = false;
        bool  useMultiPoint  = false;

        switch (step) {
          case 0:
            useMultiPoint = true;
            break;
          case 1:
            label          = F("2 Point Calibration & Multipoint");
            color          = F("rgb(54, 162, 235)");
            use2PointCalib = true;
            useMultiPoint  = true;
            break;
          case 2:
            label          = F("2 Point Calibration");
            color          = F("rgb(153, 102, 255)");
            use2PointCalib = true;
            break;
        }

        bool hidden = !((use2PointCalib == _use2pointCalibration) &&
                        useMultiPoint);

        for (int i = 0; i < valueCount; ++i) {
          values[i] = xAxisValues[i];

          if (use2PointCalib) {
            values[i] = P002_data_struct::applyCalibration(event, values[i], true);
          }

          if (useMultiPoint) {
            values[i] = applyMultiPointInterpolation(values[i], true);
          }
        }

        ChartJS_dataset_config config(
          label,
          color);
        config.hidden = hidden;

        if (step != 0) {
          addHtml(',');
        }

        add_ChartJS_dataset(
          config,
          values,
          valueCount,
          Cache.getTaskDeviceValueDecimals(event->TaskIndex, 0));
      }
      add_ChartJS_chart_footer();
    }
  }
}

# endif // if FEATURE_CHART_JS

String P002_data_struct::webformSave(struct EventStruct *event)
{
  P002_OVERSAMPLING = getFormItemInt(F("oversampling"), 0); // Set a default for LIMIT_BUILD_SIZE

  P002_CALIBRATION_ENABLED = isFormItemChecked(F("cal"));
  # ifdef ESP32
  P002_APPLY_FACTORY_CALIB = isFormItemChecked(F("fac_cal"));
  P002_ATTENUATION         = getFormItemInt(F("attn"));
  # endif // ifdef ESP32

  // Map the input "point" values to the nearest int.
  setTwoPointCalibration(
    event,
    getFormItemFloat(F("adc1")),
    getFormItemFloat(F("adc2")),
    getFormItemFloat(F("out1")),
    getFormItemFloat(F("out2")));

# ifndef LIMIT_BUILD_SIZE
  P002_MULTIPOINT_ENABLED = isFormItemChecked(F("multi_en"));

  P002_NR_MULTIPOINT_ITEMS = getFormItemInt(F("nr_mp"));

  const size_t nr_lines = P002_Nlines;
  String lines[nr_lines];

  // Store nr of lines that were saved, so no 'old' data will be read when nr of multi-point items has changed.
  lines[P002_SAVED_NR_LINES] = String(nr_lines);

  if (hasArg(getPluginCustomArgName(P002_LINE_INDEX_FORMULA))) {
    lines[P002_LINE_INDEX_FORMULA] = webArg(getPluginCustomArgName(P002_LINE_INDEX_FORMULA));
  }

  // const int nrDecimals = webArg(F("TDVD1")).toInt();

  for (size_t varNr = P002_LINE_IDX_FIRST_MP; varNr < nr_lines; varNr += P002_STRINGS_PER_MP)
  {
    float adc, value = 0.0f;
    const String adc_str = webArg(getPluginCustomArgName(varNr));
    const String val_str = webArg(getPluginCustomArgName(varNr + 1));

    if (validFloatFromString(adc_str, adc) && validFloatFromString(val_str, value)) {
      // Only store valid floats
      lines[varNr]     = adc_str;
      lines[varNr + 1] = val_str;
    }
  }

  return SaveCustomTaskSettings(event->TaskIndex, lines, nr_lines, 0);
# else // ifndef LIMIT_BUILD_SIZE
  return EMPTY_STRING;
# endif // ifndef LIMIT_BUILD_SIZE
}

void P002_data_struct::takeSample()
{
  if (_sampleMode == P002_USE_CURENT_SAMPLE) { return; }
  const int raw = analog_read();

# if FEATURE_PLUGIN_STATS
  PluginStats *stats = getPluginStats(0);

  if (stats != nullptr) {
    stats->trackPeak(raw);
  }
# endif // if FEATURE_PLUGIN_STATS

  switch (_sampleMode) {
    case P002_USE_OVERSAMPLING:
      addOversamplingValue(raw);
      break;
# ifndef LIMIT_BUILD_SIZE
    case P002_USE_BINNING:
      addBinningValue(raw);
      break;
# endif // ifndef LIMIT_BUILD_SIZE
  }
}

bool P002_data_struct::getValue(float& float_value,
                                int  & raw_value) const
{
  bool mustTakeSample = false;

  switch (_sampleMode) {
    case P002_USE_OVERSAMPLING:

      if (getOversamplingValue(float_value, raw_value)) {
        return true;
      }
      mustTakeSample = true;
      break;
# ifndef LIMIT_BUILD_SIZE
    case P002_USE_BINNING:

      if (getBinnedValue(float_value, raw_value)) {
        return true;
      }
      mustTakeSample = true;
      break;
# endif // ifndef LIMIT_BUILD_SIZE
    case P002_USE_CURENT_SAMPLE:
      mustTakeSample = true;
      break;
  }

  if (!mustTakeSample) {
    return false;
  }

  raw_value = analog_read();
# if FEATURE_PLUGIN_STATS

  PluginStats *stats = getPluginStats(0);

  if (stats != nullptr) {
    stats->trackPeak(raw_value);
  }
# endif // if FEATURE_PLUGIN_STATS
  float_value = raw_value;
  # ifdef ESP32

  if (_useFactoryCalibration) {
    float_value = applyADCFactoryCalibration(raw_value, _attenuation);
  }
  # endif // ifdef ESP32

  float_value = applyCalibration(float_value);

# ifndef LIMIT_BUILD_SIZE

  switch (_sampleMode) {
    case P002_USE_OVERSAMPLING:
      float_value = applyMultiPointInterpolation(float_value);
      break;
    case P002_USE_BINNING:
    {
      const int index = computeADC_to_bin(raw_value);

      if ((index >= 0) && (static_cast<int>(_binning.size()) > index)) {
        float_value = _multipoint[index]._value;
      }

      break;
    }
  }
# endif // ifndef LIMIT_BUILD_SIZE

  return true;
}

void P002_data_struct::reset()
{
# ifndef LIMIT_BUILD_SIZE

  switch (_sampleMode) {
    case P002_USE_OVERSAMPLING:
      resetOversampling();
      break;
    case P002_USE_BINNING:
    {
      for (auto it = _binning.begin(); it != _binning.end(); ++it) {
        *it = 0;
      }

      break;
    }
  }
# else // ifndef LIMIT_BUILD_SIZE
  resetOversampling();
# endif // ifndef LIMIT_BUILD_SIZE
}

uint32_t P002_data_struct::getOversamplingCount() const
{
  return OverSampling.getCount();
}

void P002_data_struct::resetOversampling() {
  OverSampling.reset();
}

void P002_data_struct::addOversamplingValue(int currentValue) {
  OverSampling.add(currentValue);
}

bool P002_data_struct::getOversamplingValue(float& float_value, int& raw_value) const {
  if (OverSampling.peek(float_value)) {
    raw_value = static_cast<int>(float_value);

# ifdef ESP32

    if (_useFactoryCalibration) {
      float_value = applyADCFactoryCalibration(float_value, _attenuation);
    }
# endif // ifdef ESP32

    // We counted the raw oversampling values, so now we need to apply the calibration and multi-point processing
    float_value = applyCalibration(float_value);
# ifndef LIMIT_BUILD_SIZE
    float_value = applyMultiPointInterpolation(float_value);
# endif // ifndef LIMIT_BUILD_SIZE

    return true;
  }
  return false;
}

# ifndef LIMIT_BUILD_SIZE
int P002_data_struct::getBinIndex(float currentValue) const
{
  const size_t mp_size = _multipoint.size();

  if (mp_size == 0) { return -1; }

  if (mp_size == 1) { return 0; }

  if (currentValue <= _multipoint[0]._adc) { return 0; }

  const size_t last_mp_index = mp_size - 1;

  if (currentValue >= _multipoint[last_mp_index]._adc) { return last_mp_index; }

  for (unsigned int i = 0; i < last_mp_index; ++i) {
    const float dist_left  = currentValue - _multipoint[i]._adc;
    const float dist_right = _multipoint[i + 1]._adc - currentValue;

    if ((dist_left >= 0) && (dist_right >= 0)) {
      // Inbetween 2 points of the multipoint array
      return (dist_left < dist_right) ? i : i + 1;
    }
  }

  return -1;
}

int P002_data_struct::computeADC_to_bin(const int& currentValue) const
{
  // First apply calibration, then find the bin index
  float calibrated_value = static_cast<float>(currentValue);

#  ifdef ESP32

  if (_useFactoryCalibration) {
    calibrated_value = applyADCFactoryCalibration(calibrated_value, _attenuation);
  }
#  endif // ifdef ESP32


  calibrated_value = applyCalibration(calibrated_value);

  if (!_formula_preprocessed.isEmpty()) {
    // Formula, must be applied before binning
    String formula = _formula_preprocessed;

    formula.replace(F("%value%"), toString(calibrated_value, _nrDecimals));

    ESPEASY_RULES_FLOAT_TYPE result{};

    if (!isError(RulesCalculate.doCalculate(parseTemplate(formula).c_str(), &result))) {
      calibrated_value = result;
    }
  }

  return getBinIndex(calibrated_value);
}

void P002_data_struct::addBinningValue(int currentValue)
{
  for (size_t index = 0; index < _binningRange.size(); ++index) {
    if (_binningRange[index].inRange(currentValue)) {
      ++_binning[index];
      return;
    }
  }

  const int index = computeADC_to_bin(currentValue);

  if ((index >= 0) && (static_cast<int>(_binning.size()) > index)) {
    _binningRange[index].set(currentValue);
    ++_binning[index];
  }
}

bool P002_data_struct::getBinnedValue(float& float_value, int& raw_value) const
{
  unsigned int highest_bin_count = 0;

  const size_t nr_bin_elements = std::min(_binning.size(), _multipoint.size());

  for (size_t i = 0; i < nr_bin_elements; ++i) {
    if (_binning[i] > highest_bin_count) {
      highest_bin_count = _binning[i];
      float_value       = _multipoint[i]._value;
      raw_value         = _multipoint[i]._adc;
    }
  }
  #  ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLogMove(LOG_LEVEL_DEBUG,
               strformat(F("ADC getBinnedValue: bin cnt: %u  Value: %f RAW: %d"),
                         highest_bin_count,
                         float_value,
                         raw_value));
  }
  #  endif // ifndef BUILD_NO_DEBUG

  return highest_bin_count != 0;
}

# endif // ifndef LIMIT_BUILD_SIZE

float P002_data_struct::applyCalibration(struct EventStruct *event, float float_value, bool force) {
  if (force || P002_CALIBRATION_ENABLED)
  {
    float_value = mapADCtoFloat(float_value,
                                P002_CALIBRATION_POINT1,
                                P002_CALIBRATION_POINT2,
                                P002_CALIBRATION_VALUE1,
                                P002_CALIBRATION_VALUE2);
  }
  return float_value;
}

float P002_data_struct::getCurrentValue(struct EventStruct *event, int& raw_value)
{
  # ifdef ESP8266
  const int pin = A0;
  # endif // ifdef ESP8266
  # ifdef ESP32
  const int pin = CONFIG_PIN1;

  auto att = getAttenuation(event);

  analogSetPinAttenuation(pin, static_cast<adc_attenuation_t>(att));
  # endif // ifdef ESP32

  raw_value = espeasy_analogRead(pin);

  # ifdef ESP32

  if (useFactoryCalibration(event)) {
    return applyADCFactoryCalibration(raw_value, att);
  }
  # endif // ifdef ESP32

  return raw_value;
}

float P002_data_struct::applyCalibration(float float_value) const
{
  if (!_use2pointCalibration) { return float_value; }
  return mapADCtoFloat(
    float_value,
    _calib_adc1,
    _calib_adc2,
    _calib_out1,
    _calib_out2);
}

# ifdef ESP32
bool P002_data_struct::useFactoryCalibration(struct EventStruct *event) {
  if (P002_APPLY_FACTORY_CALIB) {
    const int adc_num = getADC_num_for_gpio(CONFIG_PIN1);

    if ((adc_num == 1) || (adc_num == 2)) {
      return true;
    }
  }
  return false;
}

# endif // ifdef ESP32

# ifndef LIMIT_BUILD_SIZE
float P002_data_struct::applyMultiPointInterpolation(float float_value, bool force) const
{
  if (!_useMultipoint && !force) { return float_value; }

  // First find the surrounding bins
  const size_t mp_size = _multipoint.size();

  if (mp_size == 0) { return float_value; }

  if (float_value <= _multipoint[0]._adc) {
    if (mp_size > 1) {
      // Just extrapolate the first multipoint line segment.
      return mapADCtoFloat(
        float_value,
        _multipoint[0]._adc,
        _multipoint[1]._adc,
        _multipoint[0]._value,
        _multipoint[1]._value);
    }

    // just one point, so all we can do is consider it to be a slight deviation of the calibration.
    return mapADCtoFloat(
      float_value,
      0,
      _multipoint[0]._adc,
      applyCalibration(0),
      _multipoint[0]._value);
  }

  const size_t last_mp_index = mp_size - 1;

  if (float_value >= _multipoint[last_mp_index]._adc)
  {
    if (mp_size > 1) {
      // Just extrapolate the last multipoint line segment.
      return mapADCtoFloat(
        float_value,
        _multipoint[last_mp_index - 1]._adc,
        _multipoint[last_mp_index]._adc,
        _multipoint[last_mp_index - 1]._value,
        _multipoint[last_mp_index]._value);
    }

    // just one point, so all we can do is consider it to be a slight deviation of the calibration.
    return mapADCtoFloat(
      float_value,
      _multipoint[last_mp_index]._adc,
      MAX_ADC_VALUE,
      _multipoint[last_mp_index]._value,
      applyCalibration(MAX_ADC_VALUE));
  }

  for (unsigned int i = 0; i < last_mp_index; ++i) {
    const float dist_left  = float_value - _multipoint[i]._adc;
    const float dist_right = _multipoint[i + 1]._adc - float_value;

    if ((dist_left >= 0) && (dist_right >= 0) &&
        (_multipoint[i]._adc != _multipoint[i + 1]._adc)) {
      // Inbetween 2 points of the multipoint array
      return mapADCtoFloat(
        float_value,
        _multipoint[i]._adc,
        _multipoint[i + 1]._adc,
        _multipoint[i]._value,
        _multipoint[i + 1]._value);
    }
  }

  return float_value;
}

# endif // ifndef LIMIT_BUILD_SIZE

void P002_data_struct::setTwoPointCalibration(
  struct EventStruct *event,
  float               adc1,
  float               adc2,
  float               out1,
  float               out2)
{
  P002_CALIBRATION_POINT1 = lround(adc1);
  P002_CALIBRATION_POINT2 = lround(adc2);
  P002_CALIBRATION_VALUE1 = mapADCtoFloat(
    P002_CALIBRATION_POINT1,
    adc1, adc2,
    out1, out2);
  P002_CALIBRATION_VALUE2 = mapADCtoFloat(
    P002_CALIBRATION_POINT2,
    adc1, adc2,
    out1, out2);
}

/*****************************************************
 * plugin_set_config
 ****************************************************/
bool P002_data_struct::plugin_set_config(struct EventStruct *event,
                                         String            & string) {
  bool success     = false;
  const String cmd = parseString(string, 1);

  if (equals(cmd, F("setcalib"))) {
    const String sub = parseString(string, 2);

    if (equals(sub, F("twopoint"))) {
      // Command:
      // 1 point : adcsetcalib,twopoint,ADC1,out1
      // 2 points: adcsetcalib,twopoint,ADC1,out1,ADC2,out2
      float adc1{};
      float out1{};
      float adc2{};
      float out2{};

      if (validFloatFromString(parseString(string, 3), adc1) &&
          validFloatFromString(parseString(string, 4), out1))
      {
        success = true;
      }

      if (!validFloatFromString(parseString(string, 5), adc2) ||
          !validFloatFromString(parseString(string, 6), out2))
      {
        // Not a complete 2nd calibration point, so make sure to set both values to 0.
        adc2 = 0;
        out2 = 0;
      }

      if (success) {
        setTwoPointCalibration(event, adc1, adc2, out1, out2);
      }
    }
  }

  return success;
}

int P002_data_struct::analog_read() const {
#ifdef ESP32
  int channel{};
  const int adc = getADC_num_for_gpio(_pin_analogRead, channel);

  if ((adc == 1) || (adc == 2)) {
    analogSetPinAttenuation(_pin_analogRead, static_cast<adc_attenuation_t>(_attenuation));
  }
#endif

  return espeasy_analogRead(_pin_analogRead);
}

#endif
#endif // ifdef USES_P002

#include "../PluginStructs/P094_Filter.h"

#ifdef USES_P094


# include "../DataStructs/mBusPacket.h"

# include "../Globals/ESPEasy_time.h"
# include "../Globals/TimeZone.h"
# include "../Helpers/ESPEasy_Storage.h"
# include "../Helpers/StringConverter.h"


// *INDENT-OFF*
# define P094_FILTER_WEBARG_LABEL(x)         getPluginCustomArgName((x * 10) + 10)
# define P094_FILTER_WEBARG_MANUFACTURER(x)  getPluginCustomArgName((x * 10) + 11)
# define P094_FILTER_WEBARG_METERTYPE(x)     getPluginCustomArgName((x * 10) + 12)
# define P094_FILTER_WEBARG_SERIAL(x)        getPluginCustomArgName((x * 10) + 13)
# define P094_FILTER_WEBARG_FILTER_WINDOW(x) getPluginCustomArgName((x * 10) + 14)
// *INDENT-ON*

const char P094_Filter_Window_names[] PROGMEM = "none|all|1m|5m|15m|1h|day|month|once";

P094_Filter_Window get_FilterWindow(const String& str)
{
  char tmp[10]{};
  const int command_i = GetCommandCode(tmp, sizeof(tmp), str.c_str(), P094_Filter_Window_names);

  if (command_i == -1) {
    // No match found
    return P094_Filter_Window::None;
  }
  return static_cast<P094_Filter_Window>(command_i);
}

String Filter_WindowToString(P094_Filter_Window filterWindow)
{
  char   tmp[10]{};
  String res(GetTextIndexed(tmp, sizeof(tmp), static_cast<uint32_t>(filterWindow), P094_Filter_Window_names));

  return res;
}

P094_filter::P094_filter() {
  _filter._manufacturer = mBus_packet_wildcard_manufacturer;
  _filter._meterType    = mBus_packet_wildcard_metertype;
  _filter._serialNr     = mBus_packet_wildcard_serial;
  _filter._filterWindow = static_cast<int>(P094_Filter_Window::None);
}

void P094_filter::fromString(String str)
{
  // Set everything to wildcards
  _filter._manufacturer = mBus_packet_wildcard_manufacturer;
  _filter._meterType    = mBus_packet_wildcard_metertype;
  _filter._serialNr     = mBus_packet_wildcard_serial;
  _filter._filterWindow = static_cast<int>(P094_Filter_Window::None);

  const int semicolonPos = str.indexOf(';');

  if (semicolonPos != -1) {
    _filter._filterWindow = static_cast<int>(get_FilterWindow(str.substring(semicolonPos + 1)));
    str                   = str.substring(0, semicolonPos);
  }

  for (size_t i = 0; i < 3; ++i) {
    String tmp;

    if (GetArgv(str.c_str(), tmp, (i + 1), '.')) {
      if (!(tmp.isEmpty() || tmp.startsWith(F("*")))) {
        if (i != 0) {
          // Make sure the numerical values are parsed as HEX
          if (!tmp.startsWith(F("0x")) && !tmp.startsWith(F("0X"))) {
            tmp = concat(F("0x"), tmp);
          }
        }

        switch (i) {
          case 0: // Manufacturer
            _filter._manufacturer = mBusPacket_header_t::encodeManufacturerID(tmp);
            break;
          case 1: // Meter type
          {
            int32_t metertype = mBus_packet_wildcard_metertype;

            if (validIntFromString(tmp, metertype)) {
              _filter._meterType = metertype;
            }
            break;
          }
          case 2: // Serial
          {
            int32_t serial = mBus_packet_wildcard_serial;

            if (validIntFromString(tmp, serial)) {
              _filter._serialNr = serial;
            }
            break;
          }
        }
      }
    }
  }
}

String P094_filter::toString() const
{
  String res;

  res += getManufacturer();
  res += '.';

  res += getMeterType();
  res += '.';

  res += getSerial();
  res += ';';

  res += Filter_WindowToString(getFilterWindow());

  return res;
}

const uint8_t * P094_filter::toBinary(size_t& size) const
{
  size = getBinarySize();
  return (uint8_t *)this;
}

size_t P094_filter::fromBinary(const uint8_t *data)
{
  memcpy(this, data, getBinarySize());
  return getBinarySize();
}

bool P094_filter::isValid() const
{
  if ((_filter._manufacturer == 0) &&
      (_filter._meterType == 0) &&
      (_filter._serialNr == 0) &&
      (getFilterWindow() == P094_Filter_Window::None)) {
    return false;
  }
  return
    !isWildcardManufacturer() ||
    !isWildcardMeterType() ||
    !isWildcardSerial() ||
    getFilterWindow() != P094_Filter_Window::None;
}

bool P094_filter::operator<(const P094_filter& rhs) const
{
  if (isValid() != rhs.isValid()) {
    return isValid();
  }
/*
  // Disable sorting, only sort by having valid filters at top.
  if (isWildcardManufacturer() != rhs.isWildcardManufacturer()) {
    return rhs.isWildcardManufacturer();
  }

  if (isWildcardMeterType() != rhs.isWildcardMeterType()) {
    return rhs.isWildcardMeterType();
  }

  if (isWildcardSerial() != rhs.isWildcardSerial()) {
    return rhs.isWildcardSerial();
  }

  if (!isWildcardManufacturer() && (_filter._manufacturer != rhs._filter._manufacturer)) {
    return _filter._manufacturer < rhs._filter._manufacturer;
  }

  if (!isWildcardMeterType() && (_filter._meterType != rhs._filter._meterType)) {
    return _filter._meterType < rhs._filter._meterType;
  }

  if (!isWildcardSerial() && (_filter._serialNr != rhs._filter._serialNr)) {
    return _filter._serialNr < rhs._filter._serialNr;
  }
*/
  return false;
}

bool P094_filter::operator==(const P094_filter& rhs) const
{
  return equals(*this, rhs);
}

bool P094_filter::operator!=(const P094_filter& rhs) const
{
  return !equals(*this, rhs);
}

bool P094_filter::equals(const P094_filter& lhs, const P094_filter& rhs)
{
  if (!lhs.isValid() && !rhs.isValid()) { return true; }

  return lhs.toString() == rhs.toString();
}

size_t P094_filter::getBinarySize()
{
  // Only store the filter
  constexpr size_t P094_filter_size = sizeof(_filter);

  return P094_filter_size;
}

bool P094_filter::matches(const mBusPacket_header_t& other) const
{
  if (!isWildcardManufacturer()) {
    if (_filter._manufacturer != other._manufacturer) { return false; }
  }

  if (!isWildcardMeterType()) {
    if (_filter._meterType != other._meterType) { return false; }
  }

  if (!isWildcardSerial()) {
    if (_filter._serialNr != other._serialNr) { return false; }
  }

  return true;
}

unsigned long P094_filter::computeUnixTimeExpiration() const
{
  // Match the interval window.
  const P094_Filter_Window filterWindow = getFilterWindow();

  if ((filterWindow == P094_Filter_Window::None) ||
      (filterWindow == P094_Filter_Window::Once)) {
    // Return date infinitely far in the future
    return 0xFFFFFFFF;
  }

  if (filterWindow == P094_Filter_Window::All) {
    // Return timestamp in the past
    return 0;
  }

  // Using UnixTime
  const unsigned long currentTime = node_time.getUnixTime();
  unsigned long window_max        = currentTime;

  if ((filterWindow == P094_Filter_Window::One_hour) ||
      (filterWindow == P094_Filter_Window::Day) ||
      (filterWindow == P094_Filter_Window::Month))
  {
    // Create time struct in local time.
    struct tm tm_max;
    breakTime(time_zone.toLocal(currentTime), tm_max);
    tm_max.tm_sec = 59;
    tm_max.tm_min = 59;

    if (filterWindow == P094_Filter_Window::Day) {
      // Using local time, thus incl. timezone and DST.
      if (tm_max.tm_hour < 23) {
        // Either:
        // - between 00:00 and 12:00 => Max: 11:59:59
        // - between 12:00 and 23:00 => Max: 22:59:59

        tm_max.tm_hour = (tm_max.tm_hour < 12) ? 11 : 22;
      } else {
        // between 23:00 and 00:00 => Max: 23:59:59
        tm_max.tm_hour = 23;
      }
    } else if (filterWindow == P094_Filter_Window::Month) {
      // First set minute to midnight of today => Max: 23:59:59
      tm_max.tm_hour = 23;

      if (tm_max.tm_mday < 15) {
        // - between 1st of month 00:00:00 and 15th of month 00:00:00
        tm_max.tm_mday = 14;
      } else {
        // Check if this is the last day of the month.
        // Add 24h to the time and see if it is still the same month.
        const uint8_t maxMonthDay = getMonthDays(tm_max);

        if (tm_max.tm_mday < maxMonthDay) {
          // - between 15th of month 00:00:00 and last of month 00:00:00
          // So we must subtract one day.
          tm_max.tm_mday = maxMonthDay - 1;
        } else {
          // - between last of month 00:00:00 and 1st of next month 00:00:00
          // Thus do not change the date as it is already at the last day of the month
        }
      }
    }

    // Convert from local time.
    window_max = time_zone.fromLocal(makeTime(tm_max));
  } else {
    switch (filterWindow) {
      case P094_Filter_Window::One_minute:
        window_max = currentTime - (currentTime % (1 * 60)) + (1 * 60 - 1);
        break;
      case P094_Filter_Window::Five_minutes:
        window_max = currentTime - (currentTime % (5 * 60)) + (5 * 60 - 1);
        break;
      case P094_Filter_Window::Fifteen_minutes:
        window_max = currentTime - (currentTime % (15 * 60)) + (15 * 60 - 1);
        break;

      default:
        break;
    }
  }
  return window_max;
}

void P094_filter::WebformLoad(uint8_t filterIndex) const
{
  addRowLabel_tr_id(
    concat(F("Filter "), static_cast<int>(filterIndex + 1)),
    P094_FILTER_WEBARG_LABEL(filterIndex));

  // Manufacturer
  addTextBox(
    P094_FILTER_WEBARG_MANUFACTURER(filterIndex),
    getManufacturer(),
    3, false, false, EMPTY_STRING, F("widenumber")
# if FEATURE_TOOLTIPS
    , F("Manufacturer")
# endif // if FEATURE_TOOLTIPS
    );

  // Meter Type
  addTextBox(
    P094_FILTER_WEBARG_METERTYPE(filterIndex),
    getMeterType(),
    4, false, false, EMPTY_STRING, F("widenumber")
# if FEATURE_TOOLTIPS
    , F("Meter Type (HEX)")
# endif // if FEATURE_TOOLTIPS
    );

  // Serial nr
  addTextBox(
    P094_FILTER_WEBARG_SERIAL(filterIndex),
    getSerial(),
    10, false, false, EMPTY_STRING, F("widenumber")
# if FEATURE_TOOLTIPS
    , F("Serial (HEX)")
# endif // if FEATURE_TOOLTIPS
    );

  {
    // Filter Window
    const int optionValues[] = {
      static_cast<int>(P094_Filter_Window::All),
      static_cast<int>(P094_Filter_Window::One_minute),
      static_cast<int>(P094_Filter_Window::Five_minutes),
      static_cast<int>(P094_Filter_Window::Fifteen_minutes),
      static_cast<int>(P094_Filter_Window::One_hour),
      static_cast<int>(P094_Filter_Window::Day),
      static_cast<int>(P094_Filter_Window::Month),
      static_cast<int>(P094_Filter_Window::Once),
      static_cast<int>(P094_Filter_Window::None)
    };

    constexpr size_t nrOptions = sizeof(optionValues) / sizeof(optionValues[0]);

    String options[nrOptions];

    for (size_t i = 0; i < nrOptions; ++i) {
      const P094_Filter_Window filterWindow = static_cast<P094_Filter_Window>(optionValues[i]);
      options[i] = Filter_WindowToString(filterWindow);
    }
    FormSelectorOptions selector(
          nrOptions,
          options,
          optionValues);
    selector.classname = F("widenumber");
    # if FEATURE_TOOLTIPS
    selector.tooltip = F("Filter Window");
    # endif 
    selector.addSelector(
      P094_FILTER_WEBARG_FILTER_WINDOW(filterIndex),
      _filter._filterWindow);
  }
}

String P094_WebformSave_GetWebArg(const String& id) {
  String webarg_str = webArg(id);

  if (webarg_str.isEmpty()) {
    webarg_str = '*';
  }
  return webarg_str;
}

bool P094_filter::WebformSave(uint8_t filterIndex)
{
  String filterString;

  // Manufacturer
  filterString += P094_WebformSave_GetWebArg(P094_FILTER_WEBARG_MANUFACTURER(filterIndex));
  filterString += '.';

  // Meter Type
  filterString += P094_WebformSave_GetWebArg(P094_FILTER_WEBARG_METERTYPE(filterIndex));
  filterString += '.';

  // Serial nr
  filterString += P094_WebformSave_GetWebArg(P094_FILTER_WEBARG_SERIAL(filterIndex));

  fromString(filterString);

  // Filter Window
  _filter._filterWindow = getFormItemInt(
    P094_FILTER_WEBARG_FILTER_WINDOW(filterIndex),
    0);

  return isValid();
}

String P094_filter::getManufacturer() const
{
  String manufacturer;

  if (isWildcardManufacturer()) {
    manufacturer = '*';
  }  else {
    manufacturer = mBusPacket_header_t::decodeManufacturerID(_filter._manufacturer);
  }
  return manufacturer;
}

String P094_filter::getMeterType() const
{
  String metertype;

  if (isWildcardMeterType()) {
    metertype = '*';
  } else {
    metertype = formatToHex_no_prefix(_filter._meterType, 2);
  }
  return metertype;
}

String P094_filter::getSerial() const
{
  String serial;

  if (isWildcardSerial()) {
    serial = '*';
  } else {
    serial = formatToHex_no_prefix(_filter._serialNr, 8);
  }
  return serial;
}

P094_Filter_Window P094_filter::getFilterWindow() const
{
  return static_cast<P094_Filter_Window>(_filter._filterWindow);
}


#endif // ifdef USES_P094
#include "../PluginStructs/P141_data_struct.h"

#ifdef USES_P141

/****************************************************************************
 * toString: return the command string selected
 ***************************************************************************/
const __FlashStringHelper* toString(P141_CommandTrigger cmd) {
  switch (cmd) {
    case P141_CommandTrigger::lcd: return F("lcd");
    case P141_CommandTrigger::pcd8544: break;
  }
  return F("pcd8544"); // Default command trigger
}

/****************************************************************************
 * Constructor
 ***************************************************************************/
P141_data_struct::P141_data_struct(uint8_t             rotation,
                                   uint8_t             fontscaling,
                                   AdaGFXTextPrintMode textmode,
                                   int8_t              backlightPin,
                                   uint8_t             backlightPercentage,
                                   uint8_t             contrast,
                                   uint32_t            displayTimer,
                                   String              commandTrigger,
                                   uint16_t            fgcolor,
                                   uint16_t            bgcolor,
                                   bool                textBackFill,
                                   bool                displayInverted
                                   # if                ADAGFX_FONTS_INCLUDED
                                   ,
                                   const uint8_t       defaultFontId
                                   # endif // if ADAGFX_FONTS_INCLUDED
                                   )
  : _rotation(rotation), _fontscaling(fontscaling), _textmode(textmode), _backlightPin(backlightPin),
  _backlightPercentage(backlightPercentage), _contrast(contrast), _displayTimer(displayTimer),
  _displayTimeout(displayTimer), _commandTrigger(commandTrigger), _fgcolor(fgcolor), _bgcolor(bgcolor),
  _textBackFill(textBackFill), _displayInverted(displayInverted)
  # if ADAGFX_FONTS_INCLUDED
  , _defaultFontId(defaultFontId)
  # endif // if ADAGFX_FONTS_INCLUDED
{
  _commandTrigger.toLowerCase();
  _commandTriggerCmd = concat(_commandTrigger, F("cmd"));
}

/****************************************************************************
 * Destructor
 ***************************************************************************/
P141_data_struct::~P141_data_struct() {
  cleanup();
}

/****************************************************************************
 * plugin_init: Initialize display
 ***************************************************************************/
bool P141_data_struct::plugin_init(struct EventStruct *event) {
  updateFontMetrics();
  bool success = false;

  ButtonState     = false; // button not touched
  ButtonLastState = 0xFF;  // Last state checked (debouncing in progress)
  DebounceCounter = 0;     // debounce counter

  if (nullptr == pcd8544) {
    addLog(LOG_LEVEL_INFO, F("PCD8544: Init start."));

    pcd8544 = new (std::nothrow) Adafruit_PCD8544(P141_DC_PIN, P141_CS_PIN, P141_RST_PIN);
    # ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      String log;
      log.reserve(90);
      log += strformat(F("PCD8544: Init done, address: 0x%x "), reinterpret_cast<ulong>(pcd8544));

      if (nullptr == pcd8544) {
        log += F("in");
      }
      log += strformat(F("valid, commands: %s, display: PCD8544"), _commandTrigger.c_str());
      addLogMove(LOG_LEVEL_INFO, log);
    }
    # endif // ifndef BUILD_NO_DEBUG
  } else {
    addLog(LOG_LEVEL_INFO, F("PCD8544: No init?"));
  }

  if (nullptr != pcd8544) {
    pcd8544->begin(); // Start the display
    gfxHelper = new (std::nothrow) AdafruitGFX_helper(pcd8544,
                                                      _commandTrigger,
                                                      _xpix,
                                                      _ypix,
                                                      AdaGFXColorDepth::Monochrome,
                                                      _textmode,
                                                      _fontscaling,
                                                      _fgcolor,
                                                      _bgcolor,
                                                      true,
                                                      _textBackFill
                                                      # if ADAGFX_FONTS_INCLUDED
                                                      , _defaultFontId
                                                      # endif // if ADAGFX_FONTS_INCLUDED
                                                      );

    displayOnOff(true);

    if (nullptr != gfxHelper) {
      gfxHelper->initialize();
      pcd8544->setContrast(_contrast);
      gfxHelper->invertDisplay(_displayInverted);
      gfxHelper->setRotation(_rotation);
      pcd8544->fillScreen(_bgcolor);             // fill screen with black color
      pcd8544->setTextColor(_fgcolor, _bgcolor); // set text color to white and black background

      gfxHelper->setColumnRowMode(bitRead(P141_CONFIG_FLAGS, P141_CONFIG_FLAG_USE_COL_ROW));
      gfxHelper->setLineSpacing(P141_CONFIG_FLAG_GET_LINESPACING);
      pcd8544->setTextSize(_fontscaling); // Handles 0 properly, text size, default 1 = very small
      pcd8544->setCursor(0, 0);           // move cursor to position (0, 0) pixel
      pcd8544->display();
      updateFontMetrics();


      if (P141_CONFIG_BUTTON_PIN != -1) {
        pinMode(P141_CONFIG_BUTTON_PIN, INPUT_PULLUP);
      }

      if (!stringsLoaded) {
        LoadCustomTaskSettings(event->TaskIndex, strings, P141_Nlines, 0);
        stringsLoaded = true;

        for (uint8_t x = 0; x < P141_Nlines && !stringsHasContent; ++x) {
          stringsHasContent = !strings[x].isEmpty();
        }
      }
      success = true;
    }
  }
  return success;
}

/****************************************************************************
 * updateFontMetrics: recalculate x and y columns, based on font size and font scale
 ***************************************************************************/
void P141_data_struct::updateFontMetrics() {
  if (nullptr != gfxHelper) {
    gfxHelper->getTextMetrics(_textcols, _textrows, _fontwidth, _fontheight, _fontscaling, _heightOffset, _xpix, _ypix);
    gfxHelper->getColors(_fgcolor, _bgcolor);
  } else {
    _textcols = _xpix / (_fontwidth * _fontscaling);
    _textrows = _ypix / (_fontheight * _fontscaling);
  }
}

/****************************************************************************
 * plugin_exit: De-initialize before destruction
 ***************************************************************************/
bool P141_data_struct::plugin_exit(struct EventStruct *event) {
  # ifdef BUILD_NO_DEBUG
  addLog(LOG_LEVEL_INFO, F("PCD8544: Exit."));
  # endif // ifdef BUILD_NO_DEBUG

  if ((nullptr != pcd8544) && bitRead(P141_CONFIG_FLAGS, P141_CONFIG_FLAG_CLEAR_ON_EXIT)) {
    pcd8544->fillScreen(_displayInverted
                        ? ADAGFX_WHITE
                        : ADAGFX_BLACK); // fill screen with black or white color
    pcd8544->display();
    displayOnOff(false);
  }
  cleanup();
  return true;
}

/****************************************************************************
 * cleanup: De-initialize pointers
 ***************************************************************************/
void P141_data_struct::cleanup() {
  delete gfxHelper;
  gfxHelper = nullptr;

  delete pcd8544;
  pcd8544 = nullptr;
}

/****************************************************************************
 * plugin_read: Re-draw the default content
 ***************************************************************************/
bool P141_data_struct::plugin_read(struct EventStruct *event) {
  if (nullptr != pcd8544) {
    if (stringsHasContent) {
      gfxHelper->setColumnRowMode(false); // Turn off column mode
      uint8_t  yPos  = 0;                 // Bound to the display
      int16_t  dum   = 0;
      uint16_t udum  = 0;
      uint16_t hText = 0;

      for (uint8_t x = 0; x < P141_Nlines; ++x) {
        String newString = AdaGFXparseTemplate(strings[x], _textcols, gfxHelper);

        # if ADAGFX_PARSE_SUBCOMMAND
        updateFontMetrics();
        # endif // if ADAGFX_PARSE_SUBCOMMAND

        if ((yPos < _ypix) && !newString.isEmpty()) {
          gfxHelper->printText(newString.c_str(), 0, yPos, _fontscaling, _fgcolor, _bgcolor);
        }
        delay(0);

        if (15 == P141_CONFIG_FLAG_GET_LINESPACING) {
          yPos += (_fontheight * _fontscaling);                             // Auto, using font-height and scaling
        } else {
          pcd8544->getTextBounds(F("Ay"), 0, 0, &dum, &dum, &udum, &hText); // Measure current font-height
          yPos += hText + P141_CONFIG_FLAG_GET_LINESPACING;                 // Explicit distance
        }
      }
      pcd8544->display();
      gfxHelper->setColumnRowMode(bitRead(P141_CONFIG_FLAGS, P141_CONFIG_FLAG_USE_COL_ROW)); // Restore column mode
      # if P141_FEATURE_CURSOR_XY_VALUES
      updateValues(event);
      # endif // if P141_FEATURE_CURSOR_XY_VALUES
    }
  }
  return false; // Always return false, so no attempt to send to
                // Controllers or generate events is started
}

/****************************************************************************
 * plugin_ten_per_second: check button, if any, that wakes up the display
 ***************************************************************************/
bool P141_data_struct::plugin_ten_per_second(struct EventStruct *event) {
  if ((P141_CONFIG_BUTTON_PIN != -1) && (getButtonState()) && (nullptr != pcd8544)) {
    displayOnOff(true);
    markButtonStateProcessed();
  }
  return true;
}

/****************************************************************************
 * plugin_once_a_second: Count down display timer, if any, and turn display off if countdown reached
 ***************************************************************************/
bool P141_data_struct::plugin_once_a_second(struct EventStruct *event) {
  if (_displayTimer > 0) {
    _displayTimer--;

    if ((nullptr != pcd8544) && (_displayTimer == 0)) {
      displayOnOff(false);
    }
  }
  return true;
}

/****************************************************************************
 * plugin_write: Handle commands
 ***************************************************************************/
bool P141_data_struct::plugin_write(struct EventStruct *event,
                                    const String      & string) {
  bool success     = false;
  const String cmd = parseString(string, 1);

  if ((nullptr != pcd8544) && cmd.equals(_commandTriggerCmd)) {
    const String arg1 = parseString(string, 2);
    success = true;

    if (equals(arg1, F("off"))) {              // Screen off
      displayOnOff(false);
    }
    else if (equals(arg1, F("on"))) {          // Screen on
      displayOnOff(true);
    }
    else if (equals(arg1, F("clear"))) {       // Empty screen
      pcd8544->fillScreen(_bgcolor);
      pcd8544->display();                      // Put on display
    }
    else if (equals(arg1, F("inv")) &&         // Invert display
             (event->Par2 >= 0) && (event->Par2 <= 1)) {
      if (parseString(string, 3).isEmpty()) {  // No argument: flip previous state
        _displayInverted = !_displayInverted;
      } else {
        _displayInverted = (event->Par2 == 1); // Set state
      }

      // Store in settings, but do not save
      bitWrite(P141_CONFIG_FLAGS, P141_CONFIG_FLAG_INVERTED, _displayInverted);

      if (nullptr != gfxHelper) {
        gfxHelper->invertDisplay(_displayInverted);
      } else {
        pcd8544->invertDisplay(_displayInverted);
      }
      pcd8544->display();                            // Put on display
    }
    else if (equals(arg1, F("backlight"))) {         // Backlight percentage
      if ((P141_CONFIG_BACKLIGHT_PIN != -1) &&       // All is valid?
          (event->Par2 >= 0) &&
          (event->Par2 <= 100)) {
        P141_CONFIG_BACKLIGHT_PERCENT = event->Par2; // Set but don't store
        _backlightPercentage          = event->Par2; // Also set to current
        displayOnOff(true);
      } else {
        success = false;
      }
    }
    else if (equals(arg1, F("contrast")) && // Display contrast
             (event->Par2 >= 0) &&
             (event->Par2 <= 100)) {
      P141_CONFIG_CONTRAST = event->Par2;   // Set but don't store
      _contrast            = event->Par2;   // Also set to current
      pcd8544->setContrast(_contrast);
    } else {
      success = false;
    }
  }
  else if (pcd8544 && (cmd.equals(_commandTrigger) ||
                       (gfxHelper && gfxHelper->isAdaGFXTrigger(cmd)))) {
    if (!bitRead(P141_CONFIG_FLAGS, P141_CONFIG_FLAG_NO_WAKE)) { // Wake display?
      displayOnOff(true);
    }

    if (nullptr != gfxHelper) {
      success = gfxHelper->processCommand(string);

      updateFontMetrics();  // Font or color may have changed

      if (success) {
        pcd8544->display(); // Put on display
        # if P141_FEATURE_CURSOR_XY_VALUES
        updateValues(event);
        # endif // if P141_FEATURE_CURSOR_XY_VALUES
      }
    }
  }
  return success;
}

# if P141_FEATURE_CURSOR_XY_VALUES

/****************************************************************************
 * updateValues: put x and y coordinates in Values 0 and 1
 ***************************************************************************/
void P141_data_struct::updateValues(struct EventStruct *event) {
  int16_t curX = 0;
  int16_t curY = 0;

  if (nullptr != gfxHelper) {
    gfxHelper->getCursorXY(curX, curY); // Get current X and Y coordinates, and put into Values
  }
  UserVar.setFloat(event->TaskIndex, 0, curX);
  UserVar.setFloat(event->TaskIndex, 1, curY);
}

# endif // if P141_FEATURE_CURSOR_XY_VALUES

# if ADAGFX_ENABLE_GET_CONFIG_VALUE

/****************************************************************************
 * plugin_get_config_value: Retrieve values like [<taskname>#<valuename>]
 ***************************************************************************/
bool P141_data_struct::plugin_get_config_value(struct EventStruct *event,
                                               String            & string) {
  bool success = false;

  if (gfxHelper != nullptr) {
    success = gfxHelper->pluginGetConfigValue(string);
  }
  return success;
}

# endif // if ADAGFX_ENABLE_GET_CONFIG_VALUE

/****************************************************************************
 * displayOnOff: Turn display on or off
 ***************************************************************************/
void P141_data_struct::displayOnOff(bool state) {
  if (validGpio(_backlightPin)) {
    # if defined(ESP8266)
    analogWrite(_backlightPin, state ? ((1024 / 100) * _backlightPercentage) : 0);
    # endif // if defined(ESP8266)
    # if defined(ESP32)
    analogWriteESP32(_backlightPin, state ? ((1024 / 100) * _backlightPercentage) : 0, 0);
    # endif // if defined(ESP32)
  }

  if (!state && pcd8544) { pcd8544->fillScreen(ADAGFX_BLACK); } // Can't turn off, clearing the display is the least bad alternative
  _displayTimer = (state ? _displayTimeout : 0);
}

/****************************************************************************
 * registerButtonState: the button has been pressed, apply some debouncing
 ***************************************************************************/
void P141_data_struct::registerButtonState(uint8_t newButtonState,
                                           bool    bPin3Invers) {
  if ((ButtonLastState == 0xFF) || (bPin3Invers != (!!newButtonState))) {
    ButtonLastState = newButtonState;
    DebounceCounter++;
  } else {
    ButtonLastState = 0xFF; // Reset
    markButtonStateProcessed();
  }

  if ((ButtonLastState == newButtonState) &&
      (DebounceCounter >= P141_DebounceTreshold)) {
    ButtonState = true;
  }
}

/****************************************************************************
 * markButtonStateProcessed: reset the button state
 ***************************************************************************/
void P141_data_struct::markButtonStateProcessed() {
  ButtonState     = false;
  DebounceCounter = 0;
}

#endif // ifdef USES_P141

#include "../PluginStructs/P148_data_struct.h"

#ifdef USES_P148


# include "../Helpers/StringConverter.h"

# define TM1621_PULSE_WIDTH   10   // microseconds (Sonoff = 100)

// Commands
# define TM1621_SYS_DIS       0x00 // 0b00000000 = Turn off system oscillator and LCD bias generator
# define TM1621_SYS_EN        0x01 // 0b00000001 = Turn on the system oscillator
# define TM1621_LCD_OFF       0x02 // 0b00000010 = disables the LCD bias generator
# define TM1621_LCD_ON        0x03 // 0b00000011 = enables the LCD bias generator
# define TM1621_TIMER_DIS     0x04 // 0b00000100 = disable timer
# define TM1621_WDT_DIS       0x05 // 0b00000101 = disable WDT
# define TM1621_TIMER_EN      0x06 // 0b00000110 = enable timer
# define TM1621_WDT_EN        0x07 // 0b00000111 = enable WDT
# define TM1621_TONE_OFF      0x08 // 0b00001000 = turn off sound output
# define TM1621_TONE_ON       0x09 // 0b00001001 = turn on sound output
# define TM1621_CLR_TIMER     0x0C // 0b000011XX = clear timer
# define TM1621_CLR_WDT       0x0E // 0b0000111X = clear WDT status

# define TM1621_BIAS          0x29 // 0b00101001 = LCD 1/3 bias 4 commons option
                                   // 0b0010abXc =>
                                   // c=0: optional 1/2 bias
                                   // c=1: optional 1/3 bias
                                   // ab=00: Optional 2 segment common ports
                                   // ab=01: Optional 3 segment common ports
                                   // ab=10: optional 4 segment common ports
                                   // N.B. 1/3 bias only possible with 1:3 and 1:4 mux ratio (segment common ports)
# define TM1621_IRQ_DIS       0x80 // 0b100x0xxx

// FIXME TD-er: Must make the contrast/bias configurable
// Commands sent during init.
const uint8_t tm1621_commands[] =
{ TM1621_SYS_EN, TM1621_LCD_ON, TM1621_BIAS, TM1621_TIMER_DIS, TM1621_WDT_DIS, TM1621_TONE_OFF, TM1621_IRQ_DIS };


uint8_t P148_data_struct::TM1621GetFontCharacter(char character, bool firstrow) {
  // Uncrustify may mangle the readability of the fonts
  //
  // *INDENT-OFF*

  // HEX bit values per segment
  // Row 1              Row 2
  //        1                  80
  //      -----              -----
  //     |     |            |     |
  //    2|     |10        40|     |4
  //     |     |            |     |
  //      --20-              --2--
  //     |     |            |     |
  //    4|     |40        20|     |1
  //     |     |            |     |
  //      -----              -----
  //        8                  10

  // TM1621 Font
  //                                                  0     1     2     3     4     5     6     7     8     9     -  
  static const uint8_t tm1621_digit_row[2][11] = { { 0x5F, 0x50, 0x3D, 0x79, 0x72, 0x6B, 0x6F, 0x51, 0x7F, 0x7B, 0x20 },
                                                   { 0xF5, 0x05, 0xB6, 0x97, 0x47, 0xD3, 0xF3, 0x85, 0xF7, 0xD7, 0x02 } };
  //                                                 'A'   'b'   'C'   'd'   'E'   'F'   'G'   'H'   'i'   'J'   'K'   'L'   'M'   'n'   'o'   'P'   'q'   'r'   'S'   't'   'U'   'v'   'W'   'X'   'Y'   'Z'   '?'
  static const uint8_t tm1621_char_row[2][27] = { { 0x77, 0x6E, 0x0F, 0x7C, 0x2F, 0x27, 0x4f, 0x76, 0x40, 0x5C, 0x00, 0x0E, 0x00, 0x64, 0x6C, 0x37, 0x73, 0x24, 0x6B, 0x2E, 0x5E, 0x4C, 0x00, 0x00, 0x7A, 0x00, 0x35 },
                                                  { 0xE7, 0x73, 0xF0, 0x37, 0xF2, 0xE2, 0xF1, 0x67, 0x01, 0x35, 0x00, 0x70, 0x00, 0x23, 0x33, 0xE6, 0xC7, 0x22, 0xD3, 0x72, 0x75, 0x31, 0x00, 0x00, 0x57, 0x00, 0xA6 } };
 // *INDENT-ON*

  const char c       = toLowerCase(character);
  const uint32_t row = firstrow ? 0 : 1;

  if (isdigit(c) || (c == '-')) {
    // Part of a number
    if (c == '-') {
      return tm1621_digit_row[row][10];
    } else {
      return tm1621_digit_row[row][c - '0'];
    }
  }

  // String
  if (c == '?') { return tm1621_char_row[row][26]; }

  if (('a' <= c) && (c <= 'z')) {
    return tm1621_char_row[row][c - 'a'];
  }

  // Return a 'space'
  return 0u;
}


// FIXME TD-er: When used on ESP8266, this conversion union may not work
// However this is probably only used on ESP32 Sonoff units with display.

// Do not change the order of these as it is stored.
union MonitorTaskValue_conversion {
  struct {
    uint16_t TaskIndex : 9;
    uint16_t unit      : 3;
    uint16_t taskVar   : 3;
    uint16_t showname  : 1;
  };
  int16_t pconfigval;
};

P148_data_struct::MonitorTaskValue_t::MonitorTaskValue_t(int16_t pconfigvalue) {
  MonitorTaskValue_conversion conv;

  conv.pconfigval = pconfigvalue;
  TaskIndex       = conv.TaskIndex;
  taskVar         = conv.taskVar;
  unit            = static_cast<Tm1621UnitOfMeasure>(conv.unit);
  showname        = conv.showname;
}

int16_t P148_data_struct::MonitorTaskValue_t::getPconfigValue() const {
  MonitorTaskValue_conversion conv;

  conv.TaskIndex = TaskIndex;
  conv.taskVar   = taskVar;
  conv.unit      = static_cast<int>(unit);
  conv.showname  = showname;
  return conv.pconfigval;
}

void P148_data_struct::MonitorTaskValue_t::webformLoad(int index) const {
  const bool firstrow = (index % 2) == 0;

  {
    String label = concat(F("Page "), index / 2);
    label += firstrow ? F(" Top Row") : F(" Bottom Row");
    addTableSeparator(label, 2, 2);
  }

  addRowLabel(F("Task"));
  addTaskSelect(concat(F("ptask"), index), TaskIndex);

  if (validTaskIndex(TaskIndex)) {
    addRowLabel(F("Value"));
    addTaskValueSelect(concat(F("pvalue"), index), taskVar, TaskIndex);

    addFormCheckBox(F("Show Var Name"), concat(F("pshowname"), index), showname);

    if (firstrow)
    {
      const __FlashStringHelper *options[] = {
        F("None"),
        F("Celsius"),
        F("Fahrenheit"),
        F("V & A"),
        F("kWh & W")
      };
      int optionValues[] {
        static_cast<int>(P148_data_struct::Tm1621UnitOfMeasure::None),
        static_cast<int>(P148_data_struct::Tm1621UnitOfMeasure::Celsius),
        static_cast<int>(P148_data_struct::Tm1621UnitOfMeasure::Fahrenheit),
        static_cast<int>(P148_data_struct::Tm1621UnitOfMeasure::Volt_Amp),
        static_cast<int>(P148_data_struct::Tm1621UnitOfMeasure::kWh_Watt)
      };
      constexpr size_t nrElements = sizeof(optionValues) / sizeof(optionValues[0]);
      const FormSelectorOptions selector(nrElements, options, optionValues);
      selector.addFormSelector(
        F("Unit Symbols"), concat(F("punit"), index),
        static_cast<int>(unit));
    } else {
      const __FlashStringHelper *options[] = {
        F("None"),
        F("%RH (humidity)"),
        F("V & A"),
        F("kWh & W")
      };
      int optionValues[] {
        static_cast<int>(P148_data_struct::Tm1621UnitOfMeasure::None),
        static_cast<int>(P148_data_struct::Tm1621UnitOfMeasure::Humidity),
        static_cast<int>(P148_data_struct::Tm1621UnitOfMeasure::Volt_Amp),
        static_cast<int>(P148_data_struct::Tm1621UnitOfMeasure::kWh_Watt)
      };
      constexpr size_t nrElements = sizeof(optionValues) / sizeof(optionValues[0]);

      const FormSelectorOptions selector(nrElements, options, optionValues);
      selector.addFormSelector(
        F("Unit Symbols"), concat(F("punit"), index),
        static_cast<int>(unit));
    }
  } else {
    addFormCheckBox(F("Clear Line"), concat(F("pshowname"), index), showname);
  }
}

int16_t P148_data_struct::MonitorTaskValue_t::webformSave(int index) {
  TaskIndex = getFormItemInt(concat(F("ptask"), index), INVALID_TASK_INDEX);
  taskVar   = getFormItemInt(concat(F("pvalue"), index), INVALID_TASKVAR_INDEX);
  unit      =
    static_cast<P148_data_struct::Tm1621UnitOfMeasure>(
      getFormItemInt(concat(F("punit"), index), static_cast<int>(P148_data_struct::Tm1621UnitOfMeasure::None))
      );
  showname = isFormItemChecked(concat(F("pshowname"), index));

  return getPconfigValue();
}

bool P148_data_struct::MonitorTaskValue_t::isValid() const {
  return validTaskIndex(TaskIndex) && validTaskVarIndex(taskVar);
}

String P148_data_struct::MonitorTaskValue_t::formatTaskValue(bool& writeToDisplay) const {
  if (!isValid()) {
    writeToDisplay = showname;
    return EMPTY_STRING;
  }
  writeToDisplay = true;

  if (showname) {
    return getTaskValueName(TaskIndex, taskVar);
  }
  return formatUserVarNoCheck(TaskIndex, taskVar);
}

bool P148_data_struct::Tm1621_t::isValid() const {
  // FIXME TD-er: Must check if the selected pins also are usable
  return pin_da != -1 &&
         pin_wr != -1 &&
         pin_rd != -1 &&
         pin_cs != -1;
}

P148_data_struct::P148_data_struct(const Tm1621_t& config) : Tm1621(config) {
  if (Tm1621.isValid()) {
    pinMode(Tm1621.pin_da, OUTPUT);
    digitalWrite(Tm1621.pin_da, 1);
    pinMode(Tm1621.pin_cs, OUTPUT);
    digitalWrite(Tm1621.pin_cs, 1);
    pinMode(Tm1621.pin_rd, OUTPUT);
    digitalWrite(Tm1621.pin_rd, 1);
    pinMode(Tm1621.pin_wr, OUTPUT);
    digitalWrite(Tm1621.pin_wr, 1);
  }
}

bool P148_data_struct::init() {
  if (!Tm1621.isValid()) {
    return false;
  }
  TM1621Init();
  return true;
}

/*********************************************************************************************/

void P148_data_struct::TM1621Init() {
  digitalWrite(Tm1621.pin_cs, 0);
  delayMicroseconds(80);
  digitalWrite(Tm1621.pin_rd, 0);
  delayMicroseconds(15);
  digitalWrite(Tm1621.pin_wr, 0);
  delayMicroseconds(25);
  digitalWrite(Tm1621.pin_da, 0);
  delayMicroseconds(TM1621_PULSE_WIDTH);
  digitalWrite(Tm1621.pin_da, 1);

  constexpr uint32_t nr_commands = sizeof(tm1621_commands) / sizeof(tm1621_commands[0]);

  for (uint32_t command = 0; command < nr_commands; command++) {
    TM1621SendCmnd(tm1621_commands[command]);
  }

  // Clear entire display buffer
  TM1621SendAddress(0x00);

  for (uint32_t segment = 0; segment < 16; segment++) {
    TM1621SendCommon(0);
  }
  TM1621StopSequence();

  snprintf_P(Tm1621.row[0], sizeof(Tm1621.row[0]), PSTR("----"));
  snprintf_P(Tm1621.row[1], sizeof(Tm1621.row[1]), PSTR("----"));
  TM1621SendRows();
}

void P148_data_struct::TM1621WriteBit(bool value) const {
  digitalWrite(Tm1621.pin_wr, 0);             // Start write sequence
  digitalWrite(Tm1621.pin_da, value ? 1 : 0); // Set data
  delayMicroseconds(TM1621_PULSE_WIDTH);
  digitalWrite(Tm1621.pin_wr, 1);             // On the rising edge of the /WR signal,
                                              // the data on the DATA line is written to the TM1621
  delayMicroseconds(TM1621_PULSE_WIDTH);
}

void P148_data_struct::TM1621StartSequence() const {
  digitalWrite(Tm1621.pin_cs, 0); // Start command sequence
  delayMicroseconds(TM1621_PULSE_WIDTH / 2);
}

void P148_data_struct::TM1621StopSequence() const {
  digitalWrite(Tm1621.pin_cs, 1); // Stop command sequence
  delayMicroseconds(TM1621_PULSE_WIDTH / 2);
  digitalWrite(Tm1621.pin_da, 1); // Reset data
}

void P148_data_struct::TM1621SendCmnd(uint16_t command) const {
  uint16_t full_command = (0x0400 | command) << 5; // 0b100cccccccc00000

  TM1621StartSequence();

  for (uint32_t i = 0; i < 12; i++) {
    TM1621WriteBit(full_command & 0x8000);
    full_command <<= 1;
  }
  TM1621StopSequence();
}

void P148_data_struct::TM1621SendAddress(uint16_t address) const {
  uint16_t full_address = (address | 0x0140) << 7; // 0b101aaaaaa0000000

  TM1621StartSequence();

  for (uint32_t i = 0; i < 9; i++) {
    TM1621WriteBit(full_address & 0x8000);
    full_address <<= 1;
  }
}

void P148_data_struct::TM1621SendCommon(uint8_t common) const {
  for (uint32_t i = 0; i < 8; i++) {
    TM1621WriteBit(common & 1);
    common >>= 1;
  }
}

void P148_data_struct::TM1621WritePixelBuffer(const uint8_t *buf, size_t size, uint16_t address) const {
  TM1621SendAddress(address);

  for (uint32_t i = 0; i < size; i++) {
    TM1621SendCommon(buf[i]);
  }
  TM1621StopSequence();
}

void P148_data_struct::TM1621SendRows() const {
  // Tm1621.row[x] = "text", "----", "    " or a number with one decimal like "0.4", "237.5", "123456.7"
  // "123456.7" will be shown as "9999" being a four digit overflow

  uint8_t buffer[8] = { 0 }; // TM1621 16-segment 4-bit common buffer

  for (uint32_t j = 0; j < 2; j++) {
    const bool firstrow = 0 == j;

    float value = 0.0;

    if (validFloatFromString(Tm1621.row[j], value)) {
      // 0.4V => "  04", 0.0A => "  ", 1234.5V => "1234"

      // Check to see if the original string had a dot.
      bool hadDot = false;

      for (size_t i = 0; i < 4 && !hadDot; ++i) {
        if (Tm1621.row[j][i] == '.') { hadDot = true; }
      }

      char row[4]{};

      if (value > 9999.0f) { value = 9999.0f; }

      if (value < -999.0f) { value = -999.0f; }
      bool dot = false;

      if ((-99.9f < value) && (value < 999.9f)) {
        if (hadDot) {
          dot    = true;
          value *= 10.0f;
        }
      }
      const String value_str(static_cast<int>(value));
      const size_t strlen = value_str.length();
      size_t pos          = 0;

      for (size_t i = 4 - strlen; i < 4 && pos < strlen; ++i, ++pos) {
        row[i] = value_str[pos];
      }

      for (uint32_t i = 0; i < 4; i++) {
        buffer[bufferIndex(firstrow, i)] = TM1621GetFontCharacter(row[i], firstrow);
      }

      if (dot) {
        if (firstrow) {
          buffer[2] |= 0x80; // Row 1 decimal point
        } else {
          buffer[5] |= 0x08; // Row 2 decimal point
        }
      }
    } else {
      for (uint32_t i = 0; i < 4; i++) {
        buffer[bufferIndex(firstrow, i)] = TM1621GetFontCharacter(Tm1621.row[j][i], firstrow);
      }
    }
  }

  if (Tm1621.fahrenheit) { buffer[1] |= 0x80; }

  if (Tm1621.celsius) { buffer[3] |= 0x80; }

  if (Tm1621.kwh) { buffer[4] |= 0x08; }

  if (Tm1621.humidity) { buffer[6] |= 0x08; }

  if (Tm1621.voltage) { buffer[7] |= 0x08; }

  TM1621WritePixelBuffer(buffer, 8, 0x10); // Sonoff only uses the upper 16 Segments
}

void P148_data_struct::showPage() {
  if (pagenr >= 3) {
    pagenr = 0;
  }

  // Skip pages which do not change the display.
  bool somethingWritten = false;

  while (!somethingWritten && pagenr < 3) {
    for (int row = 2 * pagenr; row <= (2 * pagenr + 1); ++row) {
      const bool firstrow   = (row % 2) == 0;
      bool   writeToDisplay = true;
      String value          = MonitorTaskValues[row].formatTaskValue(writeToDisplay);

      if (writeToDisplay) {
        somethingWritten = true;
        setUnit(MonitorTaskValues[row].unit, firstrow);
        writeString(firstrow, value);
      }
    }
    ++pagenr;
  }
}

void P148_data_struct::writeString(bool firstrow, const String& str) {
  safe_strncpy(Tm1621.row[firstrow ? 0 : 1], str, sizeof(Tm1621.row[0]));
  TM1621SendRows();
}

void P148_data_struct::writeStrings(const String& str1, const String& str2) {
  safe_strncpy(Tm1621.row[0], str1, sizeof(Tm1621.row[0]));
  safe_strncpy(Tm1621.row[1], str2, sizeof(Tm1621.row[0]));
  TM1621SendRows();
}

void P148_data_struct::writeFloats(float value1, float value2) {
  writeStrings(String(value1, 1), String(value2, 1));
}

void P148_data_struct::writeFloat(bool firstrow, float value) {
  writeString(firstrow, String(value, 1));
}

void P148_data_struct::writeRawData(uint64_t rawdata) const {
  uint8_t buffer[8] = { 0 }; // TM1621 16-segment 4-bit common buffer

  for (uint32_t j = 0; j < 2; j++) {
    for (uint32_t i = 0; i < 4; i++) {
      buffer[bufferIndex((0 == j), i)] = ((rawdata >> 56) & 0xFF);
      rawdata                        <<= 8;
    }
  }

  TM1621WritePixelBuffer(buffer, 8, 0x10); // Sonoff only uses the upper 16 Segments
}

void P148_data_struct::setUnit(Tm1621UnitOfMeasure unit) {
  setUnit(unit, true);
  setUnit(unit, false);
}

void P148_data_struct::setUnit(P148_data_struct::Tm1621UnitOfMeasure unit, bool firstrow) {
  switch (unit) {
    case P148_data_struct::Tm1621UnitOfMeasure::None:

      if (firstrow) {
        Tm1621.celsius    = false;
        Tm1621.fahrenheit = false;
        Tm1621.voltage    = false;
        Tm1621.kwh        = false;
      } else {
        Tm1621.humidity = false;
      }
      break;
    case P148_data_struct::Tm1621UnitOfMeasure::Celsius:

      if (firstrow) {
        Tm1621.celsius    = true;
        Tm1621.fahrenheit = false;
        Tm1621.voltage    = false;
        Tm1621.kwh        = false;
      }
      break;
    case P148_data_struct::Tm1621UnitOfMeasure::Fahrenheit:

      if (firstrow) {
        Tm1621.fahrenheit = true;
        Tm1621.celsius    = false;
        Tm1621.voltage    = false;
        Tm1621.kwh        = false;
      }
      break;
    case P148_data_struct::Tm1621UnitOfMeasure::kWh_Watt:
      Tm1621.kwh = true;

      if (firstrow) {
        Tm1621.celsius    = false;
        Tm1621.fahrenheit = false;
        Tm1621.voltage    = false;
      } else {
        Tm1621.humidity = false;
      }

      break;
    case P148_data_struct::Tm1621UnitOfMeasure::Humidity:

      if (!firstrow) {
        Tm1621.humidity = true;
        Tm1621.voltage  = false;
        Tm1621.kwh      = false;
      }
      break;
    case P148_data_struct::Tm1621UnitOfMeasure::Volt_Amp:
      Tm1621.voltage = true;
      Tm1621.kwh     = false;

      if (firstrow) {
        Tm1621.celsius    = false;
        Tm1621.fahrenheit = false;
      } else {
        Tm1621.humidity = false;
      }
      break;
  }
}

const char p148_subcommands[] PROGMEM = "raw|writerow|write|voltamp|energy|celcius|fahrenheit|humidity";
enum class p148_subcommands_e {
  raw,
  writerow,
  write,
  voltamp,
  energy,
  celcius,
  fahrenheit,
  humidity
};


bool P148_data_struct::plugin_write(struct EventStruct *event, const String& string)
{
  const String command = parseString(string, 1);

  if (equals(command, F("tm1621"))) {
    const String subcommand = parseString(string, 2);

    const int command_i = GetCommandCode(subcommand.c_str(), p148_subcommands);

    if (command_i == -1) {
      // No matching subcommand found
      return false;
    }

    switch (static_cast<p148_subcommands_e>(command_i)) {
      case p148_subcommands_e::raw:
      {
        // Write raw data to the display
        // Typical use case: testing fonts
        const String rawdata_str = parseString(string, 3);
        uint64_t     rawdata;

        if (validUInt64FromString(rawdata_str, rawdata)) {
          writeRawData(rawdata);
          return true;
        }

        break;
      }
      case p148_subcommands_e::writerow:
      {
        // tm1621,write,<rownr>,<string>
        const bool firstrow = event->Par2 <= 1;
        setUnit(P148_data_struct::Tm1621UnitOfMeasure::None, firstrow);
        writeString(firstrow, parseString(string, 4));

        return true;
      }
      case p148_subcommands_e::write:
      {
        // tm1621,write,<string1>,<string2>
        setUnit(P148_data_struct::Tm1621UnitOfMeasure::None);
        const String str1 = parseString(string, 3);
        const String str2 = parseString(string, 4);

        if (str2.isEmpty()) {
          writeString(true, str1);
        } else {
          writeStrings(str1, str2);
        }

        return true;
      }
      case p148_subcommands_e::voltamp:
      {
        // tm1621,voltamp,<volt>,<amp>
        setUnit(P148_data_struct::Tm1621UnitOfMeasure::Volt_Amp);
        writeStrings(parseString(string, 3), parseString(string, 4));

        return true;
      }
      case p148_subcommands_e::energy:
      {
        // tm1621,energy,<kWh>,<Watt>
        setUnit(P148_data_struct::Tm1621UnitOfMeasure::kWh_Watt);
        writeStrings(parseString(string, 3), parseString(string, 4));

        return true;
      }
      case p148_subcommands_e::celcius:
      {
        // tm1621,celcius,<temperture>
        setUnit(P148_data_struct::Tm1621UnitOfMeasure::Celsius, true);
        writeString(true, parseString(string, 3));

        return true;
      }
      case p148_subcommands_e::fahrenheit:
      {
        // tm1621,fahrenheit,<temperture>
        setUnit(P148_data_struct::Tm1621UnitOfMeasure::Fahrenheit, true);
        writeString(true, parseString(string, 3));

        return true;
      }
      case p148_subcommands_e::humidity:
      {
        // tm1621,humidity,<%humidity>
        setUnit(P148_data_struct::Tm1621UnitOfMeasure::Humidity, false);
        writeString(false, parseString(string, 3));

        return true;
      }
    }
  }
  return false;
}

#endif // ifdef USES_P148

#include "../WebServer/FactoryResetPage.h"


#ifdef WEBSERVER_FACTORY_RESET

#include "../WebServer/ESPEasy_WebServer.h"
#include "../WebServer/HTML_wrappers.h"
#include "../WebServer/JSON.h"
#include "../WebServer/Markup.h"
#include "../WebServer/Markup_Buttons.h"
#include "../WebServer/Markup_Forms.h"

#include "../DataTypes/DeviceModel.h"

#include "../Globals/ResetFactoryDefaultPref.h"

#include "../Helpers/ESPEasy_FactoryDefault.h"
#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/Hardware.h"

// ********************************************************************************
// Web Interface Factory Reset
// ********************************************************************************
void handle_factoryreset() {
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_factoryreset"));
  #endif

  if (!isLoggedIn()) { return; }
  navMenuIndex = MENU_INDEX_TOOLS;
  TXBuffer.startStream();
  sendHeadandTail_stdtemplate(_HEAD);
  html_add_form();
  html_table_class_normal();
  html_TR();
  addFormHeader(F("Factory Reset"));

#ifndef LIMIT_BUILD_SIZE
  if (hasArg(F("fdm"))) {
    DeviceModel model = static_cast<DeviceModel>(getFormItemInt(F("fdm")));

    if (modelMatchingFlashSize(model)) {
      setFactoryDefault(model);
    }
  }


  if (hasArg(F("savepref"))) {
    // User choose a pre-defined config and wants to save it as the new default.
    ResetFactoryDefaultPreference.keepUnitName(isFormItemChecked(F("kun")));
    ResetFactoryDefaultPreference.keepWiFi(isFormItemChecked(F("kw")));
    ResetFactoryDefaultPreference.keepNetwork(isFormItemChecked(F("knet")));
    ResetFactoryDefaultPreference.keepNTP(isFormItemChecked(F("kntp")));
    ResetFactoryDefaultPreference.keepLogConsoleSettings(isFormItemChecked(F("klog")));
# if FEATURE_ALTERNATIVE_CDN_URL
    ResetFactoryDefaultPreference.keepCustomCdnUrl(isFormItemChecked(F("kcdn")));
#endif
    applyFactoryDefaultPref();
    addHtmlError(SaveSettings());
  }
#endif

  if (hasArg(F("performfactoryreset"))) {
    // User confirmed to really perform the reset.
    applyFactoryDefaultPref();

    // No need to call SaveSettings(); ResetFactory() will save the new settings.
    ResetFactory();
  } else {
    #ifndef LIMIT_BUILD_SIZE
    // Nothing chosen yet, show options.
    addTableSeparator(F("Settings to keep"), 2, 3);

    addRowLabel(F("Keep Unit/Name"));
    addCheckBox(F("kun"), ResetFactoryDefaultPreference.keepUnitName());

    addRowLabel(F("Keep WiFi config"));
    addCheckBox(F("kw"), ResetFactoryDefaultPreference.keepWiFi());

    addRowLabel(F("Keep Network config"));
    addCheckBox(F("knet"), ResetFactoryDefaultPreference.keepNetwork());

    addRowLabel(F("Keep NTP/DST config"));
    addCheckBox(F("kntp"), ResetFactoryDefaultPreference.keepNTP());

    addRowLabel(F("Keep Console/Log config"));
    addCheckBox(F("klog"), ResetFactoryDefaultPreference.keepLogConsoleSettings());

# if FEATURE_ALTERNATIVE_CDN_URL
    addRowLabel(F("Keep Custom CDN url"));
    addCheckBox(F("kcdn"), ResetFactoryDefaultPreference.keepCustomCdnUrl());
#endif

    addTableSeparator(F("Pre-defined configurations"), 2, 3);
    addRowLabel(F("Pre-defined config"));
    addPreDefinedConfigSelector();


    html_TR_TD();
    html_TD();
    addSubmitButton(F("Save Preferences"), F("savepref"));
    #endif

    html_TR_TD_height(30);

    addTableSeparator(F("Immediate full reset"), 2, 3);
    addRowLabel(F("Erase settings files"));
    addSubmitButton(F("Factory Reset"), F("performfactoryreset"), F("red"));
  }

  html_end_table();
  html_end_form();
  sendHeadandTail_stdtemplate(_TAIL);
  TXBuffer.endStream();
}

// ********************************************************************************
// Create pre-defined config selector
// ********************************************************************************
void addPreDefinedConfigSelector() {
  DeviceModel active_model = ResetFactoryDefaultPreference.getDeviceModel();

  addSelector_Head_reloadOnChange(F("fdm"));

  for (uint8_t x = 0; x < static_cast<uint8_t>(DeviceModel::DeviceModel_MAX); ++x) {
    DeviceModel model = static_cast<DeviceModel>(x);
    if (modelMatchingFlashSize(model)) {
      addSelector_Item(
        getDeviceModelString(model),
        x,
        model == active_model);
    }
  }
  addSelector_Foot(true);
}

#ifdef WEBSERVER_NEW_UI
void handle_factoryreset_json() {
  if (!isLoggedIn()) { return; }
  TXBuffer.startJsonStream();
  addHtml('{');
#ifndef LIMIT_BUILD_SIZE
  if (hasArg(F("fdm"))) {
    DeviceModel model = static_cast<DeviceModel>(getFormItemInt(F("fdm")));

    if (modelMatchingFlashSize(model)) {
      setFactoryDefault(model);
    }
  }

  if (hasArg(F("kun"))) {
    ResetFactoryDefaultPreference.keepUnitName(isFormItemChecked(F("kun")));
  }

  if (hasArg(F("kw"))) {
    ResetFactoryDefaultPreference.keepWiFi(isFormItemChecked(F("kw")));
  }

  if (hasArg(F("knet"))) {
    ResetFactoryDefaultPreference.keepNetwork(isFormItemChecked(F("knet")));
  }

  if (hasArg(F("kntp"))) {
    ResetFactoryDefaultPreference.keepNTP(isFormItemChecked(F("kntp")));
  }

  if (hasArg(F("klog"))) {
    ResetFactoryDefaultPreference.keepLogConsoleSettings(isFormItemChecked(F("klog")));
  }

# if FEATURE_ALTERNATIVE_CDN_URL
  if (hasArg(F("kcdn"))) {
    ResetFactoryDefaultPreference.keepCustomCdnUrl(isFormItemChecked(F("kcdn")));
  }
#endif


#endif
  String error;
  bool   performReset = false;
  bool   savePref     = false;

  if (hasArg(F("savepref"))) {
    // User choose a pre-defined config and wants to save it as the new default.
    savePref = true;
  }

  if (hasArg(F("performfactoryreset"))) {
    // User confirmed to really perform the reset.
    performReset = true;
    savePref     = true;
  } else {
    error = F("no reset");
  }

  if (savePref) {
    applyFactoryDefaultPref();
    error = SaveSettings();
  }

  if (error.isEmpty()) {
    error = F("ok");
  }

  stream_last_json_object_value(F("status"), error);
  addHtml('}');
  TXBuffer.endStream();

  if (performReset) {
    ResetFactory();
  }
}

#endif // WEBSERVER_NEW_UI

#endif // ifdef WEBSERVER_FACTORY_RESET

#include "../WebServer/JSON.h"

#include "../WebServer/ESPEasy_WebServer.h"
#include "../WebServer/JSON.h"
#include "../WebServer/Markup_Forms.h"

#include "../CustomBuild/CompiletimeDefines.h"

#include "../DataStructs/TimingStats.h"

#include "../Globals/Cache.h"
#include "../Globals/Nodes.h"
#include "../Globals/Device.h"
#include "../Globals/Plugins.h"
#include "../Globals/NPlugins.h"

#include "../Helpers/_Plugin_init.h"
#include "../Helpers/ESPEasyStatistics.h"
#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/ESPEasy_UnitOfMeasure.h"
#include "../Helpers/Numerical.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringProvider.h"
#include "../Helpers/StringGenerator_System.h"

#include "../../_Plugin_Helper.h"
#include "../../ESPEasy-Globals.h"

void stream_comma_newline() {
  addHtml(',', '\n');
}


// ********************************************************************************
// Web Interface get CSV value from task
// ********************************************************************************
#ifdef WEBSERVER_CSVVAL
void handle_csvval()
{
  TXBuffer.startJsonStream();
  const int printHeader = getFormItemInt(F("header"), 1);
  bool printHeaderValid = true;
  if (printHeader != 1 && printHeader != 0)
  {
    addHtml(F("ERROR: Header not valid!\n"));
    printHeaderValid = false;
  }

  const taskIndex_t taskNr    = getFormItemInt(F("tasknr"), INVALID_TASK_INDEX);
  const bool taskValid = validTaskIndex(taskNr);
  if (!taskValid)
  {
    addHtml(F("ERROR: TaskNr not valid!\n"));
  }

  const int INVALID_VALUE_NUM = INVALID_TASKVAR_INDEX + 1;
  const taskVarIndex_t valNr    = getFormItemInt(F("valnr"), INVALID_VALUE_NUM);
  bool valueNumberValid = true;
  if (valNr != INVALID_VALUE_NUM && !validTaskVarIndex(valNr))
  {
    addHtml(F("ERROR: ValueId not valid!\n"));
    valueNumberValid = false;
  }

  if (taskValid && valueNumberValid && printHeaderValid)
  {
    const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(taskNr);

    if (validDeviceIndex(DeviceIndex))
    {
      const uint8_t taskValCount = getValueCountForTask(taskNr);

      if (printHeader)
      {
        for (uint8_t x = 0; x < taskValCount; x++)
        {
          if (valNr == INVALID_VALUE_NUM || valNr == x)
          {
            addHtml(Cache.getTaskDeviceValueName(taskNr, x));
            if (x != taskValCount - 1)
            {
              addHtml(';');
            }
          }
        }
        addHtml('\n');
      }

      struct EventStruct TempEvent(taskNr);

      for (uint8_t x = 0; x < taskValCount; x++)
      {
        if ((valNr == INVALID_VALUE_NUM) || (valNr == x))
        {
          addHtml(formatUserVarNoCheck(&TempEvent, x));

          if (x != taskValCount - 1)
          {
            addHtml(';');
          }
        }
      }
      addHtml('\n');
    }
  }
  TXBuffer.endStream();
}
#endif

// ********************************************************************************
// Web Interface JSON page (no password!)
// ********************************************************************************
void handle_json()
{
  START_TIMER
  const taskIndex_t taskNr    = getFormItemInt(F("tasknr"), INVALID_TASK_INDEX);
  const bool showSpecificTask = validTaskIndex(taskNr);
  bool showSystem             = true;
  bool showWifi               = true;

  #if FEATURE_ETHERNET
  bool showEthernet = true;
  #endif // if FEATURE_ETHERNET
  bool showDataAcquisition = true;
  bool showTaskDetails     = true;
  #if FEATURE_ESPEASY_P2P
  bool showNodes           = true;
  #endif
  #if FEATURE_PLUGIN_STATS
  bool showPluginStats     = getFormItemInt(F("showpluginstats"), 0) != 0;
  #endif

  if (equals(webArg(F("view")), F("sensorupdate"))) {
    showSystem = false;
    showWifi   = false;
    #if FEATURE_ETHERNET
    showEthernet = false;
    #endif // if FEATURE_ETHERNET
    showDataAcquisition = false;
    showTaskDetails     = false;
    #if FEATURE_ESPEASY_P2P
    showNodes           = false;
    #endif
    #if FEATURE_PLUGIN_STATS
    showPluginStats     = hasArg(F("showpluginstats"));
    #endif
  }

  TXBuffer.startJsonStream();

  if (!showSpecificTask)
  {
    addHtml('{');

    if (showSystem) {
      addHtml(F("\"System\":{\n"));

      if (wdcounter > 0)
      {
        stream_next_json_object_value(LabelType::LOAD_PCT);
        stream_next_json_object_value(LabelType::LOOP_COUNT);
      }

      static const LabelType::Enum labels[] PROGMEM =
      {
        LabelType::BUILD_DESC,
        LabelType::GIT_BUILD,
        LabelType::SYSTEM_LIBRARIES,
#ifdef ESP32
        LabelType::ESP_IDF_SDK_VERSION,
#endif
        LabelType::PLUGIN_COUNT,
        LabelType::PLUGIN_DESCRIPTION,
        LabelType::BUILD_TIME,
        LabelType::BINARY_FILENAME,
        LabelType::LOCAL_TIME,
        #if FEATURE_EXT_RTC
        LabelType::EXT_RTC_UTC_TIME,
        #endif
        LabelType::TIME_SOURCE,
        LabelType::TIME_WANDER,
        LabelType::ISNTP,
        LabelType::UNIT_NR,
        LabelType::UNIT_NAME,
        LabelType::UPTIME,
        LabelType::UPTIME_MS,
#if FEATURE_INTERNAL_TEMPERATURE
        LabelType::INTERNAL_TEMPERATURE,
#endif
        LabelType::BOOT_TYPE,
        LabelType::RESET_REASON,
        LabelType::CPU_ECO_MODE,

    #if defined(CORE_POST_2_5_0) || defined(ESP32)
      #ifndef LIMIT_BUILD_SIZE
        LabelType::HEAP_MAX_FREE_BLOCK, // 7654
      #endif
    #endif // if defined(CORE_POST_2_5_0) || defined(ESP32)
    #if defined(CORE_POST_2_5_0)
      #ifndef LIMIT_BUILD_SIZE
        LabelType::HEAP_FRAGMENTATION,  // 12
      #endif
    #endif // if defined(CORE_POST_2_5_0)
        LabelType::FREE_MEM,
      #ifdef USE_SECOND_HEAP
        LabelType::FREE_HEAP_IRAM,
      #endif
        LabelType::FREE_STACK,

    #ifdef ESP32
        LabelType::HEAP_SIZE,
        LabelType::HEAP_MIN_FREE,
        #ifdef BOARD_HAS_PSRAM
        LabelType::PSRAM_SIZE,
        LabelType::PSRAM_FREE,
        LabelType::PSRAM_MIN_FREE,
        LabelType::PSRAM_MAX_FREE_BLOCK,
        #endif // BOARD_HAS_PSRAM
    #endif // ifdef ESP32
        LabelType::ESP_CHIP_MODEL,
    #ifdef ESP32
        LabelType::ESP_CHIP_REVISION,
    #endif // ifdef ESP32
        LabelType::FLASH_CHIP_ID,
        LabelType::FLASH_CHIP_VENDOR,
        LabelType::FLASH_CHIP_MODEL,
        LabelType::FLASH_CHIP_REAL_SIZE,
        LabelType::FLASH_CHIP_SPEED,
        LabelType::FLASH_IDE_MODE,
        LabelType::FS_SIZE,

        LabelType::SUNRISE,
        LabelType::SUNSET,
        LabelType::TIMEZONE_OFFSET,
        LabelType::LATITUDE,
        LabelType::LONGITUDE,
        LabelType::SYSLOG_LOG_LEVEL,
        LabelType::SERIAL_LOG_LEVEL,
        LabelType::WEB_LOG_LEVEL,
        #if FEATURE_SD
        LabelType::SD_LOG_LEVEL,
        #endif // if FEATURE_SD


        LabelType::MAX_LABEL
      };

      stream_json_object_values(labels);
      stream_comma_newline();
    }

    if (showWifi) {
      addHtml(F("\"WiFi\":{\n"));
      static const LabelType::Enum labels[] PROGMEM =
      {
        LabelType::HOST_NAME,
        #if FEATURE_MDNS
        LabelType::M_DNS,
        #endif // if FEATURE_MDNS
        LabelType::IP_CONFIG,
        LabelType::IP_ADDRESS,
#if FEATURE_USE_IPV6
        LabelType::IP6_LOCAL,
        LabelType::IP6_GLOBAL,
        LabelType::ENABLE_IPV6,
#endif
        LabelType::IP_SUBNET,
        LabelType::GATEWAY,
        LabelType::STA_MAC,
        LabelType::DNS_1,
        LabelType::DNS_2,
        LabelType::SSID,
        LabelType::BSSID,
        LabelType::CHANNEL,
        LabelType::ENCRYPTION_TYPE_STA,
        LabelType::CONNECTED_MSEC,
        LabelType::LAST_DISCONNECT_REASON,
        LabelType::LAST_DISC_REASON_STR,
        LabelType::NUMBER_RECONNECTS,
        LabelType::WIFI_STORED_SSID1,
        LabelType::WIFI_STORED_SSID2,
        LabelType::FORCE_WIFI_BG,
        LabelType::RESTART_WIFI_LOST_CONN,
        LabelType::FORCE_WIFI_NOSLEEP,
#ifdef SUPPORT_ARP
        LabelType::PERIODICAL_GRAT_ARP,
#endif // ifdef SUPPORT_ARP
#ifdef USES_ESPEASY_NOW
        LabelType::USE_ESPEASY_NOW,
        LabelType::FORCE_ESPEASY_NOW_CHANNEL,
#endif
        LabelType::CONNECTION_FAIL_THRESH,
#if FEATURE_SET_WIFI_TX_PWR
        LabelType::WIFI_TX_MAX_PWR,
        LabelType::WIFI_CUR_TX_PWR,
        LabelType::WIFI_SENS_MARGIN,
        LabelType::WIFI_SEND_AT_MAX_TX_PWR,
#endif
        LabelType::WIFI_NR_EXTRA_SCANS,
#ifdef ESP32
        LabelType::WIFI_PASSIVE_SCAN,
#endif
        LabelType::WIFI_USE_LAST_CONN_FROM_RTC,
        LabelType::WIFI_RSSI,
#ifndef ESP32
        LabelType::WAIT_WIFI_CONNECT,
#endif
        LabelType::HIDDEN_SSID_SLOW_CONNECT,
        LabelType::CONNECT_HIDDEN_SSID,
        LabelType::SDK_WIFI_AUTORECONNECT,

        LabelType::MAX_LABEL
      };

      stream_json_object_values(labels);

      // TODO: PKR: Add ETH Objects
      stream_comma_newline();
    }

    #if FEATURE_ETHERNET

    if (showEthernet) {
      addHtml(F("\"Ethernet\":{\n"));
      static const LabelType::Enum labels[] PROGMEM =
      {
        LabelType::ETH_WIFI_MODE,
        LabelType::ETH_CONNECTED,
        LabelType::ETH_CHIP,
        LabelType::ETH_DUPLEX,
        LabelType::ETH_SPEED,
        LabelType::ETH_STATE,
        LabelType::ETH_SPEED_STATE,


        LabelType::MAX_LABEL
      };

      stream_json_object_values(labels);
      stream_comma_newline();
    }
    #endif // if FEATURE_ETHERNET

  #if FEATURE_ESPEASY_P2P
    if (showNodes) {
      bool comma_between = false;

      for (auto it = Nodes.begin(); it != Nodes.end(); ++it)
      {
        if (it->second.ip[0] != 0)
        {
          if (comma_between) {
            addHtml(',');
          } else {
            comma_between = true;
            addHtml(F("\"nodes\":[\n")); // open json array if >0 nodes
          }

          addHtml('{');
          stream_next_json_object_value(F("nr"), it->first);
          stream_next_json_object_value(F("name"),
                                        (it->first != Settings.Unit) ? it->second.getNodeName() : Settings.getName());

          if (it->second.build) {
            stream_next_json_object_value(F("build"), formatSystemBuildNr(it->second.build));
          }

          if (it->second.nodeType) {
            stream_next_json_object_value(F("platform"), it->second.getNodeTypeDisplayString());
          }
          const int8_t rssi = it->second.getRSSI();
          if (rssi < 0) {
            stream_next_json_object_value(F("rssi"), rssi);
          }
          if (it->second.build >= 20107) {
            stream_next_json_object_value(F("load"), toString(it->second.getLoad(), 2));
            if (it->second.webgui_portnumber != 80) {
              stream_next_json_object_value(F("webport"), it->second.webgui_portnumber);
            }
          }
          stream_next_json_object_value(F("ip"), formatIP(it->second.IP()));
#if FEATURE_USE_IPV6
          if (it->second.hasIPv6_mac_based_link_local) {
            stream_next_json_object_value(F("ipv6local"), formatIP(it->second.IPv6_link_local(true), true));
          }
          if (it->second.hasIPv6_mac_based_link_global) {
            stream_next_json_object_value(F("ipv6global"), formatIP(it->second.IPv6_global()));
          }
#endif
          stream_last_json_object_value(F("age"), it->second.getAge());
        } // if node info exists
      }   // for loop

      if (comma_between) {
        addHtml(F("],\n")); // close array if >0 nodes
      }
    }
  #endif
  }

  taskIndex_t firstTaskIndex = 0;
  taskIndex_t lastTaskIndex  = TASKS_MAX - 1;

  if (showSpecificTask)
  {
    firstTaskIndex = taskNr - 1;
    lastTaskIndex  = taskNr - 1;
  }
  taskIndex_t lastActiveTaskIndex = 0;

  for (taskIndex_t TaskIndex = firstTaskIndex; TaskIndex <= lastTaskIndex; TaskIndex++) {
    if (validPluginID_fullcheck(Settings.getPluginID_for_task(TaskIndex))) {
      lastActiveTaskIndex = TaskIndex;
    }
  }

  if (!showSpecificTask) {
    addHtml(F("\"Sensors\":[\n"));
  }

  // Keep track of the lowest reported TTL and use that as refresh interval.
  unsigned long lowest_ttl_json = 60;

  for (taskIndex_t TaskIndex = firstTaskIndex; TaskIndex <= lastActiveTaskIndex && validTaskIndex(TaskIndex); TaskIndex++)
  {
    const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(TaskIndex);

    if (validDeviceIndex(DeviceIndex))
    {
      const unsigned long taskInterval = Settings.TaskDeviceTimer[TaskIndex];
      //LoadTaskSettings(TaskIndex);
      addHtml('{', '\n');

      unsigned long ttl_json = 60; // Default value

      // For simplicity, do the optional values first.
      const uint8_t valueCount = getValueCountForTask(TaskIndex);

      if (valueCount != 0) {
        if (Settings.TaskDeviceEnabled[TaskIndex]) {
          if (taskInterval == 0) {
            ttl_json = 1;
          } else {
            ttl_json = taskInterval;
          }

          if (ttl_json < lowest_ttl_json) {
            lowest_ttl_json = ttl_json;
          }
        }
        addHtml(F("\"TaskValues\": [\n"));

        struct EventStruct TempEvent(TaskIndex);

        for (uint8_t x = 0; x < valueCount; x++)
        {
          uint8_t nrDecimals = Cache.getTaskDeviceValueDecimals(TaskIndex, x);
          String  value      = formatUserVarNoCheck(&TempEvent, x);
          #if FEATURE_STRING_VARIABLES
          bool hasPresentation;
          const String presentation = formatUserVarForPresentation(&TempEvent, x, hasPresentation, value, DeviceIndex);
          #endif // if FEATURE_STRING_VARIABLES
          
          if (mustConsiderAsJSONString(value)) {
            // Flag as not to treat as a float
            nrDecimals = 255;
          }
          #if FEATURE_TASKVALUE_UNIT_OF_MEASURE
          String uom;
          const uint8_t uomIndex = Cache.getTaskVarUnitOfMeasure(TaskIndex, x);
          if (uomIndex != 0) {
            uom = toUnitOfMeasureName(uomIndex);
          }
          #else // if FEATURE_TASKVALUE_UNIT_OF_MEASURE
          const String uom;
          #endif // if FEATURE_TASKVALUE_UNIT_OF_MEASURE
          handle_json_stream_task_value_data(x + 1,
                                             Cache.getTaskDeviceValueName(TaskIndex, x),
                                             nrDecimals,
                                             value,
                                             #if FEATURE_STRING_VARIABLES
                                             presentation,
                                             #else // if FEATURE_STRING_VARIABLES
                                             EMPTY_STRING,
                                             #endif // if FEATURE_STRING_VARIABLES
                                             uom,
                                             x < (valueCount - 1));
        }
        #if FEATURE_STRING_VARIABLES
        if (Settings.ShowDerivedTaskValues(TaskIndex)) {
          int varNr = VARS_PER_TASK;
          String taskName = getTaskDeviceName(TaskIndex);
          taskName.toLowerCase();
          String postfix;
          const String search = getDerivedValueSearchAndPostfix(taskName, postfix);

          auto it = customStringVar.begin();
          while (it != customStringVar.end()) {
            if (it->first.startsWith(search) && it->first.endsWith(postfix)) {
              String valueName = it->first.substring(search.length(), it->first.indexOf('-'));
              String uom;
              String vType;
              const String vname2 = getDerivedValueNameUomAndVType(taskName, valueName, uom, vType);
              if (!vname2.isEmpty()) {
                valueName = vname2;
              }
              if (!it->second.isEmpty()) {
                String value(it->second);
                stripEscapeCharacters(value);
                value = parseTemplate(value);
                uint8_t nrDecimals = 255; // FIXME Use the minimal number of decimals needed
                bool hasPresentation;
                const String presentation = formatUserVarForPresentation(&TempEvent, INVALID_TASKVAR_INDEX, hasPresentation, value, DeviceIndex, valueName);

                stream_comma_newline(); // Push out a comma and newline
                handle_json_stream_task_value_data(varNr + 1,
                                                  valueName,
                                                  nrDecimals,
                                                  value,
                                                  presentation,
                                                  uom,
                                                  false); // No comma here
                ++varNr;
              }
            }
            else if (it->first.substring(0, search.length()).compareTo(search) > 0) {
              break;
            }
            ++it;
          }
        }
        #endif // if FEATURE_STRING_VARIABLES
        addHtml(F("],\n"));
      }

#if FEATURE_PLUGIN_STATS && FEATURE_CHART_JS
      if (showPluginStats && Device[DeviceIndex].PluginStats) {
        PluginTaskData_base *taskData = getPluginTaskDataBaseClassOnly(TaskIndex);
        if (taskData != nullptr && taskData->nrSamplesPresent() > 0) {
          addHtml(F("\"PluginStats\":\n"));
          taskData->plot_ChartJS(true);
          stream_comma_newline();
        }
      }
#endif


      if (showSpecificTask) {
        stream_next_json_object_value(F("TTL"), ttl_json * 1000);
        #if FEATURE_TASKVALUE_UNIT_OF_MEASURE
        stream_next_json_object_value(F("ShowUoM"), jsonBool(Settings.ShowUnitOfMeasureOnDevicesPage()));
        #endif // if FEATURE_TASKVALUE_UNIT_OF_MEASURE
      }

      if (showDataAcquisition) {
        addHtml(F("\"DataAcquisition\": [\n"));

        for (controllerIndex_t x = 0; x < CONTROLLER_MAX; x++)
        {
          addHtml('{');
          stream_next_json_object_value(F("Controller"), x + 1);
          stream_next_json_object_value(F("IDX"),        Settings.TaskDeviceID[x][TaskIndex]);
          stream_last_json_object_value(F("Enabled"), jsonBool(Settings.TaskDeviceSendData[x][TaskIndex]));

          if (x < (CONTROLLER_MAX - 1)) {
            stream_comma_newline();
          }
        }
        addHtml(F("],\n"));
      }

      if (showTaskDetails) {
        stream_next_json_object_value(F("TaskInterval"),     taskInterval);
        stream_next_json_object_value(F("Type"),             getPluginNameFromDeviceIndex(DeviceIndex));
        stream_next_json_object_value(F("TaskName"),         getTaskDeviceName(TaskIndex));
        stream_next_json_object_value(F("TaskDeviceNumber"), Settings.getPluginID_for_task(TaskIndex).value);
        for(int i = 0; i < 3; i++) {
          if (Settings.TaskDevicePin[i][TaskIndex] >= 0) {
            stream_next_json_object_value(concat(F("TaskDeviceGPIO"), i + 1) , static_cast<int>(Settings.TaskDevicePin[i][TaskIndex]));
          }
        }

        #if FEATURE_I2CMULTIPLEXER
        uint8_t i2cBus = 0;
        #if FEATURE_I2C_MULTIPLE
        i2cBus = Settings.getI2CInterface(TaskIndex);
        #endif
        if (Device[DeviceIndex].Type == DEVICE_TYPE_I2C && isI2CMultiplexerEnabled(i2cBus)) {
          #if FEATURE_I2C_MULTIPLE
          stream_next_json_object_value(F("I2C_Interface"), static_cast<int>(i2cBus + 1));
          #endif
          int8_t channel = Settings.I2C_Multiplexer_Channel[TaskIndex];
          if (bitRead(Settings.I2C_Flags[TaskIndex], I2C_FLAGS_MUX_MULTICHANNEL)) {
            addHtml(F("\"I2CBus\" : ["));
            uint8_t b = 0;
            for (uint8_t c = 0; c < I2CMultiplexerMaxChannels(i2cBus); ++c) {
              if (bitRead(channel, c)) {
                if (b > 0) { stream_comma_newline(); }
                b++;
                addHtml(F("\"Multiplexer channel "));
                addHtmlInt(c);
                addHtml('"');
              }
            }
            addHtml(F("],\n"));
          } else {
            if (channel == -1){
              stream_next_json_object_value(F("I2Cbus"),       F("Standard I2C bus"));
            } else {
              String i2cChannel = concat(F("Multiplexer channel "), channel);
              stream_next_json_object_value(F("I2Cbus"),       i2cChannel);
            }
          }
        }
        #endif // if FEATURE_I2CMULTIPLEXER
      }
      stream_next_json_object_value(F("TaskEnabled"), 
        // jsonBool(Settings.TaskDeviceEnabled[TaskIndex].enabled));
        jsonBool(Settings.TaskDeviceEnabled[TaskIndex]));

      stream_last_json_object_value(F("TaskNumber"), TaskIndex + 1);

      if (TaskIndex != lastActiveTaskIndex) {
        addHtml(',');
      }
      addHtml('\n');
    }
  }

  if (!showSpecificTask) {
    addHtml(F("],\n"));
    #if FEATURE_TASKVALUE_UNIT_OF_MEASURE
    stream_next_json_object_value(F("ShowUoM"), jsonBool(Settings.ShowUnitOfMeasureOnDevicesPage()));
    #endif // if FEATURE_TASKVALUE_UNIT_OF_MEASURE
    stream_last_json_object_value(F("TTL"), lowest_ttl_json * 1000);
  }

  TXBuffer.endStream();
  STOP_TIMER(HANDLE_SERVING_WEBPAGE_JSON);
}

void handle_json_stream_task_value_data(uint16_t       valueNumber,
                                        const String & valueName,
                                        uint8_t        nrDecimals,
                                        const String & value,
                                        const String & presentation,
                                        const String & uom,
                                        bool           appendComma) {
  addHtml('{');
  stream_next_json_object_value(F("ValueNumber"), valueNumber);
  stream_next_json_object_value(F("Name"),        valueName);
  stream_next_json_object_value(F("NrDecimals"),  nrDecimals);
  #if FEATURE_STRING_VARIABLES
  if (!presentation.isEmpty()) {
    stream_next_json_object_value(F("Presentation"), presentation);
  }
  #endif // if FEATURE_STRING_VARIABLES
  if (!uom.isEmpty()) {
    stream_next_json_object_value(F("UoM"), uom);
  }
  stream_last_json_object_value(F("Value"),       value);

  if (appendComma) {
    stream_comma_newline();
  }
}
// ********************************************************************************
// JSON formatted timing statistics
// ********************************************************************************

#ifdef WEBSERVER_NEW_UI
void handle_timingstats_json() {
  TXBuffer.startJsonStream();
  json_init();
  json_open();
  # if FEATURE_TIMING_STATS
  jsonStatistics(false);
  # endif // if FEATURE_TIMING_STATS
  json_close();
  TXBuffer.endStream();
}

#endif // WEBSERVER_NEW_UI

#ifdef WEBSERVER_NEW_UI

#if FEATURE_ESPEASY_P2P
void handle_nodes_list_json() {
  if (!isLoggedIn()) { return; }
  TXBuffer.startJsonStream();
  json_init();
  json_open(true);

  for (auto it = Nodes.begin(); it != Nodes.end(); ++it)
  {
    if (it->second.ip[0] != 0)
    {
      json_open();
      bool isThisUnit = it->first == Settings.Unit;

      if (isThisUnit) {
        json_number(F("thisunit"), String(1));
      }

      json_number(F("first"), String(it->first));
      json_prop(F("name"), isThisUnit ? Settings.getName() : it->second.getNodeName());

      if (it->second.build) { json_prop(F("build"), formatSystemBuildNr(it->second.build)); }
      json_prop(F("type"), it->second.getNodeTypeDisplayString());
      json_prop(F("ip"),   formatIP(it->second.ip));
      json_number(F("age"), String(it->second.getAge() / 1000)); // time in seconds
      json_close();
    }
  }
  json_close(true);
  TXBuffer.endStream();
}
#endif

void handle_buildinfo() {
  if (!isLoggedIn()) { return; }
  TXBuffer.startJsonStream();
  json_init();
  json_open();
  {
    json_open(true, F("plugins"));

    for (deviceIndex_t x; x <= getDeviceCount(); x++) {
      const pluginID_t pluginID = getPluginID_from_DeviceIndex(x);
      if (validPluginID(pluginID)) {
        json_open();
        json_number(F("id"), String(pluginID));
        json_prop(F("name"), getPluginNameFromDeviceIndex(x));
        json_close();
      }
    }
    json_close(true);
  }
  {
    json_open(true, F("controllers"));

    for (protocolIndex_t x = 0; x < getHighestIncludedCPluginID(); x++) {
      if (getCPluginID_from_ProtocolIndex(x) != INVALID_C_PLUGIN_ID) {
        json_open();
        json_number(F("id"), String(x + 1));
        json_prop(F("name"), getCPluginNameFromProtocolIndex(x));
        json_close();
      }
    }
    json_close(true);
  }
#if FEATURE_NOTIFIER
  {
    json_open(true, F("notifications"));

    for (uint8_t x = 0; x < NPLUGIN_MAX; x++) {
      if (validNPluginID(NPlugin_id[x])) {
        json_open();
        json_number(F("id"), String(x + 1));
        json_prop(F("name"), getNPluginNameFromNotifierIndex(x));
        json_close();
      }
    }
    json_close(true);
  }
#endif
  json_prop(LabelType::BUILD_DESC);
  json_prop(LabelType::GIT_BUILD);
  json_prop(LabelType::SYSTEM_LIBRARIES);
#ifdef ESP32
  json_prop(LabelType::ESP_IDF_SDK_VERSION);
#endif
  json_prop(LabelType::PLUGIN_COUNT);
  json_prop(LabelType::PLUGIN_DESCRIPTION);
  json_close();
  TXBuffer.endStream();
}

#endif // WEBSERVER_NEW_UI


/*********************************************************************************************\
   Streaming versions directly to TXBuffer
\*********************************************************************************************/
void stream_to_json_object_value(const __FlashStringHelper *  object, const String& value) {
  stream_to_json_object_value(String(object), value);
}

void stream_to_json_object_value(const String& object, const String& value) {
  addHtml(wrap_String(object, '"', '"'));
  addHtml(':');
  addHtml(to_json_value(value));
}

void stream_to_json_object_value(const __FlashStringHelper *  object, int value) {
  stream_to_json_object_value(String(object), value);
}

void stream_to_json_object_value(const String& object, int value) {
  addHtml(wrap_String(object, '"', '"'));
  addHtml(':');
  addHtmlInt(value);
}

String jsonBool(bool value) {
  return boolToString(value);
}


// Add JSON formatted data directly to the TXbuffer, including a trailing comma.
void stream_next_json_object_value(const __FlashStringHelper * object, const String& value) {
  stream_to_json_object_value(object, value);
  stream_comma_newline();
}

void stream_next_json_object_value(const __FlashStringHelper * object, String&& value) {
  stream_to_json_object_value(object, value);
  stream_comma_newline();
}

void stream_next_json_object_value(const String& object, const String& value) {
  stream_to_json_object_value(object, value);
  stream_comma_newline();
}

void stream_next_json_object_value(const __FlashStringHelper * object, int value) {
  stream_to_json_object_value(object, value);
  stream_comma_newline();
}

void stream_next_json_object_value(const String& object, int value) {
  stream_to_json_object_value(object, value);
  stream_comma_newline();
}

void stream_newline_close_brace() {
  addHtml('\n', '}');
}


// Add JSON formatted data directly to the TXbuffer, including a closing '}'
void stream_last_json_object_value(const __FlashStringHelper * object, const String& value) {
  stream_to_json_object_value(object, value);
  stream_newline_close_brace();
}

void stream_last_json_object_value(const __FlashStringHelper * object, String&& value) {
  stream_to_json_object_value(object, value);
  stream_newline_close_brace();
}

void stream_last_json_object_value(const String& object, const String& value) {
  stream_to_json_object_value(object, value);
  stream_newline_close_brace();
}

void stream_last_json_object_value(const __FlashStringHelper * object, int value) {
  stream_to_json_object_value(object, value);
  stream_newline_close_brace();
}

void stream_json_object_values(const LabelType::Enum labels[])
{
  size_t i = 0;
  LabelType::Enum cur  = static_cast<const LabelType::Enum>(pgm_read_byte(labels + i));

  while (true) {
    const LabelType::Enum next = static_cast<const LabelType::Enum>(pgm_read_byte(labels + i + 1));
    const bool nextIsLast      = next == LabelType::MAX_LABEL;

    if (nextIsLast) {
      stream_last_json_object_value(cur);
      return;
    } else {
      stream_next_json_object_value(cur);
    }
    ++i;
    cur = next;
  }
}

void stream_next_json_object_value(LabelType::Enum label) {
  stream_next_json_object_value(getLabel(label), getValue(label));
}

void stream_last_json_object_value(LabelType::Enum label) {
  stream_last_json_object_value(getLabel(label), getValue(label));
}
#include "../WebServer/CustomPage.h"

#ifdef WEBSERVER_CUSTOM

#include "../WebServer/ESPEasy_WebServer.h"
#include "../WebServer/AccessControl.h"
#include "../WebServer/HTML_wrappers.h"
#include "../WebServer/Markup.h"
#include "../WebServer/Markup_Forms.h"

#include "../Commands/ExecuteCommand.h"
#include "../Globals/Nodes.h"
#include "../Globals/Device.h"
#include "../Globals/Plugins.h"
#include "../Globals/Settings.h"

#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/StringParser.h"

#include "../../_Plugin_Helper.h"

// ********************************************************************************
// Web Interface custom page handler
// ********************************************************************************
bool handle_custom(const String& path) {
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_custom"));
  #endif

  if (!clientIPallowed()) { return false; }

  // create a dynamic custom page, parsing task values into [<taskname>#<taskvalue>] placeholders and parsing %xx% system variables
  fs::File   dataFile      = tryOpenFile(path.c_str(), "r");
  const bool dashboardPage = path.startsWith(F("dashboard")) || path.startsWith(F("/dashboard"));

  if (!dataFile && !dashboardPage) {
    return false;    // unknown file that does not exist...
  }

  #if FEATURE_ESPEASY_P2P
  if (dashboardPage) // for the dashboard page, create a default unit dropdown selector
  {
    // handle page redirects to other unit's as requested by the unit dropdown selector
    uint8_t unit    = getFormItemInt(F("unit"));
    uint8_t btnunit = getFormItemInt(F("btnunit"));

    if (!unit) { unit = btnunit; // unit element prevails, if not used then set to btnunit
    }

    navMenuIndex = MENU_INDEX_CUSTOM_PAGE;
    if (unit && (unit != Settings.Unit))
    {
      auto it = Nodes.find(unit);

      if (it != Nodes.end()) {
        TXBuffer.startStream();
        sendHeadandTail(F("TmplDsh"), _HEAD);
        addHtml(F("<meta http-equiv=\"refresh\" content=\"0; URL=http://"));
        addHtml(formatIP(it->second.IP()));
        addHtml(F("/dashboard.esp\">"));
        sendHeadandTail(F("TmplDsh"), _TAIL);
        TXBuffer.endStream();
        return true;
      }
    }

    TXBuffer.startStream();
    sendHeadandTail(F("TmplDsh"), _HEAD);
    html_add_JQuery_script();

    #if FEATURE_CHART_JS
    html_add_ChartJS_script();
    #endif // if FEATURE_CHART_JS
    
    #if FEATURE_RULES_EASY_COLOR_CODE
    html_add_Easy_color_code_script();
    #endif

    html_add_autosubmit_form();
    html_add_form();

    // create unit selector dropdown
    addSelector_Head_reloadOnChange(F("unit"));
    uint8_t choice = Settings.Unit;

    for (auto it = Nodes.begin(); it != Nodes.end(); ++it)
    {
      if ((it->second.ip[0] != 0) || (it->first == Settings.Unit))
      {
        String name = String(it->first) + F(" - ");

        if (it->first != Settings.Unit) {
          name += it->second.getNodeName();
        }
        else {
          name += Settings.getName();
        }
        addSelector_Item(name, it->first, choice == it->first);
      }
    }
    addSelector_Foot(true);

    // create <> navigation buttons
    uint8_t prev = Settings.Unit;
    uint8_t next = Settings.Unit;

    for (uint8_t x = Settings.Unit - 1; x > 0; x--) {
      auto it = Nodes.find(x);

      if (it != Nodes.end()) {
        if (it->second.ip[0] != 0) { prev = x; break; }
      }
    }

    for (uint8_t x = Settings.Unit + 1; x < UNIT_NUMBER_MAX; x++) {
      auto it = Nodes.find(x);

      if (it != Nodes.end()) {
        if (it->second.ip[0] != 0) { next = x; break; }
      }
    }

    html_add_button_prefix();
    addHtml(path);
    addHtml(F("?btnunit="));
    addHtmlInt(prev);
    addHtml(F("'>&lt;</a>"));
    html_add_button_prefix();
    addHtml(path);
    addHtml(F("?btnunit="));
    addHtmlInt(next);
    addHtml(F("'>&gt;</a>"));
  }
  #endif

  // handle commands from a custom page
  String webrequest = webArg(F("cmd"));

  if (webrequest.length() > 0) {
    ExecuteCommand_all_config({EventValueSource::Enum::VALUE_SOURCE_HTTP, webrequest.c_str()});

    // handle some update processes first, before returning page update...
    String dummy;
    PluginCall(PLUGIN_TEN_PER_SECOND, 0, dummy);
  }


  if (dataFile)
  {
    if (!dashboardPage) {
      TXBuffer.startStream(); // Start streaming as it hasn't been started yet, to avoid HTTP/0.9 fallback response
    }
    // Read the file per line and serve per line to reduce amount of memory needed.
    size_t available = dataFile.available();
    String line;
    line.reserve(128);
    while (available > 0) {
      size_t chunksize = 64;
      if (available < chunksize) {
        chunksize = available;
      }
      uint8_t buf[64] = {0};
      const size_t read = dataFile.read(buf, chunksize);
      if (read == chunksize) {
        for (size_t i = 0; i < chunksize; ++i) {
          const char c = (char)buf[i];
          line += c;
          if (c == '\n') {
            addHtml(parseTemplate(line));
            line.clear();
            line.reserve(128);
          }
        }
        available = dataFile.available();
      } else {
        available = 0;
      }
    }
    if (!line.isEmpty()) {
      addHtml(parseTemplate(line));
    }
    dataFile.close();
  }
  else // if the requestef file does not exist, create a default action in case the page is named "dashboard*"
  {
    if (dashboardPage)
    {
      // if the custom page does not exist, create a basic task value overview page in case of dashboard request...
      addHtml(F(
                "<meta name='viewport' content='width=width=device-width, initial-scale=1'><STYLE>* {font-family:sans-serif; font-size:16pt;}.button {margin:4px; padding:4px 16px; background-color:#07D; color:#FFF; text-decoration:none; border-radius:4px}</STYLE>"));
      html_table_class_normal();


      for (taskIndex_t x = 0; x < TASKS_MAX; x++)
      {
        if (validPluginID_fullcheck(Settings.getPluginID_for_task(x)))
        {
          const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(x);

          if (validDeviceIndex(DeviceIndex)) {
            html_TR_TD();
            addHtml(getTaskDeviceName(x));

            const uint8_t valueCount = getValueCountForTask(x);

            struct EventStruct TempEvent(x);
            for (uint8_t varNr = 0; varNr < valueCount; varNr++)
            {
              const String taskValueName = Cache.getTaskDeviceValueName(x, varNr);
              if (!taskValueName.isEmpty())
              {
                if (varNr > 0) {
                  html_TR_TD();
                }
                html_TD();
                addHtml(taskValueName);
                html_TD();
                addHtml(formatUserVarNoCheck(&TempEvent, varNr));
              }
            }
          }
        }
      }
    }
  }
  sendHeadandTail(F("TmplDsh"), _TAIL);
  TXBuffer.endStream();
  return true;
}

#endif
#include "../WebServer/HardwarePage.h"

#ifdef WEBSERVER_HARDWARE

#include "../WebServer/ESPEasy_WebServer.h"
#include "../WebServer/HTML_wrappers.h"
#include "../WebServer/Markup.h"
#include "../WebServer/Markup_Buttons.h"
#include "../WebServer/Markup_Forms.h"

#include "../CustomBuild/ESPEasyLimits.h"

#include "../DataStructs/DeviceStruct.h"

#include "../Globals/Settings.h"

#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/Hardware_GPIO.h"
#include "../Helpers/Hardware_I2C.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringGenerator_GPIO.h"

#if FEATURE_I2C_MULTIPLE
#include "../Helpers/I2C_access.h"
#include "../Helpers/Hardware_device_info.h"
#endif // if FEATURE_I2C_MULTIPLE

// ********************************************************************************
// Web Interface hardware page
// ********************************************************************************
void handle_hardware() {
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_hardware"));
  #endif

  if (!isLoggedIn()) { return; }
  navMenuIndex = MENU_INDEX_HARDWARE;
  TXBuffer.startStream();
  sendHeadandTail_stdtemplate(_HEAD);

  if (isFormItem(F("pled"))) {
    String error;
    Settings.Pin_status_led           = getFormItemInt(F("pled"));
    Settings.Pin_status_led_Inversed  = isFormItemChecked(F("pledi"));
    Settings.Pin_Reset                = getFormItemInt(F("pres"));
    #if FEATURE_PLUGIN_PRIORITY
    if (!isI2CPriorityTaskActive(0))
    #endif //if FEATURE_PLUGIN_PRIORITY
    {
      update_whenset_FormItemInt(F("psda0"), Settings.Pin_i2c_sda);
      update_whenset_FormItemInt(F("pscl0"), Settings.Pin_i2c_scl);
    }
    Settings.I2C_clockSpeed           = getFormItemInt(F("pi2csp0"), DEFAULT_I2C_CLOCK_SPEED);
    Settings.I2C_clockSpeed_Slow      = getFormItemInt(F("pi2cspslow0"), DEFAULT_I2C_CLOCK_SPEED_SLOW);
    Settings.WireClockStretchLimit    = getFormItemInt(F("wirestretch"));
    #if FEATURE_I2CMULTIPLEXER
    Settings.I2C_Multiplexer_Type     = getFormItemInt(F("pi2cmuxtype0"));
    if (Settings.I2C_Multiplexer_Type != I2C_MULTIPLEXER_NONE) {
      Settings.I2C_Multiplexer_Addr   = getFormItemInt(F("pi2cmuxaddr0"));
    } else {
      Settings.I2C_Multiplexer_Addr   = -1;
    }
    Settings.I2C_Multiplexer_ResetPin = getFormItemInt(F("pi2cmuxreset0"));
    #endif // if FEATURE_I2CMULTIPLEXER
    #if FEATURE_I2C_MULTIPLE // No loop used here, to avoid adding setters to the SettingsStruct template code
    if (getI2CBusCount() > 1) {
      #if FEATURE_PLUGIN_PRIORITY
      if (!isI2CPriorityTaskActive(1))
      #endif //if FEATURE_PLUGIN_PRIORITY
      {
        update_whenset_FormItemInt(F("psda1"), Settings.Pin_i2c2_sda);
        update_whenset_FormItemInt(F("pscl1"), Settings.Pin_i2c2_scl);
      }
      Settings.I2C2_clockSpeed            = getFormItemInt(F("pi2csp1"),     DEFAULT_I2C_CLOCK_SPEED);
      Settings.I2C2_clockSpeed_Slow       = getFormItemInt(F("pi2cspslow1"), DEFAULT_I2C_CLOCK_SPEED_SLOW);
      #if FEATURE_I2CMULTIPLEXER
      Settings.I2C2_Multiplexer_Type      = getFormItemInt(F("pi2cmuxtype1"));
      if (Settings.I2C2_Multiplexer_Type != I2C_MULTIPLEXER_NONE) {
        Settings.I2C2_Multiplexer_Addr    = getFormItemInt(F("pi2cmuxaddr1"));
      } else {
        Settings.I2C2_Multiplexer_Addr    = -1;
      }
      Settings.I2C2_Multiplexer_ResetPin  = getFormItemInt(F("pi2cmuxreset1"));
      #endif // if FEATURE_I2CMULTIPLEXER
    }
    #if FEATURE_I2C_INTERFACE_3
    if (getI2CBusCount() > 2) {
      #if FEATURE_PLUGIN_PRIORITY
      if (!isI2CPriorityTaskActive(2))
      #endif //if FEATURE_PLUGIN_PRIORITY
      {
        update_whenset_FormItemInt(F("psda2"), Settings.Pin_i2c3_sda);
        update_whenset_FormItemInt(F("pscl2"), Settings.Pin_i2c3_scl);
      }
      Settings.I2C3_clockSpeed            = getFormItemInt(F("pi2csp2"),     DEFAULT_I2C_CLOCK_SPEED);
      Settings.I2C3_clockSpeed_Slow       = getFormItemInt(F("pi2cspslow2"), DEFAULT_I2C_CLOCK_SPEED_SLOW);
      #if FEATURE_I2CMULTIPLEXER
      Settings.I2C3_Multiplexer_Type      = getFormItemInt(F("pi2cmuxtype2"));
      if (Settings.I2C3_Multiplexer_Type != I2C_MULTIPLEXER_NONE) {
        Settings.I2C3_Multiplexer_Addr    = getFormItemInt(F("pi2cmuxaddr2"));
      } else {
        Settings.I2C3_Multiplexer_Addr    = -1;
      }
      Settings.I2C3_Multiplexer_ResetPin  = getFormItemInt(F("pi2cmuxreset2"));
      #endif // if FEATURE_I2CMULTIPLEXER
    }
    #endif // if FEATURE_I2C_INTERFACE_3
    set3BitToUL(Settings.I2C_peripheral_bus, I2C_PERIPHERAL_BUS_PCFMCP, getFormItemInt(F("pi2cbuspcf")));
    #endif // if FEATURE_I2C_MULTIPLE
    #ifdef ESP32
      Settings.InitSPI                = getFormItemInt(F("initspi"), static_cast<int>(SPI_Options_e::None));
      if (Settings.InitSPI == static_cast<int>(SPI_Options_e::UserDefined)) { // User-define SPI GPIO pins
        Settings.SPI_SCLK_pin         = getFormItemInt(F("spipinsclk"), -1);
        Settings.SPI_MISO_pin         = getFormItemInt(F("spipinmiso"), -1);
        Settings.SPI_MOSI_pin         = getFormItemInt(F("spipinmosi"), -1);
        if (!Settings.isSPI_valid()) { // Checks
          error += F("User-defined SPI pins not configured correctly!\n");
        }
      }
    #else //for ESP8266 we keep the old UI
      Settings.InitSPI                = isFormItemChecked(F("initspi")); // SPI Init
    #endif
    Settings.Pin_sd_cs                = getFormItemInt(F("sd"));
    #if FEATURE_ETHERNET
    Settings.ETH_Phy_Addr             = getFormItemInt(F("ethphy"));
    Settings.ETH_Pin_mdc_cs              = getFormItemInt(F("ethmdc"));
    Settings.ETH_Pin_mdio_irq             = getFormItemInt(F("ethmdio"));
    Settings.ETH_Pin_power_rst            = getFormItemInt(F("ethpower"));
    Settings.ETH_Phy_Type             = static_cast<EthPhyType_t>(getFormItemInt(F("ethtype")));
#if CONFIG_ETH_USE_ESP32_EMAC
    Settings.ETH_Clock_Mode           = static_cast<EthClockMode_t>(getFormItemInt(F("ethclock")));
#endif
    Settings.NetworkMedium            = static_cast<NetworkMedium_t>(getFormItemInt(F("ethwifi")));
    #endif // if FEATURE_ETHERNET
    int gpio = 0;

    while (gpio <= MAX_GPIO) {
      if (isSerialConsolePin(gpio)) {
        // do not add the pin state select for these pins.
      } else {
        if (validGpio(gpio)) {
          String int_pinlabel('p');
          int_pinlabel       += gpio;
          Settings.setPinBootState(gpio, static_cast<PinBootState>(getFormItemInt(int_pinlabel)));
        }
      }
      ++gpio;
    }
    error += SaveSettings();
    addHtmlError(error);
    if (error.isEmpty()) {
      // Apply I2C settings.
      initI2C();
    }
  }

  addHtml(F("<form  method='post'>"));
  html_table_class_normal();
  addFormHeader(F("Hardware Settings"), F(""), F("Hardware/Hardware.html"));

  addFormSubHeader(F("Wifi Status LED"));
  addFormPinSelect(PinSelectPurpose::Status_led, formatGpioName_output(F("LED")), F("pled"), Settings.Pin_status_led);
  addFormCheckBox(F("Inversed LED"), F("pledi"), Settings.Pin_status_led_Inversed);
  addFormNote(F("Use &rsquo;GPIO-2 (D4)&rsquo; with &rsquo;Inversed&rsquo; checked for onboard LED"));

  addFormSubHeader(F("Reset Pin"));
  addFormPinSelect(PinSelectPurpose::Reset_pin, formatGpioName_input(F("Switch")), F("pres"), Settings.Pin_Reset);
  addFormNote(F("Press about 10s for factory reset"));

  #if FEATURE_I2CMULTIPLEXER
  const __FlashStringHelper *i2c_muxtype_options[] = {
    F("- None -"),
    F("TCA9548a - 8 channel"),
    F("TCA9546a - 4 channel"),
    F("TCA9543a - 2 channel"),
    F("PCA9540 - 2 channel (experimental)")
  };
  const int i2c_muxtype_choices[] = {
    I2C_MULTIPLEXER_NONE,
    I2C_MULTIPLEXER_TCA9548A,
    I2C_MULTIPLEXER_TCA9546A,
    I2C_MULTIPLEXER_TCA9543A,
    I2C_MULTIPLEXER_PCA9540
  };
  const FormSelectorOptions muxSelector(NR_ELEMENTS(i2c_muxtype_choices),
                  i2c_muxtype_options, i2c_muxtype_choices);

// Select the I2C address for a port multiplexer

  String  i2c_mux_options[9];
  int     i2c_mux_choices[9];
  uint8_t mux_opt = 0;
  i2c_mux_options[mux_opt] = F("- None -");
  i2c_mux_choices[mux_opt] = I2C_MULTIPLEXER_NONE;
  mux_opt++;
  for (int8_t x = 0; x < 8; x++) {
    i2c_mux_options[mux_opt] = formatToHex_decimal(0x70 + x);
    if (x == 0) { // PCA9540 has a fixed address 0f 0x70
      i2c_mux_options[mux_opt] += F(" [TCA9543a/6a/8a, PCA9540]");
    } else if (x < 4) {
      i2c_mux_options[mux_opt] += F(" [TCA9543a/6a/8a]");
    } else {
      i2c_mux_options[mux_opt] += F(" [TCA9546a/8a]");
    }
    i2c_mux_choices[mux_opt] = 0x70 + x;
    mux_opt++;
  }
  const FormSelectorOptions addrSelector(mux_opt, i2c_mux_options, i2c_mux_choices);
  #endif // if FEATURE_I2CMULTIPLEXER

  uint8_t i2cBus = 0;
  #if FEATURE_I2C_MULTIPLE
  for (uint8_t i2cBus = 0; i2cBus < getI2CBusCount(); ++i2cBus)
  #endif // if FEATURE_I2C_MULTIPLE
  {
    #if !FEATURE_I2C_MULTIPLE
    addFormSubHeader(F("I2C Bus"));
    #else // if !FEATURE_I2C_MULTIPLE
    addFormSubHeader(strformat(F("I2C Bus %u"), i2cBus));
    #endif // if !FEATURE_I2C_MULTIPLE
    #if FEATURE_PLUGIN_PRIORITY
    if (isI2CPriorityTaskActive(i2cBus)) {
      I2CShowSdaSclReadonly(Settings.getI2CSdaPin(i2cBus), Settings.getI2CSclPin(i2cBus), i2cBus);
    } else
    #endif // if FEATURE_PLUGIN_PRIORITY
    {
      addFormPinSelectI2C(formatGpioName_bidirectional(F("SDA")), strformat(F("psda%u"), i2cBus), i2cBus, Settings.getI2CSdaPin(i2cBus));
      addFormPinSelectI2C(formatGpioName_output(F("SCL")),        strformat(F("pscl%u"), i2cBus), i2cBus, Settings.getI2CSclPin(i2cBus));
    }
    addFormNumericBox(F("Clock Speed"), strformat(F("pi2csp%u"), i2cBus), Settings.getI2CClockSpeed(i2cBus), 100, 3400000);
    addUnit(F("Hz"));
    addFormNote(F("Use 100 kHz for old I2C devices, 400 kHz is max for most."));
    addFormNumericBox(F("Slow device Clock Speed"), strformat(F("pi2cspslow%u"), i2cBus), Settings.getI2CClockSpeedSlow(i2cBus), 100, 3400000);
    addUnit(F("Hz"));

    if (0 == i2cBus) { // Only support Clock-stretching on Bus 1
      addFormNumericBox(F("I2C ClockStretchLimit"), F("wirestretch"), Settings.WireClockStretchLimit, 0);
      #ifdef ESP8266
      addUnit(F("usec"));
      #endif
      #ifdef ESP32
      addUnit(F("1/80 usec"));
      #endif
    }

    #if FEATURE_I2CMULTIPLEXER
    #if !FEATURE_I2C_MULTIPLE
    addFormSubHeader(F("I2C Multiplexer"));
    #else // if !FEATURE_I2C_MULTIPLE
    addFormSubHeader(strformat(F("I2C Multiplexer %u"), i2cBus));
    #endif // if FEATURE_I2C_MULTIPLE
    // Select the type of multiplexer to use
    {
      muxSelector.addFormSelector(F("I2C Multiplexer type"), strformat(F("pi2cmuxtype%u"), i2cBus), Settings.getI2CMultiplexerType(i2cBus));
      addrSelector.addFormSelector(F("I2C Multiplexer address"), strformat(F("pi2cmuxaddr%u"), i2cBus),  Settings.getI2CMultiplexerAddr(i2cBus));
      // addFormPinSelect(PinSelectPurpose::Generic_output, formatGpioName_output_optional(F("Reset")), strformat(F("pi2cmuxreset%u"), i2cBus), Settings.getI2CMultiplexerResetPin(i2cBus));
      const String id = strformat(F("pi2cmuxreset%u"), i2cBus);
      addRowLabel_tr_id(formatGpioName_output_optional(F("Reset")), id);
      addPinSelect(PinSelectPurpose::Generic_output, id, Settings.getI2CMultiplexerResetPin(i2cBus));
      addFormNote(F("Will be pulled low to force a reset. Reset is not available on PCA9540."));
    }
    #endif // if FEATURE_I2CMULTIPLEXER
  }
  #if FEATURE_I2C_MULTIPLE
  const uint8_t i2cMaxBusCount = (getI2CBusCount() > 1
                                  ? ((Settings.isI2CEnabled(1) ? 1 : 0)
                                    # if FEATURE_I2C_INTERFACE_3
                                     + (Settings.isI2CEnabled(2) ? 1 : 0)
                                    # endif // if FEATURE_I2C_INTERFACE_3
                                     )
                                  : 0) + (Settings.isI2CEnabled(0) ? 1 : 0);
  if (i2cMaxBusCount > 1) {
    addFormSubHeader(F("PCF &amp; MCP Direct I/O"));
    const uint8_t i2cBus = Settings.getI2CInterfacePCFMCP();
    I2CInterfaceSelector(F("I2C Bus"),
                        F("pi2cbuspcf"),
                        i2cBus,
                        false);

  }
  #endif // if FEATURE_I2C_MULTIPLE

  // SPI Init
  addFormSubHeader(F("SPI Interface"));
  #ifdef ESP32
  {
    // Script to show GPIO pins for User-defined SPI GPIOs
    // html_add_script(F("function spiOptionChanged(elem) {var spipinstyle = elem.value == 9 ? '' : 'none';document.getElementById('tr_spipinsclk').style.display = spipinstyle;document.getElementById('tr_spipinmiso').style.display = spipinstyle;document.getElementById('tr_spipinmosi').style.display = spipinstyle;}"),
    // Minified:
    html_add_script(F("function spiOptionChanged(e){var i=9==e.value?'':'none';"
                      "document.getElementById('tr_spipinsclk').style.display=i,"
                      "document.getElementById('tr_spipinmiso').style.display=i,"
                      "document.getElementById('tr_spipinmosi').style.display=i}"),
                    false);
    const __FlashStringHelper * spi_options[] = {
      getSPI_optionToString(SPI_Options_e::None), 
      getSPI_optionToString(SPI_Options_e::Vspi_Fspi), 
      #ifdef ESP32_CLASSIC
      getSPI_optionToString(SPI_Options_e::Hspi), 
      #endif
      getSPI_optionToString(SPI_Options_e::UserDefined)};
    const int spi_index[] = {
      static_cast<int>(SPI_Options_e::None),
      static_cast<int>(SPI_Options_e::Vspi_Fspi),
      #ifdef ESP32_CLASSIC
      static_cast<int>(SPI_Options_e::Hspi),
      #endif
      static_cast<int>(SPI_Options_e::UserDefined)
    };
    constexpr size_t nrOptions = NR_ELEMENTS(spi_index);
    FormSelectorOptions selector(nrOptions, spi_options, spi_index);
    selector.onChangeCall = F("spiOptionChanged(this)");
    selector.addFormSelector(F("Init SPI"), F("initspi"), Settings.InitSPI);
    // User-defined pins
    addFormPinSelect(PinSelectPurpose::SPI, formatGpioName_output(F("CLK")),  F("spipinsclk"), Settings.SPI_SCLK_pin);
    addFormPinSelect(PinSelectPurpose::SPI_MISO, formatGpioName_input(F("MISO")),  F("spipinmiso"), Settings.SPI_MISO_pin);
    addFormPinSelect(PinSelectPurpose::SPI, formatGpioName_output(F("MOSI")), F("spipinmosi"), Settings.SPI_MOSI_pin);
    html_add_script(F("document.getElementById('initspi').onchange();"), false); // Initial trigger onchange script
    addFormNote(F("Changing SPI settings requires to press the hardware-reset button or power off-on!"));
  }
  #else //for ESP8266 we keep the existing UI
  addFormCheckBox(F("Init SPI"), F("initspi"), Settings.InitSPI > static_cast<int>(SPI_Options_e::None));
  addFormNote(F("CLK=GPIO-14 (D5), MISO=GPIO-12 (D6), MOSI=GPIO-13 (D7)"));
  #endif
  addFormNote(F("Chip Select (CS) config must be done in the plugin"));
  
#if FEATURE_SD
  addFormSubHeader(F("SD Card"));
  addFormPinSelect(PinSelectPurpose::SD_Card, formatGpioName_output(F("SD Card CS")), F("sd"), Settings.Pin_sd_cs);
#endif // if FEATURE_SD
  
#if FEATURE_ETHERNET
  addFormSubHeader(F("Ethernet"));
  addRowLabel_tr_id(F("Preferred network medium"), F("ethwifi"));
  {
    const __FlashStringHelper * ethWifiOptions[2] = {
      toString(NetworkMedium_t::WIFI), 
      toString(NetworkMedium_t::Ethernet) 
      };
    const FormSelectorOptions selector(2, ethWifiOptions);
    selector.addSelector(F("ethwifi"), static_cast<int>(Settings.NetworkMedium));
  }
  addFormNote(F("Change Switch between WiFi and Ethernet requires reboot to activate"));
  {
    const __FlashStringHelper * ethPhyTypes[] = { 
      toString(EthPhyType_t::notSet),			  

# if CONFIG_ETH_USE_ESP32_EMAC
      toString(EthPhyType_t::LAN8720),			  
      toString(EthPhyType_t::TLK110),				  
#if ESP_IDF_VERSION_MAJOR > 3
      toString(EthPhyType_t::RTL8201),				
#if ETH_TYPE_JL1101_SUPPORTED
      toString(EthPhyType_t::JL1101),				  
#endif
      toString(EthPhyType_t::DP83848),				
      toString(EthPhyType_t::KSZ8041),				
      toString(EthPhyType_t::KSZ8081),				
#endif
# endif // if CONFIG_ETH_USE_ESP32_EMAC

#if ESP_IDF_VERSION_MAJOR >= 5
# if CONFIG_ETH_SPI_ETHERNET_DM9051
      toString(EthPhyType_t::DM9051),				  
# endif // if CONFIG_ETH_SPI_ETHERNET_DM9051
# if CONFIG_ETH_SPI_ETHERNET_W5500
      toString(EthPhyType_t::W5500),				  
# endif // if CONFIG_ETH_SPI_ETHERNET_W5500
# if CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL
      toString(EthPhyType_t::KSZ8851),				
# endif // if CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL
#endif
      };
    const int ethPhyTypes_index[] = {
      static_cast<int>(EthPhyType_t::notSet),			  

# if CONFIG_ETH_USE_ESP32_EMAC
      static_cast<int>(EthPhyType_t::LAN8720),			  
      static_cast<int>(EthPhyType_t::TLK110),				  
#if ESP_IDF_VERSION_MAJOR > 3
      static_cast<int>(EthPhyType_t::RTL8201),			
#if ETH_TYPE_JL1101_SUPPORTED	
      static_cast<int>(EthPhyType_t::JL1101),				  
#endif
      static_cast<int>(EthPhyType_t::DP83848),				
      static_cast<int>(EthPhyType_t::KSZ8041),				
      static_cast<int>(EthPhyType_t::KSZ8081),				
#endif
# endif // if CONFIG_ETH_USE_ESP32_EMAC

#if ESP_IDF_VERSION_MAJOR >= 5
# if CONFIG_ETH_SPI_ETHERNET_DM9051
      static_cast<int>(EthPhyType_t::DM9051),				  
# endif // if CONFIG_ETH_SPI_ETHERNET_DM9051
# if CONFIG_ETH_SPI_ETHERNET_W5500
      static_cast<int>(EthPhyType_t::W5500),				  
# endif // if CONFIG_ETH_SPI_ETHERNET_W5500
# if CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL
      static_cast<int>(EthPhyType_t::KSZ8851),				
# endif // if CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL
#endif
    };

    constexpr unsigned nrItems = NR_ELEMENTS(ethPhyTypes_index);


    const int choice = isValid(Settings.ETH_Phy_Type) 
      ? static_cast<int>(Settings.ETH_Phy_Type) 
      : static_cast<int>(EthPhyType_t::notSet);

    const FormSelectorOptions selector(      
      nrItems, 
      ethPhyTypes, 
      ethPhyTypes_index);
    selector.addFormSelector(
      F("Ethernet PHY type"), 
      F("ethtype"),
      choice);
  }

#if CONFIG_ETH_USE_SPI_ETHERNET && CONFIG_ETH_USE_ESP32_EMAC
#define MDC_CS_PIN_DESCR  "Ethernet MDC/CS pin"
#define MIO_IRQ_PIN_DESCR  "Ethernet MDIO/IRQ pin"
#define PWR_RST_PIN_DESCR  "Ethernet Power/RST pin"
#elif CONFIG_ETH_USE_SPI_ETHERNET
#define MDC_CS_PIN_DESCR  "Ethernet CS pin"
#define MIO_IRQ_PIN_DESCR  "Ethernet IRQ pin"
#define PWR_RST_PIN_DESCR  "Ethernet RST pin"
#else // #elif CONFIG_ETH_USE_ESP32_EMAC
#define MDC_CS_PIN_DESCR  "Ethernet MDC pin"
#define MIO_IRQ_PIN_DESCR  "Ethernet MIO pin"
#define PWR_RST_PIN_DESCR  "Ethernet Power pin"
#endif

  addFormNumericBox(F("Ethernet PHY Address"), F("ethphy"), Settings.ETH_Phy_Addr, -1, 127);
  addFormNote(F("I&sup2;C-address of Ethernet PHY"
#if CONFIG_ETH_USE_ESP32_EMAC
  " (0 or 1 for LAN8720, 31 for TLK110, -1 autodetect)"
#endif
  ));
  addFormPinSelect(PinSelectPurpose::Ethernet, formatGpioName_output(
    F(MDC_CS_PIN_DESCR)), 
    F("ethmdc"), Settings.ETH_Pin_mdc_cs);
  addFormPinSelect(PinSelectPurpose::Ethernet, formatGpioName_input(
    F(MIO_IRQ_PIN_DESCR)), 
    F("ethmdio"), Settings.ETH_Pin_mdio_irq);
  addFormPinSelect(PinSelectPurpose::Ethernet, formatGpioName_output(
    F(PWR_RST_PIN_DESCR)), 
    F("ethpower"), Settings.ETH_Pin_power_rst);
#if CONFIG_ETH_USE_ESP32_EMAC
  addRowLabel_tr_id(F("Ethernet Clock"), F("ethclock"));
  {
    const __FlashStringHelper * ethClockOptions[] = { 
      toString(EthClockMode_t::Ext_crystal_osc),
      toString(EthClockMode_t::Int_50MHz_GPIO_0),
      toString(EthClockMode_t::Int_50MHz_GPIO_16),
      toString(EthClockMode_t::Int_50MHz_GPIO_17_inv)
      };
    const FormSelectorOptions selector(NR_ELEMENTS(ethClockOptions), ethClockOptions);
    selector.addSelector(F("ethclock"), static_cast<int>(Settings.ETH_Clock_Mode));
  }
#endif
#endif // if FEATURE_ETHERNET

  addFormSubHeader(F("GPIO boot states"));

  for (int gpio = 0; gpio <= MAX_GPIO; ++gpio) {
    addFormPinStateSelect(gpio, static_cast<int>(Settings.getPinBootState(gpio)));
  }
  addFormSeparator(2);

  html_TR_TD();
  html_TD();
  addSubmitButton();
  html_TR_TD();
  html_end_table();
  html_end_form();

  sendHeadandTail_stdtemplate(_TAIL);
  TXBuffer.endStream();
}

#if FEATURE_PLUGIN_PRIORITY
bool isI2CPriorityTaskActive(uint8_t i2cBus) {
  bool hasI2CPriorityTask = false;
  for (taskIndex_t taskIndex = 0; taskIndex < TASKS_MAX && !hasI2CPriorityTask; taskIndex++) {
    hasI2CPriorityTask |= isPluginI2CPowerManager_from_TaskIndex(taskIndex, i2cBus);
  }
  return hasI2CPriorityTask;
}

void I2CShowSdaSclReadonly(int8_t i2c_sda, int8_t i2c_scl, uint8_t i2cBus) {
  int  pinnr = -1;
  bool input, output, warning = false;
  addFormNote(strformat(F("I2C (%d) GPIO pins can't be changed when an I2C Priority task is configured."), i2cBus));
  addRowLabel(formatGpioName_bidirectional(F("SDA")));
  getGpioInfo(i2c_sda, pinnr, input, output, warning);
  addHtml(createGPIO_label(i2c_sda, pinnr, true, true, false));
  addRowLabel(formatGpioName_output(F("SCL")));
  getGpioInfo(i2c_scl, pinnr, input, output, warning);
  addHtml(createGPIO_label(i2c_scl, pinnr, true, true, false));
}
#endif // if FEATURE_PLUGIN_PRIORITY

#endif // ifdef WEBSERVER_HARDWARE

#include "../WebServer/SettingsArchive.h"

#if FEATURE_SETTINGS_ARCHIVE

# include "../WebServer/ESPEasy_WebServer.h"
# include "../WebServer/HTML_wrappers.h"
# include "../WebServer/Markup.h"
# include "../WebServer/Markup_Buttons.h"
# include "../WebServer/Markup_Forms.h"

# include "../Globals/ResetFactoryDefaultPref.h"

# include "../Helpers/ESPEasy_FactoryDefault.h"
# include "../Helpers/ESPEasy_Storage.h"
# include "../Helpers/Misc.h"
# include "../Helpers/Networking.h"
# include "../Helpers/StringParser.h"


// ********************************************************************************
// Web Interface to manage archived settings
// ********************************************************************************
void handle_settingsarchive() {
  # ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_settingsarchive"));
  # endif // ifndef BUILD_NO_RAM_TRACKER

  if (!isLoggedIn()) { return; }
  navMenuIndex = MENU_INDEX_TOOLS;
  TXBuffer.startStream();
  sendHeadandTail_stdtemplate(_HEAD);
  html_add_form();
  html_table_class_normal();
  html_TR();
  addFormHeader(F("Settings Archive"));

  if (hasArg(F("savepref")) || hasArg(F("download"))) {
    // User choose a pre-defined config and wants to save it as the new default.
    for (int i = 0; i < FileType::MAX_FILETYPE; ++i) {
      const FileType::Enum ft = static_cast<FileType::Enum>(i);

      if (ft == FileType::RULES_TXT) {
        for (int i = 0; i < RULESETS_MAX; ++i) {
          storeDownloadFiletypeCheckbox(FileType::RULES_TXT, i);
        }
      } else {
        storeDownloadFiletypeCheckbox(ft);
      }
    }

    ResetFactoryDefaultPreference.deleteFirst(isFormItemChecked(F("del")));
    ResetFactoryDefaultPreference.delete_Bak_Files(isFormItemChecked(F("del_bak")));
# if FEATURE_CUSTOM_PROVISIONING
    ResetFactoryDefaultPreference.saveURL(isFormItemChecked(F("saveurl")));
    ResetFactoryDefaultPreference.storeCredentials(isFormItemChecked(F("savecred")));
# endif // if FEATURE_CUSTOM_PROVISIONING
    applyFactoryDefaultPref();

    String error;
# if FEATURE_CUSTOM_PROVISIONING
    {
      MakeProvisioningSettings(ProvisioningSettings);

      if (ProvisioningSettings.get()) {
        ProvisioningSettings->ResetFactoryDefaultPreference = ResetFactoryDefaultPreference.getPreference();

        ProvisioningSettings->allowedFlags.allowFetchFirmware = isFormItemChecked(F("firmware"));
        for (int i = 0; i < FileType::MAX_FILETYPE; ++i) {
          const FileType::Enum ft = static_cast<FileType::Enum>(i);

          if (ft == FileType::RULES_TXT) {
            for (int i = 0; i < RULESETS_MAX; ++i) {
              storeAllowFiletypeCheckbox(*ProvisioningSettings, FileType::RULES_TXT, i);
            }
          } else {
            storeAllowFiletypeCheckbox(*ProvisioningSettings, ft);
          }
        }


        if (ResetFactoryDefaultPreference.saveURL()) {
          ProvisioningSettings->setUrl(webArg(F("url")));
        }

        if (ResetFactoryDefaultPreference.storeCredentials()) {
          ProvisioningSettings->setUser(webArg(F("user")));
          ProvisioningSettings->setPass(webArg(F("pass")));
        }
        error = saveProvisioningSettings(*ProvisioningSettings);
      }
    }
# endif // if FEATURE_CUSTOM_PROVISIONING

    error += SaveSettings();
    addHtmlError(error);
  }

  bool showOptions = true;

  if (hasArg(F("download"))) {
    // Try downloading files.
    // Don't use the ProvisioningSettings, as not all may be stored.
    const String url  = webArg(F("url"));
    const String user = webArg(F("user"));
    const String pass = webArg(F("pass"));

    addTableSeparator(F("Download result"), 2, 3);
    bool somethingDownloaded = false;

    for (int i = 0; i < FileType::MAX_FILETYPE; ++i) {
      const FileType::Enum ft = static_cast<FileType::Enum>(i);

      if (ft != FileType::RULES_TXT) {
        if (getDownloadFiletypeChecked(ft, 0)) {
          if (tryDownloadFileType(url, user, pass, ft)) {
            somethingDownloaded = true;
          }
        }
      }
    }

    for (int i = 0; i < RULESETS_MAX; ++i) {
      if (getDownloadFiletypeChecked(FileType::RULES_TXT, i)) {
        if (tryDownloadFileType(url, user, pass, FileType::RULES_TXT, i)) {
          somethingDownloaded = true;
        }
      }
    }

    if (somethingDownloaded) {
      showOptions = false;
      html_TR_TD();
      html_TD();
      addSubmitButton(F("Reboot"), F("reboot"), F("red"));
      addFormNote(F("If settings files are updated you MUST reboot first!"));
    }
  } else if (hasArg(F("reboot"))) {
    showOptions = false;
    reboot(IntendedRebootReason_e::RestoreSettings);
  }

  if (showOptions) {
    // Nothing chosen yet, show options.
    addTableSeparator(F("Archive Location"), 2, 3);

    {
      String url, user, pass;
# if FEATURE_CUSTOM_PROVISIONING
      MakeProvisioningSettings(ProvisioningSettings);

      if (ProvisioningSettings.get()) {
        loadProvisioningSettings(*ProvisioningSettings);
        url  = ProvisioningSettings->url;
        user = ProvisioningSettings->user;
        pass = ProvisioningSettings->pass;
      }
# endif // if FEATURE_CUSTOM_PROVISIONING
      {
        if (webArg(F("url")).length() != 0) {
          url = webArg(F("url"));
        }

        if (webArg(F("user")).length() != 0) {
          user = webArg(F("user"));
        }

        if (webArg(F("pass")).length() != 0) {
          pass = webArg(F("pass"));
        }
      }

      addFormTextBox(F("URL with settings"), F("url"), url, 256);
      addFormNote(F("Only HTTP supported. Do not include filename. URL is allowed to contain system variables."));
# if FEATURE_CUSTOM_PROVISIONING
      addFormCheckBox(F("Store URL"), F("saveurl"), ResetFactoryDefaultPreference.saveURL());
# endif // if FEATURE_CUSTOM_PROVISIONING
      addFormTextBox(F("User"), F("user"), user, 64);
      addFormPasswordBox(F("Pass"), F("pass"), pass, 64);
# if FEATURE_CUSTOM_PROVISIONING
      if (ProvisioningSettings.get()) {
        addFormCheckBox(F("Store Credentials"), F("savecred"), ResetFactoryDefaultPreference.storeCredentials());

        addTableSeparator(F("Allow Fetch by Command"), 2, 3);
        addFormCheckBox(F("Allow Firmware"), F("firmware"), ProvisioningSettings->allowedFlags.allowFetchFirmware);

        for (int i = 0; i < FileType::MAX_FILETYPE; ++i) {
          const FileType::Enum ft = static_cast<FileType::Enum>(i);

          if (ft != FileType::RULES_TXT) {
            addAllowFiletypeCheckbox(*ProvisioningSettings, ft);
          }
        }

        for (int i = 0; i < RULESETS_MAX; ++i) {
          addAllowFiletypeCheckbox(*ProvisioningSettings, FileType::RULES_TXT, i);
        }

        addFormNote(F("Fetch files via a command does need stored URL (+ credentials)"));
      }
# endif // if FEATURE_CUSTOM_PROVISIONING
    }

    addTableSeparator(F("Download Settings"), 2, 3);

    addRowLabel(F("Delete First"));
    addCheckBox(F("del"), ResetFactoryDefaultPreference.deleteFirst());
    addFormNote(F("Needed on filesystem with not enough free space. Use with care!"));

    addRowLabel(F("Delete _bak Files"));
    addCheckBox(F("del_bak"), ResetFactoryDefaultPreference.delete_Bak_Files());

    addTableSeparator(F("Files to Download"), 2, 3);

    for (int i = 0; i < FileType::MAX_FILETYPE; ++i) {
      const FileType::Enum ft = static_cast<FileType::Enum>(i);

      if (ft != FileType::RULES_TXT) {
        addDownloadFiletypeCheckbox(ft);
      }
    }

    for (int i = 0; i < RULESETS_MAX; ++i) {
      addDownloadFiletypeCheckbox(FileType::RULES_TXT, i);
    }

    html_TR_TD();
    html_TD();
    addSubmitButton(F("Save Preferences"), F("savepref"));

    addFormSeparator(2);

    addRowLabel(F("Try download files"));
    addSubmitButton(F("Download"), F("download"), F("red"));
  }

  html_end_table();
  html_end_form();
  sendHeadandTail_stdtemplate(_TAIL);
  TXBuffer.endStream();
}

// ********************************************************************************
// download filetype selectors
// ********************************************************************************
void addDownloadFiletypeCheckbox(FileType::Enum filetype, unsigned int filenr) {
  const String fileName = getFileName(filetype, filenr);
  addRowLabel(fileName);
  addCheckBox(fileName, getDownloadFiletypeChecked(filetype, filenr));
}

void storeDownloadFiletypeCheckbox(FileType::Enum filetype, unsigned int filenr) {
  const bool isChecked = isFormItemChecked(getFileName(filetype, filenr));

  switch (filetype) {
    case FileType::CONFIG_DAT: ResetFactoryDefaultPreference.fetchConfigDat(isChecked); break;
    case FileType::SECURITY_DAT: ResetFactoryDefaultPreference.fetchSecurityDat(isChecked); break;
    case FileType::NOTIFICATION_DAT: ResetFactoryDefaultPreference.fetchNotificationDat(isChecked); break;
    case FileType::RULES_TXT: { ResetFactoryDefaultPreference.fetchRulesTXT(filenr, isChecked); break; }
    case FileType::PROVISIONING_DAT: { ResetFactoryDefaultPreference.fetchProvisioningDat(isChecked); break; }

    case FileType::MAX_FILETYPE:
      break;
  }
}

# if FEATURE_CUSTOM_PROVISIONING
void addAllowFiletypeCheckbox(const ProvisioningStruct& ProvisioningSettings, FileType::Enum filetype, unsigned int filenr)
{
  const String fileName = getFileName(filetype, filenr);
  addRowLabel(fileName);
  addCheckBox(concat(F("allow_"), fileName), ProvisioningSettings.fetchFileTypeAllowed(filetype, filenr));
}

void storeAllowFiletypeCheckbox(ProvisioningStruct& ProvisioningSettings, FileType::Enum filetype, unsigned int filenr)
{
  const bool isChecked = isFormItemChecked(concat(F("allow_"), getFileName(filetype, filenr)));

  ProvisioningSettings.setFetchFileTypeAllowed(filetype, filenr, isChecked);
}

# endif // if FEATURE_CUSTOM_PROVISIONING


bool tryDownloadFileType(const String& url, const String& user, const String& pass, FileType::Enum filetype, unsigned int filenr) {
  const String filename = getFileName(filetype, filenr);

  addRowLabel(filename);
  const String error = downloadFileType(url, user, pass, filetype, filenr);

  if (error.length() == 0) {
    addHtml(F("Success"));
    return true;
  }
  addHtml(error);
  return false;
}

#endif // if FEATURE_SETTINGS_ARCHIVE

#include "../WebServer/Log.h"

#include "../WebServer/ESPEasy_WebServer.h"
#include "../WebServer/404.h"
#include "../WebServer/HTML_wrappers.h"
#include "../WebServer/JSON.h"
#include "../WebServer/Markup.h"
#include "../WebServer/Markup_Buttons.h"

#include "../DataStructs/LogStruct.h"

#include "../Globals/Logging.h"
#include "../Globals/Settings.h"
#include "../Helpers/ESPEasy_time_calc.h"
#include "../Static/WebStaticData.h"

// ********************************************************************************
// Web Interface log page
// ********************************************************************************
void handle_log() {
  if (!isLoggedIn()) { return; }
  navMenuIndex = MENU_INDEX_TOOLS;

  TXBuffer.startStream();
  sendHeadandTail_stdtemplate(_HEAD);
  html_table_class_normal();

  #ifdef WEBSERVER_LOG
  addHtml(F("<TR><TH id=\"headline\" align=\"left\">Log"));
  addCopyButton(F("copyText"), EMPTY_STRING, F("Copy log to clipboard"));
  addHtml(F("</TR></table><div  id='current_loglevel' style='font-weight: bold;'>Logging: </div><div class='logviewer' id='copyText_1'></div>"));
  addHtml(F("Autoscroll: "));
  addCheckBox(F("autoscroll"), true);
  addHtml(F("<BR></body>"));

  serve_JS(JSfiles_e::FetchAndParseLog);

  #else // ifdef WEBSERVER_LOG
  addHtml(F("Not included in build"));
  #endif // ifdef WEBSERVER_LOG
  sendHeadandTail_stdtemplate(_TAIL);
  TXBuffer.endStream();
}

// ********************************************************************************
// Web Interface JSON log page
// ********************************************************************************
void handle_log_JSON() {
  if (!isLoggedIn()) { return; }
  #ifdef WEBSERVER_LOG
  TXBuffer.startJsonStream();
  String webrequest = webArg(F("view"));
  addHtml(F("{\"Log\": {"));

  if (equals(webrequest, F("legend"))) {
    addHtml(F("\"Legend\": ["));

    for (uint8_t i = 0; i < LOG_LEVEL_NRELEMENTS; ++i) {
      if (i != 0) {
        addHtml(',');
      }
      addHtml('{');
      int loglevel;
      stream_next_json_object_value(F("label"), getLogLevelDisplayStringFromIndex(i, loglevel));
      stream_last_json_object_value(F("loglevel"), loglevel);
    }
    addHtml(F("],\n"));
  }
  addHtml(F("\"Entries\": ["));
  bool logLinesAvailable       = true;
  int  nrEntries               = 0;
  unsigned long firstTimeStamp = 0;
  unsigned long lastTimeStamp  = 0;

  while (logLinesAvailable) {
    String message;
    uint8_t loglevel;
    if (Logging.getNext(logLinesAvailable, lastTimeStamp, message, loglevel)) {
      addHtml('{');
      stream_next_json_object_value(F("timestamp"), lastTimeStamp);
      stream_next_json_object_value(F("text"),  std::move(message));
      stream_last_json_object_value(F("level"), loglevel);
      if (logLinesAvailable) {
        addHtml(',', '\n');
      }
      if (nrEntries == 0) {
        firstTimeStamp = lastTimeStamp;
      }
      ++nrEntries;
    }

    // Do we need to do something here and maybe limit number of lines at once?
  }
  addHtml(F("],\n"));
  long logTimeSpan       = timeDiff(firstTimeStamp, lastTimeStamp);
  long refreshSuggestion = 1000;
  long newOptimum        = 1000;

  if ((nrEntries > 2) && (logTimeSpan > 1)) {
    // May need to lower the TTL for refresh when time needed
    // to fill half the log is lower than current TTL
    newOptimum = logTimeSpan * (LOG_STRUCT_MESSAGE_LINES / 2);
    newOptimum = newOptimum / (nrEntries - 1);
  }

  if (newOptimum < refreshSuggestion) { refreshSuggestion = newOptimum; }

  if (refreshSuggestion < 100) {
    // Reload times no lower than 100 msec.
    refreshSuggestion = 100;
  }
  stream_next_json_object_value(F("TTL"),                 refreshSuggestion);
  stream_next_json_object_value(F("timeHalfBuffer"),      newOptimum);
  stream_next_json_object_value(F("nrEntries"),           nrEntries);
  stream_next_json_object_value(F("SettingsWebLogLevel"), Settings.WebLogLevel);
  stream_last_json_object_value(F("logTimeSpan"),         logTimeSpan);
  addHtml(F("}\n"));
  TXBuffer.endStream();
  updateLogLevelCache();

  #else // ifdef WEBSERVER_LOG
  handleNotFound();
  #endif // ifdef WEBSERVER_LOG
}

#include "../WebServer/NotificationPage.h"

#if FEATURE_NOTIFIER

// #######################################################################################################
// ############################### Notifification Page: Email ############################################
// #######################################################################################################

/** Changelog:
 * 2024-07-30 ThomasB   : Added Read-the-Docs Help Button to email and buzzer plugins.
 * 2024-07-01 ThomasB   : Added User Setting for SMTP email server timeout. Display in Seconds.
 * 2024-07-01 ThomasB   : Start of changelog, older changes not logged.
 */

# include "../WebServer/ESPEasy_WebServer.h"
# include "../WebServer/HTML_wrappers.h"
# include "../WebServer/Markup.h"
# include "../WebServer/Markup_Buttons.h"
# include "../WebServer/Markup_Forms.h"

# include "../DataStructs/ESPEasy_EventStruct.h"
# include "../DataStructs/NotificationSettingsStruct.h"

# include "../Helpers/ESPEasy_Storage.h"

# include "../Globals/ESPEasy_Scheduler.h"
# include "../Globals/Settings.h"


// ********************************************************************************
// Web Interface notifcations page
// ********************************************************************************


# include "../Globals/NPlugins.h"

void handle_notifications() {
  # ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_notifications"));
  # endif // ifndef BUILD_NO_RAM_TRACKER

  if (!isLoggedIn()) { return; }
  navMenuIndex = MENU_INDEX_NOTIFICATIONS;
  TXBuffer.startStream();
  sendHeadandTail_stdtemplate(_HEAD);

  struct EventStruct TempEvent;

  // char tmpString[64];


  // 'index' value in the URL
  uint8_t notificationindex       = getFormItemInt(F("index"), 0);
  boolean notificationindexNotSet = notificationindex == 0;
  --notificationindex;

  const int notification_webarg_value = getFormItemInt(F("notification"), -1);

  if (!notificationindexNotSet && (notification_webarg_value != -1))
  {
    const npluginID_t notification = npluginID_t::toPluginID(notification_webarg_value);

    if (notification == INVALID_N_PLUGIN_ID) {
      Settings.Notification[notificationindex]        = INVALID_N_PLUGIN_ID.value;
      Settings.NotificationEnabled[notificationindex] = false;
    } else {
      MakeNotificationSettings(NotificationSettings);

      if (!AllocatedNotificationSettings()) {
        addHtmlError(F("Out of memory!"));
      } else {

        if (Settings.Notification[notificationindex] != notification.value)
        {
          Settings.Notification[notificationindex] = notification.value;
        }
        else
        {
          if (Settings.Notification[notificationindex] != INVALID_N_PLUGIN_ID.value)
          {
            nprotocolIndex_t NotificationProtocolIndex = getNProtocolIndex_from_NotifierIndex(notificationindex);

            if (validNProtocolIndex(NotificationProtocolIndex)) {
              String dummyString;
              NPlugin_ptr[NotificationProtocolIndex](NPlugin::Function::NPLUGIN_WEBFORM_SAVE, 0, dummyString);
            }

            // Reload & overwrite
            LoadNotificationSettings(notificationindex, reinterpret_cast<uint8_t *>(NotificationSettings.get()),
                                     sizeof(NotificationSettingsStruct));
            NotificationSettings->validate();
            NotificationSettings->Port = getFormItemInt(F("port"), 0);

            NotificationSettings->Timeout_ms                = getFormItemInt(F("timeout"), NPLUGIN_001_DEF_TM);
            NotificationSettings->Pin1                      = getFormItemInt(F("pin1"), -1);
            NotificationSettings->Pin2                      = getFormItemInt(F("pin2"), -1);
            Settings.NotificationEnabled[notificationindex] = isFormItemChecked(F("notificationenabled"));
            strncpy_webserver_arg(NotificationSettings->Domain,   F("domain"));
            strncpy_webserver_arg(NotificationSettings->Server,   F("server"));
            strncpy_webserver_arg(NotificationSettings->Sender,   F("sender"));
            strncpy_webserver_arg(NotificationSettings->Receiver, F("receiver"));
            strncpy_webserver_arg(NotificationSettings->Subject,  F("subject"));
            strncpy_webserver_arg(NotificationSettings->User,     F("username"));
            strncpy_webserver_arg(NotificationSettings->Body,     F("body"));

            copyFormPassword(F("password"), NotificationSettings->Pass, sizeof(NotificationSettings->Pass));
          }
        }
      }
      addHtmlError(SaveNotificationSettings(
        notificationindex, 
        reinterpret_cast<const uint8_t *>(NotificationSettings.get()),
        sizeof(NotificationSettingsStruct)));
    }

    // Save the settings.
    addHtmlError(SaveSettings());

    if (hasArg(F("test"))) {
      // Perform tests with the settings in the form.
      nprotocolIndex_t NotificationProtocolIndex = getNProtocolIndex_from_NotifierIndex(notificationindex);

      if (validNProtocolIndex(NotificationProtocolIndex) &&
          Settings.NotificationEnabled[notificationindex])
      {
        // TempEvent.NotificationProtocolIndex = NotificationProtocolIndex;
        TempEvent.NotificationIndex = notificationindex;
        Scheduler.schedule_notification_event_timer(NotificationProtocolIndex, NPlugin::Function::NPLUGIN_NOTIFY, std::move(TempEvent));
      }
    }
  }

  html_add_form();

  if (notificationindexNotSet)
  {
    html_table_class_multirow();
    html_TR();
    html_table_header(F(""),        70);
    html_table_header(F("Nr"),      50);
    html_table_header(F("Enabled"), 100);
    html_table_header(F("Service"));
    html_table_header(F("Server"));
    html_table_header(F("Port"));

    MakeNotificationSettings(NotificationSettings);

    if (!AllocatedNotificationSettings()) {
      addHtmlError(F("Out of memory!"));
    } else {
      for (uint8_t x = 0; x < NOTIFICATION_MAX; x++)
      {
        LoadNotificationSettings(x, reinterpret_cast<uint8_t *>(NotificationSettings.get()), sizeof(NotificationSettingsStruct));
        NotificationSettings->validate();
        html_TR_TD();
        html_add_button_prefix();
        addHtml(F("notifications?index="));
        addHtmlInt(x + 1);
        addHtml(F("'>Edit</a>"));
        html_TD();
        addHtmlInt(x + 1);
        html_TD();

        if (Settings.Notification[x] != INVALID_N_PLUGIN_ID.value)
        {
          addEnabled(Settings.NotificationEnabled[x]);

          html_TD();
          uint8_t NotificationProtocolIndex = getNProtocolIndex(npluginID_t::toPluginID(Settings.Notification[x]));
          String  NotificationName          = F("(plugin not found?)");

          if (validNProtocolIndex(NotificationProtocolIndex))
          {
            NPlugin_ptr[NotificationProtocolIndex](NPlugin::Function::NPLUGIN_GET_DEVICENAME, 0, NotificationName);
          }
          addHtml(NotificationName);
          html_TD();
          addHtml(NotificationSettings->Server);
          html_TD();

          if (NotificationSettings->Port) {
            addHtmlInt(NotificationSettings->Port);
          } else {
            // MFD: we display the GPIO
            addGpioHtml(NotificationSettings->Pin1);

            if (NotificationSettings->Pin2 >= 0)
            {
              html_BR();
              addGpioHtml(NotificationSettings->Pin2);
            }
          }
        }
        else {
          html_TD(3);
        }
      }
    }
    html_end_table();
    html_end_form();
  }
  else
  {
    html_table_class_normal();
    addFormHeader(F("Notification Settings"));
    addRowLabel(F("Notification"));
    uint8_t choice = Settings.Notification[notificationindex];
    addSelector_Head_reloadOnChange(F("notification"));
    addSelector_Item(F("- None -"), 0, false);

    for (uint8_t x = 0; x <= notificationCount; x++)
    {
      String NotificationName;
      NPlugin_ptr[x](NPlugin::Function::NPLUGIN_GET_DEVICENAME, 0, NotificationName);
      addSelector_Item(NotificationName,
                       Notification[x].Number,
                       choice == Notification[x].Number);
    }
    addSelector_Foot(true);

    addHelpButton(F("EasyNotifications"));
    addRTDHelpButton(F("Notify/_Notifications.html"));

    if (Settings.Notification[notificationindex] != INVALID_N_PLUGIN_ID.value)
    {
      MakeNotificationSettings(NotificationSettings);

      if (!AllocatedNotificationSettings()) {
        addHtmlError(F("Out of memory!"));
      } else {
        LoadNotificationSettings(notificationindex, reinterpret_cast<uint8_t *>(NotificationSettings.get()), sizeof(NotificationSettingsStruct));
        NotificationSettings->validate();

        nprotocolIndex_t NotificationProtocolIndex = getNProtocolIndex_from_NotifierIndex(notificationindex);

        if (validNProtocolIndex(NotificationProtocolIndex))
        {
          if (Notification[NotificationProtocolIndex].usesMessaging)
          {
            if (NotificationSettings->Port == 0) {
# if FEATURE_EMAIL_TLS
              NotificationSettings->Port = 465;
# else // if FEATURE_EMAIL_TLS
              NotificationSettings->Port = 25;
# endif // if FEATURE_EMAIL_TLS
            }

            addFormSubHeader(F("SMTP Server Settings"));
            addFormTextBox(F("Domain"), F("domain"), NotificationSettings->Domain, sizeof(NotificationSettings->Domain) - 1);
            addFormTextBox(F("Server"), F("server"), NotificationSettings->Server, sizeof(NotificationSettings->Server) - 1);
            addFormNumericBox(
              F("Port"), F("port"),
              NotificationSettings->Port,
              1,
              65535);
# if FEATURE_EMAIL_TLS
            addFormNote(F("default port SSL: 465"));
# else // if FEATURE_EMAIL_TLS
            addFormNote(F("default port: 25, SSL/TLS servers NOT supported!"));
# endif // if FEATURE_EMAIL_TLS

            if ((NotificationSettings->Timeout_ms < NPLUGIN_001_MIN_TM) ||
                (NotificationSettings->Timeout_ms > NPLUGIN_001_MAX_TM))
            {
              NotificationSettings->Timeout_ms = NPLUGIN_001_DEF_TM;
            }

            addFormNumericBox(
              F("Timeout"), F("timeout"),
              NotificationSettings->Timeout_ms,
              NPLUGIN_001_MIN_TM,
              NPLUGIN_001_MAX_TM
# if FEATURE_TOOLTIPS
              , F("Maximum Server Response Time")
# endif // if FEATURE_TOOLTIPS
              );

            addUnit(F("ms"));

            ZERO_TERMINATE(NotificationSettings->Pass);
            addFormSubHeader(F("Credentials"));

            addFormTextBox(F("Username"), F("username"), NotificationSettings->User, sizeof(NotificationSettings->User) - 1);
            addFormPasswordBox(F("Password"), F("password"), NotificationSettings->Pass, sizeof(NotificationSettings->Pass) - 1);

            addFormSubHeader(F("Email Attributes"));

            addFormTextBox(F("Sender"),   F("sender"),   NotificationSettings->Sender,   sizeof(NotificationSettings->Sender) - 1);
            addFormTextBox(F("Receiver"), F("receiver"), NotificationSettings->Receiver, sizeof(NotificationSettings->Receiver) - 1);
            addFormTextBox(F("Subject"),  F("subject"),  NotificationSettings->Subject,  sizeof(NotificationSettings->Subject) - 1);

            addRowLabel(F("Body"));
            addHtml(F("<textarea name='body' rows='20' size=512 wrap='off'>"));
            addHtml(NotificationSettings->Body);
            addHtml(F("</textarea>"));
          }

          if (Notification[NotificationProtocolIndex].usesGPIO > 0)
          {
            addRowLabel(F("1st GPIO"));
            addPinSelect(PinSelectPurpose::Generic, F("pin1"), NotificationSettings->Pin1);
          }

          addRowLabel(F("Enabled"));
          addCheckBox(F("notificationenabled"), Settings.NotificationEnabled[notificationindex]);

          TempEvent.NotificationIndex = notificationindex;
          String webformLoadString;
          NPlugin_ptr[NotificationProtocolIndex](NPlugin::Function::NPLUGIN_WEBFORM_LOAD, &TempEvent, webformLoadString);

          if (webformLoadString.length() > 0) {
            addHtmlError(F("Bug in NPlugin::Function::NPLUGIN_WEBFORM_LOAD, should not append to string, use addHtml() instead"));
          }
        }
      }
    }

    addFormSeparator(2);

    html_TR_TD();
    html_TD();
    addButton(F("notifications"), F("Close"));
    addSubmitButton();
    addSubmitButton(F("Test"), F("test"));
    html_end_table();
    html_end_form();
  }
  sendHeadandTail_stdtemplate(_TAIL);
  TXBuffer.endStream();
}

#endif // FEATURE_NOTIFIER

#include "../WebServer/ControlPage.h"


#ifdef WEBSERVER_CONTROL


# include "../WebServer/HTML_wrappers.h"
# include "../WebServer/ESPEasy_WebServer.h"
# include "../Helpers/WebServer_commandHelper.h"

# include "../../ESPEasy-Globals.h"


// ********************************************************************************
// Web Interface control page (no password!)
// ********************************************************************************
void handle_control() {
  # ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_control"));
  # endif // ifndef BUILD_NO_RAM_TRACKER

  String webrequest            = webArg(F("cmd"));
  HandledWebCommand_result res = handle_command_from_web(EventValueSource::Enum::VALUE_SOURCE_HTTP, webrequest);

  switch (res) {
    case HandledWebCommand_result::IP_not_allowed:
    case HandledWebCommand_result::NoCommand:
      return;
    case HandledWebCommand_result::CommandHandled:
    case HandledWebCommand_result::Unknown_or_restricted_command:
      break;
  }

  if (printToWebJSON) { // it may be set in PLUGIN_WRITE (SendStatus)
    TXBuffer.startJsonStream();
    addHtml(printWebString);
  } else {
    TXBuffer.startStream();
    addEncodedHtml(printWebString);
  }

  TXBuffer.endStream();

  free_string(printWebString);
  printToWeb     = false;
  printToWebJSON = false;
}

#endif // ifdef WEBSERVER_CONTROL


#include "../WebServer/Markup.h"

#include "../WebServer/HTML_wrappers.h"
#include "../WebServer/Markup_Forms.h"

#include "../CustomBuild/ESPEasyLimits.h"

#include "../Globals/Settings.h"

#include "../Helpers/Convert.h"
#include "../Helpers/ESPEasy_UnitOfMeasure.h"
#include "../Helpers/Hardware_GPIO.h"
#include "../Helpers/StringConverter_Numerical.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringGenerator_GPIO.h"

#include "../../ESPEasy_common.h"

#ifdef ESP32
# include "soc/soc_caps.h"
#endif


#if FEATURE_TOOLTIPS
void addTooltip(const String& tooltip)
{
  if (tooltip.length() > 0) {
    addHtmlAttribute(F("title"), tooltip);
  }
}
#endif


void addSelector_Head(const String& id) {
  do_addSelector_Head(id, F("wide"), EMPTY_STRING, false);
}

void addSelector_Head_reloadOnChange(const __FlashStringHelper * id) {
  addSelector_Head_reloadOnChange(String(id), F("wide"), false);
}

/*
void addSelector_Head_reloadOnChange(const String& id) {
  addSelector_Head_reloadOnChange(id, F("wide"), false);
}
*/

void addSelector_Head_reloadOnChange(const String& id, 
                                     const __FlashStringHelper * classname, 
                                     bool disabled
                                     #if FEATURE_TOOLTIPS
                                     , const String& tooltip
                                     #endif // if FEATURE_TOOLTIPS
                                     ) {
  addSelector_Head_reloadOnChange(
    id, 
    classname, 
    F("return dept_onchange(frmselect)"), 
    disabled
#if FEATURE_TOOLTIPS
    , tooltip
#endif // if FEATURE_TOOLTIPS
  );
}

void addSelector_Head_reloadOnChange(const String& id,
                                     const __FlashStringHelper * classname, 
                                     const String& onChangeCall, 
                                     bool disabled
                                     #if FEATURE_TOOLTIPS
                                     , const String& tooltip
                                     #endif // if FEATURE_TOOLTIPS
                                     ) {
  do_addSelector_Head(id, classname, onChangeCall, disabled
                      #if FEATURE_TOOLTIPS
                      , tooltip
                      #endif // if FEATURE_TOOLTIPS
                      );
}


void do_addSelector_Head(const String& id, const __FlashStringHelper * classname, const String& onChangeCall, const bool& disabled
                         #if FEATURE_TOOLTIPS
                         , const String& tooltip
                         #endif // if FEATURE_TOOLTIPS
                         )
{
  addHtml(F("<select "));
  addHtmlAttribute(F("class"), classname);
  addHtmlAttribute(F("name"),  id);
  addHtmlAttribute(F("id"),    id);

  #if FEATURE_TOOLTIPS
  addTooltip(tooltip);
  #endif // if FEATURE_TOOLTIPS

  if (disabled) {
    addDisabled();
  }

  if (onChangeCall.length() > 0) {
    addHtmlAttribute(F("onchange"), onChangeCall);
  }
  addHtml('>');
}

void addPinSelector_Item(PinSelectPurpose purpose, const String& gpio_label, int gpio, bool    selected, bool    disabled, const String& attr)
{
  if (gpio != -1) // empty selection can never be disabled...
  {
    int  pinnr = -1;
    bool input, output, warning;

    if (getGpioInfo(gpio, pinnr, input, output, warning)) {
      bool includeI2C = true;
      bool includeSPI = true;
      bool includeSerial = true;
      #if FEATURE_ETHERNET
      bool includeEthernet = true;
      #endif // if FEATURE_ETHERNET
      #if FEATURE_SD
      bool includeSDCard = true;
      #endif // if FEATURE_SD
      // bool includeStatusLed = true; // Added as place-holders, see below
      // bool includeResetPin = true;

      switch (purpose) {
        case PinSelectPurpose::SPI:
        case PinSelectPurpose::SPI_MISO:
          includeSPI = false;
          if (purpose == PinSelectPurpose::SPI && !output) {
            return;
          }
          break;
        case PinSelectPurpose::Ethernet:
          #if FEATURE_ETHERNET
          includeEthernet = false;
          #endif // if FEATURE_ETHERNET
          break;
        case PinSelectPurpose::Generic:

          if (!input && !output) {
            return;
          }
          break;

        case PinSelectPurpose::Generic_input:

          if (!input) {
            return;
          }
          break;

        case PinSelectPurpose::Generic_output:
        case PinSelectPurpose::DAC:

          if (!output) {
            return;
          }
          break;

        case PinSelectPurpose::I2C:
#if FEATURE_I2C_MULTIPLE
        case PinSelectPurpose::I2C_2:
#if FEATURE_I2C_INTERFACE_3
        case PinSelectPurpose::I2C_3:
#endif
#endif
          includeI2C = false;
          // fallthrough
        case PinSelectPurpose::Generic_bidir:

          if (!output || !input) {
            // SDA is obviously bidirectional.
            // SCL is obviously output, but can be held down by a slave device to signal clock stretch limit.
            // Thus both must be capable of input & output.
            return;
          }
          break;
          
        case PinSelectPurpose::Serial_input:
          includeSerial = false;
          if (!input) {
            return;
          }
          break;

        case PinSelectPurpose::Serial_output:
          includeSerial = false;
          if (!output) {
            return;
          }
          break;
        #if FEATURE_SD
        case PinSelectPurpose::SD_Card:
          includeSDCard = false;
          if (!output) {
            return;
          }
          break;
        #endif
        
        case PinSelectPurpose::Status_led:
          // includeStatusLed = false; // Placeholder, see below
          if (!output) {
            return;
          }
          break;

        case PinSelectPurpose::Reset_pin:
          // includeResetPin = false; // Placeholder, see below
          if (!input) {
            return;
          }  
          break;
  
      }

      if (includeI2C && Settings.isI2C_pin(gpio)) {
        disabled = true;
      }

      if (includeSerial && isSerialConsolePin(gpio)) {
        disabled = true;
      }

      if (includeSPI && Settings.isSPI_pin(gpio)) {
        disabled = true;
      }

      // Not blocking these GPIO pins, as they may already be in dual-purpose use, just a place-holder
      // if (includeStatusLed && (Settings.Pin_status_led == gpio)) {
      //   disabled = true;
      // }
      
      // if (includeResetPin && (Settings.Pin_Reset == gpio)) {
      //   disabled = true;
      // }
  
  #if FEATURE_ETHERNET

      if (Settings.isEthernetPin(gpio) || (includeEthernet && Settings.isEthernetPinOptional(gpio))) {
        disabled = true;
      }
  #endif // if FEATURE_ETHERNET

      #if FEATURE_SD
      if (includeSDCard && (Settings.Pin_sd_cs == gpio)) {
        disabled = true;
      }
      #endif
    }
  }

  addSelector_Item(gpio_label,
                   gpio,
                   selected,
                   disabled);
}

void addSelector_Item(const __FlashStringHelper *option, int index, bool    selected, bool    disabled, const String& attr)
{
  addHtml(F("<option "));
  addHtmlAttribute(F("value"), index);

  if (selected) {
    addHtml(F(" selected"));
  }

  if (disabled) {
    addDisabled();
  }

  if (attr.length() > 0)
  {
    addHtml(' ');
    addHtml(attr);
  }
  addHtml('>');
  addHtml(option);
  addHtml(F("</option>"));
}

void addSelector_Item(const String& option, int index, bool    selected, bool    disabled, const String& attr)
{
  addHtml(F("<option "));
  addHtmlAttribute(F("value"), index);

  if (selected) {
    addHtml(F(" selected"));
  }

  if (disabled) {
    addDisabled();
  }

  if (attr.length() > 0)
  {
    addHtml(' ');
    addHtml(attr);
  }
  addHtml('>');
  addHtml(option);
  addHtml(F("</option>"));
}

void addSelector_OptGroup(const String& label) {
  addHtml(F("<optgroup label=\""));
  addHtml(label);
  addHtml('\"', '>');
}

void addSelector_OptGroupFoot() {
  addHtml(F("</optgroup>"));
}

void addSelector_Foot(bool reloadonchange)
{
  addHtml(F("</select>"));
  if (reloadonchange) {
#if FEATURE_TOOLTIPS
    addHtml(F("<tt"));
    addTooltip(F("Change will submit and reload page"));
    addHtml(F(">&#128260;</tt>"));
#else
    addHtml(F("&#128260;"));
#endif
  }
}

void addUnit(const __FlashStringHelper *unit)
{
  // Needed so we can check whether it is empty
  addUnit(String(unit));
}

void addUnit(const String& unit)
{
  if (unit.isEmpty()) return;
  addHtml(F(" ["));
  addHtml(unit);
  addHtml(']');
}

void addUnit(char unit)
{
  if (unit == '\0') return;
  addHtml(F(" ["));
  addHtml(unit);
  addHtml(']');
}

#if FEATURE_TASKVALUE_UNIT_OF_MEASURE
void addUnitOfMeasureSelector(const String&  id,
                              const uint8_t  unitOfMeasure,
                              const uint64_t groupMap) {
  bool firstGrp   = true;
  bool includeGrp = false;

  do_addSelector_Head(id, F("xwide"), EMPTY_STRING, false
                      #if FEATURE_TOOLTIPS
                      , EMPTY_STRING
                      #endif // if FEATURE_TOOLTIPS
                     );
  addSelector_Item( // Empty first value
    F(""),
    0,
    unitOfMeasure == 0);

  for (uint16_t idx = 0; idx < unit_of_measure_map_size; ++idx) {
    const uint16_t uomIdx = pgm_read_word_near(&unit_of_measure_map[idx]);
    if (uomIdx < 1024) {
      if (includeGrp) {
        addSelector_Item(
          toUnitOfMeasureName(uomIdx),
          uomIdx,
          unitOfMeasure == uomIdx);
      }
    } else {
      includeGrp = bitRead(groupMap, uomIdx - 1024);
      if (includeGrp) {
        if (!firstGrp) {
          addSelector_OptGroupFoot();
        }
        addSelector_OptGroup(toUnitOfMeasureName(uomIdx));
        firstGrp = false;
      }
    }
    if ((idx & 0x07) == 0) { delay(0); }
  }
  if (!firstGrp) {
    addSelector_OptGroupFoot();
  }
  addSelector_Foot();
}
#endif // if FEATURE_TASKVALUE_UNIT_OF_MEASURE

void addRowLabel_tr_id(const __FlashStringHelper *label, const __FlashStringHelper *id)
{
  addRowLabel_tr_id(label, String(id));
}

void addRowLabel_tr_id(const __FlashStringHelper *label, const String& id)
{
  if (id.isEmpty()) {
    addRowLabel(label);
  } else {
    addRowLabel_tr_id(String(label), id);
  }
}

void addRowLabel_tr_id(const String& label, const String& id)
{
  if (id.isEmpty()) {
    addRowLabel(label);
  } else {
    addRowLabel(label, concat(F("tr_"), id));
  }
}

void addRowLabel(const __FlashStringHelper *label)
{
  html_TR_TD();
  addHtml(concat(label, F(":</td>")));
  html_TD();
}

void addRowLabel(const String& label, const String& id)
{
  if (id.length() > 0) {
    addHtml(F("<TR id='"));
    addHtml(id);
    addHtml(F("'><TD>"));
  } else {
    html_TR_TD();
  }

  if (!label.isEmpty()) {
    addHtml(label);
    addHtml(':');
  }
  addHtml(F("</td>"));
  html_TD();
}

// Add a row label and mark it with copy markers to copy it to clipboard.
void addRowLabel_copy(const __FlashStringHelper *label) {
  addHtml(F("<TR>"));
  html_copyText_TD();
  addHtml(label);
  addHtml(':');
  html_copyText_marker();
  html_copyText_TD();
}

void addRowLabel_copy(const String& label) {
  addHtml(F("<TR>"));
  html_copyText_TD();
  addHtml(label);
  addHtml(':');
  html_copyText_marker();
  html_copyText_TD();
}

void addRowLabel(LabelType::Enum label) {
  addRowLabel(getLabel(label));
}

void addRowLabelValue(LabelType::Enum label) {
  addRowLabel(getLabel(label));
  addHtml(getValue(label));
  addUnit(getFormUnit(label));
}

void addRowLabelValues(const LabelType::Enum labels[]) {
  size_t i = 0;
  LabelType::Enum cur  = static_cast<const LabelType::Enum>(pgm_read_byte(labels + i));

  while (true) {
    const LabelType::Enum next = static_cast<const LabelType::Enum>(pgm_read_byte(labels + i + 1));
    addRowLabelValue(cur);
    if (next == LabelType::MAX_LABEL) {
      return;
    }
    ++i;
    cur = next;
  }
}

void addRowLabelValue_copy(LabelType::Enum label) {
  addRowLabel_copy(getLabel(label));
  addHtml(getValue(label));
  addUnit(getFormUnit(label));
}

// ********************************************************************************
// Add a header
// ********************************************************************************
void addTableSeparator(const __FlashStringHelper *label, int colspan, int h_size)
{
  addHtml(strformat(
    F("<TR><TD colspan=%d><H%d>"),
    colspan, h_size));
  addHtml(label);
  addHtml(strformat(
    F("</H%d></TD></TR>"),
    h_size));
}

void addTableSeparator(const __FlashStringHelper *label, int colspan, int h_size, const __FlashStringHelper *helpButton)
{
  addTableSeparator(String(label), colspan, h_size, String(helpButton));
}

void addTableSeparator(const String& label, int colspan, int h_size, const String& helpButton) {
  addHtml(strformat(
    F("<TR><TD colspan=%d><H%d>"),
    colspan, h_size));
  addHtml(label);

  if (!helpButton.isEmpty()) {
    addHelpButton(helpButton);
  }
  addHtml(strformat(
    F("</H%d></TD></TR>"),
    h_size));
}

void addFormHeader(const __FlashStringHelper *header) {
  addFormHeader(header, F(""), F(""));
}

void addFormHeader(const __FlashStringHelper *header,
                   const __FlashStringHelper *helpButton)
{
  addFormHeader(header, helpButton, F(""));
}

void addFormHeader(const __FlashStringHelper *header,
                   const __FlashStringHelper *helpButton,
                   const __FlashStringHelper *rtdHelpButton)
{
  html_TR();
  html_table_header(header, helpButton, rtdHelpButton, 300);
  html_table_header(F(""));
}

/*
void addFormHeader(const String& header, const String& helpButton) {
  addFormHeader(header, helpButton, EMPTY_STRING);
}

void addFormHeader(const String& header, const String& helpButton, const String& rtdHelpButton)
{
  html_TR();
  html_table_header(header, helpButton, rtdHelpButton, 225);
  html_table_header(F(""));
}
*/

// ********************************************************************************
// Add a sub header
// ********************************************************************************
void addFormSubHeader(const __FlashStringHelper *header) {
  addTableSeparator(header, 2, 3);
}

void addFormSubHeader(const String& header)
{
  addTableSeparator(header, 2, 3);
}

// ********************************************************************************
// Add a checkbox
// ********************************************************************************
void addCheckBox(const __FlashStringHelper *id, bool    checked, bool disabled)
{
  addCheckBox(String(id), checked, disabled);
}

void addCheckBox(const String& id, bool    checked, bool disabled
                 #if FEATURE_TOOLTIPS
                 , const String& tooltip
                 #endif // if FEATURE_TOOLTIPS
                 )
{
  addHtml(F("<label class='container'>&nbsp;"));
  addHtml(F("<input "));
  addHtmlAttribute(F("type"), F("checkbox"));
  addHtmlAttribute(F("id"),   id);
  addHtmlAttribute(F("name"), id);

  if (checked) {
    addHtml(F(" checked"));
  }

  if (disabled) { addDisabled(); }
  addHtml(F("><span "));
  addHtmlAttribute(F("id"), concat(F("cs"), id)); // cs=checkbox span
  addHtml(F(" class='checkmark"));

  if (disabled) { addDisabled(); }
  addHtml('\'');
  #if FEATURE_TOOLTIPS
  addTooltip(tooltip);
  #endif // if FEATURE_TOOLTIPS
  addHtml(F("></span></label>"));
}

// ********************************************************************************
// Add a numeric box
// ********************************************************************************
void addNumericBox(const __FlashStringHelper *id, int value, int min, int max, bool disabled)
{
  addNumericBox(String(id), value, min, max, disabled);
}

void addNumericBox(const String& id, int value, int min, int max
                   #if FEATURE_TOOLTIPS
                   , const __FlashStringHelper * classname, const String& tooltip
                   #endif // if FEATURE_TOOLTIPS
                   , bool disabled
                   )
{
  addHtml(F("<input "));
  #if FEATURE_TOOLTIPS
  addHtmlAttribute(F("class"), classname);
  #else // if FEATURE_TOOLTIPS
  addHtmlAttribute(F("class"), F("widenumber"));
  #endif  // if FEATURE_TOOLTIPS
  addHtmlAttribute(F("type"),  F("number"));
  addHtmlAttribute(F("name"),  id);
  addHtmlAttribute(F("id"),    id);

  #if FEATURE_TOOLTIPS
  addTooltip(tooltip);
  #endif // if FEATURE_TOOLTIPS

  if (disabled) {
    addDisabled();
  }

  if (value < min) {
    value = min;
  }

  if (value > max) {
    value = max;
  }

  if (min != INT_MIN)
  {
    addHtmlAttribute(F("min"), min);
  }

  if (max != INT_MAX)
  {
    addHtmlAttribute(F("max"), max);
  }
  addHtmlAttribute(F("value"), value);
  addHtml('>');
}

#if FEATURE_TOOLTIPS
void addNumericBox(const String& id, int value, int min, int max, bool disabled)
{
  addNumericBox(id, value, min, max, F("widenumber"), EMPTY_STRING, disabled);
}

#endif // if FEATURE_TOOLTIPS

void addFloatNumberBox(const String& id, float value, float min, float max, unsigned int nrDecimals, float stepsize
                       #if FEATURE_TOOLTIPS
                       , const String& tooltip
                       #endif // if FEATURE_TOOLTIPS
                       )
{
  addHtml(strformat(
      F("<input type='number' name='%s' min="),
      id.c_str()));
  addHtmlFloat(min, nrDecimals);
  addHtml(F(" max="));
  addHtmlFloat(max, nrDecimals);
  addHtml(F(" step="));

  if (stepsize <= 0.0f) {
    addHtml('0', '.');

    for (uint8_t i = 1; i < nrDecimals; ++i) {
      addHtml('0');
    }
    addHtml('1');
  } else {
    addHtmlFloat(stepsize, nrDecimals);
  }

  addHtml(F(" style='width:7em;' value="));
  addHtmlFloat(value, nrDecimals);

  #if FEATURE_TOOLTIPS
  addTooltip(tooltip);
  #endif // if FEATURE_TOOLTIPS
  addHtml('>');
}

// ********************************************************************************
// Add Textbox
// ********************************************************************************
void addTextBox(const __FlashStringHelper * id,
                const String& value,
                int           maxlength,
                const __FlashStringHelper * classname)
{
  addTextBox(String(id), value, maxlength, 
             false, // readonly
             false, // required
             EMPTY_STRING, // pattern
             classname);
}

void addTextBox(const String& id,
                const String& value,
                int           maxlength,
                const __FlashStringHelper * classname)
{
  addTextBox(id, value, maxlength, 
             false, // readonly
             false, // required
             EMPTY_STRING, // pattern
             classname);
}

void addTextBox(const __FlashStringHelper * id, const String&  value, int maxlength, bool readonly, bool required, const String& pattern) {
  addTextBox(id, value, maxlength, readonly, required, pattern, F("wide"));
}

void addTextBox(const String& id, const String&  value, int maxlength, bool readonly, bool required, const String& pattern) {
  addTextBox(id, value, maxlength, readonly, required, pattern, F("wide"));
}

void addTextBox(const String  & id,
                const String  & value,
                int             maxlength,
                bool            readonly,
                bool            required,
                const String  & pattern,
                const __FlashStringHelper * classname
                #if FEATURE_TOOLTIPS
                , const String& tooltip
                #endif // if FEATURE_TOOLTIPS
                ,
                const String&   datalist
                )
{
  addHtml(F("<input "));
  addHtmlAttribute(F("class"),     classname);
  addHtmlAttribute(F("type"),      F("search"));
  addHtmlAttribute(F("name"),      id);
  addHtmlAttribute(F("id"),        id);
  if (maxlength > 0) {
    addHtmlAttribute(F("maxlength"), maxlength);
  }
  if (!datalist.isEmpty()) {
    addHtmlAttribute(F("list"),    datalist);
  }
  addHtmlAttribute(F("value"),     value);

  if (readonly) {
    addHtml(F(" readonly "));
  }

  if (required) {
    addHtml(F(" required "));
  }

  if (pattern.length() > 0) {
    addHtmlAttribute(F("pattern"), pattern);
  }

  #if FEATURE_TOOLTIPS
  addTooltip(tooltip);
  #endif // if FEATURE_TOOLTIPS
  addHtml('>');
}



// ********************************************************************************
// Add Textarea
// ********************************************************************************
void addTextArea(const String  & id,
                 const String  & value,
                 int             maxlength,
                 int             rows,
                 int             columns,
                 bool            readonly,
                 bool          required
                 #if FEATURE_TOOLTIPS
                 , const String& tooltip
                 #endif // if FEATURE_TOOLTIPS
                 )
{
  if (rows < 0) {
    rows = count_newlines(value) + 1;
  }
  addHtml(F("<textarea "));
//  addHtmlAttribute(F("class"),     F("wide"));
  addHtmlAttribute(F("type"),      F("text"));
  addHtmlAttribute(F("name"),      id);
  addHtmlAttribute(F("id"),        id);
  if (maxlength > 0) {
    addHtmlAttribute(F("maxlength"), maxlength);
  }
  if (rows > 0) {
    addHtmlAttribute(F("rows"),      rows);
  }
  if (columns > 0) {
    addHtmlAttribute(F("cols"),      columns);
  }

  if (readonly) {
    addHtml(F(" readonly "));
  }

  if (required) {
    addHtml(F(" required "));
  }

  #if FEATURE_TOOLTIPS
  addTooltip(tooltip);
  #endif // if FEATURE_TOOLTIPS
  addHtml('>');
  addHtml(value);
  addHtml(F("</textarea>"));
}

// ********************************************************************************
// Add Help Buttons
// ********************************************************************************

// adds a Help Button with points to the the given Wiki Subpage
// If url starts with "RTD", it will be considered as a Read-the-docs link
void addHelpButton(const __FlashStringHelper *url) {
  addHelpButton(String(url));
}

void addHelpButton(const String& url) {
#ifndef WEBPAGE_TEMPLATE_HIDE_HELP_BUTTON

  if (url.startsWith("RTD")) {
    addRTDHelpButton(url.substring(3));
  } else {
    addHelpButton(url, false);
  }
#endif // ifndef WEBPAGE_TEMPLATE_HIDE_HELP_BUTTON
}

void addRTDHelpButton(const String& url)
{
  addHelpButton(url, true);
}

void addHelpButton(const String& url, bool isRTD)
{
  #ifndef WEBPAGE_TEMPLATE_HIDE_HELP_BUTTON
  addHtmlLink(
    F("button help"),
    makeDocLink(url, isRTD),
    isRTD ? F("i") : F("?"));
  #endif // ifndef WEBPAGE_TEMPLATE_HIDE_HELP_BUTTON
}

void addRTDPluginButton(pluginID_t pluginID) {
  addRTDHelpButton(
    strformat(
      F("Plugin/%s.html"),
      get_formatted_Plugin_number(pluginID).c_str()));

  constexpr pluginID_t PLUGIN_ID_P076_HLW8012(76);
  constexpr pluginID_t PLUGIN_ID_P077_CSE7766(77);

  if ((pluginID == PLUGIN_ID_P076_HLW8012) || 
      (pluginID == PLUGIN_ID_P077_CSE7766)) {
    addHtmlLink(
      F("button help"),
      makeDocLink(F("Reference/Safety.html"), true),
      F("&#9889;")); // High voltage sign
  }
}

# ifndef LIMIT_BUILD_SIZE
void addRTDControllerButton(cpluginID_t cpluginID) {
  addRTDHelpButton(
    strformat(
      F("Controller/%s.html"),
      get_formatted_Controller_number(cpluginID).c_str()));
}
# endif // ifndef LIMIT_BUILD_SIZE

String makeDocLink(const String& url, bool isRTD) {
  String result;

  if (!url.startsWith(F("http"))) {
    if (isRTD) {
      result += F("https://espeasy.readthedocs.io/en/latest/");
    } else {
      result += F("http://www.letscontrolit.com/wiki/index.php/");
    }
  }
  result += url;
  return result;
}

void addPinSelect(PinSelectPurpose purpose, const __FlashStringHelper *id,  int choice)
{
  addPinSelect(purpose, String(id), choice);
}

void addPinSelect(PinSelectPurpose purpose, const String& id,  int choice)
{
  addSelector_Head(id);

  // At i == 0 && gpio == -1, add the "- None -" option first
  int i    = 0;
  int gpio = -1;

  while (gpio <= MAX_GPIO) {
    int  pinnr = -1;
    bool input, output, warning = false;

    // Make sure getGpioInfo is called (compiler may optimize it away if (i == 0))
    const bool UsableGPIO = getGpioInfo(gpio, pinnr, input, output, warning);

    if (UsableGPIO || (i == 0)) {
      addPinSelector_Item(
        purpose,
        #ifdef ESP32
        concat(
        #endif // ifdef ESP32
          concat(
            createGPIO_label(gpio, pinnr, input, output, warning),
            getConflictingUse_wrapped(gpio, purpose)),
        #ifdef ESP32
            isPSRAMInterfacePin(gpio) ? getConflictingUse_wrapped(gpio, purpose, true) : F("")
        ),
        #endif // ifdef ESP32
        gpio,
        choice == gpio);

      ++i;
    }
    ++gpio;
  }
  addSelector_Foot();
}

#ifdef ESP32
#if SOC_ADC_SUPPORTED
void addADC_PinSelect(AdcPinSelectPurpose purpose, const String& id,  int choice)
{
  addSelector_Head(id);

  // At i == 0 && gpio == -1, add the "Hall Effect" option first
  int i    = 0;
  int gpio = -1;
  bool hasADC2 = false;

  if (
#if HAS_HALL_EFFECT_SENSOR
    (purpose == AdcPinSelectPurpose::ADC_Touch_HallEffect) ||
#endif
      (purpose == AdcPinSelectPurpose::ADC_Touch_Optional)) {
    addPinSelector_Item(
      PinSelectPurpose::Generic,
      purpose == AdcPinSelectPurpose::ADC_Touch_Optional ? F("- None -") : formatGpioName_ADC(gpio),
      gpio,
      choice == gpio);
  }

  while (i <= MAX_GPIO && gpio <= MAX_GPIO) {
    int  pinnr = -1;
    bool input, output, warning;

#if SOC_TOUCH_SENSOR_SUPPORTED
    if (purpose == AdcPinSelectPurpose::TouchOnly) {
      // For touch only list, sort based on touch number
      // Default sort is on GPIO number.
      gpio = touchPinToGpio(i);
    } else {
      ++gpio;
    }
#else
    ++gpio;
#endif

    if (getGpioInfo(gpio, pinnr, input, output, warning)) {
      int adc, ch, t;

      if (getADC_gpio_info(gpio, adc, ch, t)) {
        if ((purpose != AdcPinSelectPurpose::TouchOnly) || (t >= 0)) {
          String gpio_label;
          gpio_label = formatGpioName_ADC(gpio);

          if (adc != 0) {
            if (adc == 2) {
              hasADC2 = true;
            }
            gpio_label += F(" / ");
            gpio_label += createGPIO_label(gpio, pinnr, input, output, warning);
            gpio_label += getConflictingUse_wrapped(gpio);
          }
          addPinSelector_Item(
            PinSelectPurpose::Generic,
            gpio_label,
            gpio,
            choice == gpio);
        }
      }
    }
    ++i;
  }
  addSelector_Foot();
  if (hasADC2) {
    addFormNote(F("Do not use ADC2 pins with WiFi active"));
  }
}
#endif

#if SOC_DAC_SUPPORTED
void addDAC_PinSelect(const String& id,  int choice)
{
  addSelector_Head(id);

  // At i == 0 && gpio == -1, add the "- None -" option first
  int i    = 0;
  int gpio = -1;

  while (gpio <= MAX_GPIO) {
    int  pinnr   = -1;
    bool input   = false;
    bool output  = false;
    bool warning = false;
    int  dac     = 0;

    // Make sure getGpioInfo is called (compiler may optimize it away if (i == 0))
    const bool UsableGPIO = getDAC_gpio_info(gpio, dac); // getGpioInfo(gpio, pinnr, input, output, warning);

    if (UsableGPIO || (i == 0)) {
      if (getGpioInfo(gpio, pinnr, input, output, warning) || (i == 0)) {
        String gpio_label = formatGpioName_DAC(gpio);

        if (dac != 0) {
          gpio_label += F(" / ");
          gpio_label += createGPIO_label(gpio, pinnr, input, output, warning);
          gpio_label += getConflictingUse_wrapped(gpio, PinSelectPurpose::DAC);
        }
        addPinSelector_Item(
          PinSelectPurpose::DAC,
          gpio_label,
          gpio,
          choice == gpio);
      }
      ++i;
    }
    ++gpio;
  }
  addSelector_Foot();
}
#endif

#endif // ifdef ESP32

#include "../WebServer/SetupPage.h"


#ifdef WEBSERVER_SETUP

# include "../WebServer/ESPEasy_WebServer.h"
# include "../WebServer/AccessControl.h"
# include "../WebServer/HTML_wrappers.h"
# include "../WebServer/Markup.h"
# include "../WebServer/Markup_Buttons.h"
# include "../WebServer/Markup_Forms.h"
# include "../WebServer/SysInfoPage.h"

# include "../ESPEasyCore/ESPEasyNetwork.h"
# include "../ESPEasyCore/ESPEasyWifi.h"

# include "../Globals/ESPEasyWiFiEvent.h"
# include "../Globals/NetworkState.h"
# include "../Globals/RTC.h"
# include "../Globals/Settings.h"
# include "../Globals/SecuritySettings.h"
# include "../Globals/WiFi_AP_Candidates.h"

# include "../Helpers/Misc.h"
# include "../Helpers/Networking.h"
# include "../Helpers/ESPEasy_Storage.h"
# include "../Helpers/StringConverter.h"



#ifndef SETUP_PAGE_SHOW_CONFIG_BUTTON
  #define SETUP_PAGE_SHOW_CONFIG_BUTTON true
#endif



// ********************************************************************************
// Web Interface Setup Wizard
// ********************************************************************************

# define HANDLE_SETUP_SCAN_STAGE       0
# define HANDLE_SETUP_CONNECTING_STAGE 1

void handle_setup() {
  # ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_setup"));
  # endif // ifndef BUILD_NO_RAM_TRACKER

  // Do not check client IP range allowed.
  TXBuffer.startStream();

  const bool connected = NetworkConnected();


//  if (connected) {
    navMenuIndex = MENU_INDEX_SETUP;
    sendHeadandTail_stdtemplate(_HEAD);
/*  } else {
    sendHeadandTail(F("TmplAP"));
  }
  */

  const bool clearButtonPressed = hasArg(F("performclearcredentials"));
  const bool clearWiFiCredentials = 
    isFormItemChecked(F("clearcredentials")) && clearButtonPressed;

  {
    if (clearWiFiCredentials) {
      SecuritySettings.clearWiFiCredentials();
      addHtmlError(SaveSecuritySettings());

      html_add_form();
      html_table_class_normal();

      addFormHeader(F("WiFi credentials cleared, reboot now"));
      html_end_table();
    } else {    
  //    if (active_network_medium == NetworkMedium_t::WIFI)
  //    {
        static uint8_t status       = HANDLE_SETUP_SCAN_STAGE;
        static uint8_t refreshCount = 0;

        String ssid              = webArg(F("ssid"));
        String other             = webArg(F("other"));
        String password;
        bool passwordGiven = getFormPassword(F("pass"), password);
        if (passwordGiven) {
          passwordGiven = !password.isEmpty();
        }
        const bool emptyPassAllowed = isFormItemChecked(F("emptypass"));
        const bool performRescan = hasArg(F("performrescan"));
        if (performRescan) {
          WiFiEventData.lastScanMoment.clear();
          WifiScan(false);
        }

        if (!other.isEmpty())
        {
          ssid = other;
        }

        if (!performRescan) {
          // if ssid config not set and params are both provided
          if ((status == HANDLE_SETUP_SCAN_STAGE) && (!ssid.isEmpty()) /*&& strcasecmp(SecuritySettings.WifiSSID, "ssid") == 0 */)
          {
            if (clearButtonPressed) {
              addHtmlError(F("Warning: Need to confirm to clear WiFi credentials"));
            } else if (!passwordGiven && !emptyPassAllowed) {
              addHtmlError(F("No password entered"));
            } else {
              safe_strncpy(SecuritySettings.WifiKey,  password.c_str(), sizeof(SecuritySettings.WifiKey));
              safe_strncpy(SecuritySettings.WifiSSID, ssid.c_str(),     sizeof(SecuritySettings.WifiSSID));
              // Hidden SSID
              Settings.IncludeHiddenSSID(isFormItemChecked(LabelType::CONNECT_HIDDEN_SSID));
              Settings.HiddenSSID_SlowConnectPerBSSID(isFormItemChecked(LabelType::HIDDEN_SSID_SLOW_CONNECT));
#ifdef ESP32
              Settings.PassiveWiFiScan(isFormItemChecked(LabelType::WIFI_PASSIVE_SCAN));
#endif

              addHtmlError(SaveSettings());
              WiFiEventData.wifiSetupConnect         = true;
              WiFiEventData.wifiConnectAttemptNeeded = true;
              WiFi_AP_Candidates.force_reload(); // Force reload of the credentials and found APs from the last scan

              if (loglevelActiveFor(LOG_LEVEL_INFO)) {
                String reconnectlog = F("WIFI : Credentials Changed, retry connection. SSID: ");
                reconnectlog += ssid;
                addLogMove(LOG_LEVEL_INFO, reconnectlog);
              }
              status       = HANDLE_SETUP_CONNECTING_STAGE;
              refreshCount = 0;
              AttemptWiFiConnect();
            }
          }
        }
        html_BR();
        wrap_html_tag(F("h1"), connected ? F("Connected to a network") : F("Wifi Setup wizard"));
        html_add_form();

        switch (status) {
          case HANDLE_SETUP_SCAN_STAGE:
          {
            // first step, scan and show access points within reach...
            handle_setup_scan_and_show(ssid, other, password);
            break;
          }
          case  HANDLE_SETUP_CONNECTING_STAGE:
          {
            if (!handle_setup_connectingStage(refreshCount)) {
              status = HANDLE_SETUP_SCAN_STAGE;
            }
            ++refreshCount;
            break;
          }
        }
  /*
      } else {
        html_add_form();
        addFormHeader(F("Ethernet Setup Complete"));

      }
  */

      html_table_class_normal();
      html_TR();
#if defined(WEBSERVER_SYSINFO) && !defined(WEBSERVER_SYSINFO_MINIMAL)
      handle_sysinfo_NetworkServices();
#endif
      if (connected) {

        //addFormHeader(F("Current network configuration"));

#ifdef WEBSERVER_SYSINFO
        handle_sysinfo_Network();
#endif

        addFormSeparator(2);

        html_TR_TD();
        html_TD();
        
        #if SETUP_PAGE_SHOW_CONFIG_BUTTON
        if (!clientIPinSubnet()) {
          String host = formatIP(NetworkLocalIP());
          String url  = F("http://");
          url += host;
          url += F("/config");
          addButton(url, host);
        }
        #endif

        WiFiEventData.wifiSetup = false;
      } 
      html_end_table();

      html_BR();
      html_BR();
      html_BR();
      html_BR();
      html_BR();
      html_BR();
      html_BR();

      html_table_class_normal();

      addFormHeader(F("Advanced WiFi settings"));

      addFormCheckBox(LabelType::CONNECT_HIDDEN_SSID,      Settings.IncludeHiddenSSID());

#ifdef ESP32
      addFormCheckBox(LabelType::WIFI_PASSIVE_SCAN, Settings.PassiveWiFiScan());
#endif

      addFormCheckBox(LabelType::HIDDEN_SSID_SLOW_CONNECT,      Settings.HiddenSSID_SlowConnectPerBSSID());

      html_BR();
      html_BR();

      addFormHeader(F("Clear WiFi credentials"));
      addFormCheckBox(F("Confirm clear"), F("clearcredentials"), false);

      html_TR_TD();
      html_TD();
      addSubmitButton(F("Clear and Reboot"), F("performclearcredentials"), F("red"));
      html_end_table();
    }

    html_end_form();
  }
//  if (connected) {
    sendHeadandTail_stdtemplate(_TAIL);
/*  } else {
    sendHeadandTail(F("TmplAP"), true);
  }
*/
  TXBuffer.endStream();
  delay(10);
  if (clearWiFiCredentials) {
    reboot(IntendedRebootReason_e::RestoreSettings);
  }
}

void handle_setup_scan_and_show(const String& ssid, const String& other, const String& password) {
  int8_t scanCompleteStatus = WiFi_AP_Candidates.scanComplete();
  const bool needsRescan = 
    (scanCompleteStatus == 0 || // No AP found
     scanCompleteStatus == -2)  // Scan not triggered
    && WiFiScanAllowed();
  if (needsRescan) {
    WiFiMode_t cur_wifimode = WiFi.getMode();
    WifiScan(false);
    scanCompleteStatus = WiFi_AP_Candidates.scanComplete();
    setWifiMode(cur_wifimode);
  }


  if (scanCompleteStatus <= 0) {
    addHtml(F("No Access Points found"));
  }
  else
  {
    html_table_class_multirow();
    html_TR();
    html_table_header(F("Pick"), 50);
    html_table_header(F("Network info"));
    html_table_header(F("RSSI"), 50);

    for (auto it = WiFi_AP_Candidates.scanned_begin(); it != WiFi_AP_Candidates.scanned_end(); ++it)
    {
      html_TR_TD();
      const String id = it->toString("");
      addHtml(F("<label "));
      addHtmlAttribute(F("class"), F("container2"));
      addHtmlAttribute(F("for"), id);
      addHtml('>');

      addHtml(F("<input type='radio' name='ssid' value='"));
      if (it->bits.isHidden) {
        addHtml(F("#Hidden#' disabled"));
      } else {
        String escapeBuffer = it->ssid;
        htmlStrongEscape(escapeBuffer);
        addHtml(escapeBuffer);
        addHtml('\'');
      }

      addHtmlAttribute(F("id"), id);

      {
        if (it->bssid_match(RTC.lastBSSID)) {
          if (!WiFi_AP_Candidates.SettingsIndexMatchCustomCredentials(RTC.lastWiFiSettingsIndex)) {
            addHtml(F(" checked "));  
          }
        }
      }

      addHtml(F("><span class='dotmark'></span>"));
      addHtml(F("</label>"));

      html_TD();
      addHtml(F("<label "));
      addHtmlAttribute(F("for"), id);
      addHtml('>');
      addHtml(it->toString(F("<BR>")));
      addHtml(F("</label>"));

      html_TD();
      addHtml(F("<label "));
      addHtmlAttribute(F("for"), id);
      addHtml('>');
      getWiFi_RSSI_icon(it->rssi, 45);
      addHtml(F("</label>"));
    }
    html_end_table();
  }

  html_BR();

  addSubmitButton(F("Rescan"), F("performrescan"));

  html_BR();

  html_table_class_normal();
  html_TR_TD();

  addHtml(F("<label "));
  addHtmlAttribute(F("class"), F("container2"));
  addHtml('>');
  addHtml(F("other SSID:"));
  addHtml(F("<input "));
  addHtmlAttribute(F("type"),  F("radio"));
  addHtmlAttribute(F("name"),  F("ssid"));
  addHtmlAttribute(F("id"),    F("other_ssid"));
  addHtmlAttribute(F("value"), F("other"));
  addHtml('>');
  addHtml(F("<span class='dotmark'></span></label>"));

  html_TD();

  addHtml(F("<label "));
  addHtmlAttribute(F("for"),    F("other_ssid"));
  addHtml('>');

  addHtml(F("<input "));
  addHtmlAttribute(F("class"), F("wide"));
  addHtmlAttribute(F("type"),  F("text"));
  addHtmlAttribute(F("name"),  F("other"));
  addHtmlAttribute(F("value"), other);
  addHtml('>');
  addHtml(F("</label>"));


  html_TR();

  html_BR();
  html_BR();

  addFormSeparator(2);

  html_BR();

  addFormPasswordBox(F("Password"), F("pass"), password, 63);
  addFormCheckBox(F("Allow Empty Password"), F("emptypass"), false);
  
/*
  if (SecuritySettings.hasWiFiCredentials(SecurityStruct::WiFiCredentialsSlot::first)) {
    addFormCheckBox(F("Clear Stored SSID1"), F("clearssid1"), false);
    addFormNote(String(F("Current: ")) + getValue(LabelType::WIFI_STORED_SSID1));
  }
  if (SecuritySettings.hasWiFiCredentials(SecurityStruct::WiFiCredentialsSlot::second)) {
    addFormCheckBox(F("Clear Stored SSID2"), F("clearssid2"), false);
    addFormNote(String(F("Current: ")) + getValue(LabelType::WIFI_STORED_SSID2));
  }
  */

  html_TR_TD();
  html_TD();
  html_BR();
  addSubmitButton(F("Connect"), EMPTY_STRING);

  html_end_table();
}

bool handle_setup_connectingStage(uint8_t refreshCount) {
  if (refreshCount > 0)
  {
    //      safe_strncpy(SecuritySettings.WifiSSID, "ssid", sizeof(SecuritySettings.WifiSSID));
    //      SecuritySettings.WifiKey[0] = 0;
    addButton(F("/setup"), F("Back to Setup"));
    html_TR_TD();
    html_BR();
    WiFiEventData.wifiSetupConnect = false;
    return false;
  }
  int wait = WIFI_RECONNECT_WAIT / 1000;

  if (refreshCount != 0) {
    wait = 3;
  }
  addHtml(F("Please wait for <h1 id='countdown'>20..</h1>" 
            "<script>"
            "function timedRefresh(tp) {"
            "var t=setInterval(function(){"
            "if(tp>0){"
            "tp-=1;"
            "document.getElementById('countdown').innerHTML=tp+'..'+'<br/>';"
            "}else{"
            "clearInterval(t);"
            "window.location.href=window.location.href;"
            "};"
            "},1000);"
            "};"
            "timedRefresh("));
  addHtmlInt(wait);
  addHtml(')', ';');
  html_add_script_end();
  addHtml(F("seconds while trying to connect"));
  return true;
}

#endif // ifdef WEBSERVER_SETUP

#include "../WebServer/DownloadPage.h"

#ifdef WEBSERVER_DOWNLOAD

# include "../WebServer/ESPEasy_WebServer.h"
# include "../DataTypes/SettingsType.h"
# include "../Globals/ESPEasy_time.h"
# include "../Globals/Settings.h"
# include "../Helpers/ESPEasy_Storage.h"
# include "../Helpers/StringGenerator_System.h"

# if FEATURE_TARSTREAM_SUPPORT
#  include "../Helpers/TarStream.h"
# endif // if FEATURE_TARSTREAM_SUPPORT

// ********************************************************************************
// Web Interface download page
// ********************************************************************************
void handle_download() {
# if FEATURE_TARSTREAM_SUPPORT
  handle_config_download(false, false);
}

void handle_full_backup() {
  handle_config_download(true, false);
}

void handle_full_backup_no_usr_pwd() {
  handle_config_download(true, true);
}

void handle_config_download(bool fullBackup,
                            bool noCreds) {
# else // if FEATURE_TARSTREAM_SUPPORT
  const bool noCreds = false;
# endif // if FEATURE_TARSTREAM_SUPPORT
  # ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_download"));
  # endif // ifndef BUILD_NO_RAM_TRACKER

  if (!isLoggedIn()) { return; }
  navMenuIndex = MENU_INDEX_TOOLS;

  //  TXBuffer.startStream();
  //  sendHeadandTail_stdtemplate(_HEAD);


  fs::File dataFile = tryOpenFile(getFileName(FileType::CONFIG_DAT), "r");

  if (!dataFile) {
    return;
  }

  String str = F("attachment; filename=");
  # if FEATURE_TARSTREAM_SUPPORT

  if (fullBackup) {
    str += F("backup_");
  } else
  # endif // if FEATURE_TARSTREAM_SUPPORT
  {
    str += F("config_");
  }

  if (noCreds) {
    str += F("no_creds_");
  }
  str += strformat(F("%s_U%d_Build%s_"),
                   Settings.getName().c_str(),
                   Settings.Unit,
                   getSystemBuildString().c_str());

  if (node_time.systemTimePresent())
  {
    str += node_time.getDateTimeString('\0', '\0', '\0');
  }

  # if FEATURE_TARSTREAM_SUPPORT
  bool useTarFile       = false;
  const int  equalsSign = str.indexOf('=');
  TarStream *tarStream  = (fullBackup || !Settings.DisableSaveConfigAsTar())
    ? new TarStream(str.substring(equalsSign + 1) + F(".tar"))
    : nullptr;

  if (fullBackup && (nullptr != tarStream)) {
    const String security_dat = getFileName(FileType::SECURITY_DAT);
    #  if defined(ESP8266)

    fs::Dir dir = ESPEASY_FS.openDir("");

    while (dir.next()) {
      fs::File file = dir.openFile("r");

      if (file) {
        if (!noCreds || (noCreds && (0 != strncasecmp(file.name(), security_dat.c_str(), security_dat.length())))) {
          tarStream->addFile(file.name(), file.size());
        }
        file.close();
      }
    }
    #  endif // if defined(ESP8266)
    #  if defined(ESP32)
    fs::File root = ESPEASY_FS.open("/");
    fs::File file = root.openNextFile();

    while (file) {
      if (!file.isDirectory()) {
        if (!noCreds || (noCreds && (0 != strncasecmp(file.name(), security_dat.c_str(), security_dat.length())))) {
          tarStream->addFile(file.name(), file.size());
        }
      }
      file = root.openNextFile();
    }
    #  endif // if defined(ESP32)
  } else {
    if (nullptr != tarStream) {
      tarStream->addFile(dataFile.name(), dataFile.size());

      #  if FEATURE_EXTENDED_CUSTOM_SETTINGS

      // extcfg<tasknr>.dat files
      for (uint8_t n = 0; n < TASKS_MAX; ++n) {
        tarStream->addFileIfExists(SettingsType::getSettingsFileName(SettingsType::Enum::CustomTaskSettings_Type, n));
      }
      #  endif // if FEATURE_EXTENDED_CUSTOM_SETTINGS

      // other config files
      tarStream->addFileIfExists(getFileName(FileType::NOTIFICATION_DAT));
      tarStream->addFileIfExists(getFileName(FileType::PROVISIONING_DAT));

      if (!noCreds) {
        tarStream->addFileIfExists(getFileName(FileType::SECURITY_DAT));
      }

      // rules<n>.txt files
      for (unsigned int rf = 0; rf < RULESETS_MAX; ++rf) {
        tarStream->addFileIfExists(getRulesFileName(rf));
      }
    }
  }

  if (nullptr != tarStream) {
    #  ifndef BUILD_NO_DEBUG
    addLog(LOG_LEVEL_INFO, strformat(F("Download: %d file(s) added to .tar. Size: %d bytes, filebytes: %d"),
                                     tarStream->getFileCount(), tarStream->size(), tarStream->getFilesSizes()));
    #  endif // ifndef BUILD_NO_DEBUG
    useTarFile = tarStream->getFileCount() > 1; // We should at least have config.dat, so ignore that
  }

  if (useTarFile) {
    str += F(".tar");
  } else

  # endif // if FEATURE_TARSTREAM_SUPPORT
  {
    str += F(".dat"); // This is in the 'else' part of the 'if' above!
  }

  sendHeader(F("Content-Disposition"), str);

  # if FEATURE_TARSTREAM_SUPPORT

  if (useTarFile) {
    web_server.streamFile(*tarStream, F("application/octet-stream"));
  } else {
    web_server.streamFile(dataFile, F("application/octet-stream"));
  }

  if (nullptr != tarStream) {
    delete tarStream;
  }

  # else // if FEATURE_TARSTREAM_SUPPORT
  web_server.streamFile(dataFile, F("application/octet-stream"));
  # endif // if FEATURE_TARSTREAM_SUPPORT
  dataFile.close();
}

#endif // ifdef WEBSERVER_DOWNLOAD

#include "../WebServer/WiFiScanner.h"

#include "../WebServer/ESPEasy_WebServer.h"
#include "../WebServer/AccessControl.h"
#include "../WebServer/HTML_wrappers.h"

#include "../ESPEasyCore/ESPEasyWifi.h"
#include "../Globals/WiFi_AP_Candidates.h"
#include "../Helpers/StringGenerator_WiFi.h"

#ifdef USES_ESPEASY_NOW
#include "../Globals/ESPEasy_now_handler.h"
#endif

#ifdef WEBSERVER_NEW_UI

// ********************************************************************************
// Web Interface Wifi scanner
// ********************************************************************************
void handle_wifiscanner_json() {
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_wifiscanner"));
  #endif

  if (!isLoggedIn()) { return; }
  navMenuIndex = MENU_INDEX_TOOLS;
  TXBuffer.startJsonStream();
  addHtml('[', '{');
  bool firstentry = true;

  if (WiFi_AP_Candidates.scanComplete() <= 0) {
    WiFiMode_t cur_wifimode = WiFi.getMode();
    WifiScan(false);
    setWifiMode(cur_wifimode);
  }

  for (auto it = WiFi_AP_Candidates.scanned_begin(); it != WiFi_AP_Candidates.scanned_end(); ++it)
  {
    if (firstentry) { firstentry = false; }
    else { addHtml(',', '{'); }
    const String authType = it->encryption_type();
    if (authType.length() > 0) {
      stream_next_json_object_value(F("auth"), authType);
    }
    stream_next_json_object_value(getLabel(LabelType::SSID),      it->ssid);
    stream_next_json_object_value(getLabel(LabelType::BSSID),     it->bssid.toString());
    stream_next_json_object_value(getLabel(LabelType::CHANNEL),   it->channel);
    stream_last_json_object_value(getLabel(LabelType::WIFI_RSSI), it->rssi);
  }
  if (firstentry) {
    addHtml('}');
  }
  addHtml(']');
  TXBuffer.endStream();
}

#endif // WEBSERVER_NEW_UI

#ifdef WEBSERVER_WIFI_SCANNER

void handle_wifiscanner() {
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_wifiscanner"));
  #endif

  if (!isLoggedIn()) { return; }

  WiFiMode_t cur_wifimode = WiFi.getMode();
  WifiScan(false);
  int8_t scanCompleteStatus = WiFi_AP_Candidates.scanComplete();
  setWifiMode(cur_wifimode);

  navMenuIndex = MENU_INDEX_TOOLS;
  TXBuffer.startStream();
  sendHeadandTail_stdtemplate(_HEAD);
  html_table_class_multirow();
  html_TR();
  html_table_header(getLabel(LabelType::SSID));
  html_table_header(getLabel(LabelType::BSSID));
  html_table_header(F("Network info"));
  html_table_header(F("RSSI"), 50);

  if (scanCompleteStatus <= 0) {
    addHtml(F("No Access Points found"));
  }
  else
  {
    for (auto it = WiFi_AP_Candidates.scanned_begin(); it != WiFi_AP_Candidates.scanned_end(); ++it)
    {
      html_TR_TD();
      addHtml(it->toString(F("<TD>")));
      html_TD();
      getWiFi_RSSI_icon(it->rssi, 45);
      #ifdef USES_ESPEASY_NOW
        ESPEasy_now_handler.addPeerFromWiFiScan(*it);
      #endif
    }
  }

  html_end_table();
  sendHeadandTail_stdtemplate(_TAIL);
  TXBuffer.endStream();
}

#endif // ifdef WEBSERVER_WIFI_SCANNER
#include "../WebServer/AccessControl.h"

#include "../DataTypes/FormSelectorOptions.h"

#include "../ESPEasyCore/ESPEasy_Log.h"
#include "../ESPEasyCore/ESPEasyNetwork.h"
#include "../ESPEasyCore/ESPEasyWifi.h"

#include "../Globals/SecuritySettings.h"
#include "../Globals/Services.h"

#include "../Helpers/Networking.h"
#include "../Helpers/StringConverter.h"

#include "../WebServer/Markup.h"

// ********************************************************************************
// Allowed IP range check
// ********************************************************************************

boolean ipLessEqual(const IPAddress& ip, const IPAddress& high)
{
  // FIXME TD-er: Must check whether both are of same type and check full range IPv6
  for (int i = 0; i < 4; ++i) {
    if (ip[i] != high[i]) {
      return ip[i] < high[i];
    }
  }
  // Is equal
  return true;
}

boolean ipInRange(const IPAddress& ip, const IPAddress& low, const IPAddress& high)
{
  return ipLessEqual(low, ip) && ipLessEqual(ip, high);
}

String describeAllowedIPrange() {
  String reply;

  switch (SecuritySettings.IPblockLevel) {
    case ALL_ALLOWED:
      reply +=  F("All Allowed");
      break;
    default:
    {
      IPAddress low, high;
      getIPallowedRange(low, high);
      reply +=  formatIP(low);
      reply +=  F(" - ");
      reply +=  formatIP(high);
    }
  }
  return reply;
}

bool getIPallowedRange(IPAddress& low, IPAddress& high)
{
  switch (SecuritySettings.IPblockLevel) {
    case LOCAL_SUBNET_ALLOWED:

      if (WifiIsAP(WiFi.getMode())) {
        // WiFi is active as accesspoint, do not check.
        return false;
      }
      return getSubnetRange(low, high);
    case ONLY_IP_RANGE_ALLOWED:
      low  = IPAddress(SecuritySettings.AllowedIPrangeLow);
      high = IPAddress(SecuritySettings.AllowedIPrangeHigh);
      break;
    default:
      low  = IPAddress(0, 0, 0, 0);
      high = IPAddress(255, 255, 255, 255);
      return false;
  }
  return true;
}

bool clientIPinSubnet() {
  IPAddress low, high;

  if (!getSubnetRange(low, high)) {
    // Could not determine subnet.
    return false;
  }
  return ipInRange(web_server.client().remoteIP(), low, high);
}

boolean clientIPallowed()
{
  #if ESP_IDF_VERSION_MAJOR>=5
  // FIXME TD-er: remoteIP() is reporting incorrect value
//  return true;
  #endif
  // TD-er Must implement "safe time after boot"
  IPAddress low, high;

  if (!getIPallowedRange(low, high))
  {
    // No subnet range determined, cannot filter on IP
    return true;
  }
  const IPAddress remoteIP = web_server.client().remoteIP();
  if (remoteIP == IPAddress(0, 0, 0, 0) 
  #if ESP_IDF_VERSION_MAJOR>=5
  || remoteIP.type() == IPv6
  #else
  || !remoteIP.isV4()
  #endif
  ) {
    // FIXME TD-er: Must see what's going on here, why the client doesn't send remote IP for some reason
    return true;
  }

  if (ipInRange(remoteIP, low, high)) {
    return true;
  }

  if (WifiIsAP(WiFi.getMode())) {
    // @TD-er Fixme: Should match subnet of SoftAP.
    return true;
  }
  String response = F("IP blocked: ");
  response += formatIP(remoteIP);
  web_server.send(403, F("text/html"), response);

  if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
    response += F(" Allowed: ");
    response += formatIP(low);
    response += F(" - ");
    response += formatIP(high);
    addLogMove(LOG_LEVEL_ERROR, response);
  }
  return false;
}

void clearAccessBlock()
{
  SecuritySettings.IPblockLevel = ALL_ALLOWED;
}

// ********************************************************************************
// Add a IP Access Control select dropdown list
// ********************************************************************************
void addIPaccessControlSelect(const String& name, int choice)
{
  const __FlashStringHelper *  options[] = { F("Allow All"), F("Allow Local Subnet"), F("Allow IP range") };

  const FormSelectorOptions selector(NR_ELEMENTS(options), options);
  selector.addSelector(name, choice);
}

#include "../WebServer/RootPage.h"


#ifdef WEBSERVER_ROOT

# include "../WebServer/ESPEasy_WebServer.h"
# include "../WebServer/HTML_wrappers.h"
# include "../WebServer/LoadFromFS.h"
# include "../WebServer/Markup.h"
# include "../WebServer/Markup_Buttons.h"
# include "../WebServer/Markup_Forms.h"

# include "../Commands/ExecuteCommand.h"
# include "../ESPEasyCore/ESPEasyNetwork.h"
# include "../Globals/ESPEasy_time.h"
# include "../Globals/ESPEasyWiFiEvent.h"
# include "../Globals/MainLoopCommand.h"
# include "../Globals/NetworkState.h"
# include "../Globals/Nodes.h"
# include "../Globals/Settings.h"
# include "../Globals/Statistics.h"
# include "../Helpers/ESPEasy_Storage.h"
# include "../Helpers/Memory.h"
# include "../Helpers/Misc.h"
# include "../Helpers/StringGenerator_System.h"
# include "../Helpers/WebServer_commandHelper.h"


# include "../../ESPEasy-Globals.h"

# if FEATURE_MQTT
#  include "../Globals/MQTT.h"
#  include "../ESPEasyCore/Controller.h" // For finding enabled MQTT controller
# endif // if FEATURE_MQTT


# ifndef MAIN_PAGE_SHOW_BASIC_INFO_NOT_LOGGED_IN
  #  define MAIN_PAGE_SHOW_BASIC_INFO_NOT_LOGGED_IN false
# endif // ifndef MAIN_PAGE_SHOW_BASIC_INFO_NOT_LOGGED_IN

// Define main page elements present
# ifndef MAIN_PAGE_SHOW_SYSINFO_BUTTON
  #  define MAIN_PAGE_SHOW_SYSINFO_BUTTON    true
# endif // ifndef MAIN_PAGE_SHOW_SYSINFO_BUTTON

# ifndef MAIN_PAGE_SHOW_WiFi_SETUP_BUTTON
  #  define MAIN_PAGE_SHOW_WiFi_SETUP_BUTTON   false
# endif // ifndef MAIN_PAGE_SHOW_WiFi_SETUP_BUTTON

# ifndef MAIN_PAGE_SHOW_NODE_LIST_BUILD
  #  define MAIN_PAGE_SHOW_NODE_LIST_BUILD   true
# endif // ifndef MAIN_PAGE_SHOW_NODE_LIST_BUILD
# ifndef MAIN_PAGE_SHOW_NODE_LIST_TYPE
  #  define MAIN_PAGE_SHOW_NODE_LIST_TYPE    true
# endif // ifndef MAIN_PAGE_SHOW_NODE_LIST_TYPE


// ********************************************************************************
// Web Interface root page
// ********************************************************************************
void handle_root() {
  # ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  # endif // ifdef USE_SECOND_HEAP

  # ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_root"));
  # endif // ifndef BUILD_NO_RAM_TRACKER

  if (captivePortal()) { // If captive portal redirect instead of displaying the page.
    return;
  }

  // if Wifi setup, launch setup wizard if AP_DONT_FORCE_SETUP is not set.
  if (WiFiEventData.wifiSetup && !Settings.ApDontForceSetup())
  {
    web_server.send_P(200, (PGM_P)F("text/html"), (PGM_P)F("<meta HTTP-EQUIV='REFRESH' content='0; url=/setup'>"));
    return;
  }

  if (!MAIN_PAGE_SHOW_BASIC_INFO_NOT_LOGGED_IN) {
    if (!isLoggedIn()) { return; }
  }

  const bool loggedIn = isLoggedIn(false);

  navMenuIndex = 0;

  // if index.htm exists on FS serve that one (first check if gziped version exists)
  if (loadFromFS(F("/index.htm.gz"))) { return; }

  if (loadFromFS(F("/index.htm"))) { return; }

  TXBuffer.startStream();

  boolean rebootCmd = false;
  String  sCommand  = webArg(F("cmd"));
  rebootCmd = strcasecmp_P(sCommand.c_str(), PSTR("reboot")) == 0;
  sendHeadandTail_stdtemplate(_HEAD, rebootCmd);

  int freeMem = ESP.getFreeHeap();

  // TODO: move this to handle_tools, from where it is actually called?

  // have to disconnect or reboot from within the main loop
  // because the webconnection is still active at this point
  // disconnect here could result into a crash/reboot...
  if (strcasecmp_P(sCommand.c_str(), PSTR("wifidisconnect")) == 0)
  {
    #ifndef BUILD_MINIMAL_OTA
    addLog(LOG_LEVEL_INFO, F("WIFI : Disconnecting..."));
    #endif
    cmd_within_mainloop = CMD_WIFI_DISCONNECT;
    addHtml(F("OK"));
  } else if (strcasecmp_P(sCommand.c_str(), PSTR("reboot")) == 0)
  {
    #ifndef BUILD_MINIMAL_OTA
    addLog(LOG_LEVEL_INFO, F("     : Rebooting..."));
    #endif
    cmd_within_mainloop = CMD_REBOOT;
    addHtml(F("OK"));
  } else if (strcasecmp_P(sCommand.c_str(), PSTR("reset")) == 0)
  {
    if (loggedIn) {
      #ifndef BUILD_MINIMAL_OTA
      addLog(LOG_LEVEL_INFO, F("     : factory reset..."));
      #endif
      cmd_within_mainloop = CMD_REBOOT;
      addHtml(F(
                "OK. Please wait > 1 min and connect to Access point.<BR><BR>PW=configesp<BR>URL=<a href='http://192.168.4.1'>192.168.4.1</a>"));
      TXBuffer.endStream();
      ExecuteCommand_internal({EventValueSource::Enum::VALUE_SOURCE_HTTP, sCommand.c_str()}, true);
      return;
    }
  } else {
    if (loggedIn) {
      handle_command_from_web(EventValueSource::Enum::VALUE_SOURCE_HTTP, sCommand);
      printToWeb     = false;
      printToWebJSON = false;
    }

    addHtml(F("<form>"));
    html_table_class_normal();
    addFormHeader(F("System Info"));

    addRowLabelValue(LabelType::UNIT_NR);
    addRowLabelValue(LabelType::GIT_BUILD);
    addRowLabel(LabelType::LOCAL_TIME);

    if (node_time.systemTimePresent())
    {
      addHtml(getValue(LabelType::LOCAL_TIME));
    }
    else {
      addHtml(F("<font color='red'>No system time source</font>"));
    }
    addRowLabelValue(LabelType::TIME_SOURCE);

    addRowLabel(LabelType::UPTIME);
    {
      addHtml(getExtendedValue(LabelType::UPTIME));
    }
    addRowLabel(LabelType::LOAD_PCT);

    if (wdcounter > 0)
    {
      addHtml(strformat(
        F("%.2f [%%] (LC=%d)"),
        getCPUload(),
        getLoopCountPerSec()));
    }

#if FEATURE_INTERNAL_TEMPERATURE
    addRowLabelValue(LabelType::INTERNAL_TEMPERATURE);
#endif
    {
      addRowLabel(LabelType::FREE_MEM);
      addHtmlInt(freeMem);
      addUnit(getFormUnit(LabelType::FREE_MEM));
# ifndef BUILD_NO_RAM_TRACKER
      addHtml(strformat(
        F(" (%d - %s)"),
        lowestRAM,
        lowestRAMfunction.c_str()));
# endif // ifndef BUILD_NO_RAM_TRACKER
    }
    {
# ifdef USE_SECOND_HEAP
      addRowLabelValue(LabelType::FREE_HEAP_IRAM);
# endif // ifdef USE_SECOND_HEAP
    }
    {
      addRowLabel(LabelType::FREE_STACK);
      addHtmlInt(getCurrentFreeStack());
      addUnit(getFormUnit(LabelType::FREE_STACK));
# ifndef BUILD_NO_RAM_TRACKER
      addHtml(strformat(
        F(" (%d - %s)"),
        lowestFreeStack,
        lowestFreeStackfunction.c_str()));
# endif // ifndef BUILD_NO_RAM_TRACKER
    }

  # if FEATURE_ETHERNET
    addRowLabelValue(LabelType::ETH_WIFI_MODE);
  # endif // if FEATURE_ETHERNET

    if (!WiFiEventData.WiFiDisconnected())
    {
      addRowLabelValue(LabelType::IP_ADDRESS);
#if FEATURE_USE_IPV6
      if (Settings.EnableIPv6()) {
        addRowLabelValue(LabelType::IP6_LOCAL);
        // Do not show global IPv6 on the root page
      }
#endif
      addRowLabel(LabelType::WIFI_RSSI);
      addHtml(strformat(
        F("%d [dBm] (%s)"),
        WiFi.RSSI(),
        WiFi.SSID().c_str()));
    }

  # if FEATURE_ETHERNET

    if (active_network_medium == NetworkMedium_t::Ethernet) {
      addRowLabelValue(LabelType::ETH_SPEED_STATE);
      addRowLabelValue(LabelType::ETH_IP_ADDRESS);
#if FEATURE_USE_IPV6
      if (Settings.EnableIPv6()) {
        addRowLabelValue(LabelType::ETH_IP6_LOCAL);
        // Do not show global IPv6 on the root page
      }
#endif
    }
  # endif // if FEATURE_ETHERNET

      # if FEATURE_MDNS
    {
      addRowLabel(LabelType::M_DNS);
      addHtml(F("<a href='http://"));
      const String url = getValue(LabelType::M_DNS);
      addHtml(url);
      addHtml(F("'>"));
      addHtml(url);
      addHtml(F("</a>"));
    }
      # endif // if FEATURE_MDNS

    # if FEATURE_MQTT
    {
      if (validControllerIndex(firstEnabledMQTT_ControllerIndex())) {
        addRowLabel(F("MQTT Client Connected"));
        addEnabled(MQTTclient_connected);
      }
    }
    # endif // if FEATURE_MQTT


    # if MAIN_PAGE_SHOW_SYSINFO_BUTTON
    html_TR_TD();
    html_TD();
    addButton(F("sysinfo"), F("More info"));
    # endif // if MAIN_PAGE_SHOW_SYSINFO_BUTTON
    # if MAIN_PAGE_SHOW_WiFi_SETUP_BUTTON
    html_TR_TD();
    html_TD();
    addButton(F("setup"), F("WiFi Setup"));
    # endif // if MAIN_PAGE_SHOW_WiFi_SETUP_BUTTON

    if (loggedIn) {
      if (printWebString.length() > 0)
      {
        html_BR();
        html_BR();
        addFormHeader(F("Command Argument"));
        addRowLabel(F("Command"));
        addHtml(sCommand);

        addHtml(F("<TR><TD colspan='2'>Command Output<BR><textarea readonly rows='10' wrap='on'>"));
        addHtml(printWebString);
        addHtml(F("</textarea>"));
        free_string(printWebString);
      }
    }
    html_end_table();

    # if FEATURE_ESPEASY_P2P
    html_BR();

    if ((Settings.Unit == 0) &&
        (Settings.UDPPort != 0)) { addFormNote(F("Warning: Unit number is 0, please change it if you want to send data to other units.")); }
    html_BR();
    html_table_class_multirow_noborder();
    html_TR();
    html_table_header(F("Node List"));
    html_table_header(F("Name"));

    if (MAIN_PAGE_SHOW_NODE_LIST_BUILD) {
      html_table_header(getLabel(LabelType::BUILD_DESC));
    }

    if (MAIN_PAGE_SHOW_NODE_LIST_TYPE) {
      html_table_header(F("Type"));
    }
    html_table_header(F("IP"), 160); // Should fit "255.255.255.255"
    html_table_header(F("Load"));
    html_table_header(F("Age (s)"));
    #  ifdef USES_ESPEASY_NOW

    if (Settings.UseESPEasyNow()) {
      html_table_header(F("Dist"));
      html_table_header(F("Peer Info"), 160);
    }
    #  endif // ifdef USES_ESPEASY_NOW

    for (auto it = Nodes.begin(); it != Nodes.end(); ++it)
    {
      if (it->second.valid())
      {
        bool isThisUnit = it->first == Settings.Unit;

        if (isThisUnit) {
          html_TR_TD_highlight();
        }
        else {
          html_TR_TD();
        }

        addHtml(F("Unit "));
        addHtmlInt(it->first);
        html_TD();

        if (isThisUnit) {
          addHtml(Settings.getName());
        }
        else {
          addHtml(it->second.getNodeName());
        }
        html_TD();

        if (MAIN_PAGE_SHOW_NODE_LIST_BUILD) {
          if (it->second.build) {
            addHtml(formatSystemBuildNr(it->second.build));
          }
          html_TD();
        }

        if (MAIN_PAGE_SHOW_NODE_LIST_TYPE) {
          addHtml(it->second.getNodeTypeDisplayString());
          html_TD();
        }

        if (it->second.ip[0] != 0
#if FEATURE_USE_IPV6
            || (Settings.EnableIPv6() &&
                (it->second.hasIPv6_mac_based_link_local || 
                 it->second.hasIPv6_mac_based_link_global)
               )
#endif
        )
        {
          IPAddress ip = it->second.IP();
          const uint16_t port = it->second.webgui_portnumber;

#if FEATURE_USE_IPV6
          bool isIPv6 = false;
          if (Settings.EnableIPv6()) {
            if (it->second.hasIPv6_mac_based_link_local) {
              ip = it->second.IPv6_link_local(true);
              isIPv6 = true;
            } else if (it->second.hasIPv6_mac_based_link_global) {
              ip = it->second.IPv6_global();
              isIPv6 = true;
            }
          }
          if (it->second.hasIPv4 && it->second.hasIPv6()) {
            // Add 2 buttons for IPv4 and IPv6 address
            html_add_wide_button_prefix();
            addHtml(F("http://"));
            addHtml(wrap_String(formatIP(ip), '[', ']'));
            if ((port != 0) && (port != 80)) {
              addHtml(':');
              addHtmlInt(port);
            }
            addHtml('\'', '>');
            addHtml(formatIP(ip));
            addHtml(F("</a>"));

            // Now prepare 2nd button prefix
            addHtml(F("<BR>"));
            html_add_wide_button_prefix();
            ip = it->second.IP();
            isIPv6 = false;
          } else {
            // Add single wide button
            html_add_wide_button_prefix();
          }
#else
          html_add_wide_button_prefix();
#endif
          addHtml(F("http://"));
#if FEATURE_USE_IPV6

          if (isIPv6) {
            addHtml(wrap_String(formatIP(ip), '[', ']'));
          } else {
            addHtml(formatIP(ip));
          }
          #else
          addHtml(formatIP(ip));
          #endif
          
          if ((port != 0) && (port != 80)) {
            addHtml(':');
            addHtmlInt(port);
          }
          addHtml('\'', '>');
          addHtml(formatIP(ip));
          addHtml(F("</a>"));
        }
        html_TD();
        const float load = it->second.getLoad();

        if (load > 0.1) {
          addHtmlFloat(load);
        }
        html_TD();
        addHtmlInt(static_cast<uint32_t>(it->second.getAge() / 1000)); // time in seconds
        #  ifdef USES_ESPEASY_NOW

        if (Settings.UseESPEasyNow()) {
          html_TD();

          if (it->second.distance != 255) {
            addHtmlInt(it->second.distance);
          }
          html_TD();

          if (it->second.ESPEasyNowPeer) {
            addHtml(F(ESPEASY_NOW_NAME));
            addHtml(' ');
            addHtml(it->second.ESPEasy_Now_MAC().toString());
            addHtml(F(" (ch: "));
            addHtmlInt(it->second.channel);
            int8_t rssi = it->second.getRSSI();

            if (rssi < 0) {
              addHtml(' ');
              addHtmlInt(rssi);
            }
            addHtml(')');
            const ESPEasy_now_traceroute_struct *trace = Nodes.getDiscoveryRoute(it->second.unit);

            if (trace != nullptr) {
              addHtml(' ');
              addHtml(trace->toString());
            }
          }
        }
        #  endif // ifdef USES_ESPEASY_NOW
      }
    }

    html_end_table();
  # endif // if FEATURE_ESPEASY_P2P
    html_end_form();

    free_string(printWebString);
    printToWeb     = false;
    sendHeadandTail_stdtemplate(_TAIL);
  }
  TXBuffer.endStream();
}

#endif // ifdef WEBSERVER_ROOT

#include "../WebServer/UploadPage.h"

#include "../WebServer/ESPEasy_WebServer.h"
#include "../WebServer/AccessControl.h"
#include "../WebServer/Markup_Buttons.h"
#include "../WebServer/HTML_wrappers.h"

#include "../Globals/Cache.h"
#include "../Helpers/ESPEasy_Storage.h"
#if FEATURE_TARSTREAM_SUPPORT
# include "../Helpers/TarStream.h"
#endif // if FEATURE_TARSTREAM_SUPPORT
#include "../../ESPEasy-Globals.h"


#ifdef WEBSERVER_UPLOAD

# ifndef FEATURE_UPLOAD_CLEANUP_CONFIG
#  define FEATURE_UPLOAD_CLEANUP_CONFIG 1 // Enable/Disable removing of extcfg<tasknr>.dat files when a .tar file is uploaded having
                                          // a valid config.dat included
# endif // ifndef FEATURE_UPLOAD_CLEANUP_CONFIG

// ********************************************************************************
// Web Interface upload page
// ********************************************************************************
uploadResult_e uploadResult = uploadResult_e::UploadStarted;

void handle_upload() {
  if (!isLoggedIn()) { return; }
  navMenuIndex = MENU_INDEX_TOOLS;
  TXBuffer.startStream();
  sendHeadandTail_stdtemplate(_HEAD);

  addHtml(F(
            "<form enctype='multipart/form-data' method='post'><p>Upload settings file:<br><input type='file' name='datafile' size='40'></p><div><input class='button link' type='submit' value='Upload'></div><input type='hidden' name='edit' value='1'></form>"));
  sendHeadandTail_stdtemplate(_TAIL);
  TXBuffer.endStream();
  free_string(printWebString);
  printToWeb     = false;
}

// ********************************************************************************
// Web Interface upload page
// ********************************************************************************
void handle_upload_post() {
  # ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_upload_post"));
  # endif // ifndef BUILD_NO_RAM_TRACKER

  if (!isLoggedIn()) { return; }

  navMenuIndex = MENU_INDEX_TOOLS;
  TXBuffer.startStream();
  sendHeadandTail_stdtemplate(_HEAD);

  switch (uploadResult) {
    case uploadResult_e::Success:
    case uploadResult_e::SuccessReboot:
      addHtml(F("Upload OK!<BR>"));

      if (uploadResult_e::SuccessReboot == uploadResult) { // Enable string de-duplication
        addHtml(F("<font color='red'>"));
        addHtml(F("You REALLY need to reboot to apply all settings..."));
        addHtml(F("</font>&nbsp;"));
        addWideButton(F("/?cmd=reboot"), F("Reboot now"));
        addHtml(F("<BR>"));
      } else {
        addHtml(F("You may"));
        addHtml(F(" need to reboot to apply all settings..."));
      }
      LoadSettings();
      break;
    case uploadResult_e::InvalidFile:
      addHtml(F("<font color='red'>"));
      addHtml(F("Upload file invalid!"));
      addHtml(F("</font><BR>"));
      break;
    case uploadResult_e::NoFilename:
      addHtml(F("<font color='red'>"));
      addHtml(F("No filename!"));
      addHtml(F("</font><BR>"));
      break;
    case uploadResult_e::UploadStarted:
      break;
  }

  addHtml(F("Upload finished"));
  sendHeadandTail_stdtemplate(_TAIL);
  TXBuffer.endStream();
  free_string(printWebString);
  printToWeb     = false;
}

# ifdef WEBSERVER_NEW_UI
void handle_upload_json() {
  #  ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_upload_post"));
  #  endif // ifndef BUILD_NO_RAM_TRACKER
  uint8_t result = static_cast<int>(uploadResult);

  if (!isLoggedIn()) { result = 255; }

  TXBuffer.startJsonStream();
  addHtml('{');
  stream_next_json_object_value(F("status"), result);
  addHtml('}');

  TXBuffer.endStream();
}

# endif // WEBSERVER_NEW_UI

// ********************************************************************************
// Web Interface upload handler
// ********************************************************************************
fs::File uploadFile;
# if FEATURE_TARSTREAM_SUPPORT
TarStream *tarStream = nullptr;
# endif // if FEATURE_TARSTREAM_SUPPORT
void handleFileUpload() {
  # ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handleFileUpload"));
  # endif // ifndef BUILD_NO_RAM_TRACKER
  handleFileUploadBase(false);
}

# if FEATURE_SD
void handleSDFileUpload() {
  #  ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handleSDFileUpload"));
  #  endif // ifndef BUILD_NO_RAM_TRACKER
  handleFileUploadBase(true);
}

# endif // if FEATURE_SD

void handleFileUploadBase(bool toSDcard) {
  if (!isLoggedIn()) { return; }

  static boolean valid                = false;
  bool receivedConfigDat              = false;
  const FileDestination_e destination = toSDcard ? FileDestination_e::SD : FileDestination_e::ANY;

  HTTPUpload& upload = web_server.upload();

  if (upload.filename.isEmpty())
  {
    uploadResult = uploadResult_e::NoFilename;
    return;
  }

  if (upload.status == UPLOAD_FILE_START)
  {
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, concat(F("Upload: START, filename: "), upload.filename));
    }
    valid        = false;
    uploadResult = uploadResult_e::UploadStarted;
  }
  else if (upload.status == UPLOAD_FILE_WRITE)
  {
    // first data block, if this is the config file, check PID/Version
    if (upload.totalSize == 0)
    {
      if (matchFileType(upload.filename, FileType::CONFIG_DAT)) {
        valid             = validateUploadConfigDat(upload.buf);
        receivedConfigDat = true;
      } else {
        // other files are always assumed valid...
        valid = true;
      }

      if (valid) {
        # if FEATURE_TARSTREAM_SUPPORT

        if ((upload.filename.length() > 3) && upload.filename.substring(upload.filename.length() - 4).equalsIgnoreCase(F(".tar"))) {
          constexpr unsigned size = sizeof(TarStream);
          void *ptr               = special_calloc(1, size);
    
          if (ptr != nullptr) {  
            tarStream = new (ptr) TarStream(upload.filename, destination);
          }
          #  ifndef BUILD_NO_DEBUG

          if (loglevelActiveFor(LOG_LEVEL_INFO)) {
            addLogMove(LOG_LEVEL_INFO, concat(F("Upload: TAR Processing .tar file: "), upload.filename));
          }
          #  endif // ifndef BUILD_NO_DEBUG
        } else
        # endif // if FEATURE_TARSTREAM_SUPPORT
        {
          // once we're safe, remove file and create empty one...
          tryDeleteFile(upload.filename, destination);
          uploadFile = tryOpenFile(upload.filename, F("w"), destination);
        }

        // dont count manual uploads: flashCount();
      }
    }

    # if FEATURE_TARSTREAM_SUPPORT

    if (nullptr != tarStream) {
      tarStream->write(upload.buf, upload.currentSize);
    } else
    # endif // if FEATURE_TARSTREAM_SUPPORT
    {
      if (uploadFile) { uploadFile.write(upload.buf, upload.currentSize); }
    }

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, concat(F("Upload: WRITE, Bytes: "), upload.currentSize));
    }
  }
  else if (upload.status == UPLOAD_FILE_END)
  {
    # if FEATURE_TARSTREAM_SUPPORT

    if (nullptr != tarStream) {
      tarStream->flush();
      #  if FEATURE_EXTENDED_CUSTOM_SETTINGS && FEATURE_UPLOAD_CLEANUP_CONFIG

      // If we have received a valid config.dat, and extended custom settings is available,
      // delete all NOT included extcfg<tasknr>.dat files
      // FIXME Keep this feature enabled ??? (the extra files _can't_ be deleted manually)
      if (tarStream->isFileIncluded(getFileName(FileType::CONFIG_DAT))) { // Is config.dat included?
        receivedConfigDat = true;

        for (uint8_t n = 0; n < TASKS_MAX; n++) {
          const String extcfgFilename = SettingsType::getSettingsFileName(SettingsType::Enum::CustomTaskSettings_Type, n);

          if (!tarStream->isFileIncluded(extcfgFilename) &&
              tryDeleteFile(extcfgFilename) && loglevelActiveFor(LOG_LEVEL_INFO)) {
            addLogMove(LOG_LEVEL_INFO, concat(F("Upload: Removing not included extended settings: "), extcfgFilename));
          }
        }
      }
      #  endif // if FEATURE_EXTENDED_CUSTOM_SETTINGS && FEATURE_UPLOAD_CLEANUP_CONFIG
      delete tarStream;
    } else
    # endif // if FEATURE_TARSTREAM_SUPPORT
    {
      if (uploadFile) { uploadFile.close(); }
    }

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, concat(F("Upload: END, Size: "), upload.totalSize));
    }
  }

  if (valid) {
    if (receivedConfigDat) {
      uploadResult = uploadResult_e::SuccessReboot;
    } else {
      uploadResult = uploadResult_e::Success;
    }
  }
  else {
    uploadResult = uploadResult_e::InvalidFile;
  }
}

#endif // ifdef WEBSERVER_UPLOAD

#include "../WebServer/Markup_Forms.h"

#include "../WebServer/ESPEasy_WebServer.h"
#include "../WebServer/AccessControl.h"
#include "../WebServer/Markup.h"
#include "../WebServer/HTML_wrappers.h"

#include "../Globals/Settings.h"

#include "../Helpers/Hardware_GPIO.h"
#include "../Helpers/Numerical.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringGenerator_GPIO.h"

# if FEATURE_MQTT_DISCOVER && FEATURE_MQTT_DEVICECLASS
#include "../Helpers/_CPlugin_Helper_mqtt.h"
# endif // if FEATURE_MQTT_DISCOVER && FEATURE_MQTT_DEVICECLASS

// ********************************************************************************
// Add a separator as row start
// ********************************************************************************
void addFormSeparator(int clspan)
{
  addHtml(strformat(
    F("<TR><TD colspan='%d'><hr>"),
    clspan));
}

// ********************************************************************************
// Add a note as row start
// ********************************************************************************
void addFormNote(const __FlashStringHelper * text)
{
  addRowLabel_tr_id(EMPTY_STRING, EMPTY_STRING);
  addHtml(F(" <div class='note'>Note: "));
  addHtml(text);
  addHtml(F("</div>"));
}

void addFormNote(const String& text, const String& id)
{
  if (text.isEmpty())  return;
  addRowLabel_tr_id(EMPTY_STRING, id);
  addHtmlDiv(F("note"), concat(F("Note: "), text));
}

void addFormNote(const LabelType::Enum& label)
{
  addUnit(getFormUnit(label));
  addFormNote(getFormNote(label));
}

// ********************************************************************************
// Create Forms
// ********************************************************************************


// ********************************************************************************
// Add a checkbox Form
// ********************************************************************************

void addFormCheckBox_disabled(const String& label, const String& id, bool checked
                              #if FEATURE_TOOLTIPS
                              , const String& tooltip
                              #endif // if FEATURE_TOOLTIPS
                              ) {
  addFormCheckBox(label, id, checked, true
                  #if FEATURE_TOOLTIPS
                  , tooltip
                  #endif // if FEATURE_TOOLTIPS
                  );
}

void addFormCheckBox(const __FlashStringHelper * label, const __FlashStringHelper * id, bool checked, bool disabled)
{
  addRowLabel_tr_id(label, id);
  addCheckBox(id, checked, disabled);
}

void addFormCheckBox(const __FlashStringHelper * label, const String& id, bool checked, bool disabled)
{
  addRowLabel_tr_id(label, id);
  addCheckBox(id, checked, disabled);
}

void addFormCheckBox(const String& label, const String& id, bool checked, bool disabled
                     #if FEATURE_TOOLTIPS
                     , const String& tooltip
                     #endif // if FEATURE_TOOLTIPS
                     )
{
  addRowLabel_tr_id(label, id);
  addCheckBox(id, checked, disabled
              #if FEATURE_TOOLTIPS
              , tooltip
              #endif // if FEATURE_TOOLTIPS
              );
}

void addFormCheckBox(LabelType::Enum label, bool checked, bool disabled
                     #if FEATURE_TOOLTIPS
                     , const String& tooltip
                     #endif // if FEATURE_TOOLTIPS
                     ) {
  addFormCheckBox(getLabel(label), getInternalLabel(label), checked, disabled
                  #if FEATURE_TOOLTIPS
                  , tooltip
                  #endif // if FEATURE_TOOLTIPS
                  );
  addFormNote(label);
}

void addFormCheckBox_disabled(LabelType::Enum label, bool checked) {
  addFormCheckBox(label, checked, true);
}

// ********************************************************************************
// Add a Numeric Box form
// ********************************************************************************
void addFormNumericBox(LabelType::Enum label, int value, int min, int max
                       #if FEATURE_TOOLTIPS
                       , const String& tooltip
                       #endif // if FEATURE_TOOLTIPS
                       , bool disabled
                       )
{
  addFormNumericBox(getLabel(label), getInternalLabel(label), value, min, max
                    #if FEATURE_TOOLTIPS
                    , tooltip
                    #endif // if FEATURE_TOOLTIPS
                    , disabled
                    );
  addFormNote(label);
}

void addFormNumericBox(const __FlashStringHelper * label, 
                       const __FlashStringHelper * id, 
                       int value, 
                       int min, 
                       int max
                       #if FEATURE_TOOLTIPS
                       ,
                       const String& tooltip
                       #endif // if FEATURE_TOOLTIPS
                       ,
                       bool disabled
                       )
{
  addFormNumericBox(String(label), String(id), value, min, max
                    #if FEATURE_TOOLTIPS
                    , tooltip
                    #endif // if FEATURE_TOOLTIPS
                    , disabled
                    );

}

void addFormNumericBox(const String& label, const String& id, int value, int min, int max
                       #if FEATURE_TOOLTIPS
                       , const String& tooltip
                       #endif // if FEATURE_TOOLTIPS
                       , bool disabled
                       )
{
  addRowLabel_tr_id(label, id);
  addNumericBox(id, value, min, max
                #if FEATURE_TOOLTIPS
                , F("widenumber")
                , tooltip
                #endif // if FEATURE_TOOLTIPS
                , disabled
                );
}

void addFormFloatNumberBox(LabelType::Enum label, float value, float min, float max, uint8_t nrDecimals, float stepsize
                           #if FEATURE_TOOLTIPS
                           , const String& tooltip
                           #endif // if FEATURE_TOOLTIPS
                           ) {
  addFormFloatNumberBox(getLabel(label), getInternalLabel(label), value, min, max, nrDecimals, stepsize
                        #if FEATURE_TOOLTIPS
                        , tooltip
                        #endif // if FEATURE_TOOLTIPS
                        );
  addFormNote(label);
}

void addFormFloatNumberBox(const String& label,
                           const String& id,
                           float         value,
                           float         min,
                           float         max,
                           uint8_t       nrDecimals,
                           float         stepsize
                           #if FEATURE_TOOLTIPS
                           ,
                           const String& tooltip
                           #endif // if FEATURE_TOOLTIPS
                           )
{
  addRowLabel_tr_id(label, id);
  addFloatNumberBox(id, value, min, max, nrDecimals, stepsize
                    #if FEATURE_TOOLTIPS
                    , tooltip
                    #endif // if FEATURE_TOOLTIPS
                    );
}

void addFormFloatNumberBox(const __FlashStringHelper * label,
                           const __FlashStringHelper * id,
                           float         value,
                           float         min,
                           float         max,
                           uint8_t       nrDecimals,
                           float         stepsize
                           #if FEATURE_TOOLTIPS
                           ,
                           const String& tooltip
                           #endif // if FEATURE_TOOLTIPS
                           )
{
  addRowLabel_tr_id(label, id);
  addFloatNumberBox(id, value, min, max, nrDecimals, stepsize
                    #if FEATURE_TOOLTIPS
                    , tooltip
                    #endif // if FEATURE_TOOLTIPS
                    );
}



// ********************************************************************************
// Add a task selector form
// ********************************************************************************
void addTaskSelectBox(const String& label, const String& id, taskIndex_t choice)
{
  addRowLabel_tr_id(label, id);
  addTaskSelect(id, choice);
}

// ********************************************************************************
// Add a Text Box form
// ********************************************************************************
void addFormTextBox(const __FlashStringHelper * label,
                    const __FlashStringHelper * id,
                    const String& value,
                    int           maxlength,
                    bool          readonly,
                    bool          required,
                    const String& pattern)
{
  addRowLabel_tr_id(label, id);
  addTextBox(id, value, maxlength, readonly, required, pattern);
}

void addFormTextBox(const String  & label,
                    const String  & id,
                    const String  & value,
                    int             maxlength,
                    bool            readonly,
                    bool            required,
                    const String& pattern
                    #if FEATURE_TOOLTIPS
                    , const String& tooltip
                    #endif // if FEATURE_TOOLTIPS
                    ,
                    const String&   datalist
                    )
{
  addRowLabel_tr_id(label, id);
  addTextBox(id, value, maxlength, readonly, required, pattern, F("wide")
             #if FEATURE_TOOLTIPS
             , tooltip
             #endif // if FEATURE_TOOLTIPS
             , datalist
             );
}

void addFormTextBox(const __FlashStringHelper * classname,
                    const String& label,
                    const String& id,
                    const String& value,                    
                    int           maxlength,
                    bool          readonly ,
                    bool          required ,
                    const String& pattern  
                    #if FEATURE_TOOLTIPS
                    ,
                    const String& tooltip 
                    #endif // if FEATURE_TOOLTIPS
                    ,
                    const String& datalist
                    )
{
  addRowLabel_tr_id(label, id);
  addTextBox(id, value, maxlength, readonly, required, pattern, classname
             #if FEATURE_TOOLTIPS
             , tooltip
             #endif // if FEATURE_TOOLTIPS
             , datalist
             );
}



void addFormTextArea(const String  & label,
                     const String  & id,
                     const String  & value,
                     int             maxlength,
                     int             rows,
                     int             columns,
                     bool            readonly,
                     bool            required
                     #if FEATURE_TOOLTIPS
                     , const String& tooltip
                     #endif // if FEATURE_TOOLTIPS
                     )
{
  addRowLabel_tr_id(label, id);
  addTextArea(id, value, maxlength, rows, columns, readonly, required
              #if FEATURE_TOOLTIPS
              , tooltip
              #endif // if FEATURE_TOOLTIPS
              );
}

// ********************************************************************************
// Add a Password Box form
// ********************************************************************************

void addFormPasswordBox(const String& label, const String& id, const String& password, int maxlength
                        #if FEATURE_TOOLTIPS
                        , const String& tooltip
                        #endif // if FEATURE_TOOLTIPS
                        )
{
  addRowLabel_tr_id(label, id);

  addHtml(F("<input "));
  addHtmlAttribute(F("class"),     F("wide"));
  addHtmlAttribute(F("type"),      F("password"));
  addHtmlAttribute(F("name"),      id);
  addHtmlAttribute(F("id"),        id);
  addHtmlAttribute(F("maxlength"), maxlength);

  #if FEATURE_TOOLTIPS
  addTooltip(tooltip);
  #endif // if FEATURE_TOOLTIPS
  addHtmlAttribute(F("value"), (password.length() == 0) ? F("") : F("*****"));
  addHtml('>');
}

bool getFormPassword(const String& id, String& password)
{
  password = webArg(id);
  /*
  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLog(LOG_LEVEL_DEBUG, concat(F("getFormPassword: "), password));
  } 
  */ 
  return !equals(password, F("*****"));
}

// ********************************************************************************
// Add a IP Box form
// ********************************************************************************
void addFormIPBox(const __FlashStringHelper *label,
                  const __FlashStringHelper *id,
                  const uint8_t ip[4])
{
  addFormIPBox(String(label), String(id), ip);
}

void addFormTextBox(const String& label, const String& id, const String& value)
{
  addRowLabel_tr_id(label, id);

  addHtml(strformat(
    F("<input class='wide' type='text' name='%s' id='%s' value='%s'>"),
    id.c_str(),
    id.c_str(),
    value.c_str()
  ));
}

void addFormIPBox(const String& label, const String& id, const uint8_t ip[4])
{
  const bool empty_IP = (ip[0] == 0 && ip[1] == 0 && ip[2] == 0 && ip[3] == 0);

  addFormTextBox(label, id, (empty_IP) ? EMPTY_STRING : formatIP(ip));
}

// ********************************************************************************
// Add a MAC Box form
// ********************************************************************************
void addFormMACBox(const String& label, const String& id, const MAC_address mac)
{
  addFormTextBox(
    label, 
    id, 
    mac.all_zero() ? EMPTY_STRING.c_str() : mac.toString());
}

// ********************************************************************************
// Add a IP Access Control select dropdown list
// ********************************************************************************
void addFormIPaccessControlSelect(const __FlashStringHelper * label, const __FlashStringHelper * id, int choice)
{
  addRowLabel_tr_id(label, id);
  addIPaccessControlSelect(id, choice);
}

// ********************************************************************************
// a Separator character selector
// ********************************************************************************
void addFormSeparatorCharInput(const __FlashStringHelper *rowLabel,
                               const __FlashStringHelper *id,
                               int                        value,
                               const String             & charset,
                               const __FlashStringHelper *additionalText) {
  const int len = charset.length() + 1;
  String    charList[len];
  int charOpts[len];

  charList[0] = F("None");
  charOpts[0] = 0;

  for (uint16_t i = 0; i < charset.length(); i++) {
    charList[i + 1] = charset[i];
    charOpts[i + 1] = static_cast<int>(charset[i]);
  }
  const FormSelectorOptions selector(len, charList, charOpts);
  selector.addFormSelector(rowLabel, id, value);

  if (!String(additionalText).isEmpty()) {
    addUnit(additionalText);
  }
}

// ********************************************************************************
// Add a selector form
// ********************************************************************************
void addFormPinSelect(PinSelectPurpose purpose, const String& label, const __FlashStringHelper * id, int choice)
{
  addRowLabel_tr_id(label, id);
  addPinSelect(purpose, id, choice);
}

void addFormPinSelect(PinSelectPurpose purpose, const __FlashStringHelper * label, const __FlashStringHelper * id, int choice)
{
  addRowLabel_tr_id(label, id);
  addPinSelect(purpose, id, choice);
}

/*
void addFormPinSelect(const String& label, const __FlashStringHelper * id, int choice)
{
  addRowLabel_tr_id(label, id);
  addPinSelect(PinSelectPurpose::Generic, id, choice);
}

void addFormPinSelect(const __FlashStringHelper * label,
                      const __FlashStringHelper * id,
                      int           choice)
{
  addRowLabel_tr_id(label, id);
  addPinSelect(PinSelectPurpose::Generic, id, choice);
}

void addFormPinSelect(const String& label, const String & id, int choice)
{
  addRowLabel_tr_id(label, id);
  addPinSelect(PinSelectPurpose::Generic, id, choice);
}
*/

void addFormPinSelectI2C(const String& label, const String& id, uint8_t i2cBus, int choice)
{
  addRowLabel_tr_id(label, id);
  const PinSelectPurpose purpose = static_cast<PinSelectPurpose>(
  static_cast<uint8_t>(PinSelectPurpose::I2C) + i2cBus);

  addPinSelect(purpose, id, choice);
}

void addFormSelectorI2C(const String& id,
                        int           addressCount,
                        const uint8_t addresses[],
                        int           selectedIndex,
                        uint8_t       defaultAddress
                        #if FEATURE_TOOLTIPS
                        , const String& tooltip
                        #endif // if FEATURE_TOOLTIPS
                        )
{
  addRowLabel_tr_id(F("I2C Address"), id);
  do_addSelector_Head(id, F(""), EMPTY_STRING, false
                      #if FEATURE_TOOLTIPS
                      , tooltip
                      #endif // if FEATURE_TOOLTIPS
                      );

  for (int x = 0; x < addressCount; x++)
  {
    String option = formatToHex_decimal(addresses[x]);

    if (((x == 0) && (defaultAddress == 0)) || (defaultAddress == addresses[x])) {
      option += F(" (default)");
    }
    addSelector_Item(option, addresses[x], addresses[x] == selectedIndex);
  }
  addSelector_Foot();
}

void addFormSelector_YesNo(const __FlashStringHelper * label,
                           const __FlashStringHelper * id,
                           int           selectedIndex,
                           bool       reloadonchange)
{
  addFormSelector_YesNo(label, String(id), selectedIndex, reloadonchange);
}

void addFormSelector_YesNo(const __FlashStringHelper * label,
                           const String& id,
                           int           selectedIndex,
                           bool       reloadonchange)
{
  const __FlashStringHelper *optionsNoYes[] = { F("No"), F("Yes") };
  //int optionValuesNoYes[]                   = { 0, 1 };
  FormSelectorOptions selector(NR_ELEMENTS(optionsNoYes), optionsNoYes);
  selector.reloadonchange = reloadonchange;
  selector.addFormSelector(label, id, selectedIndex);
}



// ********************************************************************************
// Add a GPIO pin select dropdown list
// ********************************************************************************
void addFormPinStateSelect(int gpio, int choice)
{
  bool enabled = true;

  if (isSerialConsolePin(gpio)) {
    // do not add the pin state select for these pins.
    enabled = false;
  }
  if (Settings.isEthernetPin(gpio)) {
    // do not add the pin state select for non-optional Ethernet pins
    enabled = false;
  }
  int  pinnr = -1;
  bool input, output, warning;

  if (getGpioInfo(gpio, pinnr, input, output, warning)) {
    bool hasPullUp, hasPullDown;
    getGpioPullResistor(gpio, hasPullUp, hasPullDown);
    int nr_options = 0;
    const __FlashStringHelper * options[6];
    int    option_val[6];
    options[nr_options]    = F("Default");
    option_val[nr_options] = static_cast<int>(PinBootState::Default_state);
    ++nr_options;

    if (output) {
      options[nr_options]    = F("Output Low");
      option_val[nr_options] = static_cast<int>(PinBootState::Output_low);
      ++nr_options;
      options[nr_options]    = F("Output High");
      option_val[nr_options] = static_cast<int>(PinBootState::Output_high);
      ++nr_options;
    }

    if (input) {
      if (hasPullUp) {
        options[nr_options]    = F("Input pullup");
        option_val[nr_options] = static_cast<int>(PinBootState::Input_pullup);
        ++nr_options;
      }

      if (hasPullDown) {
        options[nr_options]    = F("Input pulldown");
        option_val[nr_options] = static_cast<int>(PinBootState::Input_pulldown);
        ++nr_options;
      }

      if (!hasPullUp && !hasPullDown) {
        options[nr_options]    = F("Input");
        option_val[nr_options] = static_cast<int>(PinBootState::Input);
        ++nr_options;
      }
    }
    FormSelectorOptions selector(nr_options, options, option_val);
    selector.enabled = enabled;

    const String id = String('p') + gpio;
    selector.addFormSelector(
      concat(
        F("Pin mode "), 
        createGPIO_label(gpio, pinnr, input, output, warning)), 
      id,
      choice);
    addUnit(getConflictingUse(gpio));
    #ifdef ESP32
    if (isPSRAMInterfacePin(gpio)) {
      addUnit(getConflictingUse(gpio, PinSelectPurpose::Generic, true));
    }
    #endif // ifdef ESP32
  }
}

// ********************************************************************************
// Retrieve return values from form/checkbox.
// ********************************************************************************
int getFormItemInt(const __FlashStringHelper * key, int defaultValue) {
  return getFormItemInt(String(key), defaultValue);
}

int getFormItemInt(const String& key, int defaultValue) {
  int value = defaultValue;

  getCheckWebserverArg_int(key, value);
  return value;
}

bool getCheckWebserverArg_int(const String& key, int& value) {
  const String valueStr = webArg(key);
  if (valueStr.isEmpty()) return false;
  // FIXME TD-er: Since ESP_IDF 5.1 int32_t != int
  int32_t tmp{};
  const bool res = validIntFromString(valueStr, tmp);
  value = tmp;
  return res;
}

bool update_whenset_FormItemInt(const __FlashStringHelper * key,
                                int         & value) 
{
  return update_whenset_FormItemInt(String(key), value);
}

bool update_whenset_FormItemInt(const String& key, int& value) {
  int tmpVal;

  if (getCheckWebserverArg_int(key, tmpVal)) {
    value = tmpVal;
    return true;
  }
  return false;
}

bool update_whenset_FormItemInt(const __FlashStringHelper * key,
                                int8_t& value) 
{
  return update_whenset_FormItemInt(String(key), value);
}


bool update_whenset_FormItemInt(const String& key, int8_t& value) {
  int tmpVal;

  if (getCheckWebserverArg_int(key, tmpVal)) {
    value = tmpVal;
    return true;
  }
  return false;
}

bool update_whenset_FormItemInt(const __FlashStringHelper * key,
                                uint8_t& value) 
{
  return update_whenset_FormItemInt(String(key), value);
}


bool update_whenset_FormItemInt(const String& key, uint8_t& value) {
  int tmpVal;

  if (getCheckWebserverArg_int(key, tmpVal)) {
    value = tmpVal;
    return true;
  }
  return false;
}

// Note: Checkbox values will not appear in POST Form data if unchecked.
// So if webserver does not have an argument for a checkbox form, it means it should be considered unchecked.
bool isFormItemChecked(const __FlashStringHelper * id)
{
  return isFormItemChecked(String(id));
}

bool isFormItemChecked(const String& id)
{
  return equals(webArg(id), F("on"));
}

bool isFormItemChecked(const LabelType::Enum& id)
{
  return isFormItemChecked(getInternalLabel(id));
}

int getFormItemInt(const __FlashStringHelper * id)
{
  return getFormItemInt(String(id), 0);
}

int getFormItemInt(const String& id)
{
  return getFormItemInt(id, 0);
}

int getFormItemInt(const LabelType::Enum& id)
{
  return getFormItemInt(getInternalLabel(id), 0);
}

float getFormItemFloat(const __FlashStringHelper * id)
{
  return getFormItemFloat(String(id));
}

float getFormItemFloat(const String& id)
{
  const String val = webArg(id);
  float res = 0.0;
  if (val.length() > 0) {
    validFloatFromString(val, res);
  }
  return res;
}

float getFormItemFloat(const LabelType::Enum& id)
{
  return getFormItemFloat(getInternalLabel(id));
}

bool isFormItem(const String& id)
{
  return !webArg(id).isEmpty();
}

void copyFormPassword(const __FlashStringHelper * id, char *pPassword, int maxlength)
{
  String password;

  if (getFormPassword(id, password)) {
    safe_strncpy(pPassword, password.c_str(), maxlength);
  }
}

#if FEATURE_MQTT_DISCOVER && FEATURE_MQTT_DEVICECLASS
void addFormSelector_binarySensorDeviceClass(const __FlashStringHelper*label, 
                                             const __FlashStringHelper*id, 
                                             int selectedIndex,
                                             bool addLabelAsFormSubHeader) {
  if (addLabelAsFormSubHeader) {
    addFormSubHeader(label);
  }

  std::vector<String> binaryDeviceClasses;
  int devClassIndex   = 0;
  String devClassName = MQTT_binary_deviceClassName(devClassIndex);
  if (MQTT_binary_deviceClassTwoWay(devClassIndex)) {
    devClassName += F("²");
  }
  if (MQTT_binary_deviceClassSwitch(devClassIndex)) {
    devClassName += F("÷"); // These are multi-byte chars, so we have to use the F() macro
  }

  while (!devClassName.isEmpty() || (0 == devClassIndex)) {
    binaryDeviceClasses.push_back(devClassName);
    ++devClassIndex;
    devClassName = MQTT_binary_deviceClassName(devClassIndex);
    if (MQTT_binary_deviceClassTwoWay(devClassIndex)) {
      devClassName += F("²");
    }
    if (MQTT_binary_deviceClassSwitch(devClassIndex)) {
      devClassName += F("÷");
    }
  }
  const FormSelectorOptions deviceClass(
    devClassIndex,
    &binaryDeviceClasses[0]);

  deviceClass.addFormSelector(
    label,
    id,
    selectedIndex);
}

#endif // if FEATURE_MQTT_DISCOVER && FEATURE_MQTT_DEVICECLASS

#include "../WebServer/ToolsPage.h"

#ifdef WEBSERVER_TOOLS

#include "../WebServer/ESPEasy_WebServer.h"
#include "../WebServer/HTML_wrappers.h"
#include "../WebServer/Markup.h"
#include "../WebServer/Markup_Buttons.h"
#include "../WebServer/Markup_Forms.h"

#include "../Helpers/OTA.h"

#include "../../ESPEasy-Globals.h"

# include "../Commands/ExecuteCommand.h"
# include "../Helpers/WebServer_commandHelper.h"

// ********************************************************************************
// Web Interface Tools page
// ********************************************************************************
void handle_tools() {
  if (!isLoggedIn()) { return; }
  navMenuIndex = MENU_INDEX_TOOLS;
  TXBuffer.startStream();
  sendHeadandTail_stdtemplate(_HEAD);

  String webrequest = webArg(F("cmd"));

  handle_command_from_web(EventValueSource::Enum::VALUE_SOURCE_WEB_FRONTEND, webrequest);
  printToWeb     = false;
  printToWebJSON = false;

  addHtml(F("<form>"));
  html_table_class_normal();

  addFormHeader(F("Tools"));

  addFormSubHeader(F("Command"));
  html_TR_TD();
  addHtml(F("<TR><TD colspan='2'>"));
  addHtml(F("<input "));
  addHtmlAttribute(F("style"), F("width: 98%"));
  addHtmlAttribute(F("type"),  F("text"));
  addHtmlAttribute(F("name"),  F("cmd"));
  addHtmlAttribute(F("value"), webArg(F("cmd")));
  addHtml('>');

  html_TR_TD();
  addSubmitButton();
  // addHelpButton(F("ESPEasy_Command_Reference")); // Old documentation is just that: Old and out-dated.
  addRTDHelpButton(F("Reference/Command.html"));
  html_TR_TD();

  if (printWebString.length() > 0)
  {
    addHtml(F("<TR><TD colspan='2'>Command Output<BR><textarea readonly rows='10' wrap='on'>"));
    addHtml(printWebString);
    addHtml(F("</textarea>"));
    free_string(printWebString);
  }


  addFormSubHeader(F("System"));

  addWideButtonPlusDescription(F("/?cmd=reboot"), F("Reboot"), F("Reboots ESP"));

  # ifdef WEBSERVER_LOG
  addWideButtonPlusDescription(F("log"),          F("Log"),    F("Open log output"));
  # endif // ifdef WEBSERVER_LOG

  # ifdef WEBSERVER_SYSINFO
  addWideButtonPlusDescription(F("sysinfo"), F("Info"), F("Open system info page"));
  # endif // ifdef WEBSERVER_SYSINFO

  # ifdef WEBSERVER_ADVANCED
  addWideButtonPlusDescription(F("advanced"),    F("Advanced"),     F("Open advanced settings"));
  # endif // ifdef WEBSERVER_ADVANCED

  addWideButtonPlusDescription(F("json"),        F("Show JSON"),    F("Open JSON output"));

  # ifdef WEBSERVER_METRICS
  addWideButtonPlusDescription(F("metrics"),        F("Show Metrics"),    F("Open Prometheus Metrics"));
  # endif //ifdef WEBSERVER_METRICS

  # ifdef WEBSERVER_TIMINGSTATS
  addWideButtonPlusDescription(F("timingstats"), F("Timing stats"), F("Open timing statistics of system"));
  # endif // WEBSERVER_TIMINGSTATS

  # ifdef WEBSERVER_PINSTATES
  addWideButtonPlusDescription(F("pinstates"), F("Pin state buffer"), F("Show Pin state buffer"));
  # endif // ifdef WEBSERVER_PINSTATES

  # ifdef WEBSERVER_SYSVARS
  addWideButtonPlusDescription(F("sysvars"), F("System Variables"), F("Show all system variables and conversions"));
  # endif // ifdef WEBSERVER_SYSVARS

  #if FEATURE_PLUGIN_LIST
  addWideButtonPlusDescription(F("pluginlist"), F("Included Plugins"), F("Show all plugins that are included in this build"));
  #endif // if FEATURE_PLUGIN_LIST

  addFormSubHeader(F("Wifi"));

  addWideButtonPlusDescription(F("/?cmd=wificonnect"),    F("Connect"),    F("Connects to known Wifi network"));
  addWideButtonPlusDescription(F("/?cmd=wifidisconnect"), F("Disconnect"), F("Disconnect from wifi network"));
  # ifdef WEBSERVER_SETUP
  addWideButtonPlusDescription(F("setup"),                F("Setup WiFi"), F("WiFi setup page"));
  #endif // ifdef WEBSERVER_SETUP

  # ifdef WEBSERVER_WIFI_SCANNER
  addWideButtonPlusDescription(F("wifiscanner"),          F("Scan"),       F("Scan for wifi networks"));
  # endif // ifdef WEBSERVER_WIFI_SCANNER

  # ifdef WEBSERVER_I2C_SCANNER
  addFormSubHeader(F("Interfaces"));

  addWideButtonPlusDescription(F("i2cscanner"), F("I2C Scan"), F("Scan for I2C devices"));
  # endif // ifdef WEBSERVER_I2C_SCANNER

  addFormSubHeader(F("Settings"));

  addWideButtonPlusDescription(F("upload"), F("Load"), F("Loads a settings file"));
  addFormNote(F("(File MUST be renamed to \"config.dat\" before upload!"
                #if FEATURE_TARSTREAM_SUPPORT
                " Or a .tar file containing \"config.dat\" and other files can be uploaded"
                #endif // if FEATURE_TARSTREAM_SUPPORT
                ")"));
  addWideButtonPlusDescription(F("download"), F("Save"),
                               # if FEATURE_TARSTREAM_SUPPORT
                               F("Save all configuration in a single .tar archive")
                               # else // if FEATURE_TARSTREAM_SUPPORT
                               F("Saves a settings file")
                               # endif // if FEATURE_TARSTREAM_SUPPORT
                               );
  #if FEATURE_TARSTREAM_SUPPORT
  addWideButtonPlusDescription(F("backup"), F("Backup files"), F("Save all files as a .tar archive"));
  addWideButtonPlusDescription(F("backupnup"), F("Backup w/o credentials"), F("Save all files as a .tar archive but exclude usernames & passwords"));
  #endif // if FEATURE_TARSTREAM_SUPPORT

# ifdef WEBSERVER_NEW_UI
  #  if defined(ESP8266)

  if ((SpiffsFreeSpace() / 1024) > 50) {
    html_TR_TD();
    addHtml(F(
              "<script>function downloadUI() { fetch('https://raw.githubusercontent.com/letscontrolit/espeasy_ui/master/build/index.htm.gz').then(r=>r.arrayBuffer()).then(r => {var f=new FormData();f.append('file', new File([new Blob([new Uint8Array(r)])], 'index.htm.gz'));f.append('edit', 1);fetch('/upload',{method:'POST',body:f}).then(() => {window.location.href='/';});}); }</script>"));
    addHtml(F("<a class=\"button link wide\" onclick=\"downloadUI()\">Download new UI</a>"));
    addHtml(F("</TD><TD>Download new UI(alpha)</TD></TR>"));
  }
  #  endif // if defined(ESP8266)
# endif    // WEBSERVER_NEW_UI

# if defined(ESP8266) || defined(ESP32)
  {
    #  ifndef NO_HTTP_UPDATER
    {
      uint32_t maxSketchSize;
      bool     use2step;
      bool     otaEnabled = OTA_possible(maxSketchSize, use2step);
      addFormSubHeader(F("Firmware"));
      html_TR_TD_height(30);
      addWideButton(F("update"), F("Update Firmware"), EMPTY_STRING, otaEnabled);
      addHelpButton(F("RTDTools/Tools.html#id1"));
      html_TD();
      addHtml(F("Load a new firmware "));

      if (otaEnabled) {
        if (use2step) {
          html_B(F("WARNING"));
          addHtml(F(" only use 2-step OTA update."));
        }
      } else {
        html_B(F("WARNING"));
        addHtml(F(" Not enough space to safely update. Update might fail. "));
      }
      addHtml(F(" Max sketch size: "));
      addHtmlInt(maxSketchSize / 1024);
      addHtml(F(" kB ("));
      addHtmlInt(maxSketchSize);
      addHtml(F(" bytes)"));
    }
    #  endif // ifndef NO_HTTP_UPDATER
  }
# endif     // if defined(ESP8266)

  # if defined(WEBSERVER_FILELIST) || defined(WEBSERVER_FACTORY_RESET) || defined(FEATURE_SETTINGS_ARCHIVE) || defined(FEATURE_SD)
  addFormSubHeader(F("Filesystem"));
  # endif // if defined(WEBSERVER_FILELIST) || defined(WEBSERVER_FACTORY_RESET) || defined(FEATURE_SETTINGS_ARCHIVE) || defined(FEATURE_SD)

  # ifdef WEBSERVER_FILELIST
  addWideButtonPlusDescription(F("filelist"),         F("File browser"),     F("Show files on internal flash file system"));
  # endif // ifdef WEBSERVER_FILELIST
  # ifdef WEBSERVER_FACTORY_RESET
  addWideButtonPlusDescription(F("/factoryreset"),    F("Factory Reset"),    F("Select pre-defined configuration or full erase of settings"));
  # endif // ifdef WEBSERVER_FACTORY_RESET
  # if FEATURE_SETTINGS_ARCHIVE
  addWideButtonPlusDescription(F("/settingsarchive"), F("Settings Archive"), F("Download settings from some archive"));
  # endif // if FEATURE_SETTINGS_ARCHIVE
# if FEATURE_SD
  addWideButtonPlusDescription(F("SDfilelist"),       F("SD Card"),          F("Show files on SD-Card"));
# endif   // if FEATURE_SD

  html_end_table();
  html_end_form();
  sendHeadandTail_stdtemplate(_TAIL);
  TXBuffer.endStream();
  free_string(printWebString);
  printToWeb     = false;
}

// ********************************************************************************
// Web Interface debug page
// ********************************************************************************
void addWideButtonPlusDescription(const __FlashStringHelper * url,
                                  const __FlashStringHelper * buttonText,
                                  const __FlashStringHelper * description)
{
  html_TR_TD_height(30);
  addWideButton(url, buttonText);
  html_TD();
  addHtml(description);
}

void addWideButtonPlusDescription(const String& url, const String& buttonText, const String& description)
{
  html_TR_TD_height(30);
  addWideButton(url, buttonText);
  html_TD();
  addHtml(description);
}

#endif // ifdef WEBSERVER_TOOLS

#include "../WebServer/DevicesPage.h"

#ifdef WEBSERVER_DEVICES

# include "../WebServer/ESPEasy_WebServer.h"
# include "../WebServer/HTML_wrappers.h"
# include "../WebServer/Markup.h"
# include "../WebServer/Markup_Buttons.h"
# include "../WebServer/Markup_Forms.h"

# include "../DataStructs/NodeStruct.h"
# if FEATURE_PLUGIN_STATS
#  include "../DataStructs/PluginStats_Config.h"
# endif // if FEATURE_PLUGIN_STATS


# include "../Globals/CPlugins.h"
# include "../Globals/Device.h"
# include "../Globals/ExtraTaskSettings.h"
# if FEATURE_MQTT_DISCOVER
#  include "../Globals/MQTT.h"
# endif // if FEATURE_MQTT_DISCOVER
# include "../Globals/Nodes.h"
# include "../Globals/Plugins.h"

# include "../Static/WebStaticData.h"

# include "../Helpers/_CPlugin_init.h"
# include "../Helpers/_Plugin_init.h"
# include "../Helpers/_Plugin_SensorTypeHelper.h"
# include "../Helpers/_Plugin_Helper_serial.h"
# include "../Helpers/ESPEasy_Storage.h"
# include "../Helpers/I2C_Plugin_Helper.h"
# include "../Helpers/StringConverter.h"
# include "../Helpers/StringGenerator_GPIO.h"

# include "../../_Plugin_Helper.h"

# include <ESPeasySerial.h>

#if FEATURE_TASKVALUE_UNIT_OF_MEASURE
#include "../Helpers/ESPEasy_UnitOfMeasure.h"
#endif // if FEATURE_TASKVALUE_UNIT_OF_MEASURE

void handle_devices() {
  # ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_devices"));
  # endif // ifndef BUILD_NO_RAM_TRACKER

  if (!isLoggedIn()) { return; }
  navMenuIndex = MENU_INDEX_DEVICES;
  TXBuffer.startStream();
  sendHeadandTail_stdtemplate(_HEAD);


  // char tmpString[41];


  // String taskindex = webArg(F("index"));

  pluginID_t taskdevicenumber;

  if (hasArg(F("del"))) {
    taskdevicenumber.setInvalid();
  }
  else {
    taskdevicenumber = pluginID_t::toPluginID(getFormItemInt(F("TDNUM"), 0));
  }


  // String taskdeviceid[CONTROLLER_MAX];
  // String taskdevicepin1 = webArg(F("taskdevicepin1"));   // "taskdevicepin*" should not be changed because it is uses by plugins
  // and expected to be saved by this code
  // String taskdevicepin2 = webArg(F("taskdevicepin2"));
  // String taskdevicepin3 = webArg(F("taskdevicepin3"));
  // String taskdevicepin1pullup = webArg(F("TDPPU"));
  // String taskdevicepin1inversed = webArg(F("TDPI"));
  // String taskdevicename = webArg(F("TDN"));
  // String taskdeviceport = webArg(F("TDP"));
  // String taskdeviceformula[VARS_PER_TASK];
  // String taskdevicevaluename[VARS_PER_TASK];
  // String taskdevicevaluedecimals[VARS_PER_TASK];
  // String taskdevicesenddata[CONTROLLER_MAX];
  // String taskdeviceglobalsync = webArg(F("TDGS"));
  // String taskdeviceenabled = webArg(F("TDE"));

  // for (uint8_t varNr = 0; varNr < VARS_PER_TASK; varNr++)
  // {
  //   char argc[25];
  //   String arg = F("TDF");
  //   arg += varNr + 1;
  //   arg.toCharArray(argc, 25);
  //   taskdeviceformula[varNr] = webArg(argc);
  //
  //   arg = F("TDVN");
  //   arg += varNr + 1;
  //   arg.toCharArray(argc, 25);
  //   taskdevicevaluename[varNr] = webArg(argc);
  //
  //   arg = F("TDVD");
  //   arg += varNr + 1;
  //   arg.toCharArray(argc, 25);
  //   taskdevicevaluedecimals[varNr] = webArg(argc);
  // }

  // for (controllerIndex_t controllerNr = 0; controllerNr < CONTROLLER_MAX; controllerNr++)
  // {
  //   char argc[25];
  //   String arg = F("TDID");
  //   arg += controllerNr + 1;
  //   arg.toCharArray(argc, 25);
  //   taskdeviceid[controllerNr] = webArg(argc);
  //
  //   arg = F("TDSD");
  //   arg += controllerNr + 1;
  //   arg.toCharArray(argc, 25);
  //   taskdevicesenddata[controllerNr] = webArg(argc);
  // }

  uint8_t page = getFormItemInt(F("page"), 0);

  if (page == 0) {
    page = 1;
  }
  uint8_t setpage = getFormItemInt(F("setpage"), 0);

  if (setpage > 0)
  {
    if (setpage <= (TASKS_MAX / TASKS_PER_PAGE)) {
      page = setpage;
    }
    else {
      page = TASKS_MAX / TASKS_PER_PAGE;
    }
  }
  const int edit = getFormItemInt(F("edit"), 0);

  // taskIndex in the URL is 1 ... TASKS_MAX
  // For use in other functions, set it to 0 ... (TASKS_MAX - 1)
  taskIndex_t taskIndex       = getFormItemInt(F("index"), 0);
  boolean     taskIndexNotSet = taskIndex == 0;

  const bool nosave = isFormItemChecked(F("nosave"));

  if (!taskIndexNotSet) {
    --taskIndex;

    //    LoadTaskSettings(taskIndex); // Make sure ExtraTaskSettings are up-to-date
  }

  // FIXME TD-er: Might have to clear any caches here.
  if ((edit != 0) && !taskIndexNotSet) // when form submitted
  {
    if (Settings.getPluginID_for_task(taskIndex) != taskdevicenumber)
    {
      // change of device: cleanup old device and reset default settings
      setTaskDevice_to_TaskIndex(taskdevicenumber, taskIndex);
      const deviceIndex_t DeviceIndex = getDeviceIndex(taskdevicenumber);

      if (validDeviceIndex(DeviceIndex)) {
        const DeviceStruct& device = Device[DeviceIndex];

        if ((device.Type == DEVICE_TYPE_I2C) && device.I2CMax100kHz) {      // 100 kHz-only I2C device?
          bitWrite(Settings.I2C_Flags[taskIndex], I2C_FLAGS_SLOW_SPEED, 1); // Then: Enable Force Slow I2C speed checkbox by default
        }
      }
    }
    else if (taskdevicenumber != INVALID_PLUGIN_ID) // save settings
    {
      handle_devices_CopySubmittedSettings(taskIndex, taskdevicenumber);
    }

    if (taskdevicenumber != INVALID_PLUGIN_ID) {
      // Task index has a task device number, so it makes sense to save.
      // N.B. When calling delete, the settings were already saved.
      if (nosave) {
        Cache.updateExtraTaskSettingsCache();
      } else {
        addHtmlError(SaveTaskSettings(taskIndex));
        addHtmlError(SaveSettings());
      }

      struct EventStruct TempEvent(taskIndex);
      String dummy;

      if (Settings.TaskDeviceEnabled[taskIndex]) {
        if (PluginCall(PLUGIN_INIT, &TempEvent, dummy)) {
          PluginCall(PLUGIN_READ, &TempEvent, dummy);
        }
      } else {
        PluginCall(PLUGIN_EXIT, &TempEvent, dummy);
      }
    }
  }

  // show all tasks as table
  if (taskIndexNotSet)
  {
    handle_devicess_ShowAllTasksTable(page);
  }

  // Show edit form if a specific entry is chosen with the edit button
  else
  {
    handle_devices_TaskSettingsPage(taskIndex, page);
  }

  # ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_devices"));
  # endif // ifndef BUILD_NO_RAM_TRACKER
# ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG_DEV)) {
    addLogMove(LOG_LEVEL_DEBUG_DEV, concat(F("DEBUG: String size:"), static_cast<int>(TXBuffer.sentBytes)));
  }
# endif // ifndef BUILD_NO_DEBUG
  sendHeadandTail_stdtemplate(_TAIL);
  TXBuffer.endStream();
}

// ********************************************************************************
// Add a device select dropdown list
// TODO TD-er: Add JavaScript filter:
//             https://www.w3schools.com/howto/howto_js_filter_dropdown.asp
// ********************************************************************************
void addDeviceSelect(const __FlashStringHelper *name,  pluginID_t choice)
{
  String deviceName;

  addSelector_Head_reloadOnChange(name);
  addSelector_Item(F("- None -"), 0, false);

  deviceIndex_t x;
  bool done = false;

  while (!done) {
    const deviceIndex_t deviceIndex = getDeviceIndex_sorted(x);

    if (!validDeviceIndex(deviceIndex)) {
      done = true;
    } else {
      const pluginID_t pluginID = getPluginID_from_DeviceIndex(deviceIndex);

      if (validPluginID(pluginID)) {
        deviceName = getPluginNameFromDeviceIndex(deviceIndex);


        # if defined(PLUGIN_BUILD_DEV) || defined(PLUGIN_SET_MAX)
        deviceName = concat(get_formatted_Plugin_number(pluginID), F(" - ")) + deviceName;
        # endif // if defined(PLUGIN_BUILD_DEV) || defined(PLUGIN_SET_MAX)

        addSelector_Item(deviceName,
                         pluginID.value,
                         choice == pluginID);
      }
    }
    ++x;
  }

  addSelector_Foot(true);
}

// ********************************************************************************
// Collect all submitted form data and store the task settings
// ********************************************************************************
void handle_devices_CopySubmittedSettings(taskIndex_t taskIndex, pluginID_t taskdevicenumber)
{
  if (!validTaskIndex(taskIndex)) { return; }
  const deviceIndex_t DeviceIndex = getDeviceIndex(taskdevicenumber);

  if (!validDeviceIndex(DeviceIndex)) { return; }

  const DeviceStruct& device = Device[DeviceIndex];

  # if FEATURE_MQTT_DISCOVER
  controllerIndex_t discoverController = INVALID_CONTROLLER_INDEX;
  # endif // if FEATURE_MQTT_DISCOVER

  unsigned long taskdevicetimer = getFormItemInt(F("TDT"), 0);

  Settings.TaskDeviceNumber[taskIndex] = taskdevicenumber.value;

  if (device.Type == DEVICE_TYPE_I2C) {
    uint8_t flags = 0;
    bitWrite(flags, I2C_FLAGS_SLOW_SPEED, isFormItemChecked(F("taskdeviceflags0")));
#if FEATURE_I2C_MULTIPLE || FEATURE_I2CMULTIPLEXER
    uint8_t i2cBus = 0;
#endif

#if FEATURE_I2C_MULTIPLE
    if ((getI2CBusCount() > 1) && (Settings.isI2CEnabled(1) 
                                    #if FEATURE_I2C_INTERFACE_3
                                    || Settings.isI2CEnabled(2)
                                    #endif
                                   )
      && !Device[DeviceIndex].I2CNoBusSelection
      ) {
      i2cBus = getFormItemInt(F("pi2cbus"));
      set3BitToUL(flags, I2C_FLAGS_BUS_NUMBER, i2cBus);
    }
#endif // if FEATURE_I2C_MULTIPLE

# if FEATURE_I2CMULTIPLEXER

    if (isI2CMultiplexerEnabled(i2cBus)) {
      int multipleMuxPortsOption = getFormItemInt(F("taskdeviceflags1"), 0);
      bitWrite(flags, I2C_FLAGS_MUX_MULTICHANNEL, multipleMuxPortsOption == 1);

      if (multipleMuxPortsOption == 1) {
        uint8_t selectedPorts = 0;

        for (int x = 0; x < I2CMultiplexerMaxChannels(i2cBus); ++x) {
          bitWrite(selectedPorts, x, isFormItemChecked(concat(F("taskdeviceflag1ch"), x)));
        }
        Settings.I2C_Multiplexer_Channel[taskIndex] = selectedPorts;
      } else {
        Settings.I2C_Multiplexer_Channel[taskIndex] = getFormItemInt(F("taskdevicei2cmuxport"), 0);
      }
    }

# endif // if FEATURE_I2CMULTIPLEXER

    Settings.I2C_Flags[taskIndex] = flags;
  }

  // Must load from file system to make sure all caches and checksums match.
  ExtraTaskSettings.clear();
  ExtraTaskSettings.TaskIndex = taskIndex;
  Cache.clearTaskCache(taskIndex);

  struct EventStruct TempEvent(taskIndex);

  // Save selected output type.
  switch (device.OutputDataType) {
    case Output_Data_type_t::Default:
    {
      String dummy;
      PluginCall(PLUGIN_GET_DEVICEVALUENAMES, &TempEvent, dummy);
      break;
    }
    case Output_Data_type_t::Simple:
    case Output_Data_type_t::All:
    {
      int pconfigIndex   = checkDeviceVTypeForTask(&TempEvent);
      Sensor_VType VType = TempEvent.sensorType;

      if ((pconfigIndex >= 0) && (pconfigIndex < PLUGIN_CONFIGVAR_MAX)) {
        VType =
          static_cast<Sensor_VType>(getFormItemInt(sensorTypeHelper_webformID(pconfigIndex), 0));
        Settings.TaskDevicePluginConfig[taskIndex][pconfigIndex] = static_cast<int>(VType);
      }
      ExtraTaskSettings.clearUnusedValueNames(getValueCountFromSensorType(VType));
      break;
    }
  }

  {
    int pins[] = { -1, -1, -1 };

    for (int i = 0; i < 3; ++i) {
      update_whenset_FormItemInt(concat(F("taskdevicepin"), i + 1), pins[i]);
    }

    const bool taskEnabled = isFormItemChecked(F("TDE"));
    setBasicTaskValues(taskIndex, taskdevicetimer,
                       taskEnabled, webArg(F("TDN")),
                       pins);
  }

  # if FEATURE_PLUGIN_PRIORITY

  if (device.PowerManager // Check extra priority device flags when available
      ) {
    bool disablePrio = false;

    for (taskIndex_t t = 0; t < TASKS_MAX && !disablePrio; t++) {
      if (t != taskIndex) {
        disablePrio = Settings.isPriorityTask(t);
      }
    }
    bool statePriority = isFormItemChecked(F("TPRE"));

    if (device.PowerManager) {
      Settings.setPowerManagerTask(taskIndex, statePriority);
    }

    // Set alternative Priority flags
    // Set to readonly if set as Priority task
    Settings.setTaskEnableReadonly(taskIndex, statePriority);
  }
  # endif // if FEATURE_PLUGIN_PRIORITY
  Settings.TaskDevicePort[taskIndex] = getFormItemInt(F("TDP"), 0);
  update_whenset_FormItemInt(F("remoteFeed"), Settings.TaskDeviceDataFeed[taskIndex]);
  Settings.CombineTaskValues_SingleEvent(taskIndex, isFormItemChecked(F("TVSE")));
  #if FEATURE_STRING_VARIABLES
  if (!device.HideDerivedValues) {
    Settings.ShowDerivedTaskValues(taskIndex, isFormItemChecked(F("TSDV")));
    Settings.EventAndLogDerivedTaskValues(taskIndex, isFormItemChecked(F("TELD")));
  }
  #endif // if FEATURE_STRING_VARIABLES
  
  for (controllerIndex_t controllerNr = 0; controllerNr < CONTROLLER_MAX; controllerNr++)
  {
    Settings.TaskDeviceID[controllerNr][taskIndex]       = getFormItemInt(getPluginCustomArgName(F("TDID"), controllerNr));
    Settings.TaskDeviceSendData[controllerNr][taskIndex] = isFormItemChecked(getPluginCustomArgName(F("TDSD"), controllerNr));
    # if FEATURE_MQTT_DISCOVER
    
    if (isFormItemChecked(getPluginCustomArgName(F("TDDSC"), controllerNr)) &&
        Settings.TaskDeviceSendData[controllerNr][taskIndex]) {
      discoverController = controllerNr;
    }
    # endif // if FEATURE_MQTT_DISCOVER
    #if FEATURE_STRING_VARIABLES
    if (!device.HideDerivedValues) 
    Settings.SendDerivedTaskValues(taskIndex, controllerNr, isFormItemChecked(getPluginCustomArgName(F("TSND"), controllerNr)));
    #endif // if FEATURE_STRING_VARIABLES
    #if FEATURE_MQTT && FEATURE_MQTT_DISCOVER
    Settings.SendRetainedTaskValues(taskIndex, controllerNr, isFormItemChecked(getPluginCustomArgName(F("TSRT"), controllerNr)));
    #endif // if FEATURE_MQTT && FEATURE_MQTT_DISCOVER
  }

  if (device.PullUpOption) {
    Settings.TaskDevicePin1PullUp[taskIndex] = isFormItemChecked(F("TDPPU"));
  }

  if (device.InverseLogicOption) {
    Settings.TaskDevicePin1Inversed[taskIndex] = isFormItemChecked(F("TDPI"));
  }

  if (device.isSerial())
  {
    # ifdef PLUGIN_USES_SERIAL
    serialHelper_webformSave(&TempEvent);
    # else // ifdef PLUGIN_USES_SERIAL
    addLog(LOG_LEVEL_ERROR, F("PLUGIN_USES_SERIAL not defined"));
    # endif // ifdef PLUGIN_USES_SERIAL
  }

  const uint8_t valueCount = getValueCountForTask(taskIndex);

  for (uint8_t varNr = 0; varNr < valueCount; varNr++)
  {
    strncpy_webserver_arg(ExtraTaskSettings.TaskDeviceFormula[varNr], getPluginCustomArgName(F("TDF"), varNr));
    update_whenset_FormItemInt(getPluginCustomArgName(F("TDVD"), varNr), ExtraTaskSettings.TaskDeviceValueDecimals[varNr]);
    strncpy_webserver_arg(ExtraTaskSettings.TaskDeviceValueNames[varNr], getPluginCustomArgName(F("TDVN"), varNr));
# if FEATURE_PLUGIN_FILTER
    ExtraTaskSettings.enablePluginFilter(varNr, isFormItemChecked(getPluginCustomArgName(F("TDFIL"), varNr)));
# endif // if FEATURE_PLUGIN_FILTER
# if FEATURE_PLUGIN_STATS
    PluginStats_Config_t pluginStats_Config;
    pluginStats_Config.setEnabled(isFormItemChecked(getPluginCustomArgName(F("TDS"), varNr)));
    pluginStats_Config.setHidden(isFormItemChecked(getPluginCustomArgName(F("TDSH"), varNr)));
    const int selectedAxis = getFormItemInt(getPluginCustomArgName(F("TDSA"), varNr));
    pluginStats_Config.setAxisIndex(selectedAxis);
    pluginStats_Config.setAxisPosition(
      ((selectedAxis >> 2) == 0)
      ? PluginStats_Config_t::AxisPosition::Left
      : PluginStats_Config_t::AxisPosition::Right);

    ExtraTaskSettings.setPluginStatsConfig(varNr, pluginStats_Config);
# endif // if FEATURE_PLUGIN_STATS
    #if FEATURE_TASKVALUE_UNIT_OF_MEASURE
    ExtraTaskSettings.setTaskVarUnitOfMeasure(varNr, getFormItemInt(getPluginCustomArgName(F("TUOM"), varNr)));
    #endif // if FEATURE_TASKVALUE_UNIT_OF_MEASURE

    #if FEATURE_CUSTOM_TASKVAR_VTYPE
    ExtraTaskSettings.setTaskVarCustomVType(varNr, getFormItemInt(getPluginCustomArgName(F("TDTV"), varNr)));
    #endif // if FEATURE_CUSTOM_TASKVAR_VTYPE

    #if FEATURE_MQTT_STATE_CLASS
    ExtraTaskSettings.setTaskVarStateClass(varNr, getFormItemInt(getPluginCustomArgName(F("TDSC"), varNr)));
    #endif // if FEATURE_MQTT_STATE_CLASS
  }
  ExtraTaskSettings.clearUnusedValueNames(valueCount);

  // ExtraTaskSettings has changed.
  // The content of it is needed for sending CPLUGIN_TASK_CHANGE_NOTIFICATION and TaskInit/TaskExit events
  Cache.updateExtraTaskSettingsCache();

  // allow the plugin to save plugin-specific form settings.
  {
    String dummy;

    if (device.ExitTaskBeforeSave) {
      PluginCall(PLUGIN_EXIT, &TempEvent, dummy);
    }

    PluginCall(PLUGIN_WEBFORM_SAVE, &TempEvent, dummy);

    if (device.ErrorStateValues) {
      // FIXME TD-er: Must collect these from the web page.
      PluginCall(DeviceIndex, PLUGIN_INIT_VALUE_RANGES, &TempEvent, dummy);
    }

    // Make sure the task needs to reload using the new settings.
    if (!device.ExitTaskBeforeSave) {
      PluginCall(PLUGIN_EXIT, &TempEvent, dummy);
    }
    # if FEATURE_MQTT_DISCOVER

    if (validControllerIndex(discoverController) && validControllerIndex(mqttDiscoveryController)) {
      mqttDiscoverOnlyTask = taskIndex;
      mqttDiscoveryTimeout = random(10, MQTT_DISCOVERY_MAX_DELAY_0_1_SECONDS);

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLog(LOG_LEVEL_INFO, strformat(F("MQTT : Resend AutoDiscovery for Task %d on Controller %d in %.1f sec."),
                                         taskIndex + 1,
                                         mqttDiscoveryController + 1,
                                         mqttDiscoveryTimeout / 10.0f));
      }
    }
    # endif // if FEATURE_MQTT_DISCOVER
  }

  // Store all PCONFIG values on the web page
  // Must be done after PLUGIN_WEBFORM_SAVE, to allow tasks to clear the default task value names
  // Output type selectors are typically stored in PCONFIG
  if (device.OutputDataType != Output_Data_type_t::Default) {
    for (int pconfigIndex = 0; pconfigIndex < PLUGIN_CONFIGVAR_MAX; ++pconfigIndex) {
      pconfig_webformSave(&TempEvent, pconfigIndex);
    }
  }

  // ExtraTaskSettings may have changed during PLUGIN_WEBFORM_SAVE, so again update the cache.
  Cache.updateExtraTaskSettingsCache();

  loadDefaultTaskValueNames_ifEmpty(taskIndex);
  Cache.updateExtraTaskSettingsCache();

  // notify controllers: CPlugin::Function::CPLUGIN_TASK_CHANGE_NOTIFICATION
  for (controllerIndex_t x = 0; x < CONTROLLER_MAX; x++)
  {
    TempEvent.ControllerIndex = x;

    if (Settings.TaskDeviceSendData[TempEvent.ControllerIndex][TempEvent.TaskIndex] &&
        Settings.ControllerEnabled[TempEvent.ControllerIndex] && Settings.Protocol[TempEvent.ControllerIndex])
    {
      protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(TempEvent.ControllerIndex);
      String dummy;
      CPluginCall(ProtocolIndex, CPlugin::Function::CPLUGIN_TASK_CHANGE_NOTIFICATION, &TempEvent, dummy);
    }
  }

  // FIXME TD-er: Is this still needed as it is also cleared on PLUGIN_INIT and PLUGIN_EXIT?
  UserVar.clear_computed(taskIndex);
}

void html_add_setPage(uint8_t page, bool isLinkToPrev) {
  addHtml(strformat(
            F("devices?setpage=%u'>&%ct;</a>"),
            static_cast<unsigned int>(page),
            isLinkToPrev ? 'l' : 'g'));
}

// ********************************************************************************
// Show table with all selected Tasks/Devices
// ********************************************************************************
void handle_devicess_ShowAllTasksTable(uint8_t page)
{
  serve_JS(JSfiles_e::UpdateSensorValuesDevicePage);
  html_table_class_multirow();
  html_TR();
  html_table_header(F(""), 70);

  if (TASKS_MAX != TASKS_PER_PAGE)
  {
    html_add_button_prefix();

    html_add_setPage((page > 1) ? page - 1 : page, true);
    html_add_button_prefix();
    html_add_setPage((page < (TASKS_MAX / TASKS_PER_PAGE)) ? page + 1 : page, false);
  }

  html_table_header(F("Task"),    50);
  html_table_header(F("Enabled"), 100);
  html_table_header(F("Device"));
  html_table_header(F("Name"));
  html_table_header(F("Port"));
  html_table_header(F("Ctr (IDX)"), 100);
  html_table_header(F("GPIO"));
  html_table_header(F("Values"));

  String deviceName;

  for (taskIndex_t x = (page - 1) * TASKS_PER_PAGE; x < ((page) * TASKS_PER_PAGE) && validTaskIndex(x); x++)
  {
    const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(x);
    const bool pluginID_set         = INVALID_PLUGIN_ID != Settings.getPluginID_for_task(x);

    html_TR_TD();

    if (pluginID_set && !supportedPluginID(Settings.getPluginID_for_task(x))) {
      html_add_button_prefix(F("red"), true);
    } else {
      html_add_button_prefix();
    }
    {
      const int pageIndex = static_cast<int>(x + 1);
      addHtml(strformat(
                F("devices?index=%d&page=%u'>"),
                pageIndex,
                static_cast<unsigned int>(page)));
      addHtml(pluginID_set ? F("Edit") : F("Add"));
      addHtml(concat(F("</a><TD>"), pageIndex));
      html_TD();
    }

    // Show table of all configured tasks
    // A task may also refer to a non supported plugin.
    // This will be shown as not supported.
    // Editing a task which has a non supported plugin will present the same as when assigning a new plugin to a task.
    if (pluginID_set)
    {
      // LoadTaskSettings(x);
      int8_t spi_gpios[3] { -1, -1, -1 };
      struct EventStruct TempEvent(x);
      addEnabled(Settings.TaskDeviceEnabled[x]  && validDeviceIndex(DeviceIndex));

      html_TD();
      addHtml(getPluginNameFromPluginID(Settings.getPluginID_for_task(x)));
      html_TD();
      addHtml(getTaskDeviceName(x));
      html_TD();

      if (validDeviceIndex(DeviceIndex)) {
        if (Settings.TaskDeviceDataFeed[x] != 0) {
          # if FEATURE_ESPEASY_P2P

          // Show originating node number
          const uint8_t remoteUnit = Settings.TaskDeviceDataFeed[x];
          format_originating_node(remoteUnit);
          # endif // if FEATURE_ESPEASY_P2P
        } else {
          String portDescr;

          if (PluginCall(PLUGIN_WEBFORM_SHOW_CONFIG, &TempEvent, portDescr)) {
            addHtml(portDescr);
          } else {
            const DeviceStruct& device = Device[DeviceIndex];

            if (device.Type == DEVICE_TYPE_I2C) {
              format_I2C_port_description(x);
            } else if (device.isSPI()) {
              format_SPI_port_description(spi_gpios);
            } else if (device.isSerial()) {
                # ifdef PLUGIN_USES_SERIAL
              addHtml(serialHelper_getSerialTypeLabel(&TempEvent));
                # else // ifdef PLUGIN_USES_SERIAL
              addHtml(F("PLUGIN_USES_SERIAL not defined"));
                # endif // ifdef PLUGIN_USES_SERIAL
            } else {
              // Plugin has no custom port formatting, show default one.
              if (device.Ports != 0)
              {
                addHtml(formatToHex_decimal(Settings.TaskDevicePort[x]));
              }
            }
          }
        }
      }

      html_TD();

      if (validDeviceIndex(DeviceIndex)) {
        if (Device[DeviceIndex].SendDataOption)
        {
          boolean doBR = false;

          for (controllerIndex_t controllerNr = 0; controllerNr < CONTROLLER_MAX; controllerNr++)
          {
            if (Settings.TaskDeviceSendData[controllerNr][x])
            {
              if (doBR) {
                html_BR();
              }
              addHtml(getControllerSymbol(controllerNr));
              protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(controllerNr);

              if (validProtocolIndex(ProtocolIndex)) {
                if (getProtocolStruct(ProtocolIndex).usesID && (Settings.Protocol[controllerNr] != 0))
                {
                  addHtml(strformat(
                            F(" (%d)"),
                            static_cast<int>(Settings.TaskDeviceID[controllerNr][x])));

                  if (Settings.TaskDeviceID[controllerNr][x] == 0) {
                    addHtml(' ');
                    addHtml(F(HTML_SYMBOL_WARNING));
                  }
                }
                doBR = true;
              }
            }
          }
        }
      }

      html_TD();

      if (validDeviceIndex(DeviceIndex)) {
        const DeviceStruct& device = Device[DeviceIndex];

        if (Settings.TaskDeviceDataFeed[x] == 0)
        {
          String description;
          bool   pluginHasGPIODescription = pluginWebformShowGPIOdescription(x, F("<BR>"), description);

          bool showpin1 = false;
          bool showpin2 = false;
          bool showpin3 = false;

          switch (device.Type) {
            case DEVICE_TYPE_I2C:
            {
              format_I2C_pin_description(x);
              html_BR();
              break;
            }
            case DEVICE_TYPE_SPI3:
              showpin3 = !pluginHasGPIODescription;

            // Fall Through
            case DEVICE_TYPE_SPI2:
              showpin2 = !pluginHasGPIODescription;

            // Fall Through
            case DEVICE_TYPE_SPI:
              format_SPI_pin_description(spi_gpios, x, !pluginHasGPIODescription);
              break;
            case DEVICE_TYPE_ANALOG:
            {
              # ifdef ESP8266
              #  if FEATURE_ADC_VCC
              addHtml(F("ADC (VCC)"));
              #  else // if FEATURE_ADC_VCC
              addHtml(F("ADC (TOUT)"));
              #  endif // if FEATURE_ADC_VCC
              # endif // ifdef ESP8266
              # ifdef ESP32
              #if SOC_ADC_SUPPORTED
              showpin1 = true;
              addHtml(formatGpioName_ADC(Settings.TaskDevicePin1[x]));
              html_BR();
              #endif
              # endif // ifdef ESP32

              break;
            }
            case DEVICE_TYPE_SERIAL_PLUS1:
              showpin3 = true;

            // fallthrough
            case DEVICE_TYPE_SERIAL:
            {
              # ifdef PLUGIN_USES_SERIAL
              const String serialDescription = serialHelper_getGpioDescription(static_cast<ESPEasySerialPort>(Settings.TaskDevicePort[x]),
                                                                               Settings.TaskDevicePin1[x],
                                                                               Settings.TaskDevicePin2[x],
                                                                               F("<BR>"));
              addHtml(serialDescription);
              # else // ifdef PLUGIN_USES_SERIAL
              addHtml(F("PLUGIN_USES_SERIAL not defined"));
              # endif // ifdef PLUGIN_USES_SERIAL

              if (
# ifdef PLUGIN_USES_SERIAL
                serialDescription.length() ||
# endif // ifdef PLUGIN_USES_SERIAL
                showpin3) {
                html_BR();
              }
              break;
            }
            case DEVICE_TYPE_CUSTOM3:
              showpin3 = true;

            // fallthrough
            case DEVICE_TYPE_CUSTOM2:
              showpin2 = true;

            // fallthrough
            case DEVICE_TYPE_CUSTOM1:
            case DEVICE_TYPE_CUSTOM0:
            {
              showpin1 = true;

              if (pluginHasGPIODescription || (device.Type == DEVICE_TYPE_CUSTOM0)) {
                addHtml(description);
                showpin1 = false;
                showpin2 = false;
                showpin3 = false;
              }
              break;
            }

            default:
              showpin1 = true;
              showpin2 = true;
              showpin3 = true;
              break;
          }

          if (showpin1)
          {
            addGpioHtml(Settings.getTaskDevicePin(x, 1));
          }

          if (showpin2)
          {
            html_BR();
            addGpioHtml(Settings.getTaskDevicePin(x, 2));
          }

          if (showpin3)
          {
            html_BR();
            addGpioHtml(Settings.getTaskDevicePin(x, 3));
          }

          // Allow for tasks to show their own specific GPIO pins.
          if (!device.isCustom() &&
              pluginHasGPIODescription) {
            if (showpin1 || showpin2 || showpin3) {
              html_BR();
            }
            addHtml(description);
          }
        }
      }

      html_TD();

      if (validDeviceIndex(DeviceIndex)) {
        #if FEATURE_STRING_VARIABLES
        const DeviceStruct& device = Device[DeviceIndex];
        #endif // #if FEATURE_STRING_VARIABLES
        String customValuesString;
        const bool customValues = PluginCall(PLUGIN_WEBFORM_SHOW_VALUES, &TempEvent, customValuesString);

        if (!customValues)
        {
          const uint8_t valueCount = getValueCountForTask(x);

          for (uint8_t varNr = 0; varNr < valueCount; varNr++)
          {
            if (validPluginID_fullcheck(Settings.getPluginID_for_task(x)))
            {
              #if FEATURE_TASKVALUE_UNIT_OF_MEASURE
              const uint8_t uomIndex = Cache.getTaskVarUnitOfMeasure(x, varNr);
              String uom;
              if ((uomIndex != 0) && Settings.ShowUnitOfMeasureOnDevicesPage()) {
                uom = concat(F(" "), toUnitOfMeasureName(uomIndex));
              }
              #endif // if FEATURE_TASKVALUE_UNIT_OF_MEASURE
              const String value = formatUserVarNoCheck(&TempEvent, varNr);
              #if FEATURE_STRING_VARIABLES
              bool hasPresentation = false;
              String presentation;
              if (!device.HideDerivedValues)
                presentation = formatUserVarForPresentation(&TempEvent, varNr, hasPresentation, value, DeviceIndex);
              #endif // if FEATURE_STRING_VARIABLES
              pluginWebformShowValue(
                x,
                varNr,
                Cache.getTaskDeviceValueName(x, varNr),
                #if FEATURE_STRING_VARIABLES
                hasPresentation ? presentation :
                #endif // if FEATURE_STRING_VARIABLES
                #if FEATURE_TASKVALUE_UNIT_OF_MEASURE
                concat(value, uom)
                #else // if FEATURE_TASKVALUE_UNIT_OF_MEASURE
                value
                #endif // if FEATURE_TASKVALUE_UNIT_OF_MEASURE
              );
            }
          }

          #if FEATURE_STRING_VARIABLES
          if (!device.HideDerivedValues) {
            int varNr = VARS_PER_TASK;
            if (Settings.ShowDerivedTaskValues(x)) {
              String taskName = getTaskDeviceName(x);
              taskName.toLowerCase();
              String postfix;
              const String search = getDerivedValueSearchAndPostfix(taskName, postfix);

              auto it = customStringVar.begin();
              while (it != customStringVar.end()) {
                if (it->first.startsWith(search) && it->first.endsWith(postfix)) {
                  String valueName = it->first.substring(search.length(), it->first.indexOf('-'));
                  String uom;
                  String vType;
                  const String vname2 = getDerivedValueNameUomAndVType(taskName, valueName, uom, vType);
                  if (!vname2.isEmpty()) {
                    valueName = vname2;
                  }
                  if (!it->second.isEmpty()) {
                    String value(it->second);
                    value = parseTemplateAndCalculate(value);
                    String presentation = getCustomStringVar(strformat(F(TASK_VALUE_PRESENTATION_PREFIX_TEMPLATE), taskName.c_str(), valueName.c_str()));
                    if (!uom.isEmpty()) {
                      value = strformat(F("%s %s"), value.c_str(), uom.c_str());
                    }
                    if (!presentation.isEmpty()) {
                      stripEscapeCharacters(presentation);
                      presentation.replace(F("%value%"), value);
                      value = parseTemplate(presentation);
                    }
                    pluginWebformShowValue(
                      x,
                      varNr,
                      valueName,
                      value);
                    ++varNr;
                  }
                }
                else if (it->first.substring(0, search.length()).compareTo(search) > 0) {
                  break;
                }
                ++it;
              }
            }
          }
          #endif // if FEATURE_STRING_VARIABLES
        }
      }
    }
    else {
      html_TD(6);
    }
  } // next
  html_end_table();
  html_end_form();
}

# if FEATURE_ESPEASY_P2P
void format_originating_node(uint8_t remoteUnit) {
  addHtml(F("Unit "));
  addHtmlInt(remoteUnit);

  if (remoteUnit != 255) {
    const NodeStruct *node = Nodes.getNode(remoteUnit);

    if (node != nullptr) {
      addHtml(F(" - "));
      addHtml(node->getNodeName());
    } else {
      addHtml(F(" - Not Seen recently"));
    }
  }
}

# endif // if FEATURE_ESPEASY_P2P

void format_I2C_port_description(taskIndex_t x)
{
  addHtml(F("I2C"));
  # if FEATURE_I2C_GET_ADDRESS
  const uint8_t i2cAddr = getTaskI2CAddress(x);

  if (i2cAddr > 0) {
    addHtml(' ');
    addHtml(formatToHex(i2cAddr, 2));
  }
  # endif // if FEATURE_I2C_GET_ADDRESS
  # if FEATURE_I2CMULTIPLEXER
  #if FEATURE_I2C_MULTIPLE
  const uint8_t i2cBus = Settings.getI2CInterface(x);
  if ((i2cBus > 0) || Settings.isI2CEnabled(1) || Settings.isI2CEnabled(2)) {
    html_BR();
    addHtml(F("I2C Bus"));
    addHtml(' ');
    addHtmlInt(i2cBus);
  }
  #else
  const uint8_t i2cBus = 0;
  #endif // if FEATURE_I2C_MULTIPLE

  if (isI2CMultiplexerEnabled(i2cBus) && I2CMultiplexerPortSelectedForTask(x)) {
    String mux;

    if (bitRead(Settings.I2C_Flags[x], I2C_FLAGS_MUX_MULTICHANNEL)) { // Multi-channel
      mux = F("<BR>Multiplexer channel(s)");
      uint8_t b = 0;                                                  // For adding lineBreaks

      for (uint8_t c = 0; c < I2CMultiplexerMaxChannels(i2cBus); ++c) {
        if (bitRead(Settings.I2C_Multiplexer_Channel[x], c)) {
          mux += b == 0 ? F("<BR>") : F(", ");
          b++;
          mux += c;
        }
      }
    } else { // Single channel
      mux = concat(F("<BR>Multiplexer channel "), static_cast<int>(Settings.I2C_Multiplexer_Channel[x]));
    }
    addHtml(mux);
  }
  # endif // if FEATURE_I2CMULTIPLEXER
}

void format_SPI_port_description(int8_t spi_gpios[3])
{
  if (!Settings.getSPI_pins(spi_gpios)) {
    addHtml(F("SPI (Not enabled)"));
    return;
  }
  # ifdef ESP32
  addHtml(getSPI_optionToShortString(static_cast<SPI_Options_e>(Settings.InitSPI)));
  # endif // ifdef ESP32
  # ifdef ESP8266
  addHtml(F("SPI"));
  # endif // ifdef ESP8266
}

void format_I2C_pin_description(taskIndex_t x)
{
  #if FEATURE_I2C_MULTIPLE
  const uint8_t i2cBus = Settings.getI2CInterface(x);
  #else
  const uint8_t i2cBus = 0;
  #endif // if FEATURE_I2C_MULTIPLE
  if (checkI2CConfigValid_toHtml(x)) {
    Label_Gpio_toHtml(F("SDA"), formatGpioLabel(Settings.getI2CSdaPin(i2cBus), false));
    html_BR();
    Label_Gpio_toHtml(F("SCL"), formatGpioLabel(Settings.getI2CSclPin(i2cBus), false));
  }
}

void format_SPI_pin_description(int8_t spi_gpios[3], taskIndex_t x, bool showCSpin)
{
  if (Settings.InitSPI > static_cast<int>(SPI_Options_e::None)) {
    const __FlashStringHelper*labels[] = { F("CLK"), F("MISO"), F("MOSI") };

    for (int i = 0; i < 3; ++i) {
      if (i != 0) {
        html_BR();
      }

      Label_Gpio_toHtml(labels[i], formatGpioLabel(spi_gpios[i], false));
    }

    if (showCSpin) {
      html_BR();
      Label_Gpio_toHtml(F("CS"), formatGpioLabel(Settings.TaskDevicePin1[x], false));
    }
  }
}

// ********************************************************************************
// Show the task settings page
// ********************************************************************************
void handle_devices_TaskSettingsPage(taskIndex_t taskIndex, uint8_t page)
{
  if (!validTaskIndex(taskIndex)) { return; }

  const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(taskIndex);

  // LoadTaskSettings(taskIndex);

  html_add_form();
  html_table_class_normal();
  addFormHeader(F("Task Settings"));


  addHtml(F("<TR><TD style='width:150px;' align='left'>Device:<TD>"));

  // no (supported) device selected, this effectively checks for validDeviceIndex
  if (!supportedPluginID(Settings.getPluginID_for_task(taskIndex)))
  {
    // takes lots of memory/time so call this only when needed.
    addDeviceSelect(F("TDNUM"), Settings.getPluginID_for_task(taskIndex)); // ="taskdevicenumber"
    addFormSeparator(4);
  }

  // device selected
  else
  {
    const DeviceStruct& device = Device[DeviceIndex];

    // remember selected device number
    addHtml(F("<input "));
    addHtmlAttribute(F("type"),  F("hidden"));
    addHtmlAttribute(F("name"),  F("TDNUM"));
    addHtmlAttribute(F("value"), Settings.getPluginID_for_task(taskIndex).value);
    addHtml('>');

    // show selected device name and delete button
    addHtml(getPluginNameFromDeviceIndex(DeviceIndex));

    const uint8_t pid = Settings.getPluginID_for_task(taskIndex).value;
    if (pid <= 79) { // Up to P079 seem to be listed in the old Wiki (and a few incomplete pages), so lets keep pointing there too
      addHelpButton(concat(F("Plugin"), Settings.getPluginID_for_task(taskIndex).value));
    }
    addRTDPluginButton(Settings.getPluginID_for_task(taskIndex));

    addFormTextBox(F("Name"), F("TDN"), getTaskDeviceName(taskIndex), NAME_FORMULA_LENGTH_MAX); // ="taskdevicename"

    addFormCheckBox(F("Enabled"), F("TDE"),
                    Settings.TaskDeviceEnabled[taskIndex],

                    //    Settings.TaskDeviceEnabled[taskIndex].enabled,
                    Settings.isTaskEnableReadonly(taskIndex)); // ="taskdeviceenabled"

    # if FEATURE_PLUGIN_PRIORITY

    if (device.PowerManager) { // Check extra priority device flags when available
      bool disablePrio = !Settings.TaskDeviceEnabled[taskIndex];

      for (taskIndex_t t = 0; t < TASKS_MAX && !disablePrio; t++) {
        if (t != taskIndex) {   // Ignore current device
          if (device.PowerManager && Settings.isPowerManagerTask(t)) {
            disablePrio = true; // Allow only a single PowerManager plugin
          }

          // Add other Priority options checks
        }
      }
      addFormSubHeader(F("Priority task"));
      addFormCheckBox(F("Priority task"), F("TPRE"), Settings.isPriorityTask(taskIndex), disablePrio); // ="taskpriorityenabled"

      if (!disablePrio) {
        addFormNote(F("After enabling a Priority task, a reboot is required to activate. See documentation."));
      }
    }
    # endif // if FEATURE_PLUGIN_PRIORITY

    # if FEATURE_ESPEASY_P2P
    const controllerIndex_t p2p_controllerIndex = findFirstEnabledControllerWithId(13);
    const uint8_t remoteUnit = 
      (p2p_controllerIndex != INVALID_CONTROLLER_INDEX)
      ? Settings.TaskDeviceDataFeed[taskIndex]
      : 0;
    #else
    const uint8_t remoteUnit = 0;
    #endif
    # if FEATURE_ESPEASY_P2P
    if (device.SendDataOption && p2p_controllerIndex != INVALID_CONTROLLER_INDEX)
    {
      // Show remote feed information.
      addFormSubHeader(F("Data Source"));
      addFormNumericBox(F("Remote Unit"), F("remoteFeed"), remoteUnit, 0, 255);

      if (remoteUnit != 0 && remoteUnit != 255) {
        const NodeStruct*node = Nodes.getNode(remoteUnit);

        if (node != nullptr) {
          addUnit(node->getNodeName());
        } else {
          addUnit(F("Unknown Unit Name"));
        }
      }
      addFormNote(F("0 = disable remote feed, 255 = broadcast")); // FIXME TD-er: Must verify if broadcast can be set.
    }
    # endif // if FEATURE_ESPEASY_P2P

    bool addPinConfig = false;

    // section: Sensor / Actuator
    if (!device.Custom && (Settings.TaskDeviceDataFeed[taskIndex] == 0) &&
        ((device.Ports != 0) ||
         (device.PullUpOption) ||
         (device.InverseLogicOption) ||
         (device.connectedToGPIOpins())))
    {
      addFormSubHeader((device.SendDataOption) ? F("Sensor") : F("Actuator"));

      if (device.Ports != 0) {
        addFormNumericBox(F("Port"), F("TDP"), Settings.TaskDevicePort[taskIndex]); // ="taskdeviceport"
      }

      addPinConfig = true;
    }

    if (addPinConfig || (device.Type == DEVICE_TYPE_I2C)) {
      if (device.isSerial()) {
        # ifdef PLUGIN_USES_SERIAL
        devicePage_show_serial_config(taskIndex);
        # else // ifdef PLUGIN_USES_SERIAL
        addHtml(F("PLUGIN_USES_SERIAL not defined"));
        # endif // ifdef PLUGIN_USES_SERIAL

        devicePage_show_pin_config(taskIndex, DeviceIndex);
        addPinConfig = false;

        html_add_script(F("document.getElementById('serPort').onchange();"), false);
      } else if (device.Type == DEVICE_TYPE_I2C) {
        devicePage_show_pin_config(taskIndex, DeviceIndex);
        addPinConfig = false;

        if (Settings.TaskDeviceDataFeed[taskIndex] == 0) {
          devicePage_show_I2C_config(taskIndex, DeviceIndex);
        }
      }

      if (addPinConfig) {
        devicePage_show_pin_config(taskIndex, DeviceIndex);
      }
    }

    String webformLoadString;
    struct EventStruct TempEvent(taskIndex);


    // add plugins content
    if ((DEVICE_TYPE_DUMMY != device.Type) && (remoteUnit == 0)) {
      addFormSubHeader(F("Device Settings"));
    }

    if (Settings.TaskDeviceDataFeed[taskIndex] == 0) { // only show additional config for local connected sensors
      PluginCall(PLUGIN_WEBFORM_LOAD, &TempEvent, webformLoadString);
      # ifndef BUILD_NO_DEBUG

      if (webformLoadString.length() > 0) {
        String errorMessage;
        PluginCall(PLUGIN_GET_DEVICENAME, &TempEvent, errorMessage);
        errorMessage += F(": Bug in PLUGIN_WEBFORM_LOAD, should not append to string, use addHtml() instead");
        addHtmlError(errorMessage);
      }
      # endif // ifndef BUILD_NO_DEBUG
    }
    PluginCall(PLUGIN_WEBFORM_LOAD_ALWAYS, &TempEvent, webformLoadString); // Load settings also useful for remote-datafeed devices

    devicePage_show_output_data_type(taskIndex, DeviceIndex);

    # if FEATURE_PLUGIN_STATS

    // Task statistics and historic data in a chart
    devicePage_show_task_statistics(taskIndex, DeviceIndex);
    # endif // if FEATURE_PLUGIN_STATS

    // section: Data Acquisition
    devicePage_show_controller_config(taskIndex, DeviceIndex);

    addFormSeparator(2);

    devicePage_show_interval_config(taskIndex, DeviceIndex);

    devicePage_show_task_values(taskIndex, DeviceIndex);
  }

  html_TR_TD();
  addHtml(F("<TD colspan='3'>"));
  html_add_button_prefix();
  addHtml(F("devices?setpage="));
  addHtmlInt(page);
  addHtml(F("'>Close</a>"));
  # if FEATURE_PLUGIN_PRIORITY

  if (!Settings.isPriorityTask(taskIndex))
  # endif // if FEATURE_PLUGIN_PRIORITY
  {
    addSubmitButton();
  }
  addHtml(F("<input type='hidden' name='edit' value='1'>"));
  addHtml(F("<input type='hidden' name='page' value='1'>"));

  // if user selected a device, add the delete button, except for Priority tasks
  if (validPluginID_fullcheck(Settings.getPluginID_for_task(taskIndex))
      # if FEATURE_PLUGIN_PRIORITY
      && !Settings.isPriorityTask(taskIndex)
      # endif // if FEATURE_PLUGIN_PRIORITY
      ) {
    addSubmitButton(F("Delete"), F("del"));
  }

  html_end_table();
  # if FEATURE_PLUGIN_PRIORITY

  if (Settings.isPriorityTask(taskIndex)) {
    addFormNote(F("A Priority task can't be updated or deleted. See documentation."));
  }
  # endif // if FEATURE_PLUGIN_PRIORITY
  html_end_form();
  serve_JS(JSfiles_e::SplitPasteInput);
}

void devicePage_show_pin_config(taskIndex_t taskIndex, deviceIndex_t DeviceIndex)
{
  const DeviceStruct& device = Device[DeviceIndex];

  if (device.PullUpOption)
  {
    addFormCheckBox(F("Internal PullUp"), F("TDPPU"), Settings.TaskDevicePin1PullUp[taskIndex]); // ="taskdevicepin1pullup"
    addFormNote(F("Best to (also) configure pull-up on Hardware tab under \"GPIO boot states\""));
      # if defined(ESP8266)

    if ((Settings.TaskDevicePin1[taskIndex] == 16) || (Settings.TaskDevicePin2[taskIndex] == 16) ||
        (Settings.TaskDevicePin3[taskIndex] == 16)) {
      addFormNote(F("PullDown for GPIO-16 (D0)"));
    }
      # endif // if defined(ESP8266)
  }

  if (device.InverseLogicOption)
  {
    addFormCheckBox(F("Inversed Logic"), F("TDPI"), Settings.TaskDevicePin1Inversed[taskIndex]); // ="taskdevicepin1inversed"
    addFormNote(F("Will go into effect on next input change."));
  }

  if (device.isSPI()
      && (Settings.InitSPI == static_cast<int>(SPI_Options_e::None))) {
    addFormNote(F("SPI Interface is not configured yet (Hardware page)."));
  }

  if (device.connectedToGPIOpins()) {
    // get descriptive GPIO-names from plugin
    struct EventStruct TempEvent(taskIndex);

    TempEvent.String1 = F("1st GPIO");
    TempEvent.String2 = F("2nd GPIO");
    TempEvent.String3 = F("3rd GPIO");
    String dummy;
    PluginCall(PLUGIN_GET_DEVICEGPIONAMES, &TempEvent, dummy);

    if (device.usesTaskDevicePin(1)) {
      PinSelectPurpose purpose = device.getPinSelectPurpose(1); // PinSelectPurpose::Generic;

      if (device.isSerial())
      {
        // Pin1 = GPIO <--- TX
        purpose = PinSelectPurpose::Serial_input;
      } else if (device.isSPI())
      {
        // All selectable SPI pins are output only
        purpose = PinSelectPurpose::Generic_output;
      }

      addFormPinSelect(purpose, TempEvent.String1, F("taskdevicepin1"), Settings.TaskDevicePin1[taskIndex]);
    }

    if (device.usesTaskDevicePin(2)) {
      PinSelectPurpose purpose = device.getPinSelectPurpose(2); // PinSelectPurpose::Generic;

      if (device.isSerial())
      {
        // Serial Pin2 = GPIO ---> RX
        purpose = PinSelectPurpose::Serial_output;
      }

      if (device.isSPI())
      {
        // SPI only needs output pins
        purpose = PinSelectPurpose::Generic_output;
      }
      addFormPinSelect(purpose, TempEvent.String2, F("taskdevicepin2"), Settings.TaskDevicePin2[taskIndex]);
    }

    if (device.usesTaskDevicePin(3)) {
      PinSelectPurpose purpose = device.getPinSelectPurpose(3); // PinSelectPurpose::Generic;

      if (device.isSPI())
      {
        // SPI only needs output pins
        purpose = PinSelectPurpose::Generic_output;
      }
      addFormPinSelect(purpose, TempEvent.String3, F("taskdevicepin3"), Settings.TaskDevicePin3[taskIndex]);
    }
  }
}

# ifdef PLUGIN_USES_SERIAL
void devicePage_show_serial_config(taskIndex_t taskIndex)
{
  struct EventStruct TempEvent(taskIndex);

  String webformLoadString;

  PluginCall(PLUGIN_WEBFORM_PRE_SERIAL_PARAMS, &TempEvent, webformLoadString);

  serialHelper_webformLoad(&TempEvent);

  PluginCall(PLUGIN_WEBFORM_SHOW_SERIAL_PARAMS, &TempEvent, webformLoadString);
}

# endif // ifdef PLUGIN_USES_SERIAL

void devicePage_show_I2C_config(taskIndex_t taskIndex, deviceIndex_t DeviceIndex)
{
  struct EventStruct TempEvent(taskIndex);

  addFormSubHeader(F("I2C options"));

  if (!Settings.isI2CEnabled(0)
     #if FEATURE_I2C_MULTIPLE
     && (getI2CBusCount() > 1 && !Settings.isI2CEnabled(1))
     #if FEATURE_I2C_INTERFACE_3
     && (getI2CBusCount() > 2 && !Settings.isI2CEnabled(2))
     #endif // if FEATURE_I2C_INTERFACE_3
     #endif // if FEATURE_I2C_MULTIPLE
     ) {
    addFormNote(F("I2C Bus is not configured yet (Hardware page)."));
  }

  String dummy;

  PluginCall(PLUGIN_WEBFORM_SHOW_I2C_PARAMS, &TempEvent, dummy);
  addFormCheckBox(F("Force Slow I2C speed"), F("taskdeviceflags0"), bitRead(Settings.I2C_Flags[taskIndex], I2C_FLAGS_SLOW_SPEED));

  if (Device[DeviceIndex].I2CMax100kHz) {
    addFormNote(F("This device is specified for max. 100 kHz operation!"));
  }
#if FEATURE_I2C_MULTIPLE || FEATURE_I2CMULTIPLEXER
  uint8_t i2cBus = 0;
#endif

  #if FEATURE_I2C_MULTIPLE
  if (!Device[DeviceIndex].I2CNoBusSelection) { // If the device doesn't disallow bus selection
    i2cBus = Settings.getI2CInterface(taskIndex);
    I2CInterfaceSelector(F("I2C Bus"),
                        F("pi2cbus"),
                        i2cBus,
                        true);
  }
  #endif // if FEATURE_I2C_MULTIPLE
  # if FEATURE_I2CMULTIPLEXER

  // Show selector for an I2C multiplexer port if a multiplexer is configured
  if (isI2CMultiplexerEnabled(i2cBus)) {
    bool multipleMuxPorts = bitRead(Settings.I2C_Flags[taskIndex], I2C_FLAGS_MUX_MULTICHANNEL);
    {
      const __FlashStringHelper *i2c_mux_channels[] = {
        F("Single channel"),
        F("Multiple channels") };
      constexpr int i2c_mux_channelOptions[] = { 0, 1 };
      int i2c_mux_channelCount               = 1;

      if (Settings.I2C_Multiplexer_Type == I2C_MULTIPLEXER_PCA9540) {
        multipleMuxPorts = false; // force off
      } else {
        i2c_mux_channelCount++;
      }
      FormSelectorOptions selector(
        i2c_mux_channelCount,
        i2c_mux_channels,
        i2c_mux_channelOptions);
      selector.reloadonchange = true;
      selector.addFormSelector(
        F("Multiplexer channels"),
        F("taskdeviceflags1"),
        multipleMuxPorts ? 1 : 0);
    }

    if (multipleMuxPorts) {
      addRowLabel(F("Select connections"), EMPTY_STRING);
      html_table(EMPTY_STRING, false); // Sub-table
      html_table_header(F("Channel"), 100);
      html_table_header(F("Enable"),  80);
      html_table_header(F("Channel"), 100);
      html_table_header(F("Enable"),  80);

      for (int x = 0; x < I2CMultiplexerMaxChannels(i2cBus); x++) {
        if (x % 2 == 0) { html_TR(); } // Start a new row for every 2 channels
        html_TD();
        addHtml(concat(F("Channel "), x));
        html_TD();
        addCheckBox(concat(F("taskdeviceflag1ch"), x), bitRead(Settings.I2C_Multiplexer_Channel[taskIndex], x), false);
      }
      html_end_table();
    } else {
      int taskDeviceI2CMuxPort = Settings.I2C_Multiplexer_Channel[taskIndex];
      const uint32_t mux_max   = I2CMultiplexerMaxChannels(i2cBus);
      String i2c_mux_portoptions[mux_max + 1];
      int    i2c_mux_portchoices[mux_max + 1];
      i2c_mux_portoptions[0] = F("(Not connected via multiplexer)");
      i2c_mux_portchoices[0] = -1;

      for (uint32_t x = 0; x < mux_max; x++) {
        const uint32_t mux_opt = x + 1;
        i2c_mux_portoptions[mux_opt] = concat(F("Channel "), x);
        i2c_mux_portchoices[mux_opt] = x;
      }

      if (taskDeviceI2CMuxPort >= static_cast<int>(mux_max)) { taskDeviceI2CMuxPort = -1; } // Reset if out of range
      const FormSelectorOptions selector(        
        mux_max + 1,
        i2c_mux_portoptions,
        i2c_mux_portchoices);
      selector.addFormSelector(
        F("Connected to"),
        F("taskdevicei2cmuxport"),
        taskDeviceI2CMuxPort);
    }
  }
  # endif // if FEATURE_I2CMULTIPLEXER
}

void devicePage_show_output_data_type(taskIndex_t taskIndex, deviceIndex_t DeviceIndex)
{
  struct EventStruct TempEvent(taskIndex);
  int pconfigIndex = checkDeviceVTypeForTask(&TempEvent);

  switch (Device[DeviceIndex].OutputDataType) {
    case Output_Data_type_t::Default:
      return;
    case Output_Data_type_t::Simple:

      if (pconfigIndex >= 0) {
        sensorTypeHelper_webformLoad_simple(&TempEvent, pconfigIndex);
        return;
      }
      break;
    case Output_Data_type_t::All:
    {
      if (pconfigIndex >= 0) {
        sensorTypeHelper_webformLoad_allTypes(&TempEvent, pconfigIndex);
        return;
      }
      break;
    }
  }
  addFormSubHeader(F("Output Configuration"));
  String dummy;
  PluginCall(PLUGIN_WEBFORM_LOAD_OUTPUT_SELECTOR, &TempEvent, dummy);
}

# if FEATURE_PLUGIN_STATS
void devicePage_show_task_statistics(taskIndex_t taskIndex, deviceIndex_t DeviceIndex)
{
  if (Device[DeviceIndex].PluginStats)
  {
    PluginTaskData_base *taskData = getPluginTaskDataBaseClassOnly(taskIndex);

    if (taskData != nullptr) {
      if (taskData->hasPluginStats()) {
        addFormSubHeader(F("Statistics"));
      }
      #  if FEATURE_CHART_JS

      if (taskData->nrSamplesPresent() > 0) {
        addRowLabel(F("Historic data"));
        taskData->plot_ChartJS();
      }
      #  endif // if FEATURE_CHART_JS

      struct EventStruct TempEvent(taskIndex);
      String dummy;
      bool   somethingAdded = false;

      if (!PluginCall(PLUGIN_WEBFORM_LOAD_SHOW_STATS, &TempEvent, dummy)) {
        somethingAdded = taskData->webformLoad_show_stats(&TempEvent);
      } else { somethingAdded = true; }

      if (somethingAdded) {
        if (taskData->hasPeaks()) {
          addFormNote(strformat(
                        F("Peak values recorded since last \"%s.resetpeaks\"."),
                        getTaskDeviceName(taskIndex).c_str()));
        }
      }
    }
  }
}

# endif // if FEATURE_PLUGIN_STATS


void devicePage_show_controller_config(taskIndex_t taskIndex, deviceIndex_t DeviceIndex)
{
  if (!validDeviceIndex(DeviceIndex)) { return; }

  const DeviceStruct& device = Device[DeviceIndex];

  if (device.SendDataOption)
  {
    addFormSubHeader(F("Data Acquisition"));

    if (device.ErrorStateValues) {
      struct EventStruct TempEvent(taskIndex);
      String dummy;

      PluginCall(PLUGIN_WEBFORM_SHOW_ERRORSTATE_OPT, &TempEvent, dummy); // Show extra settings for Error State Value options
    }

    addFormCheckBox(F("Single event with all values"), F("TVSE"), Settings.CombineTaskValues_SingleEvent(taskIndex));
    addFormNote(strformat(
                  F("Unchecked: Send event per value. Checked: Send single event (%s#All) containing all values"),
                  getTaskDeviceName(taskIndex).c_str()));

    #if FEATURE_STRING_VARIABLES
    if (!device.HideDerivedValues) {
      addFormCheckBox(F("Show derived values"),            F("TSDV"), Settings.ShowDerivedTaskValues(taskIndex));
      addFormCheckBox(F("Event &amp; Log derived values"), F("TELD"), Settings.EventAndLogDerivedTaskValues(taskIndex));
    }
    #endif // if FEATURE_STRING_VARIABLES

    bool separatorAdded = false;

    for (controllerIndex_t controllerNr = 0; controllerNr < CONTROLLER_MAX; controllerNr++)
    {
      if (Settings.Protocol[controllerNr] != 0)
      {
        if (!separatorAdded) {
          addFormSeparator(2);
        }
        protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(controllerNr);
        const bool showControllerIDX  = validProtocolIndex(ProtocolIndex) &&
                                        getProtocolStruct(ProtocolIndex).usesID &&
                                        (Settings.Protocol[controllerNr] != 0);
        # if FEATURE_MQTT_DISCOVER
        const bool showMqttGroup = (validProtocolIndex(ProtocolIndex) &&
                                    getProtocolStruct(ProtocolIndex).mqttAutoDiscover);
        # endif // if FEATURE_MQTT_DISCOVER
        separatorAdded = true;
        html_TR_TD();
        addHtml(F("Send to Controller "));
        addHtml(getControllerSymbol(controllerNr));
        addHtmlDiv(F("note"), wrap_braces(getCPluginNameFromCPluginID(Settings.Protocol[controllerNr]) + F(", ") + // Most compact code...
                                          (Settings.ControllerEnabled[controllerNr] ? F("enabled") : F("disabled"))
                                          #if FEATURE_MQTT_DISCOVER
                                          + (showMqttGroup ? F(", Auto Discovery") : F(""))
                                          #endif // if FEATURE_MQTT_DISCOVER
                                         ));
        html_TD();

        addHtml(F("<table style='padding-left:0;'>"));     // remove left padding 2x to align vertically with other inputs
        html_TD(F("width:50px;padding-left:0"));
        addCheckBox(
          getPluginCustomArgName(F("TDSD"), controllerNr), // ="taskdevicesenddata"
          Settings.TaskDeviceSendData[controllerNr][taskIndex]);

        # if FEATURE_STRING_VARIABLES
        const bool allowSendDerived = !device.HideDerivedValues &&
                                      (validProtocolIndex(ProtocolIndex) &&
                                       getProtocolStruct(ProtocolIndex).allowSendDerived);
        # endif // if FEATURE_STRING_VARIABLES

        if (showControllerIDX
            # if FEATURE_MQTT_DISCOVER
            || showMqttGroup
            # endif // if FEATURE_MQTT_DISCOVER
            ) {
          html_TD();
          addHtml(
            # if FEATURE_MQTT_DISCOVER
            showMqttGroup && !showControllerIDX ? F("Group:") :
            # endif // if FEATURE_MQTT_DISCOVER
            F("IDX:"));
          html_TD();
          addNumericBox(
            getPluginCustomArgName(F("TDID"), controllerNr), // ="taskdeviceid"
            Settings.TaskDeviceID[controllerNr][taskIndex], 0, DOMOTICZ_MAX_IDX);
        }
        #if FEATURE_MQTT && FEATURE_MQTT_DISCOVER
        if (showMqttGroup) {
          html_TD();
          addHtml(F("Retained:"));
          html_TD();
          addCheckBox(getPluginCustomArgName(F("TSRT"), controllerNr), Settings.SendRetainedTaskValues(taskIndex, controllerNr), false
                      #  if FEATURE_TOOLTIPS
                      , F("Send values with Retain flag")
                      #  endif // if FEATURE_TOOLTIPS
                      );
        }
        #endif // if FEATURE_MQTT && FEATURE_MQTT_DISCOVER
        # if FEATURE_STRING_VARIABLES
        if (allowSendDerived) {
          html_TD();
          addHtml(F("Send derived:"));
          html_TD();
          addCheckBox(getPluginCustomArgName(F("TSND"), controllerNr), Settings.SendDerivedTaskValues(taskIndex, controllerNr), false
                      #  if FEATURE_TOOLTIPS
                      , F("Send derived values")
                      #  endif // if FEATURE_TOOLTIPS
                     );
        }
        # endif // if FEATURE_STRING_VARIABLES

        # if FEATURE_MQTT_DISCOVER

        if (showMqttGroup &&
            (mqttDiscoveryController == controllerNr)) {
          html_TD();
          addHtml(F("Resend MQTT Discovery:"));
          html_TD();
          addCheckBox(
            getPluginCustomArgName(F("TDDSC"), controllerNr), // ="taskdevicediscover"
            false);
        }
        # endif // if FEATURE_MQTT_DISCOVER
        html_end_table();
      }
    }
  }
}

void devicePage_show_interval_config(taskIndex_t taskIndex, deviceIndex_t DeviceIndex)
{
  if (!validDeviceIndex(DeviceIndex)) { return; }

  const DeviceStruct& device = Device[DeviceIndex];

  if (device.TimerOption)
  {
    // FIXME: shoudn't the max be ULONG_MAX because Settings.TaskDeviceTimer is an unsigned long? addFormNumericBox only supports ints
    // for min and max specification
    addFormNumericBox(F("Interval"), F("TDT"), Settings.TaskDeviceTimer[taskIndex], 0, 65535); // ="taskdevicetimer"
    addUnit(F("sec"));

    if (device.TimerOptional) {
      addHtml(F(" (Optional for this Device)"));
    }
  }
}

void devicePage_show_task_values(taskIndex_t taskIndex, deviceIndex_t DeviceIndex)
{
  if (!validDeviceIndex(DeviceIndex)) { return; }

  // section: Values
  const uint8_t valueCount = getValueCountForTask(taskIndex);

  const DeviceStruct& device = Device[DeviceIndex];

  if (!device.Custom && (valueCount > 0))
  {
    int colCount = 2;
    addFormSubHeader(F("Values"));
    html_end_table();
    html_table_class_normal();

    // table header
    addHtml(F("<TR><TH style='width:30px;' align='center'>#"));
    html_table_header(F("Name"), 500);

    if (device.FormulaOption)
    {
      html_table_header(F("Formula"), F("RTDPlugin/_Plugin.html#formula"), 500);
      ++colCount;
    }

    if (device.configurableDecimals())
    {
      html_table_header(F("Decimals"), 30);
      ++colCount;
    }

# if FEATURE_PLUGIN_STATS

    if (device.PluginStats)
    {
      html_table_header(F("Stats"), 30);
      ++colCount;
      html_table_header(F("Hide"),  30);
      ++colCount;
      html_table_header(F("Axis"),  30);
      ++colCount;
    }
# endif // if FEATURE_PLUGIN_STATS

    #if FEATURE_TASKVALUE_UNIT_OF_MEASURE
    html_table_header(F("Unit of Measure"), 300);
    ++colCount;
    EventStruct uomEvent(taskIndex);
    String uomDummy;
    bool limitedUom = PluginCall(DeviceIndex, PLUGIN_GET_UOM_GROUPS, &uomEvent, uomDummy);
    if (!limitedUom) {
      PluginCall(PLUGIN_GET_DEVICEVTYPE, &uomEvent, uomDummy); // Get Sensor_VType
      limitedUom = getDefaultUoMforSensorVType(&uomEvent);     // Populate UoM groups for known Sensor_VTypes
      #if FEATURE_MQTT_DISCOVER && FEATURE_CUSTOM_TASKVAR_VTYPE
      if (!limitedUom) {
        // Fill in standard Unit of measurement and Value Type, if possible
        std::vector<DiscoveryItem> discoveryItems;
        MQTT_DiscoveryGetDeviceVType(taskIndex, discoveryItems, getValueCountForTask(taskIndex), uomDummy);

        for (uint8_t varNr = 0; varNr < VARS_PER_TASK; ++varNr) {
          // Match varNr with the DiscoveryItems to find the Sensor_VType for the value
          for (uint8_t j = 0; j < discoveryItems.size(); ++j) {
            for (uint8_t k = 0; k < discoveryItems[j].valueCount; ++k) { // Can have multiple values for 1 VType
              if (varNr == discoveryItems[j].varIndex + k) {
                const String   uom       = getValueType2DefaultHAUoM(discoveryItems[j].VType);
                const uint16_t uomGroup  = getUoMGroupForUoM(uom);
                const uint16_t uomGroup2 = getUoMGroupForUoM(Cache.getTaskVarUnitOfMeasure(taskIndex, varNr));

                if (uomGroup > 0) {
                  uomEvent.Par64N[varNr] = (uint64_t)(1ULL << (uomGroup - 1024)) | (uomGroup2 > 0 ? (uint64_t)(1ULL << (uomGroup2 - 1024)) : 0ULL);
                  limitedUom             = true;
                } else {
                  uomEvent.Par64N[varNr] = UOM_GROUP_ALL;
                }
              }
            }
          }
        }
      }
      #endif // if FEATURE_MQTT_DISCOVER && FEATURE_CUSTOM_TASKVAR_VTYPE
    }
    #endif // if FEATURE_TASKVALUE_UNIT_OF_MEASURE

    #if FEATURE_CUSTOM_TASKVAR_VTYPE
    if (device.CustomVTypeVar) {
      html_table_header(F("Value Type"),  100);
      ++colCount;
    }
    #endif // if FEATURE_CUSTOM_TASKVAR_VTYPE

    #if FEATURE_MQTT_STATE_CLASS
    if (device.MqttStateClass) {
      html_table_header(F("MQTT State Class"), 350);
      ++colCount;
    }
    #endif // if FEATURE_MQTT_STATE_CLASS


    // placeholder header
    html_table_header(F(""));
    ++colCount;

    #if FEATURE_CUSTOM_TASKVAR_VTYPE
    std::vector<uint8_t> singleOptions;
    
    if (device.CustomVTypeVar) {
      // Build a list of all single-value available value VTypes from PR #5199
      constexpr uint8_t    maxVType = static_cast<uint8_t>(Sensor_VType::SENSOR_TYPE_NOT_SET);
      singleOptions.push_back(0); // Empty/None value
      for (uint8_t i = 0; i < maxVType; ++i) {
        if (getValueCountFromSensorType(static_cast<Sensor_VType>(i), false) == 1) {
          singleOptions.push_back(i);
        }
      }
    }
    #endif // if FEATURE_CUSTOM_TASKVAR_VTYPE

    # if FEATURE_PLUGIN_STATS
    const __FlashStringHelper *chartAxis[] = {
      F("L1"),
      F("L2"),
      F("L3"),
      F("L4"),
      F("R1"),
      F("R2"),
      F("R3"),
      F("R4")
    };
    constexpr size_t chartAxisCount = NR_ELEMENTS(chartAxis);
    #endif // if FEATURE_PLUGIN_STATS

    #if FEATURE_MQTT_STATE_CLASS
    const __FlashStringHelper *stateClasses[] = {
      MQTT_sensor_StateClass(0),
      MQTT_sensor_StateClass(1),
      MQTT_sensor_StateClass(2),
      MQTT_sensor_StateClass(3),
      MQTT_sensor_StateClass(4),
    };
    constexpr size_t stateCount = NR_ELEMENTS(stateClasses);
    #endif // if FEATURE_MQTT_STATE_CLASS

    // table body
    for (uint8_t varNr = 0; varNr < valueCount; varNr++)
    {
      html_TR_TD();
      addHtmlInt(varNr + 1);
      html_TD();
      {
        const String id = getPluginCustomArgName(F("TDVN"), varNr); // ="taskdevicevaluename"
        addTextBox(id, Cache.getTaskDeviceValueName(taskIndex, varNr), NAME_FORMULA_LENGTH_MAX);
      }

      if (device.FormulaOption)
      {
        html_TD();
        const String id = getPluginCustomArgName(F("TDF"), varNr); // ="taskdeviceformula"
        addTextBox(id, Cache.getTaskDeviceFormula(taskIndex, varNr), NAME_FORMULA_LENGTH_MAX);
      }

      if (device.configurableDecimals())
      {
        html_TD();
        const String id = getPluginCustomArgName(F("TDVD"), varNr); // ="taskdevicevaluedecimals"
        addNumericBox(id, Cache.getTaskDeviceValueDecimals(taskIndex, varNr), 0, 6);
      }

# if FEATURE_PLUGIN_STATS

      if (device.PluginStats)
      {
        PluginStats_Config_t cachedConfig = Cache.getPluginStatsConfig(taskIndex, varNr);
        html_TD();
        addCheckBox(
          getPluginCustomArgName(F("TDS"), varNr), // ="taskdevicestats"
          cachedConfig.isEnabled());

        html_TD();
        addCheckBox(
          getPluginCustomArgName(F("TDSH"), varNr), // ="taskdevicestats Hidden"
          cachedConfig.showHidden());

        html_TD();

        int selected = cachedConfig.getAxisIndex();

        if (!cachedConfig.isLeft()) {
          selected += 4;
        }

        const FormSelectorOptions selector(chartAxisCount, chartAxis);
        selector.addSelector(
          getPluginCustomArgName(F("TDSA"), varNr),
          selected);
      }
# endif // if FEATURE_PLUGIN_STATS

      #if FEATURE_TASKVALUE_UNIT_OF_MEASURE
      html_TD();
      const uint64_t uomGroups = limitedUom ? uomEvent.Par64N[varNr] : UOM_GROUP_ALL;
      addUnitOfMeasureSelector(getPluginCustomArgName(F("TUOM"), varNr), Cache.getTaskVarUnitOfMeasure(taskIndex, varNr), uomGroups);
      #endif // if FEATURE_TASKVALUE_UNIT_OF_MEASURE

      #if FEATURE_CUSTOM_TASKVAR_VTYPE
      if (device.CustomVTypeVar) {
        html_TD();
        sensorTypeCategoriesHelper_Selector(
          getPluginCustomArgName(F("TDTV"), varNr),
          singleOptions.size(),
          &singleOptions[0],
          static_cast<Sensor_VType>(Cache.getTaskVarCustomVType(taskIndex, varNr)));
      }
      #endif // if FEATURE_CUSTOM_TASKVAR_VTYPE

      #if FEATURE_MQTT_STATE_CLASS
      if (device.MqttStateClass) {
        html_TD();

        const FormSelectorOptions selectorSC(stateCount, stateClasses);
        selectorSC.addSelector(
          getPluginCustomArgName(F("TDSC"), varNr),
          Cache.getTaskVarStateClass(taskIndex, varNr));
      }
      #endif // if FEATURE_MQTT_STATE_CLASS
    }
    addFormSeparator(colCount);
  }
}

#endif // ifdef WEBSERVER_DEVICES

#include "../WebServer/I2C_Scanner.h"

#ifdef WEBSERVER_I2C_SCANNER

#include "../WebServer/ESPEasy_WebServer.h"
#include "../WebServer/AccessControl.h"
#include "../WebServer/HTML_wrappers.h"

#include "../Globals/Device.h"
#include "../Globals/Settings.h"

#include "../Helpers/_Plugin_init.h"
#include "../Helpers/I2C_access.h"
#include "../Helpers/StringConverter.h"


#include <Wire.h>

#ifdef WEBSERVER_NEW_UI

// ********************************************************************************
// Web Interface I2C scanner
// ********************************************************************************

int scanI2CbusForDevices_json( // Utility function for scanning the I2C bus for valid devices, with JSON output
        uint8_t i2cBus,
        int8_t muxAddr
      , int8_t channel
      , int nDevices
      #if FEATURE_I2CMULTIPLEXER
      , i2c_addresses_t &excludeDevices
      #endif // if FEATURE_I2CMULTIPLEXER
) {
  uint8_t error, address;

  for (address = 1; address <= 127; address++)
  {
    #if FEATURE_I2CMULTIPLEXER
    bool skipCheck = false;
    if (channel != -1 && excludeDevices.size() > address) {
      skipCheck = excludeDevices[address];
    }
    if (!skipCheck) { // Ignore I2C multiplexer and addresses to exclude when scanning its channels
    #endif // if FEATURE_I2CMULTIPLEXER
      I2C_wakeup(address); // Wakeup, workaround for slow-responding devices, see https://github.com/letscontrolit/ESPEasy/issues/3781
      delay(1);
      error = I2C_wakeup(address); // Get status
      delay(1);

      if ((error == 0) || (error == 4))
      {
        json_open();
        json_prop(F("addr"), formatToHex(address, 2));
        #if FEATURE_I2CMULTIPLEXER
        if (muxAddr != -1) {
          if (channel == -1){
            json_prop(F("I2Cbus"), F("Standard I2C bus"));
            excludeDevices[address] = true;
          } else {
            String i2cChannel = F("Multiplexer channel ");
            i2cChannel += String(channel);
            json_prop(F("I2Cbus"), i2cChannel);
          }
        }
        #endif // if FEATURE_I2CMULTIPLEXER
        json_number(F("status"), String(error));

        if (error == 4) {
          json_prop(F("error"), F("Unknown error at address "));
        } else {
          String description = getKnownI2Cdevice(address);

          if (description.length() > 0) {
            json_open(true, F("known devices"));
            int pos = 0;

            while (pos >= 0) {
              int newpos = description.indexOf(',', pos);

              if (pos != 0) {
                addHtml(',');
              }

              if (newpos == -1) {
                json_quote_val(description.substring(pos));
              } else {
                json_quote_val(description.substring(pos, newpos));
              }
              pos = newpos;

              if (newpos != -1) {
                ++pos;
              }
            }
            json_close(true);
          }
          nDevices++;
        }
        json_close();
        addHtml('\n');
      }
    #if FEATURE_I2CMULTIPLEXER
    }
    #endif // if FEATURE_I2CMULTIPLEXER
  }
  return nDevices;
}

void handle_i2cscanner_json() {
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_i2cscanner"));
  #endif

  if (!isLoggedIn()) { return; }
  navMenuIndex = MENU_INDEX_TOOLS;
  TXBuffer.startJsonStream();
  json_init();
  json_open(true);

  int  nDevices = 0;
  #ifdef ESP8266
  #endif
  #if !FEATURE_I2C_MULTIPLE
  const uint8_t i2cBus = 0;
  #else
  for (uint8_t i2cBus = 0; i2cBus < getI2CBusCount(); ++i2cBus)
  #endif
  {
    if (Settings.isI2CEnabled(i2cBus))
    {
      I2CSelect_Max100kHz_ClockSpeed(i2cBus);    // Always scan in low speed to also find old/slow devices
      #if FEATURE_I2CMULTIPLEXER
      int8_t Multiplexer_Addr = Settings.I2C_Multiplexer_Addr; // TODO Move to SettingsStruct
      #if FEATURE_I2C_MULTIPLE
      if (i2cBus == 1) {
        Multiplexer_Addr = Settings.I2C2_Multiplexer_Addr;
      }
      #if FEATURE_I2C_INTERFACE_3
      else if (i2cBus == 2) {
        Multiplexer_Addr = Settings.I2C3_Multiplexer_Addr;
      }
      #endif // if FEATURE_I2C_INTERFACE_3
      #endif // if FEATURE_I2C_MULTIPLE
      i2c_addresses_t mainBusDevices;
      mainBusDevices.resize(128);
      for (int i = 0; i < 128; i++) {
        mainBusDevices[i] = false;
      }
      nDevices = scanI2CbusForDevices_json(i2cBus, Multiplexer_Addr, -1, nDevices, mainBusDevices); // Channel -1 = standard I2C bus
      #else // if FEATURE_I2CMULTIPLEXER
      nDevices = scanI2CbusForDevices_json(0, -1, -1, nDevices); // Standard scan
      #endif // if FEATURE_I2CMULTIPLEXER

      #if FEATURE_I2CMULTIPLEXER
      if (isI2CMultiplexerEnabled(i2cBus)) {
        uint8_t mux_max = I2CMultiplexerMaxChannels(i2cBus);
        for (int8_t channel = 0; channel < mux_max; channel++) {
          I2CMultiplexerSelect(i2cBus, channel);
          nDevices += scanI2CbusForDevices_json(i2cBus, Multiplexer_Addr, channel, nDevices, mainBusDevices); // Specific channels
        }
        I2CMultiplexerOff(i2cBus);
      }
      #endif // if FEATURE_I2CMULTIPLEXER
    }
  }
  I2CSelectHighClockSpeed(0); // Reset bus to standard speed
  
  json_close(true);
  TXBuffer.endStream();
}
#endif // WEBSERVER_NEW_UI


String getKnownI2Cdevice(uint8_t address) {
  String result;

  #if FEATURE_I2C_DEVICE_SCAN
  deviceIndex_t x;
  bool done = false;
  while (!done) {
    const deviceIndex_t deviceIndex = getDeviceIndex_sorted(x);
    if (!validDeviceIndex(deviceIndex)) {
      done = true;
    } else {
      const pluginID_t pluginID = getPluginID_from_DeviceIndex(deviceIndex);

      if (validPluginID(pluginID) &&
          checkPluginI2CAddressFromDeviceIndex(deviceIndex, address)) {
        result += F("(Device) ");

        # if defined(PLUGIN_BUILD_DEV) || defined(PLUGIN_SET_MAX) // Use same name as in Add Device combobox
        result += concat(get_formatted_Plugin_number(pluginID), F(" - "));
        # endif // if defined(PLUGIN_BUILD_DEV) || defined(PLUGIN_SET_MAX)
        result += getPluginNameFromDeviceIndex(deviceIndex);
        result += ',';
      }
    }
    ++x;
  }
  #endif // if FEATURE_I2C_DEVICE_SCAN
  #ifndef LIMIT_BUILD_SIZE

  switch (address)
  {
    case 0x10:
      result += F("VEML6075,VEML6040,VEML6030,VEML7700");
      break;
    case 0x11:
      result += F("VEML6075,I2C_MultiRelay");
      break;
    case 0x12:
      result += F("I2C_MultiRelay,PMSx003i");
      break;
    case 0x13:
    case 0x16:
    case 0x17:
    case 0x18:
      result += F("I2C_MultiRelay");
      break;
    case 0x14:
      result += F("I2C_MultiRelay,GT911");
      break;
    case 0x15:
      result += F("I2C_MultiRelay,CST820");
      break;
    case 0x1D:
      result += F("ADXL345");
      break;
    case 0x1E:
      result += F("HMC5883L");
      break;
    case 0x20:
    case 0x21:
    case 0x22:
    case 0x25:
    case 0x26:
    case 0x27:
      result += F("PCF8574,MCP23017,LCD,PCF8575");
      break;
    case 0x23:
      result += F("PCF8574,MCP23017,LCD,BH1750,PCF8575");
      break;
    case 0x24:
      result += F("PCF8574,MCP23017,LCD,PN532,PCF8575");
      break;
    case 0x29:
      result += F("TSL2561,TSL2591,TCS34725,VL53L0X,VL53L1X");
      break;
    case 0x2E:
      result += F("CHSC5816");
      break;
    case 0x30:
      result += F("VL53L0X,VL53L1X");
      break;
    case 0x34:
      result += F("AXP192,AXP2101");
      break;
    case 0x36:
      result += F("MAX1704x,Adafruit Rotary enc, Adafruit Soil moisture,AS5600");
      break;
    case 0x37:
      result += F("Adafruit Rotary enc, Adafruit Soil moisture");
      break;
    case 0x38:
      result += F("LCD,PCF8574A,AHT10/20/21,VEML6070,Adafruit Rotary enc,FT62x6,Adafruit Soil moisture,DHT20,AM2301B");
      break;
    case 0x39:
      result += F("LCD,PCF8574A,TSL2561,APDS9960,AHT10,Adafruit Rotary enc,Adafruit Soil moisture");
      break;
    case 0x3A:
      result += F("LCD,PCF8574A,Adafruit Rotary enc");
      break;
    case 0x3B:
      result += F("LCD,PCF8574A,Adafruit Rotary enc,AXS15231");
      break;
    case 0x3C:
    case 0x3D:
      result += F("LCD,PCF8574A,OLED,Adafruit Rotary enc");
      break;
    case 0x3E:
    case 0x3F:
      result += F("LCD,PCF8574A");
      break;
    case 0x40:
      result += F("SI7021,HTU21D,INA219,PCA9685,HDC10xx,M5Stack Rotary enc,AS5600L");
      break;
    case 0x41:
    case 0x42:
    case 0x43:
    case 0x47:
      result += F("INA219");
      break;
    case 0x44:
    case 0x45:
      result += F("SHT30/31/35,INA219,SHT4x");
      break;
    case 0x46:
      result += F("SHT4x,INA219");
      break;
    case 0x48:
      result += F("PCF8591,ADS1x15,LM75A,INA219,TMP117,VEML6030");
      break;
    case 0x4A:
    case 0x4B:
      result += F("PCF8591,ADS1x15,LM75A,INA219,TMP117");
      break;
    case 0x49:
      result += F("PCF8591,ADS1x15,TSL2561,LM75A,INA219,TMP117");
      break;
    case 0x4C:
    case 0x4E:
    case 0x4F:
      result += F("PCF8591,LM75A,INA219");
      break;
    case 0x4D:
      result += F("PCF8591,MCP3221,LM75A,INA219");
      break;
    case 0x51:
      result += F("PCF8563");
      break;
    case 0x53:
      result += F("ADXL345,LTR390");
      break;
    case 0x55:
      result += F("DFRobot Rotary enc,BeFlE Moisture");
      break;
    case 0x54:
    case 0x56:
    case 0x57:
      result += F("DFRobot Rotary enc");
      break;
    case 0x58:
      result += F("SGP30,GP8403");
      break;
    case 0x59:
      result += F("SGP4x,GP8403");
      break;
    case 0x5A:
      result += F("MLX90614,MPR121,CCS811,GP8403,CST226");
      break;
    case 0x5B:
      result += F("MPR121,CCS811,GP8403");
      break;
    case 0x5C:
      result += F("DHT12,AM2320,BH1750,MPR121,GP8403");
      break;
    case 0x5D:
      result += F("MPR121,GP8403,GT911");
      break;
    case 0x5E:
      result += F("GP8403");
      break;
    case 0x5F:
      result += F("GP8403,CardKB");
      break;
    case 0x60:
      result += F("Adafruit Motorshield v2,SI1145");
      break;
    case 0x61:
      result += F("Atlas EZO DO,SCD30");
      break;
    case 0x62:
      result += F("Atlas EZO ORP,SCD4x");
      break;
    case 0x63:
      result += F("Atlas EZO pH");
      break;
    case 0x64:
      result += F("Atlas EZO EC");
      break;
    case 0x66:
      result += F("RadSens");
      break;
    case 0x68:
      result += F("MPU6050,DS1307,DS3231,PCF8523,ITG3205,CDM7160");
      break;
    case 0x69:
      result += F("ITG3205,CDM7160,SEN5x");
      break;
    case 0x70:
      result += F("Adafruit Motorshield v2 (Catchall),HT16K33,TCA9543a/6a/8a I2C multiplexer,PCA9540 I2C multiplexer,SHTC3");
      break;
    case 0x71:
    case 0x72:
    case 0x73:
      result += F("HT16K33,TCA9543a/6a/8a I2C multiplexer");
      break;
    case 0x74:
      result += F("HT16K33,TCA9546a/8a I2C multiplexer");
      break;
    case 0x75:
      result += F("HT16K33,TCA9546a/8a I2C multiplexer,IP5306");
      break;
    case 0x76:
      result += F("BMP280,BME280,BME680,BMP3xx,MS5607,MS5611,HT16K33,TCA9546a/8a I2C multiplexer");
      break;
    case 0x77:
      result += F("BMP085,BMP180,BMP280,BME280,BME680,BMP3xx,MS5607,MS5611,HT16K33,TCA9546a/8a I2C multiplexer,LiquidLevel");
      break;
    case 0x78:
      result += F("LiquidLevel");
      break;
    case 0x7f:
      result += F("Arduino PME,XDB401");
      break;
  }
  #endif // LIMIT_BUILD_SIZE
  return result;
}

int scanI2CbusForDevices( // Utility function for scanning the I2C bus for valid devices, with HTML table output
        uint8_t i2cBus,
        int8_t muxAddr
      , int8_t channel
      , int nDevices
      #if FEATURE_I2CMULTIPLEXER
      , i2c_addresses_t &excludeDevices
      #endif // if FEATURE_I2CMULTIPLEXER
) {
  uint8_t error, address;

  for (address = 1; address <= 127; address++)
  {
    #if FEATURE_I2CMULTIPLEXER
    bool skipCheck = false;
    if (channel != -1 && excludeDevices.size() > address) {
      skipCheck = excludeDevices[address];
    }
    if (!skipCheck) { // Ignore I2C multiplexer and addresses to exclude when scanning its channels
    #endif // if FEATURE_I2CMULTIPLEXER
      I2C_wakeup(address); // Wakeup, workaround for slow-responding devices, see https://github.com/letscontrolit/ESPEasy/issues/3781
      delay(1);
      error = I2C_wakeup(address); // Get status
      delay(1);

      switch (error) {
        case 0:
      {
        html_TR_TD();
        #if FEATURE_I2CMULTIPLEXER
        if (muxAddr != -1) {
          if (channel == -1){
            addHtml(F("Standard I2C bus"));
            excludeDevices[address] = true;
          } else {
            addHtml(F("Multiplexer channel "));
            addHtmlInt(channel);
          }
          html_TD();
        }
        #endif // if FEATURE_I2CMULTIPLEXER
        addHtml(formatToHex(address, 2));
        html_TD();
        String description = getKnownI2Cdevice(address);

        if (description.length() > 0) {
          description.replace(F(","), F("<BR>"));
          addHtml(description);
        }
        nDevices++;
        break;
      }
      case 2: // NACK on transmit address, thus not found 
        break;
      case 3: 
      {
        html_TR_TD();
        addHtml(F("NACK on transmit data to address "));
        addHtml(formatToHex(address, 2));
        break;
      }
      case 4:
      {
        html_TR_TD();
        addHtml(F("SDA low at address "));
        addHtml(formatToHex(address, 2));
        I2CForceResetBus_swap_pins(i2cBus, address);
        addHtml(F(" Reset bus attempted"));
        break;
      }
      }
    #if FEATURE_I2CMULTIPLEXER
    }
    #endif // if FEATURE_I2CMULTIPLEXER
  }
  return nDevices;
}

void handle_i2cscanner() {
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_i2cscanner"));
  #endif

  if (!isLoggedIn()) { return; }
  navMenuIndex = MENU_INDEX_TOOLS;
  TXBuffer.startStream();
  sendHeadandTail_stdtemplate(_HEAD);

  int nDevices = 0;
  #if !FEATURE_I2C_MULTIPLE
  const uint8_t i2cBus = 0;
  #else // if !FEATURE_I2C_MULTIPLE
  for (uint8_t i2cBus = 0; i2cBus < getI2CBusCount(); ++i2cBus)
  #endif // if !FEATURE_I2C_MULTIPLE
  {
    html_table_class_multirow();
    #if FEATURE_I2C_MULTIPLE
    html_table_header(strformat(F("I2C Bus %d"), i2cBus));
    html_TR();
    #endif // if FEATURE_I2C_MULTIPLE
    #if FEATURE_I2CMULTIPLEXER
    if (isI2CMultiplexerEnabled(i2cBus)) {
      html_table_header(F("I2C multiplexer channel"));
    }
    #endif // if FEATURE_I2CMULTIPLEXER
    html_table_header(F("I2C Addresses found"));
    html_table_header(F("Supported devices"));

    if (Settings.isI2CEnabled(i2cBus)) {
      I2CSelect_Max100kHz_ClockSpeed(i2cBus);  // Scan bus using low speed
      #if FEATURE_I2CMULTIPLEXER
      i2c_addresses_t mainBusDevices;
      mainBusDevices.resize(128);
      for (int i = 0; i < 128; i++) {
        mainBusDevices[i] = false;
      }
      int8_t Multiplexer_Addr = Settings.I2C_Multiplexer_Addr;
      #if FEATURE_I2C_MULTIPLE
      if (i2cBus == 1) {
        Multiplexer_Addr = Settings.I2C2_Multiplexer_Addr;
      }
      #if FEATURE_I2C_INTERFACE_3
      else if (i2cBus == 2) {
        Multiplexer_Addr = Settings.I2C3_Multiplexer_Addr;
      }
      #endif // if FEATURE_I2C_INTERFACE_3
      #endif // if FEATURE_I2C_MULTIPLE
      nDevices = scanI2CbusForDevices(i2cBus, Multiplexer_Addr, -1, nDevices, mainBusDevices); // Channel -1 = standard I2C bus
      #else // if FEATURE_I2CMULTIPLEXER
      nDevices = scanI2CbusForDevices(i2cBus, -1, -1, nDevices); // Standard scan
      #endif // if FEATURE_I2CMULTIPLEXER

      #if FEATURE_I2CMULTIPLEXER
      if (isI2CMultiplexerEnabled(i2cBus)) {
        uint8_t mux_max = I2CMultiplexerMaxChannels(i2cBus);
        for (int8_t channel = 0; channel < mux_max; channel++) {
          I2CMultiplexerSelect(i2cBus, channel);
          nDevices += scanI2CbusForDevices(i2cBus, Multiplexer_Addr, channel, nDevices, mainBusDevices);
        }
        I2CMultiplexerOff(i2cBus);
      }
      #endif // if FEATURE_I2CMULTIPLEXER
      if (nDevices == 0) {
        html_TR_TD();
        addHtml(F("No I2C devices found"));
      }
      nDevices = 0; // Reset for next interface
    } else {
      html_TR_TD();
      addHtml(strformat(F("I2C pins not configured for bus %d"), i2cBus));
    }
    html_end_table();
    I2CSelectHighClockSpeed(0);   // By default the bus is in standard speed

  }
  sendHeadandTail_stdtemplate(_TAIL);
  TXBuffer.endStream();
}
#endif // WEBSERVER_I2C_SCANNER

#include "../WebServer/Chart_JS.h"

#if FEATURE_CHART_JS

# include "../Helpers/StringConverter.h"
# include "../WebServer/HTML_wrappers.h"
# include "../WebServer/JSON.h"

void add_ChartJS_array(int          valueCount,
                       const String array[])
{
  for (int i = 0; i < valueCount; ++i) {
    if (i != 0) {
      addHtml(',', '\n');
    }
    addHtml(to_json_value(array[i]));
  }
}

void add_ChartJS_array(int          valueCount,
                       const float  array[],
                       unsigned int nrDecimals)
{
  for (int i = 0; i < valueCount; ++i) {
    if (i != 0) {
      addHtml(',', '\n');
    }
    addHtmlFloat_NaN_toNull(array[i], nrDecimals);
  }
}

void add_ChartJS_array(int       valueCount,
                       const int array[])
{
  for (int i = 0; i < valueCount; ++i) {
    if (i != 0) {
      addHtml(',', '\n');
    }
    addHtmlInt(array[i]);
  }
}

void add_ChartJS_chart_header(
  const __FlashStringHelper *chartType,
  const __FlashStringHelper *id,
  const ChartJS_title      & chartTitle,
  int                        width,
  int                        height,
  const String             & options,
  bool                       enableZoom,
  size_t                     nrSamples,
  bool                       onlyJSON)
{
  add_ChartJS_chart_header(
    chartType,
    String(id),
    chartTitle,
    width,
    height,
    options,
    enableZoom,
    nrSamples,
    onlyJSON);
}

void add_ChartJS_chart_header(
  const __FlashStringHelper *chartType,
  const String             & id,
  const ChartJS_title      & chartTitle,
  int                        width,
  int                        height,
  const String             & options,
  bool                       enableZoom,
  size_t                     nrSamples,
  bool                       onlyJSON)
{
  if (!onlyJSON) {
    addHtml(F("<canvas"));
    addHtmlAttribute(F("id"),     id);
    addHtmlAttribute(F("width"),  width);
    addHtmlAttribute(F("height"), height);
    addHtml(F("></canvas>"));
    const char *id_c_str = id.c_str();
    addHtml(strformat(
              F("<script>"
                "const %sc=document.getElementById('%s');"
                "const my_%s_C=new Chart(%sc,"),
              id_c_str,
              id_c_str,
              id_c_str,
              id_c_str));
  }
  String plugins;

  if (enableZoom) {
    plugins = F(
      "\"zoom\":"
      "{\"limits\":{"
      "\"x\":{\"min\":\"original\",\"max\":\"original\",\"minRange\":1000}},"
      "\"pan\":{\"enabled\":true,\"mode\":\"x\",\"modifierKey\":\"ctrl\"},"
      "\"zoom\":{"
      "\"wheel\":{\"enabled\":true},"
      "\"drag\":{\"enabled\":true},"
      "\"pinch\":{\"enabled\":true},"
      "\"mode\":\"x\"}}"
      );
  }
  add_ChartJS_chart_JSON_header(
    chartType,
    plugins,
    chartTitle,
    options,
    nrSamples);
}

void add_ChartJS_chart_JSON_header(
  const __FlashStringHelper *chartType,
  const String             & plugins,
  const ChartJS_title      & chartTitle,
  const String             & options,
  size_t                     nrSamples)
{
  addHtml(F("{\"type\":\""));
  addHtml(chartType);
  addHtml(F("\",\"options\":{"
            "\"responsive\":false,\"plugins\":{"));
  addHtml(F("\"legend\":{"
            "\"position\":\"top\""
            "},\"title\":"));
  addHtml(chartTitle.toString());

  if (plugins.length() > 0) {
    addHtml(',');
  }
  addHtml(plugins);
  addHtml('}'); // end plugins

  if (nrSamples >= 60) {
    // Default point radius = 3
    // Typically when having > 64 samples, these points become really cluttered
    // Thus it is best to reduce their radius.
    const float radius = (plugins.length() > 0) ? 2.5f : 2.0f;
    addHtml(strformat(
      F(",\"elements\":{\"point\":{\"radius\":%.1f}}"),
      radius));
  }

  if (!options.isEmpty()) {
    addHtml(',', '\n');
    addHtml(options);
  }

  addHtml(F("}," // end options
            "\n\"data\":{"));
}

void add_ChartJS_chart_labels(
  int       valueCount,
  const int labels[])
{
  addHtml(F("\n\"labels\":["));
  add_ChartJS_array(valueCount, labels);
  addHtml(F("],\n\"datasets\":["));
}

void add_ChartJS_chart_labels(
  int          valueCount,
  const String labels[])
{
  addHtml(F("\n\"labels\":["));
  add_ChartJS_array(valueCount, labels);
  addHtml(F("],\n\"datasets\":["));
}

void add_ChartJS_scatter_data_point(float x, float y, int nrDecimals)
{
  addHtml(F("{\"x\":"));
  addHtmlFloat_NaN_toNull(x, nrDecimals);
  addHtml(F(",\"y\":"));
  addHtmlFloat_NaN_toNull(y, nrDecimals);
  addHtml('}', ',');
}

void add_ChartJS_dataset(
  const ChartJS_dataset_config& config,
  const float                   values[],
  int                           valueCount,
  unsigned int                  nrDecimals,
  const String                & options)
{
  add_ChartJS_dataset_header(config);
  add_ChartJS_array(valueCount, values, nrDecimals);
  add_ChartJS_dataset_footer(options);
}

void add_ChartJS_dataset_header(const ChartJS_dataset_config& config)
{
  addHtml('{');

  if (!config.label.isEmpty()) {
    stream_to_json_object_value(F("label"), config.label);
    addHtml(',');
  }

  if (!config.color.isEmpty()) {
    stream_to_json_object_value(F("backgroundColor"), config.color);
    addHtml(',');
    stream_to_json_object_value(F("borderColor"), config.color);
    addHtml(',');
  }

  if (!config.axisID.isEmpty()) {
    stream_to_json_object_value(F("yAxisID"), config.axisID);
    addHtml(',');
  }

  if (config.hidden || config.displayConfig.showHidden()) {
    addHtml(F("\"hidden\":true,"));
  }

  addHtml(F("\n\"data\":[\n"));
}

void add_ChartJS_dataset_footer(const String& options) {
  addHtml(']');

  if (!options.isEmpty()) {
    addHtml(',', '\n');
    addHtml(options);
  }

  addHtml('}', '\n');
}

void add_ChartJS_chart_footer(bool onlyJSON) {
  addHtml(F("]}}"));

  if (!onlyJSON) {
    addHtml(F(");</script>"));
  }
}

#endif // if FEATURE_CHART_JS

#include "../WebServer/Chart_JS_title.h"


#if FEATURE_CHART_JS

# include "../Helpers/StringConverter.h"

ChartJS_title::ChartJS_title()
  : align(Align::Center) {}

ChartJS_title::ChartJS_title(const __FlashStringHelper *titleText, Align alignment)
  : text(titleText), align(alignment)
{}

ChartJS_title::ChartJS_title(const String& titleText, Align alignment)
  : text(titleText), align(alignment)
{}

String ChartJS_title::toString() const {
  if (text.isEmpty()) {
    return F("{\"display\": false}");
  }

  const String alignStr =
    align == Align::Start ? F("start") :
    align == Align::End ? F("end") : F("center");

  String colorStr;

  if (!color.isEmpty()) {
    colorStr = strformat(F(",\"color\":\"%s\""), color.c_str());
  }

  return strformat(
    F("{\"display\": true,\"align\":\"%s\",\"text\":\"%s\"%s}"),
    alignStr.c_str(),
    text.c_str(),
    colorStr.c_str());
}

#endif // if FEATURE_CHART_JS

#include "../WebServer/Metrics.h"
#include "../WebServer/ESPEasy_WebServer.h"
#include "../../ESPEasy-Globals.h"
#include "../Commands/Diagnostic.h"
#include "../ESPEasyCore/ESPEasyNetwork.h"
#include "../ESPEasyCore/ESPEasyWifi.h"
#include "../../_Plugin_Helper.h"
#include "../Helpers/ESPEasyStatistics.h"
#include "../Static/WebStaticData.h"

#ifdef WEBSERVER_METRICS

# ifdef ESP32
#  include <esp_partition.h>
# endif // ifdef ESP32

void handle_metrics() {
  TXBuffer.startStream(F("text/plain"), F("*"));
  const __FlashStringHelper *prefixHELP = F("# HELP espeasy_");
  const __FlashStringHelper *prefixTYPE = F("# TYPE espeasy_");

  // uptime
  addHtml(prefixHELP);
  addHtml(F("uptime current device uptime in minutes\n"));
  addHtml(prefixTYPE);
  addHtml(F("uptime counter\n"));
  addHtml(F("espeasy_uptime "));
  addHtml(getValue(LabelType::UPTIME));
  addHtml('\n');

  // load
  addHtml(prefixHELP);
  addHtml(F("load device percentage load\n"));
  addHtml(prefixTYPE);
  addHtml(F("load gauge\n"));
  addHtml(F("espeasy_load "));
  addHtml(getValue(LabelType::LOAD_PCT));
  addHtml('\n');

  // Free RAM
  addHtml(prefixHELP);
  addHtml(F("free_ram device amount of RAM free in Bytes\n"));
  addHtml(prefixTYPE);
  addHtml(F("free_ram gauge\n"));
  addHtml(F("espeasy_free_ram "));
  addHtml(getValue(LabelType::FREE_MEM));
  addHtml('\n');

  // Free RAM
  addHtml(prefixHELP);
  addHtml(F("free_stack device amount of Stack free in Bytes\n"));
  addHtml(prefixTYPE);
  addHtml(F("free_stack gauge\n"));
  addHtml(F("espeasy_free_stack "));
  addHtml(getValue(LabelType::FREE_STACK));
  addHtml('\n');

  // Wifi strength
  addHtml(prefixHELP);
  addHtml(F("wifi_rssi Wifi connection Strength\n"));
  addHtml(prefixTYPE);
  addHtml(F("wifi_rssi gauge\n"));
  addHtml(F("espeasy_wifi_rssi "));
  addHtml(getValue(LabelType::WIFI_RSSI));
  addHtml('\n');

  // Wifi uptime
  addHtml(prefixHELP);
  addHtml(F("wifi_connected Time wifi has been connected in milliseconds\n"));
  addHtml(prefixTYPE);
  addHtml(F("wifi_connected counter\n"));
  addHtml(F("espeasy_wifi_connected "));
  addHtml(getValue(LabelType::CONNECTED_MSEC));
  addHtml('\n');

  // Wifi reconnects
  addHtml(prefixHELP);
  addHtml(F("wifi_reconnects Number of times Wifi has reconnected since boot\n"));
  addHtml(prefixTYPE);
  addHtml(F("wifi_reconnects counter\n"));
  addHtml(F("espeasy_wifi_reconnects "));
  addHtml(getValue(LabelType::NUMBER_RECONNECTS));
  addHtml('\n');

  # if FEATURE_INTERNAL_TEMPERATURE

  // CPU Temperature
  addHtml(prefixHELP);
  addHtml(F("cpu_temperature Level of CPU temperature in Celcius\n"));
  addHtml(prefixTYPE);
  addHtml(F("cpu_temperature gauge\n"));
  addHtml(F("espeasy_cpu_temperature "));
  addHtml(getValue(LabelType::INTERNAL_TEMPERATURE));
  addHtml('\n');
  # endif // if FEATURE_INTERNAL_TEMPERATURE

  // devices
  handle_metrics_devices();

  TXBuffer.endStream();
}

void handle_metrics_devices() {
  for (taskIndex_t x = 0; validTaskIndex(x); x++) {
    const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(x);
    const bool pluginID_set         = INVALID_PLUGIN_ID != Settings.getPluginID_for_task(x);

    if (pluginID_set) {
      if (Settings.TaskDeviceEnabled[x]) {
        String deviceName = getTaskDeviceName(x);

        if (deviceName.isEmpty()) { // Empty name, then use taskN
          deviceName  = F("task");
          deviceName += x + 1;
        }
        addHtml(F("# HELP espeasy_device_"));
        addHtml(deviceName);
        addHtml(F(" Values from connected device\n"));
        addHtml(F("# TYPE espeasy_device_"));
        addHtml(deviceName);
        addHtml(F(" gauge\n"));

        if (validDeviceIndex(DeviceIndex)) {
          String customValuesString;

          // const bool customValues = PluginCall(PLUGIN_WEBFORM_SHOW_VALUES, &TempEvent, customValuesString);
          const bool customValues = 0; // TODO: handle custom values

          if (!customValues) {
            const uint8_t valueCount = getValueCountForTask(x);
            struct EventStruct TempEvent(x);

            for (uint8_t varNr = 0; varNr < valueCount; varNr++) {
              if (validPluginID_fullcheck(Settings.getPluginID_for_task(x))) {
                addHtml(F("espeasy_device_"));
                addHtml(deviceName);
                addHtml(F("{valueName=\""));
                addHtml(Cache.getTaskDeviceValueName(x, varNr));
                addHtml(F("\"} "));
                const String value(formatUserVarNoCheck(&TempEvent, varNr));

                if (value.isEmpty()) {
                  addHtml('0'); // Return 0 for not-set values
                } else {
                  addHtml(value);
                }
                addHtml('\n');
              }
            }
          }
        }
      }
    }
  }
}

#endif // WEBSERVER_METRICS

#include "../WebServer/Chart_JS_scale.h"

#if FEATURE_CHART_JS

# include "../Helpers/StringConverter.h"

ChartJS_options_scale::ChartJS_options_scale(const String& id, const String& title)
  : axisID(id), position(Position::Left)
{
  // Set some proper defaults, based on the id string.
  if (axisID.startsWith(F("x"))) {
    position = Position::Bottom;

    // Make sure the X-axis is shown even when no dataset is selected.
    display = Display::True;
  }
  else if (axisID.indexOf(F("right")) != -1) {
    position = Position::Right;
  }
  axisTitle.text = title;
}

ChartJS_options_scale::ChartJS_options_scale(const PluginStats_Config_t& config, const String& title)
{
  const bool isLeft = config.isLeft();

  position = isLeft ? Position::Left : Position::Right;
  weight   = config.getAxisIndex();
  axisID   = strformat((isLeft ? F("y-left-%d") : F("y-right-%d")),
                       weight);
  axisTitle.text = title;
  display        = Display::Auto;
}

String ChartJS_options_scale::toString() const
{
  if (!axisID.isEmpty()) {
    // In JSON, boolean values do not need quotes
    const String displayStr =
      display == Display::Auto ? F("\"auto\"") :
      display == Display::True ? F("true") : F("false");

    String typeStr = scaleType;

    if (typeStr.isEmpty()) { typeStr = F("linear"); }

    String positionStr;

    switch (position) {
      case Position::Top:    positionStr = F("top"); break;
      case Position::Bottom: positionStr = F("bottom"); break;
      case Position::Right:  positionStr = F("right"); break;
      case Position::Center: positionStr = F("center"); break;
      case Position::Left:   positionStr = F("left"); break;
    }

    String extraOptions;
    if (typeStr.equalsIgnoreCase(F("time")) || typeStr.equalsIgnoreCase(F("timeseries"))) {
      // Make sure to use 24h time notation.
      extraOptions += F(",\"time\":{\"displayFormats\":{\"millisecond\":\"HH:mm:ss.SSS\",\"second\":\"HH:mm:ss\",\"minute\":\"HH:mm:ss\",\"hour\":\"HH:mm\",\"day\":\"dd-MMM\",\"month\":\"MMM-yyyy\",\"year\":\"yyyy\"},\"tooltipFormat\":\"yyyy-MM-dd HH:mm:ss\"}");
    }

    if (tickCount > 0) {
      extraOptions += strformat(F(",\"ticks\":{\"count\":%d}"), tickCount);
    }
    return strformat(
      F("\"%s\":{\"display\":%s,\"type\":\"%s\",\"position\":\"%s\",\"title\":%s,\"weight\":%d%s}"),
      axisID.c_str(),
      displayStr.c_str(),
      typeStr.c_str(),
      positionStr.c_str(),
      axisTitle.toString().c_str(),
      weight,
      extraOptions.c_str());
  }
  return EMPTY_STRING;
}

bool ChartJS_options_scale::is_Y_axis() const
{
  return position == Position::Left ||
         position == Position::Right;
}

ChartJS_options_scales::ChartJS_options_scales() {}

void ChartJS_options_scales::add(const ChartJS_options_scale& scale)
{
  for (auto it = _scales.begin(); it != _scales.end(); ++it) {
    if (it->axisID.equals(scale.axisID)) {
      // Found an axis with same ID.
      // Combine labels and don't create a new one.
      if (!scale.axisTitle.text.isEmpty()) {
        if (!it->axisTitle.text.isEmpty()) {
          it->axisTitle.color.clear();
          it->axisTitle.text += F(" / ");
        }
        it->axisTitle.text += scale.axisTitle.text;
      }
      return;
    }
  }
  _scales.push_back(scale);
}

void ChartJS_options_scales::update_Yaxis_TickCount()
{
  // For single Y-axis, use a dynamic tick count based on the data.
  // For multiple Y-axis, we want 10 intervals, thus 11 ticks.
  const int newTickCount = (nr_Y_scales() <= 1) ? 0 : 11;

  for (auto it = _scales.begin(); it != _scales.end(); ++it) {
    if (it->is_Y_axis()) {
      it->tickCount = newTickCount;
    }
  }
}

String ChartJS_options_scales::toString() const
{
  if (_scales.empty()) { return EMPTY_STRING; }

  String res   = F("\"scales\":{");
  bool   first = true;

  for (auto it = _scales.begin(); it != _scales.end(); ++it) {
    const String scale_str = it->toString();

    if (!scale_str.isEmpty()) {
      if (!first) {
        res += ',';
      }
      first = false;
      res  += '\n';
      res  += scale_str;
    }
  }
  res += '}';
  return res;
}

size_t ChartJS_options_scales::nr_Y_scales() const
{
  size_t count{};

  for (auto it = _scales.begin(); it != _scales.end(); ++it) {
    if (it->is_Y_axis()) { ++count; }
  }

  return count;
}

#endif // if FEATURE_CHART_JS

#include "../WebServer/common.h"

void sendHeader(const String& name, const String& value, bool first)
{
    web_server.sendHeader(name, value, first);
}

void sendHeader(const __FlashStringHelper * name, const String& value, bool first)
{
    web_server.sendHeader(name, value, first);
}

void sendHeader(const __FlashStringHelper * name, const __FlashStringHelper * value, bool first)
{
    web_server.sendHeader(name, value, first);
}


#ifdef ESP8266
const String& webArg(const __FlashStringHelper * arg)
{
  return web_server.arg(String(arg));
}

const String& webArg(const String& arg)
{
  return web_server.arg(arg);
}

const String& webArg(int i)
{
  return web_server.arg(i);
}
#endif

#ifdef ESP32
String webArg(const __FlashStringHelper * arg)
{
  return web_server.arg(String(arg));
}

String webArg(const String& arg)
{
  return web_server.arg(arg);
}

String webArg(int i)
{
  return web_server.arg(i);
}
#endif

bool hasArg(const __FlashStringHelper * arg)
{
  return web_server.hasArg(arg);
}

bool hasArg(const String& arg)
{
  return web_server.hasArg(arg);
}

#include "../WebServer/SysVarPage.h"


#ifdef WEBSERVER_SYSVARS

# include "../WebServer/ESPEasy_WebServer.h"
# include "../WebServer/AccessControl.h"
# include "../WebServer/Markup.h"
# include "../WebServer/Markup_Forms.h"
# include "../WebServer/HTML_wrappers.h"

# include "../Globals/RuntimeData.h"

# include "../Helpers/Numerical.h"
# include "../Helpers/StringConverter.h"
# if FEATURE_STRING_VARIABLES
#  include "../Helpers/StringParser.h"
# endif // if
# include "../Helpers/SystemVariables.h"


// ********************************************************************************
// Web Interface sysvars showing all system vars and their value.
// ********************************************************************************
void addSysVar_enum_html(SystemVariables::Enum enumval) {
  addSysVar_html(SystemVariables::toString(enumval), false);
}

void addSysVar_enum_html(const SystemVariables::Enum enumval[], size_t nrElements) {
  for (size_t i = 0; i < nrElements; ++i) {
    addSysVar_enum_html(enumval[i]);
  }
}

void handle_sysvars() {
  # ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_sysvars"));
  # endif // ifndef BUILD_NO_RAM_TRACKER

  if (!isLoggedIn()) { return; }
  TXBuffer.startStream();
  sendHeadandTail_stdtemplate(_HEAD);

  html_BR();
  addHtml(F("<p>This page may load slow.<BR>Do not load too often, since it may affect performance of the node.</p>"));
  html_BR();

  // the table header
  html_table_class_normal();
  html_TR();
  html_table_header(F("System Variables"));
  html_table_header(F("Normal"));
  html_table_header(F("URL encoded"), F("RTDReference/SystemVariable.html"), 0);

  addTableSeparator(F("Custom Variables"), 3, 3);

  if (customFloatVar.empty()) {
    html_TR_TD();
    addHtml(F("No variables set"));
    html_TD();
    html_TD();
  } else {
    for (auto it = customFloatVar.begin(); it != customFloatVar.end(); ++it) {
      NumericalType detectedType;
      bool isv_ = true;
      if (getNumerical(it->first, NumericalType::HexadecimalUInt, detectedType).length() > 0) {
        isv_ = detectedType != NumericalType::Integer;
      }
      addSysVar_html(strformat(F("%%%s%s%%"), FsP(isv_ ? F("v_") : F("v")), it->first.c_str()), false);
    }
  }

  # if FEATURE_STRING_VARIABLES
  addTableSeparator(F("String Variables"), 3, 3);

  if (customStringVar.empty()) {
    html_TR_TD();
    addHtml(F("No string variables set"));
    html_TD();
    html_TD();
  } else {
    for (auto it = customStringVar.begin(); it != customStringVar.end(); ++it) {
      addSysVar_html(strformat(F("[str#%s]"), it->first.c_str()), false);
    }
  }
  # endif // if FEATURE_STRING_VARIABLES

  addTableSeparator(F("Constants"), 3, 3);
  {
    const SystemVariables::Enum vars[] = {
      SystemVariables::CR,
      SystemVariables::LF,
      SystemVariables::SPACE,
      SystemVariables::S_CR,
      SystemVariables::S_LF,
      #ifndef LIMIT_BUILD_SIZE
      SystemVariables::S_E,
      SystemVariables::S_PI,
      #endif // ifndef LIMIT_BUILD_SIZE
    };
    addSysVar_enum_html(vars, NR_ELEMENTS(vars));
  }

  addTableSeparator(F("Network"), 3, 3);
  {
    const SystemVariables::Enum vars[] = {
      SystemVariables::MAC,
      SystemVariables::MAC_INT,
      SystemVariables::IP,
      SystemVariables::IP4,
      SystemVariables::SUBNET,
      SystemVariables::GATEWAY,
      SystemVariables::DNS,
      SystemVariables::DNS_1,
      SystemVariables::DNS_2,
# if FEATURE_USE_IPV6
      SystemVariables::IP6_LOCAL,
# endif // if FEATURE_USE_IPV6
      SystemVariables::RSSI,
      SystemVariables::SSID,
      SystemVariables::BSSID,
      SystemVariables::WI_CH
    };
    addSysVar_enum_html(vars, NR_ELEMENTS(vars));
  }
  # if FEATURE_ETHERNET
  addTableSeparator(F("Ethernet"), 3, 3);
  {
    const SystemVariables::Enum vars[] = {
      SystemVariables::ETHWIFIMODE,
      SystemVariables::ETHCONNECTED,
      SystemVariables::ETHDUPLEX,
      SystemVariables::ETHSPEED,
      SystemVariables::ETHSTATE,
      SystemVariables::ETHSPEEDSTATE,
    };
    addSysVar_enum_html(vars, NR_ELEMENTS(vars));
  }
  # endif // if FEATURE_ETHERNET

  addTableSeparator(F("System"), 3, 3);
  {
    const SystemVariables::Enum vars[] = {
      SystemVariables::UNIT_sysvar,
  # if FEATURE_ZEROFILLED_UNITNUMBER
      SystemVariables::UNIT_0_sysvar,
  # endif // FEATURE_ZEROFILLED_UNITNUMBER
      SystemVariables::SYSLOAD,
      SystemVariables::SYSHEAP,
      SystemVariables::SYSSTACK,
      SystemVariables::SYSNAME,
# if FEATURE_ADC_VCC
      SystemVariables::VCC,
# endif   // if FEATURE_ADC_VCC
  # if FEATURE_INTERNAL_TEMPERATURE
      SystemVariables::INTERNAL_TEMPERATURE,
  # endif // if FEATURE_INTERNAL_TEMPERATURE
      SystemVariables::BOOT_CAUSE,
      SystemVariables::ISLIMITED_BUILD,
      SystemVariables::ISVAR_DOUBLE,
    };
    addSysVar_enum_html(vars, NR_ELEMENTS(vars));
  }

  addTableSeparator(F("Services Status"), 3, 3);
  {
    const SystemVariables::Enum vars[] = {
      SystemVariables::ISWIFI,
      SystemVariables::ISNTP,
# if FEATURE_MQTT
      SystemVariables::ISMQTT,
# endif // if FEATURE_MQTT
# ifdef USES_P037
      SystemVariables::ISMQTTIMP,
# endif // USES_P037
    };
    addSysVar_enum_html(vars, NR_ELEMENTS(vars));
  }

  addTableSeparator(F("Time"), 3, 3);
  {
    const SystemVariables::Enum vars[] = {
      SystemVariables::LCLTIME,
      SystemVariables::LCLTIME_AM,
      SystemVariables::SYSTM_HM,
      SystemVariables::SYSTM_HM_0,
      SystemVariables::SYSTM_HM_SP,
      SystemVariables::SYSTM_HM_AM,
      SystemVariables::SYSTM_HM_AM_0,
      SystemVariables::SYSTM_HM_AM_SP,
      SystemVariables::SYSTIME,
      SystemVariables::SYSTIME_AM,
      SystemVariables::SYSTIME_AM_0,
      SystemVariables::SYSTIME_AM_SP,
      SystemVariables::SYSBUILD_DATE,
      SystemVariables::SYSBUILD_TIME,
      SystemVariables::SYSBUILD_FILENAME,
      SystemVariables::SYSBUILD_DESCR,
      SystemVariables::SYSBUILD_GIT
    };
    addSysVar_enum_html(vars, NR_ELEMENTS(vars));
  }


  addTableSeparator(F("System Time"), 3, 3);
  {
    const SystemVariables::Enum vars[] = {
      SystemVariables::UPTIME,
      SystemVariables::UPTIME_MS,
      SystemVariables::UNIXTIME,
      # ifndef LIMIT_BUILD_SIZE
      SystemVariables::UNIXTIME_LCL,
      # endif // ifndef LIMIT_BUILD_SIZE
      SystemVariables::UNIXDAY,
      SystemVariables::UNIXDAY_SEC,
    };
    addSysVar_enum_html(vars, NR_ELEMENTS(vars));
  }
  {
    const __FlashStringHelper *vars[] = {
      F("%sysyear%  // %sysyear_0%"),
      F("%sysyears%"),
      F("%sysmonth% // %sysmonth_0%")
    };

    for (unsigned int i = 0; i < NR_ELEMENTS(vars); ++i) {
      addSysVar_html(vars[i]);
    }
  }
  addSysVar_enum_html(SystemVariables::SYSMONTH_S);
  {
    const __FlashStringHelper *vars[] = {
      F("%sysday%   // %sysday_0%"),
      F("%syshour%  // %syshour_0%"),
      F("%sysmin%   // %sysmin_0%"),
      F("%syssec%   // %syssec_0%")
    };

    for (unsigned int i = 0; i < NR_ELEMENTS(vars); ++i) {
      addSysVar_html(vars[i]);
    }
  }

  {
    const SystemVariables::Enum vars[] = {
      SystemVariables::SYSSEC_D,
      SystemVariables::SYSWEEKDAY,
      SystemVariables::SYSWEEKDAY_S,
      SystemVariables::SYSTZOFFSET,
      # ifndef LIMIT_BUILD_SIZE
      SystemVariables::SYSTZOFFSET_S,
      # endif // ifndef LIMIT_BUILD_SIZE
    };
    addSysVar_enum_html(vars, NR_ELEMENTS(vars));
  }

  addTableSeparator(F("Sunrise/Sunset"), 3, 3);
  {
    const __FlashStringHelper *vars[] = {
      F("%sunset%"),
      F("%sunset-1h%"),
      F("%sunrise%"),
      F("%sunrise+10m%"),
      F("%s_sunset%"),
      F("%s_sunrise%"),
      F("%m_sunset%"),
      F("%m_sunrise%"),
      #if FEATURE_LAT_LONG_VAR_CMD
      F("%latitude%"),
      F("%longitude%"),
      #endif // if FEATURE_LAT_LONG_VAR_CMD
    };

    for (unsigned int i = 0; i < NR_ELEMENTS(vars); ++i) {
      addSysVar_html(vars[i]);
    }
  }

  addTableSeparator(F("ESP Board"), 3, 3);
  {
    const SystemVariables::Enum vars[] = {
      SystemVariables::ESP_CHIP_ID,
      SystemVariables::ESP_CHIP_FREQ,
      SystemVariables::ESP_CHIP_MODEL,
      SystemVariables::ESP_CHIP_REVISION,
      SystemVariables::ESP_CHIP_CORES,
      SystemVariables::BOARD_NAME,
    };
    addSysVar_enum_html(vars, NR_ELEMENTS(vars));
  }

  addTableSeparator(F("Storage"), 3, 3);
  {
    const SystemVariables::Enum vars[] = {
      SystemVariables::FLASH_FREQ,
      SystemVariables::FLASH_SIZE,
      SystemVariables::FLASH_CHIP_VENDOR,
      SystemVariables::FLASH_CHIP_MODEL,
      SystemVariables::FS_SIZE,
      SystemVariables::FS_FREE,
    };
    addSysVar_enum_html(vars, NR_ELEMENTS(vars));
  }

# ifndef BUILD_NO_SPECIAL_CHARACTERS_STRINGCONVERTER
  {
    addTableSeparator(F("Special Characters"), 3, 2);
    const __FlashStringHelper *MathSymbols[] = {
      // addTableSeparator(F("Degree"),             3, 3);
      F("{D}"),
      F("&deg;"),

      // addTableSeparator(F("Angle quotes"), 3, 3);
      F("{<<}"),
      F("&laquo;"),

      // addFormSeparator(3);
      F("{>>}"),
      F("&raquo;"),

      // addTableSeparator(F("Greek letter Mu"), 3, 3);
      F("{u}"),
      F("&micro;"),

      // addTableSeparator(F("Currency"), 3, 3);
      F("{E}"),
      F("&euro;"),

      // addFormSeparator(3);
      F("{Y}"),
      F("&yen;"),

      // addFormSeparator(3);
      F("{P}"),
      F("&pound;"),

      // addFormSeparator(3);
      F("{c}"),
      F("&cent;"),

      // addTableSeparator(F("Math symbols"), 3, 3);
      F("{^1}"),
      F("&sup1;"),

      // addFormSeparator(3);
      F("{^2}"),
      F("&sup2;"),

      // addFormSeparator(3);
      F("{^3}"),
      F("&sup3;"),

      // addFormSeparator(3);
      F("{1_4}"),
      F("&frac14;"),

      // addFormSeparator(3);
      F("{1_2}"),
      F("&frac12;"),

      // addFormSeparator(3);
      F("{3_4}"),
      F("&frac34;"),

      // addFormSeparator(3);
      F("{+-}"),
      F("&plusmn;"),

      // addFormSeparator(3);
      F("{x}"),
      F("&times;"),

      // addFormSeparator(3);
      F("{..}"),
      F("&divide;"),
    };

    constexpr unsigned int nrGroups = NR_ELEMENTS(MathSymbols) >> 1;

    for (unsigned int i = 0; i < nrGroups; ++i) {
      switch (i) {
        case 0: addTableSeparator(F("Degree"),         3, 3); break;
        case 1: addTableSeparator(F("Angle quotes"),   3, 3); break;
        case 3: addTableSeparator(F("Greek letter Mu"), 3, 3); break;
        case 4: addTableSeparator(F("Currency"),       3, 3); break;
        case 8: addTableSeparator(F("Math symbols"),   3, 3); break;
        default: addFormSeparator(3); break;
      }
      addSysVar_html_specialChar(MathSymbols[2 * i]);
      addSysVar_html_specialChar(MathSymbols[(2 * i) + 1]);
    }
  }
# endif // ifndef BUILD_NO_SPECIAL_CHARACTERS_STRINGCONVERTER
  {
    addTableSeparator(F("Standard Conversions"), 3, 3);

    const __FlashStringHelper *StdConversions[] = {
      F("Wind Dir.:    %c_w_dir%(123.4)"),
      F("{D}C to {D}F: %c_c2f%(20.4)"),
      F("m/s to Bft:   %c_ms2Bft%(5.1)"),
      F("Dew point(T,H): %c_dew_th%(18.6,67)"),
      F("Altitude(air,sea): %c_alt_pres_sea%(850,1000)"),
      F("PressureElevation(air,alt): %c_sea_pres_alt%(850,1350.03)"),

      F(""), // addFormSeparator(3,
      F("cm to imperial: %c_cm2imp%(190)"),
      F("mm to imperial: %c_mm2imp%(1900)"),

      #ifndef LIMIT_BUILD_SIZE
      F(""), // addFormSeparator(3,
      F("Degrees to radians: %c_d2r%(22)"),
      F("Radians to degrees: %c_r2d%(0.357)"),
      #endif // ifndef LIMIT_BUILD_SIZE

      F(""), // addFormSeparator(3,
      F("Mins to days: %c_m2day%(1900)"),
      F("Mins to dh:   %c_m2dh%(1900)"),
      F("Mins to dhm:  %c_m2dhm%(1900)"),
      F("Mins to hcm:  %c_m2hcm%(482)"),
      F("Secs to dhms: %c_s2dhms%(100000)"),
# if FEATURE_STRING_VARIABLES
      F("Timestamp to date/time: %c_ts2date%(%unixtime_lcl%)"),
      F("Timestamp to date/time am/pm: %c_ts2date%(%unixtime_lcl%,1)"),
      F("Timestamp to weekday: %c_ts2wday%(%unixtime_lcl%)"),
      F("Timestamp to ISO date/time: %c_ts2isodate%(%unixtime_lcl%)"),
      F("Timestamp to ISO date/time/offset: %c_ts2isodate%(%unixtime_lcl%,1)"),
# endif // if FEATURE_STRING_VARIABLES


      F(""), // addFormSeparator(3,
# ifndef LIMIT_BUILD_SIZE
      F("Random: %c_random%(0, 100)"),
# endif // ifndef LIMIT_BUILD_SIZE
      F("To HEX: %c_2hex%(100000) or: %c_2hex%(100000,6)"),

# if FEATURE_ESPEASY_P2P
      F(""), // addFormSeparator(3,
      F("Unit to IP: %c_u2ip%(%unit%, 2)"),
      F("Unit to Name: %c_uname%(%unit%)"),
      F("Unit to Age: %c_uage%(%unit%)"),
      F("Unit to Build: %c_ubuild%(%unit%)"),
      F("Unit to Build-string: %c_ubuildstr%(%unit%)"),
      F("Unit to Load: %c_uload%(%unit%)"),
      F("Unit to ESP-type: %c_utype%(%unit%)"),
      F("Unit to ESP-type-string: %c_utypestr%(%unit%)"),
# endif // if FEATURE_ESPEASY_P2P
# if FEATURE_STRING_VARIABLES
      F(""), // addFormSeparator(3,
      F("Check if numeric value (test:'123'): %c_isnum%(test)"),
      F("Format (testf:'Out $6.2f M$g'): %c_strf%(testf,123.45,2)"),
# endif // if FEATURE_STRING_VARIABLES
    };

# if FEATURE_STRING_VARIABLES
    const bool has_test  = hasCustomStringVar(F("test"));
    const bool has_testf = hasCustomStringVar(F("testf"));

    // Save current values
    String test, testf;

    if (has_test) {
      test = getCustomStringVar(F("test"));
    }

    if (has_testf) {
      testf = getCustomStringVar(F("testf"));
    }

    setCustomStringVar(F("test"),  F("123"));
    setCustomStringVar(F("testf"), F("Out $6.2f M$g"));
# endif // if FEATURE_STRING_VARIABLES
    constexpr uint16_t nrStdConv = NR_ELEMENTS(StdConversions);

    for (uint16_t i = 0; i < nrStdConv; ++i) {
      const String s(StdConversions[i]);

      if (s.isEmpty()) { addFormSeparator(3); }
      else { addSysVar_html(s); }
    }
# if FEATURE_STRING_VARIABLES

    // Restore values
    if (has_test) {
      setCustomStringVar(F("test"), test);
    }
    else {
      clearCustomStringVar(F("test"));
    }

    if (has_testf) {
      setCustomStringVar(F("testf"), testf);
    }
    else {
      clearCustomStringVar(F("testf"));
    }

# endif // if FEATURE_STRING_VARIABLES
  }
  html_end_table();
  html_end_form();
  sendHeadandTail_stdtemplate(_TAIL);
  TXBuffer.endStream();
}

void addSysVar_html(const __FlashStringHelper *input) {
  addSysVar_html(String(input), false);
}

void addSysVar_html_specialChar(const __FlashStringHelper *input) {
  addSysVar_html(String(input), true);
}

void addSysVar_html(const String& input) {
  addSysVar_html(input, false);
}

void addSysVar_html(String input, bool isSpecialChar) {
  // Make deepcopy for replacement, so parameter is a copy, not a const reference

  html_TR_TD();
  addHtml(F("<pre>")); // Make monospaced (<tt> tag?)
  addHtml(input);
  addHtml(F("</pre>"));

  if (isSpecialChar) {
    parseSpecialCharacters(input, false);
    html_TD();
    addHtml(input);
    html_TD();
    addHtml(URLEncode(input));
  } else {
    const String orig_input(input);

    for (int i = 0; i < 2; ++i) {
      const bool URLencoded = i == 1;

      if (URLencoded) {
        input = orig_input;
      }

      parseSystemVariables(input, URLencoded);
      parseStandardConversions(input, URLencoded);
      # if FEATURE_STRING_VARIABLES
      input = parseTemplate_padded(input, 0, URLencoded);
      # endif // if FEATURE_STRING_VARIABLES

      html_TD();
      addHtml(input);
    }
  }
  delay(0);
}

#endif // WEBSERVER_SYSVARS

#include "../WebServer/TimingStats.h"

#if defined(WEBSERVER_TIMINGSTATS) && FEATURE_TIMING_STATS

#include "../WebServer/ESPEasy_WebServer.h"
#include "../WebServer/HTML_wrappers.h"
#include "../WebServer/Markup.h"
#include "../WebServer/Markup_Forms.h"

#include "../DataTypes/ESPEasy_plugin_functions.h"

#include "../Globals/ESPEasy_time.h"
#include "../Globals/RamTracker.h"

#include "../Globals/Device.h"

#include "../Helpers/_Plugin_init.h"


#define TIMING_STATS_THRESHOLD 100000

void handle_timingstats() {
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_timingstats"));
  #endif
  navMenuIndex = MENU_INDEX_TOOLS;
  TXBuffer.startStream();
  sendHeadandTail_stdtemplate(_HEAD);
  html_table_class_multirow();
  html_TR();
  {
    const __FlashStringHelper * headers[] = {
      F("Description"),
      F("Function"),
      F("#calls"),
      F("call/sec"),
      F("duty (%)"),
      F("min (ms)"),
      F("Avg (ms)"),
      F("max (ms)")};
    for (unsigned int i = 0; i < NR_ELEMENTS(headers); ++i) {
      html_table_header(headers[i]);
    }
  }


  const int32_t timeSinceLastReset = stream_timing_statistics(true);
  html_end_table();

  html_table_class_normal();
  const float timespan = timeSinceLastReset / 1000.0f;
  addFormHeader(F("Statistics"));
  addRowLabel(F("Start Period"));
  struct tm startPeriod = node_time.addSeconds(node_time.local_tm, -1.0f * timespan, true, true);
  addHtml(formatDateTimeString(startPeriod, '-', ':', ' ', false));
  addRowLabelValue(LabelType::LOCAL_TIME);
  addRowLabel(F("Time span"));
  addHtmlFloat(timespan);
  addHtml(F(" sec"));
  addRowLabel(F("*"));
  addHtml(F("Duty cycle based on average < 1 msec is highly unreliable"));
  html_end_table();

  sendHeadandTail_stdtemplate(_TAIL);
  TXBuffer.endStream();
}

// ********************************************************************************
// HTML table formatted timing statistics
// ********************************************************************************
void format_using_threshhold(uint32_t value) {
  float value_msec = value / 1000.0f;

  if (value > TIMING_STATS_THRESHOLD) {
    html_B(toString(value_msec, 3));
  } else {
    addHtmlFloat(value_msec, 3);
  }
}

void stream_html_timing_stats(const TimingStats& stats, int32_t timeSinceLastReset) {
  uint32_t minVal, maxVal;
  const uint32_t c = stats.getMinMax(minVal, maxVal);

  html_TD();
  addHtmlInt(c);
  html_TD();
  const float call_per_sec = static_cast<float>(c) / static_cast<float>(timeSinceLastReset) * 1000.0f;
  const float avg = stats.getAvg();
  addHtmlFloat(call_per_sec, 2);
  html_TD();
  {
    const float duty = (call_per_sec * avg / 10000.0f);
    String duty_str = toString(duty, 2);
    if (avg < 1000) {
      // Unreliable as average is below 1 msec
      duty_str += '*';
      html_I(duty_str);
    } else if (duty > 10.0f) {
      // Over 10% of the time
      html_B(duty_str);
    } else {
      addHtml(duty_str);
    }
  }

  html_TD();
  format_using_threshhold(minVal);
  html_TD();
  format_using_threshhold(avg);
  html_TD();
  format_using_threshhold(maxVal);
}

int32_t stream_timing_statistics(bool clearStats) {
  const int32_t timeSinceLastReset = timePassedSince(timingstats_last_reset);

  for (auto& x: pluginStats) {
    if (!x.second.isEmpty()) {
      const deviceIndex_t deviceIndex = deviceIndex_t::toDeviceIndex(x.first >> 8);

      if (validDeviceIndex(deviceIndex)) {
        if (x.second.thresholdExceeded(TIMING_STATS_THRESHOLD)) {
          html_TR_TD_highlight();
        } else {
          html_TR_TD();
        }
        {
          const pluginID_t pluginID = getPluginID_from_DeviceIndex(deviceIndex);
          addHtml(get_formatted_Plugin_number(pluginID));
          addHtml(' ');
          addHtml(getPluginNameFromDeviceIndex(deviceIndex));
        }
        html_TD();
        addHtml(getPluginFunctionName(x.first % 256));
        stream_html_timing_stats(x.second, timeSinceLastReset);
      }
    }
  }

  for (auto& x: controllerStats) {
    if (!x.second.isEmpty()) {
      const int ProtocolIndex = x.first >> 8;

      if (x.second.thresholdExceeded(TIMING_STATS_THRESHOLD)) {
        html_TR_TD_highlight();
      } else {
        html_TR_TD();
      }
      {
        addHtml(get_formatted_Controller_number(getCPluginID_from_ProtocolIndex(ProtocolIndex)));
        addHtml(' ');
        addHtml(getCPluginNameFromProtocolIndex(ProtocolIndex));
      }
      html_TD();
      addHtml(getCPluginCFunctionName(static_cast<CPlugin::Function>(x.first % 256)));
      stream_html_timing_stats(x.second, timeSinceLastReset);
    }
  }

  for (auto& x: miscStats) {
    if (!x.second.isEmpty()) {
      if (x.second.thresholdExceeded(TIMING_STATS_THRESHOLD)) {
        html_TR_TD_highlight();
      } else {
        html_TR_TD();
      }
      addHtml(getMiscStatsName(x.first));
      html_TD();
      stream_html_timing_stats(x.second, timeSinceLastReset);
    }
  }

  if (clearStats) {
    pluginStats.clear();
    controllerStats.clear();
    miscStats.clear();
    timingstats_last_reset = millis();
  }
  return timeSinceLastReset;
}

#endif // WEBSERVER_TIMINGSTATS

#include "../WebServer/PluginListPage.h"


#if FEATURE_PLUGIN_LIST

# include "../WebServer/ESPEasy_WebServer.h"
# include "../WebServer/HTML_wrappers.h"

# include "../_Plugin_Helper.h"
# if FEATURE_NOTIFIER
#  include "../Globals/NPlugins.h"
#  include "../DataTypes/NPluginID.h"
# endif // if FEATURE_NOTIFIER
# ifdef WEBSERVER_NETWORK
#  include "../ESPEasy/net/DataTypes/NetworkDriverIndex.h"
# endif // ifdef WEBSERVER_NETWORK
# include "../DataStructs/ProtocolStruct.h"
# include "../Helpers/_CPlugin_Helper.h"

void handle_pluginlist() {
  # ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_pluginlist"));
  # endif // ifndef BUILD_NO_RAM_TRACKER

  # if FEATURE_MQTT_TLS || FEATURE_HTTP_TLS
  const int colspan = 6;
  # else // if FEATURE_MQTT_TLS || FEATURE_HTTP_TLS
  const int colspan = 5;
  # endif // if FEATURE_MQTT_TLS || FEATURE_HTTP_TLS

  TXBuffer.startStream();
  sendHeadandTail_stdtemplate(_HEAD);

  // the table header
  html_table_class_normal();

  for (uint8_t p = 0; p < 2; ++p) { // Sorted by Description and by PluginID
    addTableSeparator(concat(F("Plugins sorted by "), 0 == p ? F("Description") : F("Plugin ID")), colspan, 3);
    html_TR();
    html_table_header(F("Plugin"),      80);
    html_table_header(F(""),            25);
    html_table_header(F("Description"), 800);
    html_table_header(F(""),            50);
    # if FEATURE_MQTT_TLS || FEATURE_HTTP_TLS
    html_table_header(F(""),            50);
    # endif // if FEATURE_MQTT_TLS || FEATURE_HTTP_TLS
    html_table_header(F(""));

    deviceIndex_t x;
    bool done = false;

    while (!done) {
      const deviceIndex_t deviceIndex = (0 == p) ? getDeviceIndex_sorted(x) : x;

      if (!validDeviceIndex(deviceIndex)) {
        done = true;
      } else {
        const pluginID_t pluginID = getPluginID_from_DeviceIndex(deviceIndex);

        if (validPluginID(pluginID)) {
          html_TR_TD();
          addHtml(get_formatted_Plugin_number(pluginID));
          html_TD();
          addRTDPluginButton(pluginID);
          html_TD();
          addHtml(getPluginNameFromDeviceIndex(deviceIndex));
          html_TD();
        }
      }
      ++x;
    }
  }

  {
    addTableSeparator(F("Controllers"), colspan, 3);
    html_TR();
    html_table_header(F("Controller"),  80);
    html_table_header(F(""),            25);
    html_table_header(F("Description"), 800);
    html_table_header(F("MQTT"),        50);
    # if FEATURE_MQTT_TLS || FEATURE_HTTP_TLS
    html_table_header(F("TLS"),         50);
    # endif // if FEATURE_MQTT_TLS || FEATURE_HTTP_TLS
    html_table_header(F(""));
    protocolIndex_t x;
    bool done = false;

    while (!done) {
      if (!validProtocolIndex(x)) {
        done = true;
      } else {
        const cpluginID_t cpluginID = getCPluginID_from_ProtocolIndex(x);
        const ProtocolStruct& proto = getProtocolStruct(x);

        if (validCPluginID(cpluginID)) {
          html_TR_TD();
          addHtml(get_formatted_Controller_number(cpluginID));
          html_TD();
          # ifndef LIMIT_BUILD_SIZE
          addRTDControllerButton(cpluginID);
          # endif // ifndef LIMIT_BUILD_SIZE
          html_TD();
          addHtml(getCPluginNameFromProtocolIndex(x));

          html_TD();

          if (proto.usesMQTT) {
            addEnabled(true);
          }
          # if FEATURE_MQTT_TLS || FEATURE_HTTP_TLS

          html_TD();

          if (proto.usesTLS) {
            addEnabled(true);
          }
          # endif // if FEATURE_MQTT_TLS || FEATURE_HTTP_TLS
          html_TD();
        }
      }
      ++x;
    }
  }

  # if FEATURE_NOTIFIER
  {
    addTableSeparator(F("Notifications"), colspan, 3);
    html_TR();
    html_table_header(F("Notifier"),    80);
    html_table_header(F(""),            25);
    html_table_header(F("Description"), 800);
    html_table_header(F(""),            50);
    #  if FEATURE_MQTT_TLS || FEATURE_HTTP_TLS
    html_table_header(F(""),            50);
    #  endif // if FEATURE_MQTT_TLS || FEATURE_HTTP_TLS
    html_table_header(F(""));

    for (uint8_t x = 0; x <= notificationCount; x++)
    {
      html_TR_TD();
      const npluginID_t plugin(Notification[x].Number);
      addHtml(plugin.toDisplayString());
      html_TD();
      addRTDHelpButton(strformat(F("Notify/%s.html"), plugin.toDisplayString().c_str()));
      html_TD();
      String NotificationName;
      NPlugin_ptr[x](NPlugin::Function::NPLUGIN_GET_DEVICENAME, 0, NotificationName);
      addHtml(NotificationName);
      html_TD();
    }
  }
  # endif // if FEATURE_NOTIFIER

  # ifdef WEBSERVER_NETWORK
  {
    addTableSeparator(F("Networks"), colspan, 3);
    html_TR();
    html_table_header(F("Network"),     80);
    html_table_header(F(""),            25);
    html_table_header(F("Description"), 800);
    html_table_header(F(""),            50);
    #  if FEATURE_MQTT_TLS || FEATURE_HTTP_TLS
    html_table_header(F(""),            50);
    #  endif // if FEATURE_MQTT_TLS || FEATURE_HTTP_TLS
    html_table_header(F(""));

    ESPEasy::net::networkDriverIndex_t tmpNetworkDriverIndex{};

    while (validNetworkDriverIndex(tmpNetworkDriverIndex))
    {
      html_TR_TD();
      const ESPEasy::net::nwpluginID_t number = getNWPluginID_from_NetworkDriverIndex(tmpNetworkDriverIndex);
      addHtml(number.toDisplayString());
      html_TD();
      #  ifndef LIMIT_BUILD_SIZE
      addRTDNetworkDriverButton(number);
      #  endif // ifndef LIMIT_BUILD_SIZE
      html_TD();
      addHtml(getNWPluginNameFromNetworkDriverIndex(tmpNetworkDriverIndex));
      html_TD();
      ++tmpNetworkDriverIndex;
    }
  }
  # endif // ifdef WEBSERVER_NETWORK

  html_end_table();
  html_end_form();
  sendHeadandTail_stdtemplate(_TAIL);
  TXBuffer.endStream();
}

#endif // if FEATURE_PLUGIN_LIST

#include "../WebServer/PinStates.h"

#include "../WebServer/ESPEasy_WebServer.h"
#include "../WebServer/HTML_wrappers.h"
#include "../WebServer/Markup.h"
#include "../WebServer/Markup_Forms.h"

#include "../DataStructs/PinMode.h"
#include "../Globals/GlobalMapPortStatus.h"
#include "../Helpers/PortStatus.h"

#ifdef ESP32
# include <esp32-hal-periman.h>
# include <driver/gpio.h>
# include <esp_private/esp_gpio_reserve.h>

// # include <hal/gpio_types.h>
 # include <hal/gpio_hal.h>

// #include <hal/gpio_ll.h>

# include <esp_private/io_mux.h>
#endif // ifdef ESP32

#ifdef WEBSERVER_NEW_UI


// ********************************************************************************
// Web Interface pin state list
// ********************************************************************************
void handle_pinstates_json() {
  # ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_pinstates"));
  # endif // ifndef BUILD_NO_RAM_TRACKER

  if (!isLoggedIn()) { return; }
  navMenuIndex = MENU_INDEX_TOOLS;
  TXBuffer.startJsonStream();

  bool first = true;
  addHtml('[');

  for (auto it = globalMapPortStatus.begin(); it != globalMapPortStatus.end(); ++it)
  {
    if (!first) {
      addHtml(',');
    } else {
      first = false;
    }
    addHtml('{');


    const uint16_t plugin = getPluginFromKey(it->first);
    const uint16_t port   = getPortFromKey(it->first);

    stream_next_json_object_value(F("plugin"),  plugin);
    stream_next_json_object_value(F("port"),    port);
    stream_next_json_object_value(F("state"),   it->second.state);
    stream_next_json_object_value(F("task"),    it->second.task);
    stream_next_json_object_value(F("monitor"), it->second.monitor);
    stream_next_json_object_value(F("command"), it->second.command);
    stream_last_json_object_value(F("init"), it->second.init);
  }

  addHtml(']');

  TXBuffer.endStream();
}

#endif // WEBSERVER_NEW_UI

#ifdef WEBSERVER_PINSTATES

void handle_pinstates() {
  # ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_pinstates"));
  # endif // ifndef BUILD_NO_RAM_TRACKER

  if (!isLoggedIn()) { return; }
  navMenuIndex = MENU_INDEX_TOOLS;
  TXBuffer.startStream();
  sendHeadandTail_stdtemplate(_HEAD);

  # ifdef ESP32
  html_BR();
  addFormHeader(F("Pin states"));
  # endif // ifdef ESP32

  html_table_class_multirow();
  html_TR();
  html_table_header(F("Plugin"), F("RTDPlugin/_Plugin.html#list-of-official-plugins"), 0);
  html_table_header(F("GPIO"));
  html_table_header(F("Mode"));
  html_table_header(F("Value/State"));
  html_table_header(F("Task"));
  html_table_header(F("Monitor"));
  html_table_header(F("Command"));
  html_table_header(F("Init"));

  for (auto it = globalMapPortStatus.begin(); it != globalMapPortStatus.end(); ++it)
  {
    html_TR_TD();
    const pluginID_t plugin = getPluginFromKey(it->first);
    const uint16_t   port   = getPortFromKey(it->first);
    addHtml(plugin.toDisplayString());
    html_TD();
    addHtmlInt(port);
    html_TD();
    addHtml(getPinModeString(it->second.mode));
    html_TD();
    addHtmlInt(it->second.getValue());
    html_TD();
    addHtmlInt(it->second.task);
    html_TD();
    addHtmlInt(it->second.monitor);
    html_TD();
    addHtmlInt(it->second.command);
    html_TD();
    addHtmlInt(it->second.init);
  }

  html_end_table();

# ifdef ESP32

  // Collect which pin should be looked into more in-depth.
  uint64_t io_bit_mask{};

  html_BR();
  addFormHeader(F("Peripheral Bus Type per GPIO"));
  html_table_class_multirow();
  html_TR();
#  if defined(BOARD_HAS_PIN_REMAP)
  html_table_header(F("Dnnn|GPIO"));
#  else // if defined(BOARD_HAS_PIN_REMAP)
  html_table_header(F("GPIO"));
#  endif // if defined(BOARD_HAS_PIN_REMAP)
  html_table_header(F("Bus Type"));
  html_table_header(F("Bus Num"));
  html_table_header(F("Bus Channel"));

  for (int i = 0; i <= MAX_GPIO; ++i) {
    if (!perimanPinIsValid(i)) {
      continue; // invalid pin
    }
    peripheral_bus_type_t type = perimanGetPinBusType(i);

    if (type == ESP32_BUS_TYPE_INIT) {
      continue; // unused pin
    }


#  if defined(BOARD_HAS_PIN_REMAP)
    int dpin = gpioNumberToDigitalPin(i);

    if (dpin < 0) {
      continue; // pin is not exported
    } else {
      html_TR_TD();
      addHtml(strformat(F("D%-3d|%4u"), dpin, i));
    }
#  else // if defined(BOARD_HAS_PIN_REMAP)
    html_TR_TD();
    addHtml(strformat(F("%4u"), i));
#  endif // if defined(BOARD_HAS_PIN_REMAP)
    const char *extra_type = perimanGetPinBusExtraType(i);
    html_TD();

    if (extra_type) {
      addHtml(strformat(F("%s [%s]"), perimanGetTypeName(type), extra_type));
    } else {
      addHtml(strformat(F("%s"), perimanGetTypeName(type)));
    }
    int8_t bus_number = perimanGetPinBusNum(i);
    html_TD();

    if (bus_number != -1) {
      addHtmlInt(bus_number);
    }
    int8_t bus_channel = perimanGetPinBusChannel(i);
    html_TD();

    if (bus_channel != -1) {
      addHtmlInt(bus_channel);
    }
    io_bit_mask |= BIT64(i);
  }

  html_end_table();
  #  if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)

  if (io_bit_mask) {
    html_BR();

    addFormHeader(F("GPIO IO configuration"));
    html_table_class_multirow();
    html_TR();
    html_table_header(F("GPIO"));
    html_table_header(F("PullUp"));
    html_table_header(F("PullDown"));
    html_table_header(F("DriveCap"));
    html_table_header(F("InputEn"));
    html_table_header(F("OutputEn"));
    html_table_header(F("OpenDrain"));
    html_table_header(F("FuncSel"));
    html_table_header(F("MUX ID"),
                      F("https://github.com/espressif/esp-idf/blob/master/components/soc/"
                        ESP32XX
                        "/include/soc/gpio_sig_map.h"),
                      0);
    html_table_header(F("SleepSelEn"));


    bool reservedPinFound = false;
    bool simpleGPIO_found = false;

    for (int gpio_num = 0; gpio_num <= MAX_GPIO; ++gpio_num) {
      if ((io_bit_mask & BIT64(gpio_num)) == 0) {
        continue; // skip pin
      }

      html_TR_TD();
      addHtmlInt(gpio_num);

      if (esp_gpio_is_reserved(BIT64(gpio_num))) {
        reservedPinFound = true;
        addHtml(F(" (*)"));
      }

      gpio_io_config_t io_config = {};
      gpio_get_io_config((gpio_num_t)gpio_num, &io_config);

      // PullUp
      html_TD();
      addEnabled(io_config.pu);

      // PullDown
      html_TD();
      addEnabled(io_config.pd);

      // DriveCap
      html_TD();
      addHtmlInt((uint32_t)io_config.drv);

      // InputEn
      html_TD();
      addEnabled(io_config.ie);

      // OutputEn
      html_TD();
      // When the IO is used as a simple GPIO output, oe signal can only be controlled by the oe register
      // When the IO is not used as a simple GPIO output, oe signal could be controlled by the peripheral
      if ((io_config.sig_out != SIG_GPIO_OUT_IDX) && io_config.oe_ctrl_by_periph) {
        addHtml(F("[periph_sig_ctrl]"));
      } else {
        addEnabled(io_config.oe);
      }
      if ((io_config.fun_sel == PIN_FUNC_GPIO) && (io_config.oe_inv)) {
        addHtml(F(" (inversed)"));
      }

      // OpenDrain
      html_TD();
      addEnabled(io_config.od);

      // FuncSel
      html_TD();
      addHtmlInt(io_config.fun_sel);
      addHtml(strformat(F(" (%s)"), (io_config.fun_sel == PIN_FUNC_GPIO) ? "GPIO" : "IOMUX"));


      // MUX ID
      html_TD();

      if (io_config.fun_sel == PIN_FUNC_GPIO) {
        addHtml(F("Out: "));

        if (io_config.sig_out == SIG_GPIO_OUT_IDX) {
          addHtml(F("(**)")); // simple GPIO output
          simpleGPIO_found = true;
        } else {
          addHtmlInt(io_config.sig_out);
        }
        html_BR();
      }

      if (io_config.ie && (io_config.fun_sel == PIN_FUNC_GPIO)) {
        uint32_t cnt = 0;
        addHtml(F("In: "));

        for (int i = 0; i < SIG_GPIO_OUT_IDX; i++) {
          if (gpio_ll_get_in_signal_connected_io(GPIO_HAL_GET_HW(GPIO_PORT_0), i) == gpio_num) {
            cnt++;
            addHtml(' ');
            addHtmlInt(i);
          }
        }

        if (cnt == 0) {
          addHtml(F("(**)")); // simple GPIO input
          simpleGPIO_found = true;
        }
      }

      // SleepSelEn
      html_TD();
      addHtmlInt(io_config.slp_sel);
    }
    html_end_table();


    if (reservedPinFound) {
      html_BR();
      addHtml(F("(*) GPIO is reserved, or in use by some peripheral"));
    }

    if (simpleGPIO_found) {
      html_BR();
      addHtml(F("(**) Simple GPIO input/output"));
    }
  }

#  endif // if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)

# endif // ifdef ESP32
  sendHeadandTail_stdtemplate(_TAIL);
  TXBuffer.endStream();
}

#endif // ifdef WEBSERVER_PINSTATES

#include "../WebServer/ESPEasy_WebServer.h"

#include "../WebServer/common.h"

#include "../WebServer/404.h"
#include "../WebServer/AccessControl.h"
#include "../WebServer/AdvancedConfigPage.h"
#include "../WebServer/CacheControllerPages.h"
#include "../WebServer/ConfigPage.h"
#include "../WebServer/ControlPage.h"
#include "../WebServer/ControllerPage.h"
#include "../WebServer/CustomPage.h"
#include "../WebServer/DevicesPage.h"
#include "../WebServer/DownloadPage.h"
#include "../WebServer/FactoryResetPage.h"
#include "../WebServer/FileList.h"
#include "../WebServer/HTML_wrappers.h"
#include "../WebServer/HardwarePage.h"
#include "../WebServer/I2C_Scanner.h"
#include "../WebServer/JSON.h"
#include "../WebServer/LoadFromFS.h"
#include "../WebServer/Log.h"
#include "../WebServer/Markup.h"
#include "../WebServer/Markup_Buttons.h"
#include "../WebServer/Markup_Forms.h"
#include "../WebServer/NotificationPage.h"
#include "../WebServer/PluginListPage.h"
#include "../WebServer/PinStates.h"
#include "../WebServer/RootPage.h"
#include "../WebServer/Rules.h"
#include "../WebServer/SettingsArchive.h"
#include "../WebServer/SetupPage.h"
#include "../WebServer/SysInfoPage.h"
#include "../WebServer/Metrics.h"
#include "../WebServer/SysVarPage.h"
#include "../WebServer/TimingStats.h"
#include "../WebServer/ToolsPage.h"
#include "../WebServer/UploadPage.h"
#include "../WebServer/WiFiScanner.h"

#include "../WebServer/WebTemplateParser.h"


#include "../../ESPEasy-Globals.h"
#include "../../_Plugin_Helper.h"
#include "../../ESPEasy_common.h"

#include "../CustomBuild/CompiletimeDefines.h"

#include "../DataStructs/TimingStats.h"

#include "../DataTypes/SettingsType.h"

#include "../ESPEasyCore/ESPEasyNetwork.h"
#include "../ESPEasyCore/ESPEasyRules.h"
#include "../ESPEasyCore/ESPEasyWifi.h"

#include "../Globals/CPlugins.h"
#include "../Globals/Device.h"
#include "../Globals/NetworkState.h"
#include "../Globals/SecuritySettings.h"
#include "../Globals/Settings.h"

#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/Hardware_device_info.h"
#include "../Helpers/Networking.h"
#include "../Helpers/OTA.h"
#include "../Helpers/StringConverter.h"

#include "../Static/WebStaticData.h"

#include <uri/UriGlob.h>


void safe_strncpy_webserver_arg(char *dest, const String& arg, size_t max_size) {
  if (hasArg(arg)) {
    safe_strncpy(dest, webArg(arg).c_str(), max_size);
  }
}

void safe_strncpy_webserver_arg(char *dest, const __FlashStringHelper *arg, size_t max_size) {
  safe_strncpy_webserver_arg(dest, String(arg), max_size);
}

void sendHeadandTail(const __FlashStringHelper *tmplName, bool Tail, bool rebooting) {
  // This function is called twice per serving a web page.
  // So it must keep track of the timer longer than the scope of this function.
  // Therefore use a local static variable.
  #if FEATURE_TIMING_STATS
  static uint64_t statisticsTimerStart = 0;

  if (!Tail) {
    statisticsTimerStart = getMicros64();
  }
  #endif // if FEATURE_TIMING_STATS
  {
    const String fileName = concat(tmplName, F(".htm"));
    fs::File     f        = tryOpenFile(fileName, "r");

    WebTemplateParser templateParser(Tail, rebooting);

    if (f) {
      bool success = true;

      while (f.available() && success) {
        success = templateParser.process((char)f.read());
      }
      f.close();
    } else {
      getWebPageTemplateDefault(tmplName, templateParser);
    }
    #ifndef BUILD_NO_RAM_TRACKER
    checkRAM(F("sendWebPage"));
    #endif // ifndef BUILD_NO_RAM_TRACKER

    // web activity timer
    lastWeb = millis();
  }

  if (shouldReboot) {
    // we only add this here as a seperate chunk to prevent using too much memory at once
    serve_JS(JSfiles_e::Reboot);
  }
  STOP_TIMER(HANDLE_SERVING_WEBPAGE);
}

void sendHeadandTail_stdtemplate(bool Tail, bool rebooting) {
  sendHeadandTail(F("TmplStd"), Tail, rebooting);

  if (!Tail) {
    if (!clientIPinSubnet() && WifiIsAP(WiFi.getMode()) && (WiFi.softAPgetStationNum() > 0)) {
      addHtmlError(F("Warning: Connected via AP"));
    }

    #ifndef BUILD_NO_DEBUG

    /*
        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          const int nrArgs = web_server.args();

          if (nrArgs > 0) {
            String log = F(" Webserver ");
            log += nrArgs;
            log += F(" Arguments");

            if (nrArgs > 20) {
              log += F(" (First 20)");
            }
            log += ':';

            for (int i = 0; i < nrArgs && i < 20; ++i) {
              log += ' ';
              log += i;
              log += F(": '");
              log += web_server.argName(i);
              log += F("' length: ");
              log += webArg(i).length();
            }
            addLogMove(LOG_LEVEL_INFO, log);
          }
        }
     */
    #endif // ifndef BUILD_NO_DEBUG
  }

  // We have sent a lot of data at once.
  // try to flush it to the connected client to free up some RAM
  // from pending transfers
//  TXBuffer.flush();
//  delay(10);
}

bool captivePortal() {
  const IPAddress client_localIP = web_server.client().localIP();
  const bool fromAP              = client_localIP == apIP;
  const bool hasWiFiCredentials  = SecuritySettings.hasWiFiCredentials();

  if (hasWiFiCredentials || !fromAP) {
    return false;
  }

  if (!isIP(web_server.hostHeader()) && (web_server.hostHeader() != (NetworkGetHostname() + F(".local")))) {
    String redirectURL = concat(F("http://"), formatIP(client_localIP));
    #ifdef WEBSERVER_SETUP

    if (fromAP && !hasWiFiCredentials) {
      redirectURL += F("/setup");
    }
    #endif // ifdef WEBSERVER_SETUP
    sendHeader(F("Location"), redirectURL, true);
    web_server.send(302, F("text/plain"), EMPTY_STRING); // Empty content inhibits Content-length header so we have to close the socket
                                                         // ourselves.
    web_server.client().stop();                          // Stop is needed because we sent no content length
    return true;
  }
  return false;
}

// ********************************************************************************
// Web Interface init
// ********************************************************************************
// #include "core_version.h"


void WebServerInit()
{
  if (webserver_init) { return; }
  webserver_init = true;

  // Prepare webserver pages
  #ifdef WEBSERVER_ROOT
  web_server.on(F("/"), handle_root);

  // Entries for several captive portal URLs.
  // Maybe not needed. Might be handled by notFound handler.
  web_server.on(UriGlob("/generate_204*"), handle_root); // Android captive portal. Handle "/generate_204_<uuid>"-like requests.
  web_server.on(F("/fwlink"),              handle_root); // Microsoft captive portal.
  #endif // ifdef WEBSERVER_ROOT
  #ifdef WEBSERVER_ADVANCED
  web_server.on(F("/advanced"),            handle_advanced);
  # if defined(WEBSERVER_DOWNLOAD) && FEATURE_TARSTREAM_SUPPORT
  web_server.on(F("/backup"),              handle_full_backup);
  web_server.on(F("/backupnup"),           handle_full_backup_no_usr_pwd);
  # endif // if defined(WEBSERVER_DOWNLOAD) && FEATURE_TARSTREAM_SUPPORT
  #endif // ifdef WEBSERVER_ADVANCED
  #ifdef WEBSERVER_CONFIG
  web_server.on(F("/config"),      handle_config);
  #endif // ifdef WEBSERVER_CONFIG
  #ifdef WEBSERVER_CONTROL
  web_server.on(F("/control"),     handle_control);
  #endif // ifdef WEBSERVER_CONTROL
  #ifdef WEBSERVER_CONTROLLERS
  web_server.on(F("/controllers"), handle_controllers);
  #endif // ifdef WEBSERVER_CONTROLLERS
  #ifdef WEBSERVER_DEVICES
  web_server.on(F("/devices"),     handle_devices);
  #endif // ifdef WEBSERVER_DEVICES
  #ifdef WEBSERVER_DOWNLOAD
  web_server.on(F("/download"),    handle_download);
  #endif // ifdef WEBSERVER_DOWNLOAD

#ifdef USES_C016

  web_server.on(F("/dumpcache"),  handle_dumpcache);  // C016 specific entrie
  web_server.on(F("/cache_json"), handle_cache_json); // C016 specific entrie
  web_server.on(F("/cache_csv"),  handle_cache_csv);  // C016 specific entrie
#endif // USES_C016

  #ifdef WEBSERVER_FACTORY_RESET
  web_server.on(F("/factoryreset"),    handle_factoryreset);
  #endif // ifdef WEBSERVER_FACTORY_RESET
  #if FEATURE_SETTINGS_ARCHIVE
  web_server.on(F("/settingsarchive"), handle_settingsarchive);
  #endif // if FEATURE_SETTINGS_ARCHIVE
  #ifdef WEBSERVER_FILELIST
  web_server.on(F("/filelist"),        handle_filelist);
  #endif // ifdef WEBSERVER_FILELIST
  #ifdef WEBSERVER_HARDWARE
  web_server.on(F("/hardware"),        handle_hardware);
  #endif // ifdef WEBSERVER_HARDWARE
  #ifdef WEBSERVER_I2C_SCANNER
  web_server.on(F("/i2cscanner"),      handle_i2cscanner);
  #endif // ifdef WEBSERVER_I2C_SCANNER
  web_server.on(F("/json"),            handle_json);     // Also part of WEBSERVER_NEW_UI
  #ifdef WEBSERVER_CSVVAL
  web_server.on(F("/csv"),             handle_csvval);
  #endif
  web_server.on(F("/log"),             handle_log);
  web_server.on(F("/logjson"),         handle_log_JSON); // Also part of WEBSERVER_NEW_UI
#if FEATURE_NOTIFIER
  web_server.on(F("/notifications"),   handle_notifications);
#endif // if FEATURE_NOTIFIER
  #ifdef WEBSERVER_PINSTATES
  web_server.on(F("/pinstates"),       handle_pinstates);
  #endif // ifdef WEBSERVER_PINSTATES
  #ifdef WEBSERVER_RULES
  web_server.on(F("/rules"),           handle_rules_new);
  web_server.on(F("/rules/"),          Goto_Rules_Root);
  # ifdef WEBSERVER_NEW_RULES
  web_server.on(F("/rules/add"),       []()
  {
    handle_rules_edit(web_server.uri(), true);
  });
  web_server.on(F("/rules/backup"), handle_rules_backup);
  web_server.on(F("/rules/delete"), handle_rules_delete);
  # endif // WEBSERVER_NEW_RULES
  #endif  // WEBSERVER_RULES
#if FEATURE_SD
  web_server.on(F("/SDfilelist"),  handle_SDfilelist);
#endif   // if FEATURE_SD
#ifdef WEBSERVER_SETUP
  web_server.on(F("/setup"),       handle_setup);
#endif // ifdef WEBSERVER_SETUP
#ifdef WEBSERVER_SYSINFO
  web_server.on(F("/sysinfo"),     handle_sysinfo);
#endif // ifdef WEBSERVER_SYSINFO
#ifdef WEBSERVER_METRICS
  web_server.on(F("/metrics"),     handle_metrics);
#endif // ifdef WEBSERVER_METRICS
#ifdef WEBSERVER_SYSVARS
  web_server.on(F("/sysvars"),     handle_sysvars);
#endif // WEBSERVER_SYSVARS
#if FEATURE_PLUGIN_LIST
  web_server.on(F("/pluginlist"),  handle_pluginlist);
#endif // if FEATURE_PLUGIN_LIST
#ifdef WEBSERVER_TIMINGSTATS
  web_server.on(F("/timingstats"), handle_timingstats);
#endif // WEBSERVER_TIMINGSTATS
#ifdef WEBSERVER_TOOLS
  web_server.on(F("/tools"),       handle_tools);
#endif // ifdef WEBSERVER_TOOLS
#ifdef WEBSERVER_UPLOAD
  web_server.on(F("/upload"),      HTTP_GET,  handle_upload);
  web_server.on(F("/upload"),      HTTP_POST, handle_upload_post, handleFileUpload);
#endif // ifdef WEBSERVER_UPLOAD
#if FEATURE_SD
  web_server.on(F("/uploadsd"),    HTTP_GET,  handle_upload);
  web_server.on(F("/uploadsd"),    HTTP_POST, handle_upload_post, handleSDFileUpload);
#endif // if FEATURE_SD
#ifdef WEBSERVER_WIFI_SCANNER
  web_server.on(F("/wifiscanner"), handle_wifiscanner);
#endif // ifdef WEBSERVER_WIFI_SCANNER

#ifdef WEBSERVER_NEW_UI
  web_server.on(F("/buildinfo"),         handle_buildinfo); // Also part of WEBSERVER_NEW_UI
  web_server.on(F("/factoryreset_json"), handle_factoryreset_json);
  web_server.on(F("/filelist_json"),     handle_filelist_json);
  web_server.on(F("/i2cscanner_json"),   handle_i2cscanner_json);
  # if FEATURE_ESPEASY_P2P
  web_server.on(F("/node_list_json"),    handle_nodes_list_json);
  # endif // if FEATURE_ESPEASY_P2P
  web_server.on(F("/pinstates_json"),    handle_pinstates_json);
  web_server.on(F("/timingstats_json"),  handle_timingstats_json);
  web_server.on(F("/upload_json"),       HTTP_POST, handle_upload_json, handleFileUpload);
  web_server.on(F("/wifiscanner_json"),  handle_wifiscanner_json);
#endif // WEBSERVER_NEW_UI
#if SHOW_SYSINFO_JSON
  web_server.on(F("/sysinfo_json"),      handle_sysinfo_json);
#endif// SHOW_SYSINFO_JSON

  web_server.onNotFound(handleNotFound);

  // List of headers to be recorded
  // "If-None-Match" is used to see whether we need to serve a static file, or simply can reply with a 304 (not modified)
  const char *headerkeys[]        = { "If-None-Match" };
  constexpr size_t headerkeyssize = NR_ELEMENTS(headerkeys);
  web_server.collectHeaders(headerkeys, headerkeyssize);
  #if defined(ESP8266) || defined(ESP32)
  {
    # ifndef NO_HTTP_UPDATER
    uint32_t maxSketchSize;
    bool     use2step;

    // allow OTA to smaller version of ESPEasy/other firmware
    if (Settings.AllowOTAUnlimited() || OTA_possible(maxSketchSize, use2step)) {
      httpUpdater.setup(&web_server);
    }
    # endif // ifndef NO_HTTP_UPDATER
  }
  #endif    // if defined(ESP8266)

  #if defined(ESP8266)

  # if FEATURE_SSDP

  if (Settings.UseSSDP)
  {
    web_server.on(F("/ssdp.xml"), HTTP_GET, []() {
      #  ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS

      // See: https://github.com/espressif/arduino-esp32/pull/6676
      web_server.client().setTimeout((CONTROLLER_CLIENTTIMEOUT_DFLT + 500) / 1000); // in seconds!!!!
      Client& pClient = web_server.client();
      pClient.setTimeout(CONTROLLER_CLIENTTIMEOUT_DFLT);
      #  else // ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS
      web_server.client().setTimeout(CONTROLLER_CLIENTTIMEOUT_DFLT); // in msec as it should be!
      #  endif // ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS

      SSDP_schema();
    });
    SSDP_begin();
  }
  # endif // if FEATURE_SSDP
  #endif  // if defined(ESP8266)
}

void setWebserverRunning(bool state) {
  if (webserverRunning == state) {
    return;
  }

  if (state) {
    WebServerInit();
    web_server.begin(Settings.WebserverPort);
    #ifndef BUILD_MINIMAL_OTA
    addLog(LOG_LEVEL_INFO, F("Webserver: start"));
    #endif
  } else {
    web_server.client().stop();
    web_server.stop();
    #ifndef BUILD_MINIMAL_OTA
    addLog(LOG_LEVEL_INFO, F("Webserver: stop"));
    #endif
  }
  webserverRunning = state;
  CheckRunningServices(); // Uses webserverRunning state.
}

void getWebPageTemplateDefault(const String& tmplName, WebTemplateParser& parser)
{
  const bool addJS   = true;
  const bool addMeta = true;

  /*
     if (equals(tmplName, F("TmplAP")))
     {

      getWebPageTemplateDefaultHead(parser, addMeta, !addJS);

      if (!parser.isTail()) {
   #ifndef WEBPAGE_TEMPLATE_AP_HEADER
        parser.process(F("<body"
   #if FEATURE_AUTO_DARK_MODE
                         " data-theme='auto'"
   #endif // FEATURE_AUTO_DARK_MODE
                         "><header class='apheader'>"
                         "<h1>Welcome to ESP Easy Mega AP</h1>"));
   #else
        parser.process(F(WEBPAGE_TEMPLATE_AP_HEADER));
   #endif

        parser.process(F("</header>"));
      }
      getWebPageTemplateDefaultContentSection(parser);
      getWebPageTemplateDefaultFooter(parser);
     }
     else
   */
  if (equals(tmplName, F("TmplMsg")))
  {
    getWebPageTemplateDefaultHead(parser, !addMeta, !addJS);

    if (!parser.isTail()) {
      parser.process(F("<body"
                       #if FEATURE_AUTO_DARK_MODE
                       " data-theme='auto'"
                       #endif // FEATURE_AUTO_DARK_MODE
                       ">"));
    }
    getWebPageTemplateDefaultHeader(parser, F("{{name}}"), false);
    getWebPageTemplateDefaultContentSection(parser);
    getWebPageTemplateDefaultFooter(parser);
  }
  else if (equals(tmplName, F("TmplDsh")))
  {
    getWebPageTemplateDefaultHead(parser, !addMeta, addJS);
    parser.process(F("<body"));
    #if FEATURE_AUTO_DARK_MODE

    if (0 == Settings.getCssMode()) {
      parser.process(F(" data-theme='auto'"));
    } else if (2 == Settings.getCssMode()) {
      parser.process(F(" data-theme='dark'"));
    }
    #endif // FEATURE_AUTO_DARK_MODE
    parser.process(F(">"
                     "{{content}}"
                     "</body></html>"));
  }
  else // all other template names e.g. TmplStd
  {
    getWebPageTemplateDefaultHead(parser, addMeta, addJS);

    if (!parser.isTail()) {
      parser.process(F("<body class='bodymenu'"));
      #if FEATURE_AUTO_DARK_MODE

      if (0 == Settings.getCssMode()) {
        parser.process(F(" data-theme='auto'"));
      } else if (2 == Settings.getCssMode()) {
        parser.process(F(" data-theme='dark'"));
      }
      #endif // FEATURE_AUTO_DARK_MODE
      parser.process(F("><span class='message' id='rbtmsg'></span>"));
    }
    getWebPageTemplateDefaultHeader(parser, F("{{name}} {{logo}}"), true);
    getWebPageTemplateDefaultContentSection(parser);
    getWebPageTemplateDefaultFooter(parser);
  }

  //  addLog(LOG_LEVEL_INFO, concat(F("tmpl.length(): "), tmpl.length()));
}

void getWebPageTemplateDefaultHead(WebTemplateParser& parser, bool addMeta, bool addJS) {
  if (parser.isTail()) { return; }
  parser.process(F("<!DOCTYPE html><html lang='en'>"
                   "<head>"
                   "<meta charset='utf-8'/>"
                   "<meta name='viewport' content='width=device-width, initial-scale=1.0'>"
                   "<title>{{name}}</title>"));

  if (addMeta) { parser.process(F("{{meta}}")); }

  if (addJS) { parser.process(F("{{js}}")); }

  parser.process(F("{{css}}"
                   "</head>"));
}

void getWebPageTemplateDefaultHeader(WebTemplateParser& parser, const __FlashStringHelper *title, bool addMenu) {
  {
    if (parser.isTail()) { return; }
  #ifndef WEBPAGE_TEMPLATE_DEFAULT_HEADER
    parser.process(F("<header class='headermenu'><h1>ESP Easy Mega: "));
    parser.process(title);
    # if BUILD_IN_WEBHEADER
    parser.process(F(
                     "<div style='float:right;font-size:10pt'>Build: " GITHUB_RELEASES_LINK_PREFIX "{{date}}" GITHUB_RELEASES_LINK_SUFFIX
                     "</div>"));
    # endif // #if BUILD_IN_WEBHEADER
    parser.process(F("</h1><BR>"));
  #else // ifndef WEBPAGE_TEMPLATE_DEFAULT_HEADER
    String tmp = F(WEBPAGE_TEMPLATE_DEFAULT_HEADER);
    tmp.replace(F("{{title}}"), title);
    parser.process(tmp);
  #endif // ifndef WEBPAGE_TEMPLATE_DEFAULT_HEADER
  }

  if (addMenu) { parser.process(F("{{menu}}")); }
  parser.process(F("</header>"));
}

void getWebPageTemplateDefaultContentSection(WebTemplateParser& parser) {
  parser.process(F("<section>"
                   "<span class='message error'>"
                   "{{error}}"
                   "</span>"
                   "{{content}}"
                   "</section>"
                   ));
}

void getWebPageTemplateDefaultFooter(WebTemplateParser& parser) {
  if (!parser.isTail()) { return; }
  #ifndef WEBPAGE_TEMPLATE_DEFAULT_FOOTER
  parser.process(F("<footer>"
                   "<br>"
                   "<h6>Powered by <a href='http://www.letscontrolit.com' style='font-size: 15px; text-decoration: none'>Let's Control It</a> community"
            # if BUILD_IN_WEBFOOTER
                   "<div style='float: right'>Build: " GITHUB_RELEASES_LINK_PREFIX "{{build}} {{date}}" GITHUB_RELEASES_LINK_SUFFIX "</div>"
            # endif // #if BUILD_IN_WEBFOOTER
                   "</h6>"
                   "</footer>"
                   "</body></html>"
                   ));
#else // ifndef WEBPAGE_TEMPLATE_DEFAULT_FOOTER
  parser.process(F(WEBPAGE_TEMPLATE_DEFAULT_FOOTER));
#endif // ifndef WEBPAGE_TEMPLATE_DEFAULT_FOOTER
}

void writeDefaultCSS(void)
{
  return; // TODO

  /*
   #ifndef WEBSERVER_USE_CDN_JS_CSS

     if (!fileExists(F("esp.css")))
     {
      fs::File f = tryOpenFile(F("esp.css"), "w");

      if (f)
      {
        String defaultCSS;
        defaultCSS = PGMT(DATA_ESPEASY_DEFAULT_MIN_CSS);

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          String log = F("CSS  : Writing default CSS file to FS (");
          log += defaultCSS.length();
          log += F(" bytes)");
          addLog(LOG_LEVEL_INFO, log);
        }
        f.write((const unsigned char *)defaultCSS.c_str(), defaultCSS.length()); // note: content must be in RAM - a write of F("XXX") does
                                                                                 // not work
        f.close();
      }
     }
   #endif
   */
}

// ********************************************************************************
// Functions to stream JSON directly to TXBuffer
// FIXME TD-er: replace stream_xxx_json_object* into this code.
// N.B. handling of numerical values differs (string vs. no string)
// ********************************************************************************

int8_t level     = 0;
int8_t lastLevel = -1;

void json_quote_name(const __FlashStringHelper *val) {
  json_quote_name(String(val));
}

void json_quote_name(const String& val) {
  if (lastLevel == level) {
    addHtml(',');
  }

  if (val.length() > 0) {
    json_quote_val(val);
    addHtml(':');
  }
}

void json_quote_val(const String& val) {
  addHtml('\"');
  addHtml(val);
  addHtml('\"');
}

void json_open(bool arr) {
  json_open(arr, EMPTY_STRING);
}

void json_open(bool arr, const __FlashStringHelper *name) {
  json_quote_name(name);
  addHtml(arr ? '[' : '{');
  lastLevel = level;
  level++;
}

void json_open(bool arr, const String& name) {
  json_quote_name(name);
  addHtml(arr ? '[' : '{');
  lastLevel = level;
  level++;
}

void json_init() {
  level     = 0;
  lastLevel = -1;
}

void json_close() {
  json_close(false);
}

void json_close(bool arr) {
  addHtml(arr ? ']' : '}');
  level--;
  lastLevel = level;
}

void json_number(const __FlashStringHelper *name, const String& value)
{
  json_prop(name, value);
}

void json_number(const String& name, const String& value) {
  json_prop(name, value);
}

void json_prop(const __FlashStringHelper *name, const String& value)
{
  json_quote_name(name);
  json_quote_val(value);
  lastLevel = level;
}

void json_prop(const String& name, const String& value) {
  json_quote_name(name);
  json_quote_val(value);
  lastLevel = level;
}

void json_prop(LabelType::Enum label) {
  json_prop(getInternalLabel(label, '-'), getValue(label));
}

// ********************************************************************************
// Add a task select dropdown list
// This allows to select a task index based on the existing tasks.
// ********************************************************************************
void addTaskSelect(const String& name,  taskIndex_t choice)
{
  String deviceName;
  String deviceNr;
  String options[TASKS_MAX + 1];
  String attrs[TASKS_MAX + 1];

  for (taskIndex_t x = 0; x <= TASKS_MAX; x++)
  {
    deviceNr.clear();
    if (validTaskIndex(x)) {
      const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(x);
      deviceName = getPluginNameFromDeviceIndex(DeviceIndex);
      deviceNr += (x + 1);
    } else {
      deviceName = F("Not Set");
    }

    if (validTaskIndex(x) && !validPluginID_fullcheck(Settings.getPluginID_for_task(x))) {
      attrs[x] = F("disabled");
    }
    options[x] = strformat(F("%s - %s - %s"), deviceNr.c_str(), deviceName.c_str(), getTaskDeviceName(x).c_str());
  }

  FormSelectorOptions selector(TASKS_MAX + 1, options, nullptr, attrs);
  selector.reloadonchange = true;
  selector.classname = F("wide");
  selector.addSelector(name, choice);
}

// ********************************************************************************
// Add a Value select dropdown list, based on TaskIndex
// This allows to select a task value, based on the existing tasks.
// ********************************************************************************
void addTaskValueSelect(const String& name, int choice, taskIndex_t TaskIndex)
{
  if (!validTaskIndex(TaskIndex)) { return; }
  const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(TaskIndex);

  if (!validDeviceIndex(DeviceIndex)) { return; }

  addHtml(F("<select "));
  addHtmlAttribute(F("id"),   F("selectwidth"));
  addHtmlAttribute(F("name"), name);
  addHtml('>');

  const uint8_t valueCount = getValueCountForTask(TaskIndex);

  for (uint8_t x = 0; x < valueCount; x++)
  {
    addHtml(F("<option value='"));
    addHtmlInt(x);
    addHtml('\'');

    if (choice == x) {
      addHtml(F(" selected"));
    }
    addHtml('>');
    addHtml(Cache.getTaskDeviceValueName(TaskIndex, x));
    addHtml(F("</option>"));
  }
}

// ********************************************************************************
// Login state check
// ********************************************************************************
bool isLoggedIn(bool mustProvideLogin)
{
  if (!clientIPallowed()) { return false; }

  if (SecuritySettings.Password[0] == 0) { return true; }

  if (!mustProvideLogin) {
    return false;
  }

  {
    String www_username = F(DEFAULT_ADMIN_USERNAME);

    if (!web_server.authenticate(www_username.c_str(), SecuritySettings.Password))

    // Basic Auth Method with Custom realm and Failure Response
    // return server.requestAuthentication(BASIC_AUTH, www_realm, authFailResponse);
    // Digest Auth Method with realm="Login Required" and empty Failure Response
    // return server.requestAuthentication(DIGEST_AUTH);
    // Digest Auth Method with Custom realm and empty Failure Response
    // return server.requestAuthentication(DIGEST_AUTH, www_realm);
    // Digest Auth Method with Custom realm and Failure Response
    {
#ifdef CORE_PRE_2_5_0

      // See https://github.com/esp8266/Arduino/issues/4717
      HTTPAuthMethod mode = BASIC_AUTH;
#else // ifdef CORE_PRE_2_5_0
      HTTPAuthMethod mode = DIGEST_AUTH;
#endif // ifdef CORE_PRE_2_5_0
      String message = F("Login Required (default user: ");
      message += www_username;
      message += ')';
      web_server.requestAuthentication(mode, message.c_str());

      if (Settings.UseRules)
      {
        String event = F("Login#Failed");

        // TD-er: Do not add to the eventQueue, but execute right now.
        rulesProcessing(event);
      }

      return false;
    }
  }
  return true;
}

String getControllerSymbol(uint8_t index)
{
  String ret = F("<span style='font-size:20px; background: #00000000;'>&#");

  ret += 10102 + index;
  ret += F(";</span>");
  return ret;
}

/*
   String getValueSymbol(uint8_t index)
   {
   String ret = F("&#");
   ret += 10112 + index;
   ret += ';';
   return ret;
   }
 */
void addSVG_param(const char key, int value)
{
  addHtml(strformat(F(" %c=\"%d\""), key, value));
}

void addSVG_param(const char key, float value)
{
  addSVG_param(key, toString(value, 2));
}

void addSVG_param(const char key, const String& value)
{
  addHtml(strformat(F(" %c=\"%s\""), key, value.c_str()));
}

void addSVG_param(const __FlashStringHelper *key, int value) {
  addHtml(strformat(F(" %s=\"%d\""), key, value));
}

void addSVG_param(const __FlashStringHelper *key, float value) {
  addSVG_param(key, toString(value, 2));
}

void addSVG_param(const __FlashStringHelper *key, const String& value) {
  addHtml(strformat(F(" %s=\"%s\""), key, value.c_str()));
}

void createSvgRect_noStroke(const __FlashStringHelper *classname,
                            unsigned int               fillColor,
                            float                      xoffset,
                            float                      yoffset,
                            float                      width,
                            float                      height,
                            float                      rx,
                            float                      ry) {
  createSvgRect(classname, fillColor, fillColor, xoffset, yoffset, width, height, 0, rx, ry);
}

void createSvgRect(const String& classname,
                   unsigned int  fillColor,
                   unsigned int  strokeColor,
                   float         xoffset,
                   float         yoffset,
                   float         width,
                   float         height,
                   float         strokeWidth,
                   float         rx,
                   float         ry) {
  addHtml(F("<rect"));

  if (!classname.isEmpty()) {
    addSVG_param(F("class"), classname);
  }
  addSVG_param(F("fill"), formatToHex(fillColor, F("#"), 3));

  if (!approximatelyEqual(strokeWidth, 0)) {
    addSVG_param(F("stroke"),       formatToHex(strokeColor, F("#"), 3));
    addSVG_param(F("stroke-width"), strokeWidth);
  }
  addSVG_param('x',         xoffset);
  addSVG_param('y',         yoffset);
  addSVG_param(F("width"),  width);
  addSVG_param(F("height"), height);
  addSVG_param(F("rx"),     rx);
  addSVG_param(F("ry"),     ry);
  addHtml(F("/>"));
}

void createSvgHorRectPath(unsigned int color, int xoffset, int yoffset, int size, int height, int range, float SVG_BAR_WIDTH) {
  if (range == 0) {
    range = 1;
  }
  float width = (SVG_BAR_WIDTH * size) / range;

  if (width < 2) { width = 2; }
  addHtml(formatToHex(color, F("<path fill=\"#")));
  addHtml(F("\" d=\"M"));
  addHtmlFloat((SVG_BAR_WIDTH * xoffset) / range);
  addHtml(' ');
  addHtmlInt(yoffset);
  addHtml('h');
  addHtmlFloat(width);
  addHtml('v');
  addHtmlInt(height);
  addHtml('H');
  addHtmlFloat((SVG_BAR_WIDTH * xoffset) / range);
  addHtml(F("z\"/>\n"));
}

void createSvgTextElement(const String& text, float textXoffset, float textYoffset) {
  addHtml(F("<text"));
  addSVG_param('x', textXoffset);
  addSVG_param('y', textYoffset);
  addHtml('>', '\n');

  addHtml(F("<tspan"));
  addSVG_param('x', textXoffset);
  addSVG_param('y', textYoffset);
  addHtml('>');

  addHtml(text);
  addHtml(F("</tspan>\n</text>"));
}

#define SVG_BAR_HEIGHT 16
#define SVG_BAR_WIDTH 400

void write_SVG_image_header(int width, int height, bool useViewbox) {
  addHtml(F("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\""));
  addHtmlInt(width);
  addHtml(F("\" height=\""));
  addHtmlInt(height);
  addHtml(F("\" version=\"1.1\""));

  if (useViewbox) {
    addHtml(F(" viewBox=\"0 0 100 100\""));
  }
  addHtml('>');
  addHtml(F("<style>text{line-height:1.25;stroke-width:.3;font-family:sans-serif;font-size:8;letter-spacing:0;word-spacing:0;"));
  #if FEATURE_AUTO_DARK_MODE

  if (2 == Settings.getCssMode()) { // Dark
    addHtml(F("fill:#c3c3c3;"));    // Copied from espeasy_default.css var(--c4) in dark section
  }
  addHtml('}');

  if (0 == Settings.getCssMode()) {                                       // Auto
    addHtml(F("@media(prefers-color-scheme:dark){text{fill:#c3c3c3;}}")); // ditto
  }
  #else // FEATURE_AUTO_DARK_MODE
  addHtml('}');                                                           // close 'text' style
  #endif // FEATURE_AUTO_DARK_MODE
  addHtml(F("</style>"));
}

/*
   void getESPeasyLogo(int width_pixels) {
   write_SVG_image_header(width_pixels, width_pixels, true);
   addHtml(F("<g transform=\"translate(-33.686 -7.8142)\"><rect x=\"49\" y=\"23.1\" width=\"69.3\" height=\"69.3\" fill=\"#2c72da\"
      stroke=\"#2c72da\"
      stroke-linecap=\"round\"stroke-linejoin=\"round\" stroke-width=\"30.7\"/><g transform=\"matrix(3.3092 0 0 3.3092 -77.788
         -248.96)\"><path d=\"m37.4 89 7.5-7.5M37.4 96.5l15-15M37.4 96.5l15-15M37.4 104l22.5-22.5M44.9 104l15-15\"
      fill=\"none\"stroke=\"#fff\" stroke-linecap=\"round\" stroke-width=\"2.6\"/><circle cx=\"58\" cy=\"102.1\" r=\"3\"
         fill=\"#fff\"/></g></g></svg>");
   }
 */
void getWiFi_RSSI_icon(int rssi, int width_pixels)
{
  const int nbars_filled = (rssi + 100) / 8;
  const int nbars        = 5;
  int white_between_bar  = (static_cast<float>(width_pixels) / nbars) * 0.2f;

  if (white_between_bar < 1) { white_between_bar = 1; }
  const int barWidth         = (width_pixels - (nbars - 1) * white_between_bar) / nbars;
  const int svg_width_pixels = nbars * barWidth + (nbars - 1) * white_between_bar;

  write_SVG_image_header(svg_width_pixels, svg_width_pixels, true);
  const float scale           = 100.0f / svg_width_pixels;
  const int   bar_height_step = 100 / nbars;

  for (int i = 0; i < nbars; ++i) {
    const unsigned int color = i < nbars_filled ? 0x07d : 0xBFa1a1a1; // Blue/Grey75%
    const int barHeight      = (i + 1) * bar_height_step;
    createSvgRect_noStroke(i < nbars_filled ? F("bar_highlight") : F("bar_dimmed"),
                           color,
                           i * (barWidth + white_between_bar) * scale,
                           100 - barHeight,
                           barWidth,
                           barHeight,
                           0,
                           0);
  }
  addHtml(F("</svg>\n"));
}

#if FEATURE_CHART_STORAGE_LAYOUT
void getConfig_dat_file_layout() {
  const int shiftY  = 2;
  float     yOffset = shiftY;

  write_SVG_image_header(SVG_BAR_WIDTH + 250, SVG_BAR_HEIGHT + shiftY);

  int max_index, offset, max_size{};
  int struct_size = 0;

  // background
  const uint32_t realSize = SettingsType::getFileSize(SettingsType::Enum::TaskSettings_Type);

  createSvgHorRectPath(0xcdcdcd, 0, yOffset, realSize, SVG_BAR_HEIGHT - 2, realSize, SVG_BAR_WIDTH);

  for (int st = 0; st < static_cast<int>(SettingsType::Enum::SettingsType_MAX); ++st) {
    SettingsType::Enum settingsType = static_cast<SettingsType::Enum>(st);

    if (SettingsType::getSettingsFile(settingsType) == SettingsType::SettingsFileEnum::FILE_CONFIG_type) {
      unsigned int color = SettingsType::getSVGcolor(settingsType);

      if (SettingsType::getSettingsParameters(settingsType, 0, max_index, offset, max_size, struct_size))
      {
        for (int i = 0; i < max_index; ++i) {
          if (SettingsType::getSettingsParameters(settingsType, i, offset, max_size)) {
            // Struct position
            createSvgHorRectPath(color, offset, yOffset, max_size, SVG_BAR_HEIGHT - 2, realSize, SVG_BAR_WIDTH);
          }
        }
      }
    }
  }

  // Text labels
  constexpr float textXoffset = SVG_BAR_WIDTH + 2;
  float textYoffset           = yOffset + 0.9f * SVG_BAR_HEIGHT;

  createSvgTextElement(SettingsType::getSettingsFileName(SettingsType::Enum::TaskSettings_Type), textXoffset, textYoffset);
  addHtml(F("</svg>\n"));
}

void getStorageTableSVG(SettingsType::Enum settingsType) {
  uint32_t realSize   = SettingsType::getFileSize(settingsType);
  unsigned int color  = SettingsType::getSVGcolor(settingsType);
  const int    shiftY = 2;

  int max_index, offset, max_size{};
  int struct_size = 0;

  if (!SettingsType::getSettingsParameters(settingsType, 0, max_index, offset, max_size, struct_size))
  {
    return;
  }

  if (max_index == 0) { return; }

  // One more to add bar indicating struct size vs. reserved space.
  write_SVG_image_header(SVG_BAR_WIDTH + 250, (max_index + 1) * SVG_BAR_HEIGHT + shiftY);
  float yOffset = shiftY;

  for (int i = 0; i < max_index; ++i) {
    if (SettingsType::getSettingsParameters(settingsType, i, offset, max_size)) {
      // background
      createSvgHorRectPath(0xcdcdcd, 0,      yOffset, realSize, SVG_BAR_HEIGHT - 2, realSize, SVG_BAR_WIDTH);

      // Struct position
      createSvgHorRectPath(color,    offset, yOffset, max_size, SVG_BAR_HEIGHT - 2, realSize, SVG_BAR_WIDTH);

      // Text labels
      float textXoffset = SVG_BAR_WIDTH + 2;
      float textYoffset = yOffset + 0.9f * SVG_BAR_HEIGHT;
      createSvgTextElement(formatHumanReadable(offset, 1024),   textXoffset, textYoffset);
      textXoffset = SVG_BAR_WIDTH + 60;
      createSvgTextElement(formatHumanReadable(max_size, 1024), textXoffset, textYoffset);
      textXoffset = SVG_BAR_WIDTH + 130;
      createSvgTextElement(String(i),                           textXoffset, textYoffset);
      yOffset += SVG_BAR_HEIGHT;
    }
  }

  // usage
  createSvgHorRectPath(0xcdcdcd, 0, yOffset, max_size, SVG_BAR_HEIGHT - 2, max_size, SVG_BAR_WIDTH);

  // Struct size (used part of the reserved space)
  if (struct_size != 0) {
    createSvgHorRectPath(color, 0, yOffset, struct_size, SVG_BAR_HEIGHT - 2, max_size, SVG_BAR_WIDTH);
  }

  // Text labels
  float textXoffset = SVG_BAR_WIDTH + 2;
  float textYoffset = yOffset + 0.9f * SVG_BAR_HEIGHT;

  if (struct_size != 0) {
    String text = strformat(
      F("%s/%s per item"),
      formatHumanReadable(struct_size, 1024).c_str(),
      formatHumanReadable(max_size, 1024).c_str());
    createSvgTextElement(text, textXoffset, textYoffset);
  } else {
    createSvgTextElement(F("Variable size"), textXoffset, textYoffset);
  }
  addHtml(F("</svg>\n"));
}

void drawPartitionChartSVG(
  float         yOffset,
  uint32_t      realSize,
  uint32_t      partitionAddress,
  uint32_t      partitionSize,
  unsigned int  partitionColor,
  const String& label,
  const String& name)
{
  createSvgHorRectPath(0xcdcdcd,       0,                yOffset, realSize,      SVG_BAR_HEIGHT - 2, realSize, SVG_BAR_WIDTH);
  createSvgHorRectPath(partitionColor, partitionAddress, yOffset, partitionSize, SVG_BAR_HEIGHT - 2, realSize, SVG_BAR_WIDTH);
  float textXoffset       = SVG_BAR_WIDTH + 2;
  const float textYoffset = yOffset + 0.9f * SVG_BAR_HEIGHT;
  createSvgTextElement(formatHumanReadable(partitionSize, 1024), textXoffset, textYoffset);
  textXoffset = SVG_BAR_WIDTH + 60;
  createSvgTextElement(label,                                    textXoffset, textYoffset);
  textXoffset = SVG_BAR_WIDTH + 130;
  createSvgTextElement(name,                                     textXoffset, textYoffset);
}

# ifdef ESP32

#  include <esp_partition.h>


void getPartitionTableSVG(uint8_t pType, unsigned int partitionColor) {
  int nrPartitions = getPartionCount(pType);

  if (nrPartitions == 0) { return; }
  const int shiftY = 2;

  uint32_t realSize                      = getFlashRealSizeInBytes();
  esp_partition_type_t     partitionType = static_cast<esp_partition_type_t>(pType);
  const esp_partition_t   *_mypart;
  esp_partition_iterator_t _mypartiterator = esp_partition_find(partitionType, ESP_PARTITION_SUBTYPE_ANY, nullptr);

  write_SVG_image_header(SVG_BAR_WIDTH + 250, nrPartitions * SVG_BAR_HEIGHT + shiftY);
  float yOffset = shiftY;

  if (_mypartiterator) {
    do {
      _mypart = esp_partition_get(_mypartiterator);
      drawPartitionChartSVG(
        yOffset,
        realSize,
        _mypart->address,
        _mypart->size,
        partitionColor,
        _mypart->label,
        getPartitionType(_mypart->type, _mypart->subtype));
      yOffset += SVG_BAR_HEIGHT;
    } while ((_mypartiterator = esp_partition_next(_mypartiterator)) != nullptr);
  }
  addHtml(F("</svg>\n"));
  esp_partition_iterator_release(_mypartiterator);
}

# endif // ifdef ESP32

# ifdef ESP8266
void getPartitionTableSVG() {
  // sketch / OTA / FS / EEPROM / RFcal / wifi
  const int nrPartitions = 6;
  const int shiftY       = 2;

  write_SVG_image_header(SVG_BAR_WIDTH + 250, nrPartitions * SVG_BAR_HEIGHT + shiftY);
  float yOffset = shiftY;

  for (int i = 0; i < nrPartitions; ++i) {
    const ESP8266_partition_type ptype = static_cast<ESP8266_partition_type>(i);
    uint32_t partitionAddress          = 0;
    int32_t  partitionSize             = 0;
    const int32_t partitionSector      = getPartitionInfo(ptype, partitionAddress, partitionSize);

    const __FlashStringHelper *label = F("");
    String descr;
    unsigned int partitionColor = 0xab56e6;

    switch (ptype) {
      case ESP8266_partition_type::sketch:
        label          = F("sketch");
        partitionColor = 0xab56e6;
        break;
      case ESP8266_partition_type::ota:
        label          = F("ota");
        partitionColor = 0x5856e6;
        break;
      case ESP8266_partition_type::fs:
        label          = F("fs");
        partitionColor = 0xff7f00;
        #  ifdef USE_LITTLEFS
        descr = F("LittleFS");
        #  else // ifdef USE_LITTLEFS
        descr = F("SPIFFS");
        #  endif // ifdef USE_LITTLEFS
        break;
      case ESP8266_partition_type::eeprom:
        label          = F("eeprom");
        descr          = concat(F("sector:"), partitionSector);
        partitionColor = 0x7fff00;
        break;
      case ESP8266_partition_type::rf_cal:
        label          = F("RFcal");
        partitionColor = 0xff007f;
        break;
      case ESP8266_partition_type::wifi:
        label          = F("WiFi");
        partitionColor = 0xff00ff;
        break;
    }

    drawPartitionChartSVG(
      yOffset,
      getFlashRealSizeInBytes(),
      partitionAddress,
      partitionSize,
      partitionColor,
      label,
      descr);
    yOffset += SVG_BAR_HEIGHT;

    /*
        String debuglog = concat(F("partition: "), (i+1));
        debuglog += concat(F(" FS_st: "), formatToHex((uint32_t)&_FS_start));
        debuglog += concat(F(" FS_end: "), formatToHex((uint32_t)&_FS_end));
        debuglog += concat(F(" EEPROM: "), formatToHex((uint32_t)&_EEPROM_start));
        debuglog += concat(F(" addr: "), formatToHex(partitionAddress, 8));
        debuglog += concat(F(" part.size: "), partitionSize);
        debuglog += concat(F(" label: "), label);
        addLog(LOG_LEVEL_INFO, debuglog);
     */
  }
  addHtml(F("</svg>\n"));
}

# endif // ifdef ESP8266
#endif // if FEATURE_CHART_STORAGE_LAYOUT

bool webArg2ip(const __FlashStringHelper *arg, uint8_t *IP) {
  return str2ip(webArg(arg), IP);
}

// #define WEBSERVER_RULES_DEBUG

#include "../WebServer/Rules.h"

#ifdef WEBSERVER_RULES

#include "../WebServer/ESPEasy_WebServer.h"
#include "../WebServer/AccessControl.h"
#include "../WebServer/HTML_wrappers.h"
#include "../WebServer/LoadFromFS.h"
#include "../WebServer/Markup.h"
#include "../WebServer/Markup_Buttons.h"
#include "../WebServer/Markup_Forms.h"

#include "../ESPEasyCore/ESPEasyRules.h"
#include "../ESPEasyCore/Serial.h"

#include "../Globals/Settings.h"
#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/Numerical.h"
#include "../Helpers/StringConverter.h"
#include "../Static/WebStaticData.h"

#include <FS.h>

// ********************************************************************************
// Web Interface rules page
// ********************************************************************************
void handle_rules() {
  # ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_rules"));
  # endif // ifndef BUILD_NO_RAM_TRACKER

  if (!isLoggedIn() || !Settings.UseRules) { return; }
  navMenuIndex = MENU_INDEX_RULES;
  const int rulesSet = getFormItemInt(F("set"), 1);

  const String fileName = strformat(
    #ifdef ESP8266
    F("rules%d.txt")
    #else
    F("/rules%d.txt")
    #endif
    , rulesSet);

  String error;

  // Make sure file exists
  if (!fileExists(fileName))
  {
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      addLogMove(LOG_LEVEL_INFO, concat(F("Rules : Create new file: %s"), fileName));
    }
    fs::File f = tryOpenFile(fileName, "w");

    if (f) { f.close(); }
  }

  TXBuffer.startStream();
  sendHeadandTail_stdtemplate(_HEAD);
  addHtmlError(error);

  html_table_class_normal();
  html_TR();
  html_table_header(F("Rules"));

  html_TR_TD();

  // Need a separate script to only include the 'set' attribute and not also
  // send the 'rules' as that will need a lot of memory on the ESP to process.
  addHtml(F("<form id='rulesselect' name='rulesselect' method='get'>"));
  {
    // Place combo box in its own scope to release these arrays as soon as possible
    int choice = rulesSet;
    String  options[RULESETS_MAX];
    int     optionValues[RULESETS_MAX];

    for (uint8_t x = 0; x < RULESETS_MAX; x++)
    {
      options[x]      = concat(F("Rules Set "), x+1);
      optionValues[x] = x + 1;
    }

    FormSelectorOptions selector(
      RULESETS_MAX,
      options,
      optionValues);

    selector.onChangeCall = F("return rules_set_onchange(rulesselect)");
    selector.addSelector(
      F("set"),
      choice);
    // addHelpButton(F("Tutorial_Rules")); // Old documentation is just that: Old and out-dated.
    addRTDHelpButton(F("Rules/Rules.html"));
  }

  html_TR_TD();
  Rule_showRuleTextArea(fileName);

  html_TR_TD();
  html_end_form();
  addHtml(F("<button id='save_button' class='button' onClick='saveRulesFile()'>Save</button>"));
  addHtmlDiv(EMPTY_STRING, F("Saved!"), F("toastmessage"));

  addButton(fileName, F("Download to file"));
  html_end_table();

  sendHeadandTail_stdtemplate(_TAIL);
  TXBuffer.endStream();

  checkRuleSets();
}

// ********************************************************************************
// Web Interface rules page  (NEW)
// ********************************************************************************
void handle_rules_new() {
  if (!isLoggedIn() || !Settings.UseRules) { return; }

  if (!clientIPallowed()) { return; }

  if (Settings.OldRulesEngine())
  {
    handle_rules();
    return;
  }
  # ifdef WEBSERVER_NEW_RULES
  #  ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_rules"));
  #  endif // ifndef BUILD_NO_RAM_TRACKER
  navMenuIndex = 5;
  TXBuffer.startStream();
  sendHeadandTail(F("TmplStd"), _HEAD);

  // define macro
  #  if defined(ESP8266)
  String rootPath = F("rules");
  #  endif // if defined(ESP8266)
  #  if defined(ESP32)

  String rootPath = F("/rules");
  #  endif // if defined(ESP32)

  // Pagionation of rules list
  const int rulesListPageSize = 25;
  int32_t startIdx            = 0;

  const String fstart = webArg(F("start"));

  if (fstart.length() > 0)
  {
    validIntFromString(fstart, startIdx);
  }
  int endIdx = startIdx + rulesListPageSize - 1;

  // Build table header
  html_table_class_multirow();
  html_TR();
  html_table_header(F("Event Name"));
  html_table_header(F("Filename"));
  html_table_header(F("Size"));
  addHtml(F("<TH>Actions"));
  addSaveButton(F("/rules/backup"), F("Backup"));
  addHtml(F("</TH></TR>"));

  // class StreamingBuffer buffer = TXBuffer;

  // Build table detail
  int count                    = -1;
  HandlerFileInfo renderDetail =
    [/*&buffer,*/ &count, endIdx](fileInfo fi)
    {
#  ifdef WEBSERVER_RULES_DEBUG
      ESPEASY_SERIAL_0.print(F("Start generation of: "));
      ESPEASY_SERIAL_0.println(fi.Name);
#  endif // ifdef WEBSERVER_RULES_DEBUG

      if (fi.isDirectory)
      {
        html_TR_TD();
      }
      else
      {
        count++;
        addHtml(F("<TR><TD style='text-align:right'>"));
      }

      // Event Name
      addHtml(FileNameToEvent(fi.Name));

      if (fi.isDirectory)
      {
        addHtml(F("</TD><TD></TD><TD></TD><TD>"));
        addSaveButton(concat(F("/rules/backup?directory="), URLEncode(fi.Name))
                      , F("Backup")
                      );
      }
      else
      {
        String encodedPath =  URLEncode(String(fi.Name + F(".txt")));

        // File Name
        addHtml(F("</TD><TD><a href='"));
        addHtml(String(fi.Name));
        addHtml(F(".txt'>"));
        addHtml(String(fi.Name));
        addHtml(F(".txt</a></TD>"));

        // File size
        html_TD();
        addHtmlInt(fi.Size);
        addHtml(F("</TD>"));

        // Actions
        html_TD();
        addSaveButton(concat(F("/rules/backup?fileName="), encodedPath)
                      , F("Backup")
                      );

        addDeleteButton(concat(F("/rules/delete?fileName="), encodedPath)
                        , F("Delete")
                        );
      }
      addHtml(F("</TD></TR>"));
#  ifdef WEBSERVER_RULES_DEBUG
      ESPEASY_SERIAL_0.print(F("End generation of: "));
      ESPEASY_SERIAL_0.println(fi.Name);
#  endif // ifdef WEBSERVER_RULES_DEBUG

      return count < endIdx;
    };


  bool hasMore = EnumerateFileAndDirectory(rootPath
                                           , startIdx
                                           , renderDetail);
  html_TR_TD();
  addButton(F("/rules/add"), F("Add"));
  addHtml(F("</TD><TD></TD><TD></TD><TD></TD></TR>"));
  addHtml(F("</table>"));

  if (startIdx > 0)
  {
    int showIdx = startIdx - rulesListPageSize;

    if (showIdx < 0) { showIdx = 0; }
    addButton(concat(F("/rules?start="), showIdx)
              , F("Previous"));
  }

  if (hasMore && (count >= endIdx))
  {
    addButton(concat(F("/rules?start="), endIdx + 1)
              , F("Next"));
  }

  // TXBuffer += F("<BR><BR>");
  sendHeadandTail(F("TmplStd"), _TAIL);
  TXBuffer.endStream();
  #  ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_rules"));
  #  endif // ifndef BUILD_NO_RAM_TRACKER
  # endif  // WEBSERVER_NEW_RULES
}

void handle_rules_backup() {
  if (Settings.OldRulesEngine())
  {
    Goto_Rules_Root();
    return;
  }
  # ifdef WEBSERVER_NEW_RULES
  #  ifdef WEBSERVER_RULES_DEBUG
  ESPEASY_SERIAL_0.println(F("handle rules backup"));
  #  endif // ifdef WEBSERVER_RULES_DEBUG

  if (!isLoggedIn() || !Settings.UseRules) { return; }

  if (!clientIPallowed()) { return; }
  #  ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_rules_backup"));
  #  endif // ifndef BUILD_NO_RAM_TRACKER
  String directory = webArg(F("directory"));
  String fileName  = webArg(F("fileName"));
  String error;

  if (directory.length() > 0)
  {
    HandlerFileInfo downloadHandler = [&error](fileInfo fi) {
                                        if (!fi.isDirectory)
                                        {
                                          if (!Rule_Download(fi.Name))
                                          {
                                            error += concat(F("Invalid path: "), fi.Name);
                                          }
                                        }
                                        return true;
                                      };
    EnumerateFileAndDirectory(directory
                              , 0
                              , downloadHandler);
  }
  else if (fileName.length() > 0) {
    fileName = fileName.substring(0, fileName.length() - 4);

    if (!Rule_Download(fileName))
    {
      error = concat(F("Invalid path: "), fileName);
    }
  }
  else
  {
    error = F("Invalid parameters");
  }

  if (error.length() > 0) {
    TXBuffer.startStream();
    sendHeadandTail(F("TmplMsg"), _HEAD);
    addHtmlError(error);
    TXBuffer.endStream();
  }
  #  ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_rules_backup"));
  #  endif // ifndef BUILD_NO_RAM_TRACKER
  # endif  // WEBSERVER_NEW_RULES
}

void handle_rules_delete() {
  if (!isLoggedIn() || !Settings.UseRules) { return; }

  if (!clientIPallowed()) { return; }

  if (Settings.OldRulesEngine())
  {
    Goto_Rules_Root();
    return;
  }
  # ifdef WEBSERVER_NEW_RULES
  #  ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_rules_delete"));
  #  endif // ifndef BUILD_NO_RAM_TRACKER
  String fileName = webArg(F("fileName"));
  fileName = fileName.substring(0, fileName.length() - 4);
  bool removed = false;
  #  ifdef WEBSERVER_RULES_DEBUG
  ESPEASY_SERIAL_0.println(F("handle_rules_delete"));
  ESPEASY_SERIAL_0.print(F("File name: "));
  ESPEASY_SERIAL_0.println(fileName);
  #  endif // ifdef WEBSERVER_RULES_DEBUG

  if (fileName.length() > 0)
  {
    removed = tryDeleteFile(fileName);
  }

  if (removed)
  {
    sendHeader(F("Location"), F("/rules"), true);
    web_server.send(302, F("text/plain"), EMPTY_STRING);
  }
  else
  {
    String error = concat(F("Delete rule Invalid path: "), fileName);
    addLog(LOG_LEVEL_ERROR, error);
    TXBuffer.startStream();
    sendHeadandTail(F("TmplMsg"), _HEAD);
    addHtmlError(error);
    sendHeadandTail(F("TmplMsg"), _TAIL);
    TXBuffer.endStream();
  }
  #  ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_rules_delete"));
  #  endif // ifndef BUILD_NO_RAM_TRACKER
  # endif  // WEBSERVER_NEW_RULES
}

bool handle_rules_edit(const String& originalUri)
{
  return handle_rules_edit(originalUri, false);
}

bool handle_rules_edit(String originalUri, bool isAddNew) {
  // originalUri is passed via deepcopy, since it will be converted to lower case.
  originalUri.toLowerCase();
  # ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_rules_edit"));
  # endif // ifndef BUILD_NO_RAM_TRACKER
  bool handle = false;

  # ifdef WEBSERVER_RULES_DEBUG
  ESPEASY_SERIAL_0.println(originalUri);
  ESPEASY_SERIAL_0.println(F("handle_rules_edit"));
  # endif // ifdef WEBSERVER_RULES_DEBUG

  if (isAddNew || (originalUri.startsWith(F("/rules/"))
                   && originalUri.endsWith(F(".txt")))) {
    if (!isLoggedIn() || !Settings.UseRules) { return false; }

    if (Settings.OldRulesEngine())
    {
      Goto_Rules_Root();
      return true;
    }
    # ifdef WEBSERVER_NEW_RULES

    String eventName;
    String fileName;
    bool   isOverwrite = false;
    bool   isNew       = false;
    String error;


    if (isAddNew)
    {
      eventName = webArg(F("eventName"));
      fileName += EventToFileName(eventName);
    }
    else
    {
        #  if defined(ESP8266)
      fileName = F("rules/");
        #  endif // if defined(ESP8266)
        #  if defined(ESP32)
      fileName = F("/rules/");
        #  endif // if defined(ESP32)
      fileName += originalUri.substring(7, originalUri.length() - 4);
      eventName = FileNameToEvent(fileName);
    }
      #  ifdef WEBSERVER_RULES_DEBUG
    ESPEASY_SERIAL_0.print(F("File name: "));
    ESPEASY_SERIAL_0.println(fileName);
      #  endif // ifdef WEBSERVER_RULES_DEBUG
    bool isEdit = fileExists(fileName);

    if (web_server.args() > 0)
    {
      const String& rules = webArg(F("rules"));
      isNew = equals(webArg(F("IsNew")), F("yes"));

      // Overwrite verification
      if (isEdit && isNew) {
        error = concat(F("There is another rule with the same name: "),
                fileName);
        addLog(LOG_LEVEL_ERROR, error);
        isAddNew    = true;
        isOverwrite = true;
      }
      else if (!hasArg(F("rules")))
      {
        error = F("Data was not saved, rules argument missing or corrupted");
        addLog(LOG_LEVEL_ERROR, error);
      }

      // Check rules size
      else if (rules.length() > RULES_MAX_SIZE)
      {
        error = concat(F("Data was not saved, exceeds web editor limit! "),
                fileName);
        addLog(LOG_LEVEL_ERROR, error);
      }

      // Passed all checks, write file
      else
      {
        fs::File f = tryOpenFile(fileName, "w");

        if (f)
        {
          addLog(LOG_LEVEL_INFO, String(F(" Write to file: ")) + fileName);
          f.print(rules);
          f.close();
        }

        if (isAddNew) {
          sendHeader(F("Location"), F("/rules"), true);
          web_server.send(302, F("text/plain"), EMPTY_STRING);
          return true;
        }
      }
    }
    navMenuIndex = 5;
    TXBuffer.startStream();
    sendHeadandTail(F("TmplStd"));

    if (error.length() > 0) {
      addHtmlError(error);
    }
    addHtml(F("<form name = 'editRule' method = 'post'><table class='normal'><TR><TH align='left' colspan='2'>Edit Rule"));

    // hidden field to check Overwrite
    addHtml(F("<input "));
    addHtmlAttribute(F("type"),  F("hidden"));
    addHtmlAttribute(F("id"),    F("IsNew"));
    addHtmlAttribute(F("name"),  F("IsNew"));
    addHtmlAttribute(F("value"), isAddNew ? F("yes") : F("no"));
    addHtml('>');

    bool isReadOnly = !isOverwrite && ((isEdit && !isAddNew && !isNew) || (isAddNew && isNew));
      #  ifdef WEBSERVER_RULES_DEBUG
    ESPEASY_SERIAL_0.print(F("Is Overwrite: "));
    ESPEASY_SERIAL_0.println(isOverwrite);
    ESPEASY_SERIAL_0.print(F("Is edit: "));
    ESPEASY_SERIAL_0.println(isEdit);
    ESPEASY_SERIAL_0.print(F("Is addnew: "));
    ESPEASY_SERIAL_0.println(isAddNew);
    ESPEASY_SERIAL_0.print(F("Is New: "));
    ESPEASY_SERIAL_0.println(isNew);
    ESPEASY_SERIAL_0.print(F("Is Read Only: "));
    ESPEASY_SERIAL_0.println(isReadOnly);
      #  endif // ifdef WEBSERVER_RULES_DEBUG

    addFormTextBox(F("Event name")            // Label
                   , F("eventName")           // field name
                   , eventName                // field value
                   , RULE_MAX_FILENAME_LENGTH // max length
                   , isReadOnly               // is readonly
                   , isAddNew                 // required
                   , F("[A-Za-z]+#.+")        // validation pattern
                   );
    addHelpButton(F("Tutorial_Rules"));

    // load form data from flash
    addHtml(F("<TR><TD colspan='2'>"));

    Rule_showRuleTextArea(fileName);

    addFormSeparator(2);
    html_TR_TD();
    addSubmitButton();

    addHtml(F("</table></form>"));

    sendHeadandTail(F("TmplStd"), true);
    TXBuffer.endStream();
    # endif // WEBSERVER_NEW_RULES
  }
  # ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_rules_edit2"));
  # endif // ifndef BUILD_NO_RAM_TRACKER
  return handle;
}

void Rule_showRuleTextArea(const String& fileName) {
  // Read rules from file and stream directly into the textarea

  size_t size = 0;

  addHtml(F("<textarea id='rules' name='rules' rows='30' wrap='off'>"));
  size = streamFromFS(fileName, true);
  addHtml(F("</textarea>"));
  #if FEATURE_RULES_EASY_COLOR_CODE
  addHtml(F("<script>initCM();</script>"));
  #endif

  html_TR_TD();
  addHtml(F("Current size: <span id='size'>"));
  addHtmlInt(size);
  addHtml(F("</span> characters"));
}

bool Rule_Download(const String& path)
{
  # ifdef WEBSERVER_RULES_DEBUG
  ESPEASY_SERIAL_0.print(F("Rule_Download path: "));
  ESPEASY_SERIAL_0.println(path);
  # endif // ifdef WEBSERVER_RULES_DEBUG
  fs::File dataFile = tryOpenFile(path, "r");

  if (!dataFile)
  {
    addLog(LOG_LEVEL_ERROR, concat(F("Invalid path: "), path));
    return false;
  }
  String filename = concat(path, F(".txt"));
  filename.replace(RULE_FILE_SEPARAROR, '_');
  String str = concat(F("attachment; filename="), filename);
  sendHeader(F("Content-Disposition"), str);
  sendHeader(F("Cache-Control"),       F("max-age=3600, public"));
  sendHeader(F("Vary"),                "*");
  sendHeader(F("ETag"),                F("\"2.0.0\""));

  web_server.streamFile(dataFile, F("application/octet-stream"));
  dataFile.close();
  return true;
}

void Goto_Rules_Root() {
  sendHeader(F("Location"), F("/rules"), true);
  web_server.send(302, F("text/plain"), EMPTY_STRING);
}

bool EnumerateFileAndDirectory(String          & rootPath
                               , int             skip
                               , HandlerFileInfo handler)
{
  bool hasMore = false;
  int  count   = 0;
  bool next    = true;

  # ifdef ESP8266
  fs::Dir dir = ESPEASY_FS.openDir(rootPath);
  ESPEASY_SERIAL_0.print(F("Enumerate files of "));
  ESPEASY_SERIAL_0.println(rootPath);

  while (next && dir.next()) {
    // Skip files
    if (count++ < skip) {
      continue;
    }

    // Workaround for skipped unwanted files
    if (!dir.fileName().startsWith(rootPath + "/")) {
      continue;
    }
    fileInfo fi;
    fi.Name = dir.fileName() /*.substring(rootPath.length())*/;
    fs::File f = dir.openFile("r");
    fi.Size = f.size();
    f.close();
    next = handler(fi);
  }
  hasMore = dir.next();
  # endif // ifdef ESP8266
  # ifdef ESP32
  fs::File root = ESPEASY_FS.open(rootPath);

  if (root)
  {
    fs::File file = root.openNextFile();

    while (next && file) {
      if (count >= skip) {
        fileInfo fi;
        fi.Name        = file.name();
        fi.Size        = file.size();
        fi.isDirectory = file.isDirectory();
        next           = handler(fi);
      }

      if (!file.isDirectory()) {
        ++count;
      }
      file = root.openNextFile();
    }
    hasMore = file;
  }
  else
  {
    addLog(LOG_LEVEL_ERROR, F("Invalid root."));
  }
  # endif // ifdef ESP32
  return hasMore;
}

#endif // ifdef WEBSERVER_RULES

#include "../WebServer/404.h"

#include "../WebServer/ESPEasy_WebServer.h"
#include "../WebServer/HTML_wrappers.h"
#include "../WebServer/LoadFromFS.h"
#include "../WebServer/Rules.h"

#include "../Globals/Services.h"
#include "../Globals/Settings.h"

#include "../Globals/ESPEasyWiFiEvent.h"

// ********************************************************************************
// Web Interface handle other requests
// ********************************************************************************
void handleNotFound() {
  # ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  # endif // ifdef USE_SECOND_HEAP

  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handleNotFound"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  if (loadFromFS(web_server.uri())) { return; }

  if (captivePortal()) { // If captive portal redirect instead of displaying the error page.
    return;
  }

  // if Wifi setup, launch setup wizard if AP_DONT_FORCE_SETUP is not set.
  if (WiFiEventData.wifiSetup && !Settings.ApDontForceSetup())
  {
    web_server.send_P(200, (PGM_P)F("text/html"), (PGM_P)F("<meta HTTP-EQUIV='REFRESH' content='0; url=/setup'>"));
    return;
  }

#ifdef WEBSERVER_RULES

  if (handle_rules_edit(web_server.uri())) { return; }
#endif // ifdef WEBSERVER_RULES

  TXBuffer.startStream(F("text/plain"), F(""), 404);
  addHtml(F("URI: "));
  addHtml(web_server.uri());
  addHtml(F("\nMethod: "));
  addHtml((web_server.method() == HTTP_GET) ? F("GET") : F("POST"));

  addHtml(F("\nArguments: "));
  addHtmlInt(web_server.args());

#ifndef BUILD_NO_DEBUG
  for (uint8_t i = 0; i < web_server.args(); i++) {
    addHtml('\n');
    addHtml(F(" NAME:"));
    addHtml(web_server.argName(i));
    addHtml(F("\n VALUE:"));
    addHtml(webArg(i));
  }
  addHtml(F("\nHeaders: "));
  for (int i = web_server.headers(); i >= 0; --i) {
    if (!web_server.headerName(i).isEmpty()) {
      addHtml('\n');
      addHtml(F(" NAME:"));
      addHtml(web_server.headerName(i));
      addHtml(F("\n VALUE:"));
      addHtml(web_server.header(i));
    }
  }
#endif
  TXBuffer.endStream();
}

#include "../WebServer/AdvancedConfigPage.h"

#ifdef WEBSERVER_ADVANCED

#include "../WebServer/HTML_wrappers.h"
#include "../WebServer/Markup.h"
#include "../WebServer/Markup_Buttons.h"
#include "../WebServer/Markup_Forms.h"
#include "../WebServer/ESPEasy_WebServer.h"

#include "../ESPEasyCore/ESPEasyWifi.h"

#include "../Globals/ESPEasy_time.h"
#include "../Globals/Settings.h"
#include "../Globals/TimeZone.h"

#include "../Helpers/_Plugin_Helper_serial.h"
#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/ESPEasy_time.h"
#include "../Helpers/Hardware_defines.h"
#include "../Helpers/StringConverter.h"

#if FEATURE_I2C_MULTIPLE
#include "../Helpers/I2C_access.h"
#endif

void setLogLevelFor(uint8_t destination, LabelType::Enum label) {
  setLogLevelFor(destination, getFormItemInt(getInternalLabel(label)));
}

// ********************************************************************************
// Web Interface config page
// ********************************************************************************
void handle_advanced() {
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_advanced"));
  #endif

  if (!isLoggedIn()) { return; }
  navMenuIndex = MENU_INDEX_TOOLS;
  TXBuffer.startStream();
  sendHeadandTail_stdtemplate(_HEAD);

  if (!webArg(F("edit")).isEmpty())
  {
//    Settings.MessageDelay_unused = getFormItemInt(F("messagedelay"));
    Settings.IP_Octet     = webArg(F("ip")).toInt();
    strncpy_webserver_arg(Settings.NTPHost, F("ntphost"));
    Settings.TimeZone = getFormItemInt(F("timezone"));
    TimeChangeRule dst_start(getFormItemInt(F("dststartweek")), getFormItemInt(F("dststartdow")), getFormItemInt(F("dststartmonth")), getFormItemInt(F("dststarthour")), Settings.TimeZone);

    if (dst_start.isValid()) { Settings.DST_Start = dst_start.toFlashStoredValue(); }
    TimeChangeRule dst_end(getFormItemInt(F("dstendweek")), getFormItemInt(F("dstenddow")), getFormItemInt(F("dstendmonth")), getFormItemInt(F("dstendhour")), Settings.TimeZone);

    if (dst_end.isValid()) { Settings.DST_End = dst_end.toFlashStoredValue(); }
    webArg2ip(F("syslogip"), Settings.Syslog_IP);
    Settings.WebserverPort = getFormItemInt(F("webport"));
    Settings.UDPPort = getFormItemInt(F("udpport"));

    Settings.SyslogFacility = getFormItemInt(F("syslogfacility"));
    Settings.SyslogPort     = getFormItemInt(F("syslogport"));
    Settings.UseSerial      = isFormItemChecked(LabelType::ENABLE_SERIAL_PORT_CONSOLE);

#if FEATURE_DEFINE_SERIAL_CONSOLE_PORT
    Settings.console_serial_rxpin = getFormItemInt(F("taskdevicepin1"), Settings.console_serial_rxpin);
    Settings.console_serial_txpin = getFormItemInt(F("taskdevicepin2"), Settings.console_serial_txpin);

    serialHelper_webformSave(
      Settings.console_serial_port, 
      Settings.console_serial_rxpin,
      Settings.console_serial_txpin);
#if USES_ESPEASY_CONSOLE_FALLBACK_PORT
    Settings.console_serial0_fallback = isFormItemChecked(LabelType::CONSOLE_FALLBACK_TO_SERIAL0);
#endif

#endif
    setLogLevelFor(LOG_TO_SYSLOG, LabelType::SYSLOG_LOG_LEVEL);
    setLogLevelFor(LOG_TO_SERIAL, LabelType::SERIAL_LOG_LEVEL);
    setLogLevelFor(LOG_TO_WEBLOG, LabelType::WEB_LOG_LEVEL);
#if FEATURE_SD
    setLogLevelFor(LOG_TO_SDCARD, LabelType::SD_LOG_LEVEL);
#endif // if FEATURE_SD
    Settings.UseValueLogger              = isFormItemChecked(F("valuelogger"));
    Settings.BaudRate                    = getFormItemInt(F("baudrate"));
    Settings.UseNTP(isFormItemChecked(F("usentp")));
    Settings.ExtTimeSource(
      static_cast<ExtTimeSource_e>(getFormItemInt(F("exttimesource")))
    );
    #if FEATURE_I2C_MULTIPLE
    if (getI2CBusCount() > 1) {
      set3BitToUL(Settings.I2C_peripheral_bus, I2C_PERIPHERAL_BUS_CLOCK, getFormItemInt(F("pi2cbusrtc")));
      set3BitToUL(Settings.I2C_peripheral_bus, I2C_PERIPHERAL_BUS_WDT,   getFormItemInt(F("pi2cbuswdt")));
    }
    #endif // if FEATURE_I2C_MULTIPLE
    Settings.DST                         = isFormItemChecked(F("dst"));
    Settings.WDI2CAddress                = getFormItemInt(F("wdi2caddress"));
    #if FEATURE_SSDP
    Settings.UseSSDP                     = isFormItemChecked(F("usessdp"));
    #endif // if FEATURE_SSDP
    Settings.UseRules                    = isFormItemChecked(F("userules"));
    Settings.ConnectionFailuresThreshold = getFormItemInt(LabelType::CONNECTION_FAIL_THRESH);
    Settings.ArduinoOTAEnable            = isFormItemChecked(F("arduinootaenable"));
    Settings.UseRTOSMultitasking         = isFormItemChecked(F("usertosmultitasking"));

    // MQTT settings now moved to the controller settings.
//    Settings.MQTTRetainFlag_unused              = isFormItemChecked(F("mqttretainflag"));
//    Settings.MQTTUseUnitNameAsClientId   = isFormItemChecked(F("mqttuseunitnameasclientid"));
//    Settings.uniqueMQTTclientIdReconnect(isFormItemChecked(F("uniquemqttclientidreconnect")));
    Settings.Latitude  = getFormItemFloat(F("latitude"));
    Settings.Longitude = getFormItemFloat(F("longitude"));
    #ifdef WEBSERVER_NEW_RULES
    Settings.OldRulesEngine(isFormItemChecked(F("oldrulesengine")));
    #endif // WEBSERVER_NEW_RULES
    Settings.TolerantLastArgParse(isFormItemChecked(F("tolerantargparse")));
    Settings.SendToHttp_ack(isFormItemChecked(F("sendtohttp_ack")));
    Settings.SendToHTTP_follow_redirects(isFormItemChecked(F("sendtohttp_redir")));
    Settings.ForceWiFi_bg_mode(isFormItemChecked(LabelType::FORCE_WIFI_BG));
    Settings.WiFiRestart_connection_lost(isFormItemChecked(LabelType::RESTART_WIFI_LOST_CONN));
    Settings.EcoPowerMode(isFormItemChecked(LabelType::CPU_ECO_MODE));
    Settings.WifiNoneSleep(isFormItemChecked(LabelType::FORCE_WIFI_NOSLEEP));
#ifdef SUPPORT_ARP
    Settings.gratuitousARP(isFormItemChecked(LabelType::PERIODICAL_GRAT_ARP));
#endif // ifdef SUPPORT_ARP
#if FEATURE_SET_WIFI_TX_PWR
    Settings.setWiFi_TX_power(getFormItemFloat(LabelType::WIFI_TX_MAX_PWR));
    Settings.WiFi_sensitivity_margin = getFormItemInt(LabelType::WIFI_SENS_MARGIN);
    Settings.UseMaxTXpowerForSending(isFormItemChecked(LabelType::WIFI_SEND_AT_MAX_TX_PWR));
#endif
    Settings.NumberExtraWiFiScans = getFormItemInt(LabelType::WIFI_NR_EXTRA_SCANS);
    Settings.UseLastWiFiFromRTC(isFormItemChecked(LabelType::WIFI_USE_LAST_CONN_FROM_RTC));
    Settings.JSONBoolWithoutQuotes(isFormItemChecked(LabelType::JSON_BOOL_QUOTES));
#if FEATURE_TIMING_STATS
    Settings.EnableTimingStats(isFormItemChecked(LabelType::ENABLE_TIMING_STATISTICS));
#endif
    Settings.AllowTaskValueSetAllPlugins(isFormItemChecked(LabelType::TASKVALUESET_ALL_PLUGINS));
#if FEATURE_CLEAR_I2C_STUCK
    Settings.EnableClearHangingI2Cbus(isFormItemChecked(LabelType::ENABLE_CLEAR_HUNG_I2C_BUS));
#endif
    #if FEATURE_I2C_DEVICE_CHECK
    Settings.CheckI2Cdevice(isFormItemChecked(LabelType::ENABLE_I2C_DEVICE_CHECK));
    #endif // if FEATURE_I2C_DEVICE_CHECK
#ifndef ESP32
    Settings.WaitWiFiConnect(isFormItemChecked(LabelType::WAIT_WIFI_CONNECT));
#endif
    Settings.HiddenSSID_SlowConnectPerBSSID(isFormItemChecked(LabelType::HIDDEN_SSID_SLOW_CONNECT));
    Settings.SDK_WiFi_autoreconnect(isFormItemChecked(LabelType::SDK_WIFI_AUTORECONNECT));
#ifdef ESP32
    Settings.PassiveWiFiScan(isFormItemChecked(LabelType::WIFI_PASSIVE_SCAN));
#endif
#if FEATURE_USE_IPV6
    Settings.EnableIPv6(isFormItemChecked(LabelType::ENABLE_IPV6));
#endif



#ifndef BUILD_NO_RAM_TRACKER
    Settings.EnableRAMTracking(isFormItemChecked(LabelType::ENABLE_RAM_TRACKING));
#endif

    #ifdef ESP8266
    Settings.UseAlternativeDeepSleep(isFormItemChecked(LabelType::DEEP_SLEEP_ALTERNATIVE_CALL));
    #endif

    Settings.EnableRulesCaching(isFormItemChecked(LabelType::ENABLE_RULES_CACHING));
//    Settings.EnableRulesEventReorder(isFormItemChecked(LabelType::ENABLE_RULES_EVENT_REORDER)); // TD-er: Disabled for now

#ifndef NO_HTTP_UPDATER
    Settings.AllowOTAUnlimited(isFormItemChecked(LabelType::ALLOW_OTA_UNLIMITED));
#endif // NO_HTTP_UPDATER
#if FEATURE_AUTO_DARK_MODE
    Settings.setCssMode(getFormItemInt(getInternalLabel(LabelType::ENABLE_AUTO_DARK_MODE)));
#endif // FEATURE_AUTO_DARK_MODE
#if FEATURE_RULES_EASY_COLOR_CODE
    Settings.DisableRulesCodeCompletion(isFormItemChecked(LabelType::DISABLE_RULES_AUTOCOMPLETE));
#endif // if FEATURE_RULES_EASY_COLOR_CODE
#if FEATURE_TARSTREAM_SUPPORT
    Settings.DisableSaveConfigAsTar(isFormItemChecked(LabelType::DISABLE_SAVE_CONFIG_AS_TAR));
#endif // if FEATURE_TARSTREAM_SUPPORT
    #if FEATURE_TASKVALUE_UNIT_OF_MEASURE
    Settings.ShowUnitOfMeasureOnDevicesPage(isFormItemChecked(LabelType::SHOW_UOM_ON_DEVICES_PAGE));
    #endif // if FEATURE_TASKVALUE_UNIT_OF_MEASURE
    #if FEATURE_MQTT_CONNECT_BACKGROUND
    Settings.MQTTConnectInBackground(isFormItemChecked(LabelType::MQTT_CONNECT_IN_BACKGROUND));
    #endif // if FEATURE_MQTT_CONNECT_BACKGROUND

    addHtmlError(SaveSettings());

    if (node_time.systemTimePresent()) {
      node_time.initTime();
    }
  }

  addHtml(F("<form  method='post'>"));
  html_table_class_normal();

  addFormHeader(F("Advanced Settings"), F("RTDTools/Tools.html#advanced"));

  addFormSubHeader(F("Rules Settings"));

  addFormCheckBox(F("Rules"),      F("userules"),       Settings.UseRules);
  #ifdef WEBSERVER_NEW_RULES
  addFormCheckBox(F("Old Engine"), F("oldrulesengine"), Settings.OldRulesEngine());
  #endif // WEBSERVER_NEW_RULES
  addFormCheckBox(LabelType::ENABLE_RULES_CACHING, Settings.EnableRulesCaching());
//  addFormCheckBox(LabelType::ENABLE_RULES_EVENT_REORDER, Settings.EnableRulesEventReorder()); // TD-er: Disabled for now

  addFormCheckBox(F("Tolerant last parameter"), F("tolerantargparse"), Settings.TolerantLastArgParse());
  addFormNote(F("Perform less strict parsing on last argument of some commands (e.g. publish and sendToHttp)"));
  addFormCheckBox(F("SendToHTTP wait for ack"), F("sendtohttp_ack"), Settings.SendToHttp_ack());
  addFormCheckBox(F("SendToHTTP Follow Redirects"), F("sendtohttp_redir"), Settings.SendToHTTP_follow_redirects());

  /*
  // MQTT settings now moved to the controller settings.
  addFormSubHeader(F("Controller Settings"));

  addFormNumericBox(F("Message Interval"), F("messagedelay"), Settings.MessageDelay_unused, 0, INT_MAX);
  addUnit(F("ms"));

  addFormCheckBox(F("MQTT Retain Msg"), F("mqttretainflag"), Settings.MQTTRetainFlag_unused);
  addFormCheckBox(F("MQTT use unit name as ClientId"),    F("mqttuseunitnameasclientid"),   Settings.MQTTUseUnitNameAsClientId);
  addFormCheckBox(F("MQTT change ClientId at reconnect"), F("uniquemqttclientidreconnect"), Settings.uniqueMQTTclientIdReconnect_unused());
*/

  addFormSubHeader(F("Time Source"));

  addFormCheckBox(F("Use NTP"), F("usentp"), Settings.UseNTP());
  addFormTextBox(F("NTP Hostname"), F("ntphost"), Settings.NTPHost, 63);
  #if FEATURE_EXT_RTC
  addFormExtTimeSourceSelect(F("External Time Source"), F("exttimesource"), Settings.ExtTimeSource());
  if (Settings.ExtTimeSource() != ExtTimeSource_e::None) {
    addFormNote(concat(getLabel(LabelType::EXT_RTC_UTC_TIME), F(": ")) + getValue(LabelType::EXT_RTC_UTC_TIME));
  }
  #if FEATURE_I2C_MULTIPLE
  {
    const uint8_t i2cBus = Settings.getI2CInterfaceRTC();
    I2CInterfaceSelector(F("Ext. Time Source I2C Bus"),
                        F("pi2cbusrtc"),
                        i2cBus,
                        false);
  }
  #endif // if FEATURE_I2C_MULTIPLE
  #endif

  addFormSubHeader(F("DST Settings"));
  addFormDstSelect(true,  Settings.DST_Start);
  addFormDstSelect(false, Settings.DST_End);
  addFormCheckBox(F("DST"), F("dst"), Settings.DST);

  addFormSubHeader(F("Location Settings"));
  addFormNumericBox(F("Timezone Offset (UTC +)"), F("timezone"), Settings.TimeZone, -720, 840); // UTC-12H ... UTC+14h
  addUnit(F("minutes"));
  addFormFloatNumberBox(F("Latitude"), F("latitude"), Settings.Latitude, -90.0f, 90.0f);
  addUnit(F("&deg;"));
  addFormFloatNumberBox(F("Longitude"), F("longitude"), Settings.Longitude, -180.0f, 180.0f);
  addUnit(F("&deg;"));
  addFormNote(F("Longitude and Latitude are used to compute sunrise and sunset"));

  addFormSubHeader(F("Log Settings"));

  addFormIPBox(F("Syslog IP"), F("syslogip"), Settings.Syslog_IP);
  addFormNumericBox(F("Syslog UDP port"), F("syslogport"), Settings.SyslogPort, 0, 65535);

  addFormLogLevelSelect(LabelType::SYSLOG_LOG_LEVEL, Settings.SyslogLevel);
  addFormLogFacilitySelect(F("Syslog Facility"), F("syslogfacility"), Settings.SyslogFacility);
  addFormLogLevelSelect(LabelType::SERIAL_LOG_LEVEL, Settings.SerialLogLevel);
  addFormLogLevelSelect(LabelType::WEB_LOG_LEVEL,    Settings.WebLogLevel);

#if FEATURE_SD
  addFormLogLevelSelect(LabelType::SD_LOG_LEVEL,     Settings.SDLogLevel);

  addFormCheckBox(F("SD Card Value Logger"), F("valuelogger"), Settings.UseValueLogger);
#endif // if FEATURE_SD


  addFormSubHeader(F("Serial Console Settings"));
  addFormCheckBox(LabelType::ENABLE_SERIAL_PORT_CONSOLE, Settings.UseSerial);
  addFormNumericBox(F("Baud Rate"), F("baudrate"), Settings.BaudRate, 0, 1000000);

#if FEATURE_DEFINE_SERIAL_CONSOLE_PORT
  serialHelper_webformLoad(
    static_cast<ESPEasySerialPort>(Settings.console_serial_port), 
    Settings.console_serial_rxpin, 
    Settings.console_serial_txpin, 
    true);

  // Show serial port selection
  addFormPinSelect(
    PinSelectPurpose::Serial_input, 
    formatGpioName_serialRX(false),
    F("taskdevicepin1"), 
    Settings.console_serial_rxpin);
  addFormPinSelect(
    PinSelectPurpose::Serial_output, 
    formatGpioName_serialTX(false),
    F("taskdevicepin2"), 
    Settings.console_serial_txpin);

  html_add_script(F("document.getElementById('serPort').onchange();"), false);
#if USES_ESPEASY_CONSOLE_FALLBACK_PORT
  addFormCheckBox(LabelType::CONSOLE_FALLBACK_TO_SERIAL0, Settings.console_serial0_fallback);
#endif

#endif


  addFormSubHeader(F("Inter-ESPEasy Network"));
  if (Settings.UDPPort != 8266 ) addFormNote(F("Preferred P2P port is 8266"));
  addFormNumericBox(F("ESPEasy p2p UDP port"), F("udpport"), Settings.UDPPort, 0, 65535);

  // TODO sort settings in groups or move to other pages/groups
  addFormSubHeader(F("Special and Experimental Settings"));

  addFormNumericBox(F("Webserver port"), F("webport"), Settings.WebserverPort, 0, 65535);
  addFormNote(F("Requires reboot to activate"));

  addFormNumericBox(F("Fixed IP Octet"), F("ip"),           Settings.IP_Octet,     0, 255);

  addFormNumericBox(F("WD I2C Address"), F("wdi2caddress"), Settings.WDI2CAddress, 0, 127);
  addHtml(F(" (decimal)"));
  #if FEATURE_I2C_MULTIPLE
  {
    const uint8_t i2cBus = Settings.getI2CInterfaceWDT();
    I2CInterfaceSelector(F("WD I2C Bus"),
                        F("pi2cbuswdt"),
                        i2cBus,
                        false);
  }
  #endif // if FEATURE_I2C_MULTIPLE

  // TODO: Remove this code
  addRowLabel(F("I2C ClockStretchLimit"));
  addUnit(F("Moved to Hardware page"));

  #if FEATURE_ARDUINO_OTA
  addFormCheckBox(F("Enable Arduino OTA"), F("arduinootaenable"), Settings.ArduinoOTAEnable);
  #endif // if FEATURE_ARDUINO_OTA
  #if defined(ESP32)
  addFormCheckBox_disabled(F("Enable RTOS Multitasking"), F("usertosmultitasking"), Settings.UseRTOSMultitasking);
  #endif // if defined(ESP32)

  addFormCheckBox(LabelType::JSON_BOOL_QUOTES, Settings.JSONBoolWithoutQuotes());
#if FEATURE_TIMING_STATS
  addFormCheckBox(LabelType::ENABLE_TIMING_STATISTICS, Settings.EnableTimingStats());
#endif // if FEATURE_TIMING_STATS
#ifndef BUILD_NO_RAM_TRACKER
  addFormCheckBox(LabelType::ENABLE_RAM_TRACKING, Settings.EnableRAMTracking());
#endif

  addFormCheckBox(LabelType::TASKVALUESET_ALL_PLUGINS, Settings.AllowTaskValueSetAllPlugins());
#if FEATURE_CLEAR_I2C_STUCK
  addFormCheckBox(LabelType::ENABLE_CLEAR_HUNG_I2C_BUS, Settings.EnableClearHangingI2Cbus());
#endif
  #if FEATURE_I2C_DEVICE_CHECK
  addFormCheckBox(LabelType::ENABLE_I2C_DEVICE_CHECK, Settings.CheckI2Cdevice());
  #endif // if FEATURE_I2C_DEVICE_CHECK

  #if FEATURE_TASKVALUE_UNIT_OF_MEASURE
  addFormCheckBox(LabelType::SHOW_UOM_ON_DEVICES_PAGE, Settings.ShowUnitOfMeasureOnDevicesPage());
  #endif // if FEATURE_TASKVALUE_UNIT_OF_MEASURE

  #if FEATURE_MQTT_CONNECT_BACKGROUND
  addFormCheckBox(LabelType::MQTT_CONNECT_IN_BACKGROUND, Settings.MQTTConnectInBackground());
  #endif // if FEATURE_MQTT_CONNECT_BACKGROUND

  # ifndef NO_HTTP_UPDATER
  addFormCheckBox(LabelType::ALLOW_OTA_UNLIMITED, Settings.AllowOTAUnlimited());
  # endif // ifndef NO_HTTP_UPDATER
  #if FEATURE_AUTO_DARK_MODE
  const __FlashStringHelper * cssModeNames[] = {
    F("Auto"),
    F("Light"),
    F("Dark"),
  };
  //const int cssModeOptions[] = { 0, 1, 2};
  constexpr int nrCssModeOptions = NR_ELEMENTS(cssModeNames);
  const FormSelectorOptions selector(
    nrCssModeOptions,
    cssModeNames/*,
    cssModeOptions*/);
  selector.addFormSelector(
    getLabel(LabelType::ENABLE_AUTO_DARK_MODE),
    getInternalLabel(LabelType::ENABLE_AUTO_DARK_MODE),
    Settings.getCssMode());
  #endif // FEATURE_AUTO_DARK_MODE

  #if FEATURE_RULES_EASY_COLOR_CODE
  addFormCheckBox(LabelType::DISABLE_RULES_AUTOCOMPLETE, Settings.DisableRulesCodeCompletion());
  #endif // if FEATURE_RULES_EASY_COLOR_CODE
  #if FEATURE_TARSTREAM_SUPPORT
  addFormCheckBox(LabelType::DISABLE_SAVE_CONFIG_AS_TAR, Settings.DisableSaveConfigAsTar());
  #endif // if FEATURE_TARSTREAM_SUPPORT

  #ifdef ESP8266
  addFormCheckBox(LabelType::DEEP_SLEEP_ALTERNATIVE_CALL, Settings.UseAlternativeDeepSleep());
  #endif


  #if FEATURE_SSDP
  addFormCheckBox_disabled(F("Use SSDP"), F("usessdp"), Settings.UseSSDP);
  #endif // if FEATURE_SSDP

  addFormNumericBox(LabelType::CONNECTION_FAIL_THRESH, Settings.ConnectionFailuresThreshold, 0, 100);
  addFormCheckBox(LabelType::FORCE_WIFI_BG, Settings.ForceWiFi_bg_mode());

  addFormCheckBox(LabelType::RESTART_WIFI_LOST_CONN, Settings.WiFiRestart_connection_lost());
  addFormCheckBox(LabelType::FORCE_WIFI_NOSLEEP,     Settings.WifiNoneSleep());
#ifdef SUPPORT_ARP
  addFormCheckBox(LabelType::PERIODICAL_GRAT_ARP, Settings.gratuitousARP());
#endif // ifdef SUPPORT_ARP
  addFormCheckBox(LabelType::CPU_ECO_MODE,        Settings.EcoPowerMode());
#if FEATURE_SET_WIFI_TX_PWR
  addFormFloatNumberBox(LabelType::WIFI_TX_MAX_PWR, Settings.getWiFi_TX_power(), 0.0f, MAX_TX_PWR_DBM_11b, 2, 0.25f);
  addFormNumericBox(LabelType::WIFI_SENS_MARGIN, Settings.WiFi_sensitivity_margin, -20, 30);
  addFormCheckBox(LabelType::WIFI_SEND_AT_MAX_TX_PWR, Settings.UseMaxTXpowerForSending());
#endif
  {
    addFormNumericBox(LabelType::WIFI_NR_EXTRA_SCANS, Settings.NumberExtraWiFiScans, 0, 5);
  }
  addFormCheckBox(LabelType::WIFI_USE_LAST_CONN_FROM_RTC, Settings.UseLastWiFiFromRTC());

#ifndef ESP32
  addFormCheckBox(LabelType::WAIT_WIFI_CONNECT,        Settings.WaitWiFiConnect());
#endif
  addFormCheckBox(LabelType::SDK_WIFI_AUTORECONNECT,   Settings.SDK_WiFi_autoreconnect());
  addFormCheckBox(LabelType::HIDDEN_SSID_SLOW_CONNECT, Settings.HiddenSSID_SlowConnectPerBSSID());
#ifdef ESP32
  addFormCheckBox(LabelType::WIFI_PASSIVE_SCAN,        Settings.PassiveWiFiScan());
#endif
#if FEATURE_USE_IPV6
  addFormCheckBox(LabelType::ENABLE_IPV6,      Settings.EnableIPv6());
#endif



  addFormSeparator(2);

  html_TR_TD();
  html_TD();
  addSubmitButton();
  addHtml(F("<input type='hidden' name='edit' value='1'>"));
  html_end_table();
  html_end_form();
  sendHeadandTail_stdtemplate(_TAIL);
  TXBuffer.endStream();
}

void addFormDstSelect(bool isStart, uint16_t choice) {
  uint16_t tmpstart(choice);
  uint16_t tmpend(choice);

  if (!TimeChangeRule(choice, 0).isValid()) {
    time_zone.getDefaultDst_flash_values(tmpstart, tmpend);
  }
  TimeChangeRule rule(isStart ? tmpstart : tmpend, 0);
  {
    const __FlashStringHelper *  week[] = { F("Last"), F("1st"), F("2nd"), F("3rd"), F("4th") };
    constexpr int weekValues[] = { 0, 1, 2, 3, 4 };
    addRowLabel(concat(
      isStart ? F("Start")  : F("End"),
      F(" (week, dow, month)")));

    const FormSelectorOptions selector(NR_ELEMENTS(weekValues), week, weekValues);
    selector.addSelector(
      isStart ? F("dststartweek")  : F("dstendweek"), 
      rule.week);
  }
  html_BR();
  {
    const __FlashStringHelper *  dow[] = { F("Sun"), F("Mon"), F("Tue"), F("Wed"), F("Thu"), F("Fri"), F("Sat") };
    constexpr int dowValues[]  = { 1, 2, 3, 4, 5, 6, 7 };

    const FormSelectorOptions selector(NR_ELEMENTS(dowValues), dow, dowValues);
    selector.addSelector(
      isStart ? F("dststartdow")   : F("dstenddow"),
      rule.dow);
  }
  html_BR();
  {
    const __FlashStringHelper * month[] = { F("Jan"), F("Feb"), F("Mar"), F("Apr"), F("May"), F("Jun"), F("Jul"), F("Aug"), F("Sep"), F("Oct"), F("Nov"), F(
                             "Dec") };
    constexpr int monthValues[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };

    const FormSelectorOptions selector(NR_ELEMENTS(monthValues), month, monthValues);
    selector.addSelector(
      isStart ? F("dststartmonth") : F("dstendmonth"),
      rule.month);
  }

  addFormNumericBox(
    isStart ? F("Start (localtime, e.g. 2h&rarr;3h)")  : F("End (localtime, e.g. 3h&rarr;2h)"),
    isStart ? F("dststarthour")  : F("dstendhour"),
    rule.hour, 0, 23);
  addUnit(isStart ? F("hour &#x21b7;") : F("hour &#x21b6;"));
}

void addFormExtTimeSourceSelect(const __FlashStringHelper * label, const __FlashStringHelper * id, ExtTimeSource_e choice)
{
  addRowLabel(label);
  const __FlashStringHelper * options[] =
    { F("None"), F("DS1307"), F("DS3231"), F("PCF8523"), F("PCF8563")};
  constexpr int optionValues[] = { 
    static_cast<int>(ExtTimeSource_e::None),
    static_cast<int>(ExtTimeSource_e::DS1307),
    static_cast<int>(ExtTimeSource_e::DS3231),
    static_cast<int>(ExtTimeSource_e::PCF8523),
    static_cast<int>(ExtTimeSource_e::PCF8563)
    };

  const FormSelectorOptions selector(NR_ELEMENTS(optionValues), options, optionValues);
  selector.addSelector(id, static_cast<int>(choice));
}


void addFormLogLevelSelect(LabelType::Enum label, int choice)
{
  #ifdef BUILD_NO_DEBUG
  if (choice > LOG_LEVEL_INFO) choice = LOG_LEVEL_INFO;
  #endif

  addRowLabel(getLabel(label));
  const __FlashStringHelper * options[LOG_LEVEL_NRELEMENTS + 1];
  int    optionValues[LOG_LEVEL_NRELEMENTS + 1] = { 0 };

  options[0]      = getLogLevelDisplayString(0);

  for (int i = 0; i < LOG_LEVEL_NRELEMENTS; ++i) {
    options[i + 1] = getLogLevelDisplayStringFromIndex(i, optionValues[i + 1]);
  }
  const FormSelectorOptions selector(LOG_LEVEL_NRELEMENTS + 1, options, optionValues);
  selector.addSelector(getInternalLabel(label), choice);

}

void addFormLogFacilitySelect(const __FlashStringHelper * label, const __FlashStringHelper * id, int choice)
{
  addRowLabel(label);
  const __FlashStringHelper * options[] =
  { F("Kernel"), F("User"),   F("Daemon"),   F("Message"), F("Local0"),  F("Local1"),
    F("Local2"), F("Local3"), F("Local4"),   F("Local5"),  F("Local6"),  F("Local7") };
  const int optionValues[] = { 0, 1, 3, 5, 16, 17, 18, 19, 20, 21, 22, 23 };

  const FormSelectorOptions selector(NR_ELEMENTS(options), options, optionValues);
  selector.addSelector(id, choice);
}

#endif // ifdef WEBSERVER_ADVANCED

#include "../WebServer/WebTemplateParser.h"

#include "../CustomBuild/CompiletimeDefines.h"

#include "../DataTypes/ControllerIndex.h"

#include "../Globals/Settings.h"

#include "../Helpers/_CPlugin_init.h"
#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/StringConverter.h"

#include "../Static/WebStaticData.h"

#include "../WebServer/HTML_wrappers.h"
#include "../WebServer/LoadFromFS.h"

#include "../../ESPEasy_common.h"

// Determine what pages should be visible
#ifndef MENU_INDEX_MAIN_VISIBLE
  # define MENU_INDEX_MAIN_VISIBLE true
#endif // ifndef MENU_INDEX_MAIN_VISIBLE

#ifndef MENU_INDEX_CONFIG_VISIBLE
#ifdef WEBSERVER_CONFIG
  # define MENU_INDEX_CONFIG_VISIBLE true
#else
  # define MENU_INDEX_CONFIG_VISIBLE false
#endif
#endif // ifndef MENU_INDEX_CONFIG_VISIBLE

#ifndef MENU_INDEX_CONTROLLERS_VISIBLE
#ifdef WEBSERVER_CONTROLLERS
  # define MENU_INDEX_CONTROLLERS_VISIBLE true
#else
  # define MENU_INDEX_CONTROLLERS_VISIBLE false
#endif
#endif // ifndef MENU_INDEX_CONTROLLERS_VISIBLE

#ifndef MENU_INDEX_HARDWARE_VISIBLE
#ifdef WEBSERVER_HARDWARE
  # define MENU_INDEX_HARDWARE_VISIBLE true
#else
  # define MENU_INDEX_HARDWARE_VISIBLE false
#endif
#endif // ifndef MENU_INDEX_HARDWARE_VISIBLE

#ifndef MENU_INDEX_DEVICES_VISIBLE
#ifdef WEBSERVER_DEVICES
  # define MENU_INDEX_DEVICES_VISIBLE true
#else
  # define MENU_INDEX_DEVICES_VISIBLE false
#endif
#endif // ifndef MENU_INDEX_DEVICES_VISIBLE

#ifndef MENU_INDEX_RULES_VISIBLE
#ifdef WEBSERVER_RULES
  # define MENU_INDEX_RULES_VISIBLE true
#else
  # define MENU_INDEX_RULES_VISIBLE false
#endif
#endif // ifndef MENU_INDEX_RULES_VISIBLE

#ifndef MENU_INDEX_NOTIFICATIONS_VISIBLE
#if FEATURE_NOTIFIER
  # define MENU_INDEX_NOTIFICATIONS_VISIBLE true
#else
  # define MENU_INDEX_NOTIFICATIONS_VISIBLE false
#endif
#endif // ifndef MENU_INDEX_NOTIFICATIONS_VISIBLE

#ifndef MENU_INDEX_TOOLS_VISIBLE
#ifdef WEBSERVER_TOOLS
  # define MENU_INDEX_TOOLS_VISIBLE true
#else
  # define MENU_INDEX_TOOLS_VISIBLE false
#endif
#endif // ifndef MENU_INDEX_TOOLS_VISIBLE


#if defined(NOTIFIER_SET_NONE) && defined(MENU_INDEX_NOTIFICATIONS_VISIBLE)
  # undef MENU_INDEX_NOTIFICATIONS_VISIBLE
  # define MENU_INDEX_NOTIFICATIONS_VISIBLE false
#endif // if defined(NOTIFIER_SET_NONE) && defined(MENU_INDEX_NOTIFICATIONS_VISIBLE)


uint8_t navMenuIndex = MENU_INDEX_MAIN;

// See https://github.com/letscontrolit/ESPEasy/issues/1650
const __FlashStringHelper* getGpMenuIcon(uint8_t index) {
  switch (index) {
    case MENU_INDEX_MAIN: return F("&#8962;");
    case MENU_INDEX_CONFIG: return F("&#9881;");
    case MENU_INDEX_CONTROLLERS: return F("&#128172;");
    case MENU_INDEX_HARDWARE: return F("&#128204;");
    case MENU_INDEX_DEVICES: return F("&#128268;");
    case MENU_INDEX_RULES: return F("&#10740;");
    case MENU_INDEX_NOTIFICATIONS: return F("&#9993;");
    case MENU_INDEX_TOOLS: return F("&#128295;");
  }
  return F("");
}

const __FlashStringHelper* getGpMenuLabel(uint8_t index) {
  switch (index) {
    case MENU_INDEX_MAIN: return F("Main");
    case MENU_INDEX_CONFIG: return F("Config");
    case MENU_INDEX_CONTROLLERS: return F("Controllers");
    case MENU_INDEX_HARDWARE: return F("Hardware");
    case MENU_INDEX_DEVICES: return F("Devices");
    case MENU_INDEX_RULES: return F("Rules");
    case MENU_INDEX_NOTIFICATIONS: return F("Notifications");
    case MENU_INDEX_TOOLS: return F("Tools");
  }
  return F("");
}

const __FlashStringHelper* getGpMenuURL(uint8_t index) {
  switch (index) {
    case MENU_INDEX_MAIN: return F("/");
    case MENU_INDEX_CONFIG: return F("/config");
    case MENU_INDEX_CONTROLLERS: return F("/controllers");
    case MENU_INDEX_HARDWARE: return F("/hardware");
    case MENU_INDEX_DEVICES: return F("/devices");
    case MENU_INDEX_RULES: return F("/rules");
    case MENU_INDEX_NOTIFICATIONS: return F("/notifications");
    case MENU_INDEX_TOOLS: return F("/tools");
  }
  return F("");
}

bool GpMenuVisible(uint8_t index) {
  switch (index) {
    case MENU_INDEX_MAIN: return MENU_INDEX_MAIN_VISIBLE;
    case MENU_INDEX_CONFIG: return MENU_INDEX_CONFIG_VISIBLE;
    case MENU_INDEX_CONTROLLERS: return MENU_INDEX_CONTROLLERS_VISIBLE;
    case MENU_INDEX_HARDWARE: return MENU_INDEX_HARDWARE_VISIBLE;
    case MENU_INDEX_DEVICES: return MENU_INDEX_DEVICES_VISIBLE;
    case MENU_INDEX_RULES: return MENU_INDEX_RULES_VISIBLE;
    case MENU_INDEX_NOTIFICATIONS: return MENU_INDEX_NOTIFICATIONS_VISIBLE;
    case MENU_INDEX_TOOLS: return MENU_INDEX_TOOLS_VISIBLE;
  }
  return false;
}

bool WebTemplateParser::process(const char c) {
  switch (c) {
    case '{':
    case '}':

      if (prev == c) {
        parsingVarName = c == '{';

        if (c == '}') {
          // Done parsing varName, still need to process it.
          if (varName.equalsIgnoreCase(F("content"))) {
            contentVarFound = true;
          } else if (Tail == contentVarFound) {
            processVarName();
          }
          free_string(varName);
        }
      }
      break;
    default:

      if (parsingVarName) {
        varName += c;
      } else {
        // FIXME TD-er: if a template has single '{' or '}' they will not be sent. Is that a problem?
        if (Tail == contentVarFound) {
          // only send the template tail after {{content}} is found
          // Or send all until the {{content}} tag.
          addHtml(c);
        }
      }
      break;
  }


  prev = c;

  if (!Tail) { return !contentVarFound; }
  return true;
}

bool WebTemplateParser::process(const __FlashStringHelper *pstr)
{
  return process((PGM_P)pstr);
}

bool WebTemplateParser::process(PGM_P pstr)
{
  if (!pstr) { return false; }

  #ifdef USE_SECOND_HEAP

  if (mmu_is_iram(pstr)) {
    // Have to copy the string using mmu_get functions
    // This is not a flash string.
    bool done            = false;
    const char *cur_char = pstr;

    while (!done) {
      const uint8_t ch = mmu_get_uint8(cur_char++);

      if (ch == 0) {
        done = true;
      }

      if (!process(ch)) { return false; }
    }
    return true;
  }
  #endif // ifdef USE_SECOND_HEAP


  const char *c = pstr;
  size_t length = strlen_P((PGM_P)pstr);

  while (length-- > 0) {
    if (!process(static_cast<char>(pgm_read_byte(c++)))) { return false; }
  }
  return true;
}

bool WebTemplateParser::process(const String& str)
{
  size_t length    = str.length();
  const char *cstr = str.begin();

  while (length-- > 0) {
    if (!process(*(cstr++))) { return false; }
  }
  return true;
}

void WebTemplateParser::processVarName()
{
  if (!varName.length()) { return; }
  varName.toLowerCase();

  getWebPageTemplateVar(varName);
}

void WebTemplateParser::getErrorNotifications() {
  // Check number of MQTT controllers active.
  int nrMQTTenabled = 0;

  for (controllerIndex_t x = 0; x < CONTROLLER_MAX; x++) {
    if (Settings.Protocol[x] != 0) {
      const protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(x);

      if (Settings.ControllerEnabled[x] &&
          validProtocolIndex(ProtocolIndex) &&
          getProtocolStruct(ProtocolIndex).usesMQTT) {
        ++nrMQTTenabled;
      }
    }
  }

  if (nrMQTTenabled > 1) {
    // Add warning, only one MQTT protocol should be used.
    addHtmlError(F("Only one MQTT controller should be active."));
  }

  // Check checksum of stored settings.
}

void WebTemplateParser::getWebPageTemplateVar(const String& varName)
{
  if (varName.length() == 0) { return; }


  // serialPrint(varName); serialPrint(" : free: "); serialPrint(ESP.getFreeHeap());   serialPrint("var len before:  "); serialPrint
  // (varValue.length()) ;serialPrint("after:  ");
  // varValue = "";

  switch (varName[0]) {
    case 'b':

      if (equals(varName, F("build")))
      {
    #if BUILD_IN_WEBFOOTER

        // In the footer, show full build binary name, will be 'firmware.bin' when compiled using Arduino IDE.
        addHtml(get_binary_filename());
    #endif // if BUILD_IN_WEBFOOTER
        return;
      }
      break;
    case 'c':

      if (equals(varName, F("css")))
      {
        serve_favicon();

        /*
                bool defaultCssServed = false;

                if (MENU_INDEX_SETUP == navMenuIndex) {
                  // Serve embedded CSS
                  defaultCssServed = serve_CSS_inline();
                }
                if (!defaultCssServed) {
         */
        serve_CSS(CSSfiles_e::ESPEasy_default);

        //        }
    #if FEATURE_RULES_EASY_COLOR_CODE

        if (!Settings.DisableRulesCodeCompletion() &&
            ((MENU_INDEX_RULES == navMenuIndex) ||
             (MENU_INDEX_CUSTOM_PAGE == navMenuIndex))) {
          serve_CSS(CSSfiles_e::EasyColorCode_codemirror);
        }
    #endif // if FEATURE_RULES_EASY_COLOR_CODE
        return;
      }
      break;
    case 'd':

      if (equals(varName, F("date")))
      {
    #if BUILD_IN_WEBFOOTER

        // Add the compile-date
        addHtml(get_build_date());
    #endif // if BUILD_IN_WEBFOOTER
        return;
      }
      else if (equals(varName, F("debug")))
      {
        // print debug messages - not implemented yet
        return;
      }

      break;
    case 'e':

      if (equals(varName, F("error")))
      {
        getErrorNotifications();
        return;
      }
      break;
    case 'j':

      if (equals(varName, F("js")))
      {
        html_add_JQuery_script();

    #if FEATURE_CHART_JS
        html_add_ChartJS_script();
    #endif // if FEATURE_CHART_JS

    #if FEATURE_RULES_EASY_COLOR_CODE

        if (!Settings.DisableRulesCodeCompletion() &&
            ((MENU_INDEX_RULES == navMenuIndex) ||
             (MENU_INDEX_CUSTOM_PAGE == navMenuIndex))) {
          html_add_Easy_color_code_script();
        }
    #endif // if FEATURE_RULES_EASY_COLOR_CODE

        if (MENU_INDEX_RULES == navMenuIndex) {
          serve_JS(JSfiles_e::SaveRulesFile);
        }

        html_add_autosubmit_form();
        serve_JS(JSfiles_e::Toasting);
        return;
      }
      break;
    case 'l':

      if (equals(varName, F("logo")))
      {
        if (fileExists(F("esp.png")))
        {
          addHtml(F("<img src=\"esp.png\" width=48 height=48 align=right>"));
        }
        return;
      }
      break;
    case 'm':

      if (equals(varName, F("menu")))
      {
        addHtml(F("<div class='menubar'>"));

        for (uint8_t i = 0; i < 8; i++)
        {
          if (!GpMenuVisible(i)) {
            // hide menu item
            continue;
          }

          if ((i == MENU_INDEX_RULES) && !Settings.UseRules) { // hide rules menu item
            continue;
          }
#if !FEATURE_NOTIFIER

          if (i == MENU_INDEX_NOTIFICATIONS) { // hide notifications menu item
            continue;
          }
#endif // if !FEATURE_NOTIFIER

          addHtml(F("<a "));
          addHtmlAttribute(F("class"), (i == navMenuIndex) ? F("menu active") : F("menu"));
          addHtmlAttribute(F("href"),  getGpMenuURL(i));
          addHtml('>');
          addHtml(getGpMenuIcon(i));
          addHtml(F("<span class='showmenulabel'>"));
          addHtml(getGpMenuLabel(i));
          addHtml(F("</span></a>"));
        }

        addHtml(F("</div>"));
        return;
      }
      else if (equals(varName, F("meta"))) {
        if (Rebooting) {
          addHtml(F("<meta http-equiv='refresh' content='10 url=/'>"));
        }
        return;
      }

      break;
    case 'n':

      if (equals(varName, F("name")))
      {
        addHtml(Settings.getHostname());
        return;
      }
      break;
    case 'u':

      if (equals(varName, F("unit")))
      {
        addHtmlInt(Settings.Unit);
        return;
      }

      break;
  }
  {
    #ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
      String log = F("Templ: Unknown Var : ");
      log += varName;
      addLogMove(LOG_LEVEL_ERROR, log);
    }
    #endif // ifndef BUILD_NO_DEBUG

    // no return string - eat var name
  }
}

#include "../WebServer/FileList.h"

#include "../WebServer/ESPEasy_WebServer.h"
#include "../WebServer/HTML_wrappers.h"
#include "../WebServer/AccessControl.h"
#include "../WebServer/Markup.h"
#include "../WebServer/Markup_Buttons.h"

#include "../ESPEasyCore/ESPEasyRules.h"

#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/Numerical.h"

#include "../../ESPEasy_common.h"



#ifdef USES_C016
#include "../Globals/C016_ControllerCache.h"
#endif

#if FEATURE_SD
#include <SD.h>
#endif // if FEATURE_SD


#define FILES_PER_PAGE   50

#ifdef WEBSERVER_NEW_UI

// ********************************************************************************
// Web Interface file list
// ********************************************************************************
void handle_filelist_json() {
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_filelist"));
  #endif

  if (!clientIPallowed()) { return; }
  navMenuIndex = MENU_INDEX_TOOLS;
  TXBuffer.startJsonStream();

  String fdelete = webArg(F("delete"));

  if (tryDeleteFile(fdelete)) {
    # if defined(ESP32)

    // flashCount();
    # endif // if defined(ESP32)
    # if defined(ESP8266)
    checkRuleSets();
    # endif // if defined(ESP8266)
  }

  int32_t startIdx       = 0;

  String fstart = webArg(F("start"));

  if (fstart.length() > 0)
  {
    validIntFromString(fstart, startIdx);
  }
  int endIdx = startIdx + FILES_PER_PAGE - 1;

  addHtml('[', '{');
  bool firstentry = true;
  # if defined(ESP32)
  fs::File root  = ESPEASY_FS.open("/");
  fs::File file  = root.openNextFile();
  int  count = -1;

  while (file and count < endIdx)
  {
    if (!file.isDirectory()) {
      ++count;

      if (count >= startIdx)
      {
        if (firstentry) {
          firstentry = false;
        } else {
          addHtml(',', '{');
        }
        stream_next_json_object_value(F("fileName"), String(file.name()));
        stream_next_json_object_value(F("index"),    startIdx);
        stream_last_json_object_value(F("size"), file.size());
      }
    }
    file = root.openNextFile();
  }
  # endif // if defined(ESP32)
  # if defined(ESP8266)
  fs::Dir dir = ESPEASY_FS.openDir("");

  int count = -1;

  while (dir.next())
  {
    ++count;

    if (count < startIdx)
    {
      continue;
    }

    if (firstentry) {
      firstentry = false;
    } else {
      addHtml(',', '{');
    }

    stream_next_json_object_value(F("fileName"), String(dir.fileName()));

    fs::File f = dir.openFile("r");

    if (f) {
      stream_next_json_object_value(F("size"), f.size());
      f.close();
    }

    stream_last_json_object_value(F("index"), startIdx);

    if (count >= endIdx)
    {
      break;
    }
  }

  if (firstentry) {
    addHtml('}');
  }

  # endif // if defined(ESP8266)
  addHtml(']');
  TXBuffer.endStream();
}

#endif // WEBSERVER_NEW_UI

#ifdef WEBSERVER_FILELIST
void handle_filelist() {
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_filelist"));
  #endif

  if (!clientIPallowed()) { return; }
  navMenuIndex = MENU_INDEX_TOOLS;
  TXBuffer.startStream();
  sendHeadandTail_stdtemplate(_HEAD);

  if (tryDeleteFile(webArg(F("delete"))))
  {
    checkRuleSets();
  }
  # ifdef USES_C016

  if (hasArg(F("delcache"))) {
    addLog(LOG_LEVEL_INFO, F("RTC  : delcache"));
    C016_deleteAllCacheBlocks();

    while (GarbageCollection()) {
      delay(1);
    }
  }
  # endif // ifdef USES_C016
  int32_t startIdx       = 0;
  const String fstart    = webArg(F("start"));

  if (fstart.length() > 0)
  {
    validIntFromString(fstart, startIdx);
  }
  const int endIdx = startIdx + FILES_PER_PAGE - 1;
  html_table_class_multirow();
  html_table_header(F(""),        50);
  html_table_header(F("Filename"));
  html_table_header(F("Size"), 80);
  int count = -1;

  bool moreFilesPresent  = false;
#if FEATURE_RTC_CACHE_STORAGE
  bool cacheFilesPresent = false;
#endif

# if defined(ESP8266)

  fs::Dir dir = ESPEASY_FS.openDir("");

  while (dir.next() && count < endIdx)
  {
    ++count;

    if (count >= startIdx)
    {
      int filesize = -1;
      fs::File f   = dir.openFile("r");

      if (f) {
        filesize = f.size();
      }
#if FEATURE_RTC_CACHE_STORAGE
      if (!cacheFilesPresent && (getCacheFileCountFromFilename(dir.fileName()) != -1))
      {
        cacheFilesPresent = true;
      }
#endif
      handle_filelist_add_file(dir.fileName(), filesize, startIdx);
    }
  }
  moreFilesPresent = dir.next();
# endif // if defined(ESP8266)
# if defined(ESP32)
  fs::File root = ESPEASY_FS.open("/");
  fs::File file = root.openNextFile();

  while (file && count < endIdx)
  {
    if (!file.isDirectory()) {
      ++count;

      if (count >= startIdx)
      {
#if FEATURE_RTC_CACHE_STORAGE
        if (!cacheFilesPresent && (getCacheFileCountFromFilename(file.name()) != -1))
        {
          cacheFilesPresent = true;
        }
#endif
        handle_filelist_add_file(file.name(), file.size(), startIdx);
      }
    }
    file = root.openNextFile();
  }
  moreFilesPresent = file;
# endif // if defined(ESP32)

  int start_prev = -1;

  if (startIdx > 0)
  {
    start_prev = startIdx < FILES_PER_PAGE ? 0 : startIdx - FILES_PER_PAGE;
  }
  int start_next = -1;

  if ((count >= endIdx) && moreFilesPresent) {
    start_next = endIdx + 1;
  }
#if FEATURE_RTC_CACHE_STORAGE
  handle_filelist_buttons(start_prev, start_next, cacheFilesPresent);
#else
  handle_filelist_buttons(start_prev, start_next, false);
#endif
}

void handle_filelist_add_file(const String& filename, int filesize, int startIdx) {
  html_TR_TD();

  if (!isProtectedFileType(filename))
  {
    html_add_button_prefix();
    addHtml(F("filelist?delete="));
    addHtml(filename);

    if (startIdx > 0)
    {
      addHtml(F("&start="));
      addHtmlInt(startIdx);
    }
    addHtml(F("'>Del</a>"));
  }
  {
    addHtml(F("<TD><a href=\""));
    addHtml(filename);
    addHtml('"', '>');
    addHtml(filename);
    addHtml(F("</a><TD>"));

    if (filesize >= 0) {
      addHtmlInt(filesize);
    }
  }
}

void handle_filelist_buttons(int start_prev, int start_next, bool cacheFilesPresent) {
  html_end_table();
  html_end_form();
  html_BR();
  addButton(F("/upload"), F("Upload"));

  if (start_prev >= 0)
  {
    html_add_button_prefix();
    addHtml(F("/filelist?start="));
    addHtmlInt(start_prev);
    addHtml(F("'>Previous</a>"));
  }

  if (start_next >= 0)
  {
    html_add_button_prefix();
    addHtml(F("/filelist?start="));
    addHtmlInt(start_next);
    addHtml(F("'>Next</a>"));
  }
#if FEATURE_RTC_CACHE_STORAGE
  if (cacheFilesPresent) {
    html_add_button_prefix(F("red"), true);
    addHtml(F("filelist?delcache=1'>Delete Cache Files</a>"));
  }
#endif
  addHtml(F("<BR><BR>"));
  sendHeadandTail_stdtemplate(_TAIL);
  TXBuffer.endStream();
}

#endif // ifdef WEBSERVER_FILELIST

// ********************************************************************************
// Web Interface SD card file and directory list
// ********************************************************************************
#if FEATURE_SD
void handle_SDfilelist() {
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_SDfilelist"));
  #endif

  if (!clientIPallowed()) { return; }
  navMenuIndex = MENU_INDEX_TOOLS;
  TXBuffer.startStream();
  sendHeadandTail_stdtemplate(_HEAD);


  String fdelete;
  String ddelete;
  String change_to_dir;
  String current_dir;
  String parent_dir;

  for (uint8_t i = 0; i < web_server.args(); i++) {
    // FIXME TD-er: This only checks for arguments in the URL, not in POST args
    // It also takes only the last matching argument.
    if (equals(web_server.argName(i), F("delete")))
    {
      fdelete = webArg(i);
    }

    if (equals(web_server.argName(i), F("deletedir")))
    {
      ddelete = webArg(i);
    }

    if (equals(web_server.argName(i), F("chgto")))
    {
      change_to_dir = webArg(i);
    }
  }

  if (fdelete.length() > 0)
  {
    SD.remove((char *)fdelete.c_str());
  }

  if (ddelete.length() > 0)
  {
    SD.rmdir((char *)ddelete.c_str());
  }

  if (change_to_dir.length() > 0)
  {
    current_dir = change_to_dir;
  }
  else
  {
    current_dir = "/";
  }

  fs::File root = SD.open(current_dir.c_str());
  root.rewindDirectory();
  fs::File entry = root.openNextFile();
  parent_dir = current_dir;

  if (!current_dir.equals("/"))
  {
    /* calculate the position to remove
       /
       / current_dir = /dir1/dir2/   =>   parent_dir = /dir1/
       /                     ^ position to remove, second last index of "/" + 1
       /
       / current_dir = /dir1/   =>   parent_dir = /
       /                ^ position to remove, second last index of "/" + 1
     */
    parent_dir.remove(parent_dir.lastIndexOf("/", parent_dir.lastIndexOf("/") - 1) + 1);
  }


  html_BR();
  html_table_class_multirow();
  html_table_header(F(""), 50);
  html_table_header(concat(F("SD Card: "), current_dir));
  html_table_header(F("Size"), 80);
  html_TR_TD();
  {
    addHtml(F("<TD><a href=\"SDfilelist?chgto="));
    addHtml(parent_dir);
    addHtml(F("\">..</a><TD>"));
  }

  while (entry)
  {
    html_TR_TD();
    // size_t entrynameLength = strlen(entry.name());
    if (entry.isDirectory())
    {
      // take a look in the directory for entries
      String child_dir = current_dir + entry.name();
      fs::File child         = SD.open(child_dir.c_str());
      fs::File dir_has_entry = child.openNextFile();

      // when the directory is empty, display the button to delete them
      if (!dir_has_entry)
      {
        addHtml(F("<a class='button link' onclick=\"return confirm('Delete this directory?')\" href=\"SDfilelist?deletedir="));
        addHtml(current_dir);
        if (!current_dir.endsWith(F("/"))) {
          addHtml('/');
        }
        addHtml(entry.name());
        addHtml(F("&chgto="));
        addHtml(current_dir);
        addHtml(F("\">Del</a>"));
      }
      {
        addHtml(F("<TD><a href=\"SDfilelist?chgto="));
        addHtml(current_dir);
        if (!current_dir.endsWith(F("/"))) {
          addHtml('/');
        }
        addHtml(entry.name());
        addHtml('"', '>');
        addHtml(entry.name());
        addHtml(F("</a><TD>dir"));
      }
      dir_has_entry.close();
    }
    else
    {

      if (!isProtectedFileType(String(entry.name())))
      {
        addHtml(F("<a class='button link' onclick=\"return confirm('Delete this file?')\" href=\"SDfilelist?delete="));
        addHtml(current_dir);
        addHtml(entry.name());
        addHtml(F("&chgto="));
        addHtml(current_dir);
        addHtml(F("\">Del</a>"));
      }
      {
        // FIXME TD-er: There's a lot of code duplication here.
        addHtml(F("<TD><a href=\""));
        addHtml(current_dir);
        addHtml(entry.name());
        addHtml('"', '>');
        addHtml(entry.name());
        addHtml(F("</a><TD>"));
        addHtmlInt(entry.size());
      }
    }
    entry.close();
    entry = root.openNextFile();
  }
  root.close();
  html_end_table();
  html_end_form();

  html_BR();
  addButton(F("/uploadsd"), F("Upload"));
  sendHeadandTail_stdtemplate(_TAIL);
  TXBuffer.endStream();
}

#endif // if FEATURE_SD

#include "../WebServer/LoadFromFS.h"

#include "../CustomBuild/CompiletimeDefines.h"

#include "../Globals/Cache.h"
#include "../Globals/RamTracker.h"

#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/Network.h"
#include "../Helpers/Numerical.h"

#include "../WebServer/CustomPage.h"
#include "../WebServer/HTML_wrappers.h"
#include "../WebServer/ESPEasy_WebServer.h"
#include "../Static/WebStaticData.h"

// ********************************************************************************
// Helper functions to match filenames
// ********************************************************************************
bool gzipEncoded(const String& path) {
  return path.endsWith(F(".gz"));
}

bool match_ext(const String& path, const __FlashStringHelper *ext) {
  return path.endsWith(ext) ||
         (gzipEncoded(path) && path.endsWith(concat(ext, F(".gz"))));
}

String fileFromUrl(String path) {
  const int questionmarkPos = path.indexOf('?');

  if (questionmarkPos >= 0) {
    path = path.substring(0, questionmarkPos);
  }

  // First prepend slash
  if (!path.startsWith(F("/"))) {
    path = String('/') + path;
  }

  if (path.endsWith(F("/"))) { path += F("index.htm"); }

  #ifdef ESP8266

  // Remove leading slash to generate filename from it.
  if (path.startsWith(F("/"))) {
    path = path.substring(1);
  }
  #endif // ifdef ESP8266

  return path;
}

bool matchFilename(const String& path, const __FlashStringHelper *fname) {
  return fileFromUrl(path).equalsIgnoreCase(fileFromUrl(fname));
}

// ********************************************************************************
// Handle embedded files
// ********************************************************************************
bool fileIsEmbedded(const String& path) {
#if defined(EMBED_ESPEASY_DEFAULT_MIN_CSS) || defined(WEBSERVER_EMBED_CUSTOM_CSS)

  if (matchFilename(path, F("esp.css"))) {
    return true;
  }
#endif // if defined(EMBED_ESPEASY_DEFAULT_MIN_CSS) || defined(WEBSERVER_EMBED_CUSTOM_CSS)
#ifdef WEBSERVER_FAVICON

  if (matchFilename(path, F("favicon.ico"))) {
    return true;
  }
#endif // ifdef WEBSERVER_FAVICON
  return false;
}

void do_serveEmbedded(const __FlashStringHelper* contentType, PGM_P content, int length, bool serve_inline) {
  // Serve using our own Web_StreamingBuffer
  // Serving via web_server.send_P may cause memory allocation issues when sending large flash strings.
  if (!serve_inline) {
    const bool cacheable = true;
    TXBuffer.startStream(contentType, F("*"), 200, cacheable);
  }
  TXBuffer.addFlashString(content, length);
  if (!serve_inline) {
    TXBuffer.endStream();
  }
}

bool serveEmbedded(const String& path, const __FlashStringHelper* contentType, bool serve_inline) {
#if defined(EMBED_ESPEASY_DEFAULT_MIN_CSS) || defined(WEBSERVER_EMBED_CUSTOM_CSS)

  if (matchFilename(path, F("esp.css"))) {
    #ifdef EMBED_ESPEASY_DEFAULT_MIN_CSS_USE_GZ
    if (serve_inline) {
      return false;
    }
    sendHeader(F("Content-Encoding"), F("gzip"));
    do_serveEmbedded(contentType, (PGM_P)FPSTR(DATA_ESPEASY_DEFAULT_MIN_CSS_GZ), espeasy_default_min_css_gz_len, serve_inline);

    #else
    do_serveEmbedded(contentType, (PGM_P)FPSTR(DATA_ESPEASY_DEFAULT_MIN_CSS), -1, serve_inline);
    #endif
    return true;
  }
#endif // if defined(EMBED_ESPEASY_DEFAULT_MIN_CSS) || defined(WEBSERVER_EMBED_CUSTOM_CSS)
#ifdef WEBSERVER_FAVICON

  if (matchFilename(path, F("favicon.ico"))) {
    do_serveEmbedded(contentType, (PGM_P)FPSTR(favicon_8b_ico), favicon_8b_ico_len, false);
    return true;
  }
#endif // ifdef WEBSERVER_FAVICON
  addLog(LOG_LEVEL_ERROR, concat(F("serveEmbedded failed: "), path));
  return false;
}


bool serve_CSS_inline() {
  #if defined(EMBED_ESPEASY_DEFAULT_MIN_CSS) || defined(WEBSERVER_EMBED_CUSTOM_CSS)
  const __FlashStringHelper* fname = F("esp.css");
  addHtml(F("<style>"));
  bool res = true;
  if (fileExists(fname)) {
    res = streamFromFS(fname);
  } else {
    res = serveEmbedded(fname, F("text/css"), true);
  }
  addHtml(F("</style>"));
  return res;
  #else
  return false;
  #endif
}

// ********************************************************************************
// Match static files and strip "static_xxx_" prefix
// ********************************************************************************
bool isStaticFile_StripPrefix(String& path) {
  // Strip any "static_xxx_" prepended for cache purposes
  const int index_static = path.indexOf(F("static_"));

  if (index_static != -1) {
    const int index_1 = path.indexOf('_');
    const int index_2 = path.indexOf('_', index_1 + 1);

    if ((index_1 > 0) && (index_2 > index_1)) {
      path = fileFromUrl(path.substring(index_2 + 1));
      return true;
    }
  }

  if (matchFilename(path, F("favicon.ico"))) {
    return true;
  }
  return false;
}

// ********************************************************************************
// Check whether a static file is requested again 
// and therefore should be read from browser cache
// ********************************************************************************
bool reply_304_not_modified(const String& path) {
  if (path.endsWith(F("favicon.ico"))) {
    // No need in serving 304 for the favicon
    return true;
  }
  const String ifNoneMatch = stripQuotes(web_server.header(F("If-None-Match")));
  uint32_t etag_num = 0;
  bool res          = false;

  if (validUIntFromString(ifNoneMatch, etag_num)) {
    if (fileExists(path) || fileIsEmbedded(path)) {
      // call fileExists first, as this may update Cache.fileCacheClearMoment
      if (Cache.fileCacheClearMoment == etag_num) {
        // We have a request for the same file we served earlier.
        // Reply with a 304 Not Modified
        res = true;
      }
    }
  }
#ifndef BUILD_NO_DEBUG

  if (res) {
    addLog(LOG_LEVEL_INFO, concat(F("Serve 304: "), String(etag_num)) + ' ' + path);
  }
#endif // ifndef BUILD_NO_DEBUG

  return res;
}

// ********************************************************************************
// Determine HTTP content type
// ********************************************************************************
const __FlashStringHelper* get_ContentType(String& path, bool& mustCheckCredentials)
{
  mustCheckCredentials = false;
  const __FlashStringHelper *contentType = F("text/plain");

  if (path.endsWith(F(".src"))) {
    // FIXME TD-er: is this ".src" still used by anyone? Better remove it.
    path = path.substring(0, path.lastIndexOf("."));

    // Must check credentials, or else someone may download any file by simply adding .src in the GET url.
    mustCheckCredentials = true;
  }
  else if (match_ext(path, F(".html")) ||
           match_ext(path, F(".htm"))) { contentType = F("text/html"); }
  else if (match_ext(path, F(".css"))) { contentType = F("text/css"); }
  else if (match_ext(path, F(".js")))  { contentType = F("application/javascript"); }
  else if (match_ext(path, F(".png"))) { contentType = F("image/png"); }
  else if (match_ext(path, F(".gif"))) { contentType = F("image/gif"); }
  else if (match_ext(path, F(".jpg"))) { contentType = F("image/jpeg"); }
  else if (path.endsWith(F(".ico")))   { contentType = F("image/x-icon"); }
  else if (path.endsWith(F(".svg")))   { contentType = F("image/svg+xml"); }
  else if (path.endsWith(F(".json")))  { contentType = F("application/json"); }
  else if (path.endsWith(F(".txt")) ||
           path.endsWith(F(".dat"))) {
    mustCheckCredentials = true;
    contentType          = F("application/octet-stream");
  } else {
    mustCheckCredentials = true;
  }
  return contentType;
}

// ********************************************************************************
// Web Interface server web file from FS
// ********************************************************************************
bool loadFromFS(String path) {
  // path is a deepcopy, since it will be changed here.
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("loadFromFS"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  statusLED(true);

  path = fileFromUrl(path);

  // isStaticFile_StripPrefix may alter the path by stripping off the "static_" prefix.
  // Thus do this before checking whether a file is present on the file system.
  const bool static_file = isStaticFile_StripPrefix(path);
#ifndef BUILD_NO_DEBUG

  if (static_file) {
    addLog(LOG_LEVEL_INFO, strformat(
      F("static_file: %s to: %s"), 
      path.c_str(),
      web_server.client().remoteIP().toString().c_str()
      ));
  }
#endif // ifndef BUILD_NO_DEBUG
#ifdef WEBSERVER_CUSTOM

  if (path.endsWith(F(".esp"))) {
    return handle_custom(path);
  }
#endif // ifdef WEBSERVER_CUSTOM

  const bool fileEmbedded = fileIsEmbedded(path);

  if (!fileExists(path) && !fileEmbedded) {
    return false;
  }

  bool mustCheckCredentials = false;
  const __FlashStringHelper* contentType  = get_ContentType(path, mustCheckCredentials);

  const bool serve_304 = static_file && 
                         reply_304_not_modified(path); // Reply with a 304 Not Modified

#ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLogMove(LOG_LEVEL_DEBUG, concat(F("HTML : Request file "), path));
  }
#endif // ifndef BUILD_NO_DEBUG

  // prevent reloading stuff on every click
  if (static_file) {
    sendHeader(F("Cache-Control"), F("public, max-age=31536000, s-maxage=31536000, immutable"));

    //    sendHeader(F("Cache-Control"), F("max-age=86400"));
//    sendHeader(F("Expires"),       F("-1"));
    if (fileEmbedded && !fileExists(path)) {
//      sendHeader(F("Last-Modified"), get_build_date_RFC1123());
    }
    sendHeader(F("Age"),           F("100"));
    sendHeader(F("ETag"),          strformat(F("\"%u-a\""), Cache.fileCacheClearMoment)); // added "-a" to the ETag to match the same encoding
  } else {
    sendHeader(F("Cache-Control"), F("no-cache"));
    sendHeader(F("ETag"),          F("\"2.0.0\""));
  }
  sendHeader(F("Vary"), "Accept-Encoding");

  if (path.endsWith(F(".dat"))) {
    sendHeader(F("Content-Disposition"), F("attachment;"));
  }

  if (!web_server.client().connected()) {
    #ifndef BUILD_MINIMAL_OTA
    addLog(LOG_LEVEL_INFO, strformat(
      F("loadFromFS: Client %s not connected"), 
      web_server.client().remoteIP().toString().c_str()
      ));
    #endif
    return false;
  }

  if (serve_304) {
    web_server.send(304, String(contentType), EMPTY_STRING);
  } else {
    if (fileExists(path)) {
      fs::File f = tryOpenFile(path.c_str(), "r");

      if (!f) {
        return false;
      }
      web_server.streamFile(f, String(contentType));
      f.close();
      web_server.client().PR_9453_FLUSH_TO_CLEAR();
    }

    if (!fileEmbedded) {
      return false;
    }
    serveEmbedded(path, contentType, false);
    web_server.client().PR_9453_FLUSH_TO_CLEAR();
  }

  statusLED(true);
  return true;
}

size_t streamFromFS(String path, bool htmlEscape) {
  // path is a deepcopy, since it will be changed here.
  path = fileFromUrl(path);
  statusLED(true);

  size_t bytesStreamed = 0;

  fs::File f = tryOpenFile(path.c_str(), "r");

  if (!f) {
    return bytesStreamed;
  }

  size_t available = f.available();
  String escaped;

  while (available > 0) {
    size_t chunksize = 64;

    if (available < chunksize) {
      chunksize = available;
    }
    uint8_t   buf[64] = { 0 };
    const size_t read = f.read(buf, chunksize);

    if (read == chunksize) {
      for (size_t i = 0; i < chunksize; ++i) {
        const char c = (char)buf[i];

        if (htmlEscape && htmlEscapeChar(c, escaped)) {
          addHtml(escaped);
        } else {
          addHtml(c);
        }
      }
      bytesStreamed += read;
      available      = f.available();
    } else {
      available = 0;
    }
  }

  // FIXME TD-er: Is this while loop still useful?
  while (f.available()) {
    addHtml((char)f.read());
  }

  statusLED(true);

  f.close();
  return bytesStreamed;
}

#include "../WebServer/SysInfoPage.h"

#if defined(WEBSERVER_SYSINFO) || SHOW_SYSINFO_JSON

# include "../WebServer/AccessControl.h"
# include "../WebServer/ESPEasy_WebServer.h"
# include "../WebServer/HTML_wrappers.h"
# include "../WebServer/Markup.h"
# include "../WebServer/Markup_Buttons.h"


# include "../../ESPEasy-Globals.h"

# include "../Commands/Diagnostic.h"

# include "../CustomBuild/CompiletimeDefines.h"

# include "../DataStructs/RTCStruct.h"

# include "../ESPEasyCore/ESPEasyEth.h"
# include "../ESPEasyCore/ESPEasyNetwork.h"
# include "../ESPEasyCore/ESPEasyWifi.h"

# include "../Globals/CRCValues.h"
# include "../Globals/ESPEasy_time.h"
# include "../Globals/ESPEasyWiFiEvent.h"
# include "../Globals/NetworkState.h"
# include "../Globals/RTC.h"
# include "../Globals/Settings.h"

# include "../Helpers/Convert.h"
# include "../Helpers/ESPEasyStatistics.h"
# include "../Helpers/ESPEasy_Storage.h"
# include "../Helpers/Hardware_device_info.h"
# include "../Helpers/Memory.h"
# include "../Helpers/Misc.h"
# include "../Helpers/Networking.h"
# include "../Helpers/OTA.h"
# include "../Helpers/StringConverter.h"
# include "../Helpers/StringGenerator_GPIO.h"
# include "../Helpers/StringGenerator_System.h"
# include "../Helpers/StringProvider.h"

# include "../Static/WebStaticData.h"

# if FEATURE_MQTT
#  include "../Globals/MQTT.h"
#  include "../ESPEasyCore/Controller.h" // For finding enabled MQTT controller
# endif // if FEATURE_MQTT


#if FEATURE_INTERNAL_TEMPERATURE
#include "../Helpers/Hardware_temperature_sensor.h"
#endif

# ifdef ESP32
#  include <esp_partition.h>
# endif // ifdef ESP32


# if SHOW_SYSINFO_JSON

// ********************************************************************************
// Web Interface sysinfo page
// ********************************************************************************
void handle_sysinfo_json() {
  #  ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_sysinfo"));
  #  endif // ifndef BUILD_NO_RAM_TRACKER

  if (!isLoggedIn()) { return; }
  TXBuffer.startJsonStream();
  json_init();
  json_open();
  json_open(false, F("general"));
  json_number(F("unit"), String(Settings.Unit));
  json_prop(F("time"),   node_time.getDateTimeString('-', ':', ' '));
  json_prop(F("uptime"), getExtendedValue(LabelType::UPTIME));
  json_number(F("cpu_load"),   toString(getCPUload()));
#if FEATURE_INTERNAL_TEMPERATURE
  json_number(F("cpu_temp"),   toString(getInternalTemperature()));
#endif
  json_number(F("loop_count"), String(getLoopCountPerSec()));
  json_close();

  int freeMem = ESP.getFreeHeap();
  json_open(false, F("mem"));
  json_number(F("free"),    String(freeMem));
  json_number(F("low_ram"), String(
  # ifndef BUILD_NO_RAM_TRACKER
                lowestRAM
  # else // ifndef BUILD_NO_RAM_TRACKER
                0
  # endif // ifndef BUILD_NO_RAM_TRACKER
                ));
  json_prop(F("low_ram_fn"), String(
  # ifndef BUILD_NO_RAM_TRACKER
            lowestRAMfunction
  # else // ifndef BUILD_NO_RAM_TRACKER
            0
  # endif // ifndef BUILD_NO_RAM_TRACKER
            ));
  json_number(F("stack"),     String(getCurrentFreeStack()));
  json_number(F("low_stack"), String(
  # ifndef BUILD_NO_RAM_TRACKER
                lowestFreeStack
  # else // ifndef BUILD_NO_RAM_TRACKER
                0
  # endif // ifndef BUILD_NO_RAM_TRACKER
                ));
  json_prop(F("low_stack_fn"), String(
  # ifndef BUILD_NO_RAM_TRACKER
            lowestFreeStackfunction
  # else // ifndef BUILD_NO_RAM_TRACKER
            0
  # endif // ifndef BUILD_NO_RAM_TRACKER
            ));
  json_close();

  json_open(false, F("boot"));
  json_prop(F("last_cause"),    getLastBootCauseString());
  json_number(F("counter"),     String(RTC.bootCounter));
  json_prop(F("reset_reason"),  getResetReasonString());
  json_close();

  json_open(false, F("wifi"));
  json_prop(F("type"),          toString(getConnectionProtocol()));
  json_number(F("rssi"),        String(WiFi.RSSI()));
  json_prop(F("dhcp"),          useStaticIP() ? getLabel(LabelType::IP_CONFIG_STATIC) : getLabel(LabelType::IP_CONFIG_DYNAMIC));
  json_prop(F("ip"),            getValue(LabelType::IP_ADDRESS));
#if FEATURE_USE_IPV6
  if (Settings.EnableIPv6()) {
    json_prop(F("ip6_local"),     getValue(LabelType::IP6_LOCAL));
    json_prop(F("ip6_global"),    getValue(LabelType::IP6_GLOBAL));
  }
#endif

  json_prop(F("subnet"),        getValue(LabelType::IP_SUBNET));
  json_prop(F("gw"),            getValue(LabelType::GATEWAY));
  json_prop(F("dns1"),          getValue(LabelType::DNS_1));
  json_prop(F("dns2"),          getValue(LabelType::DNS_2));
  json_prop(F("allowed_range"), describeAllowedIPrange());
  json_prop(F("sta_mac"),       getValue(LabelType::STA_MAC));
  json_prop(F("ap_mac"),        getValue(LabelType::AP_MAC));
  json_prop(F("ssid"),          getValue(LabelType::SSID));
  json_prop(F("bssid"),         getValue(LabelType::BSSID));
  json_number(F("channel"),     getValue(LabelType::CHANNEL));
  json_prop(F("encryption"),    getValue(LabelType::ENCRYPTION_TYPE_STA));
  json_prop(F("connected"),     getValue(LabelType::CONNECTED));
  json_prop(F("ldr"),           getValue(LabelType::LAST_DISC_REASON_STR));
  json_number(F("reconnects"),  getValue(LabelType::NUMBER_RECONNECTS));
  json_prop(F("ssid1"),         getValue(LabelType::WIFI_STORED_SSID1));
  json_prop(F("ssid2"),         getValue(LabelType::WIFI_STORED_SSID2));
  json_close();

# if FEATURE_ETHERNET
  json_open(false, F("ethernet"));
  json_prop(F("ethwifimode"),   getValue(LabelType::ETH_WIFI_MODE));
  json_prop(F("ethconnected"),  getValue(LabelType::ETH_CONNECTED));
  json_prop(F("ethchip"),       getValue(LabelType::ETH_CHIP));
  json_prop(F("ethduplex"),     getValue(LabelType::ETH_DUPLEX));
  json_prop(F("ethspeed"),      getValue(LabelType::ETH_SPEED));
  json_prop(F("ethstate"),      getValue(LabelType::ETH_STATE));
  json_prop(F("ethspeedstate"), getValue(LabelType::ETH_SPEED_STATE));
#if FEATURE_USE_IPV6
  if (Settings.EnableIPv6())
    json_prop(F("ethipv6local"), getValue(LabelType::ETH_IP6_LOCAL));
#endif
  json_close();
# endif // if FEATURE_ETHERNET

  json_open(false, F("firmware"));
  json_prop(F("build"),          getSystemBuildString());
  json_prop(F("notes"),          F(BUILD_NOTES));
  json_prop(F("libraries"),      getSystemLibraryString());
  json_prop(F("git_version"),    getValue(LabelType::GIT_BUILD));
  json_prop(F("plugins"),        getPluginDescriptionString());
  json_prop(F("md5"),            String(CRCValues.compileTimeMD5[0], HEX));
  json_number(F("md5_check"),    String(CRCValues.checkPassed()));
  json_prop(F("build_time"),     get_build_time());
  json_prop(F("filename"),       getValue(LabelType::BINARY_FILENAME));
  json_prop(F("build_platform"), getValue(LabelType::BUILD_PLATFORM));
  json_prop(F("git_head"),       getValue(LabelType::GIT_HEAD));
  #ifdef CONFIGURATION_CODE
  json_prop(F("configuration_code"), getValue(LabelType::CONFIGURATION_CODE_LBL));
  #endif // ifdef CONFIGURATION_CODE
  json_close();

  json_open(false, F("esp"));
  json_prop(F("chip_id"),        getValue(LabelType::ESP_CHIP_ID));
  json_number(F("cpu"),          getValue(LabelType::ESP_CHIP_FREQ));
#ifdef ESP32
  json_number(F("xtal_freq"),    getValue(LabelType::ESP_CHIP_XTAL_FREQ));
  json_number(F("abp_freq"),     getValue(LabelType::ESP_CHIP_APB_FREQ));
#endif
  json_prop(F("board"),          getValue(LabelType::BOARD_NAME));
  json_close();

  json_open(false, F("storage"));

  // Set to HEX may be something like 0x1640E0.
  // Where manufacturer is 0xE0 and device is 0x4016.
  json_number(F("chip_id"), getValue(LabelType::FLASH_CHIP_ID));
  if (flashChipVendorPuya()) {
    if (puyaSupport()) {
      json_prop(F("vendor"), F("puya, supported"));
    } else {
      json_prop(F("vendor"), F("puya, error"));
    }
  } else {
    json_prop(F("vendor"),        getValue(LabelType::FLASH_CHIP_VENDOR));
  }
  json_number(F("device"),        getValue(LabelType::FLASH_CHIP_MODEL));
  json_number(F("real_size"),     String(getFlashRealSizeInBytes() / 1024));
  json_number(F("ide_size"),      String(ESP.getFlashChipSize() / 1024));

  // Please check what is supported for the ESP32
  json_number(F("flash_speed"),   getValue(LabelType::FLASH_CHIP_SPEED));

  json_prop(F("mode"),            getFlashChipMode());

  json_number(F("writes"),        String(RTC.flashDayCounter));
  json_number(F("flash_counter"), String(RTC.flashCounter));
  json_number(F("sketch_size"),   String(getSketchSize() / 1024));
  json_number(F("sketch_free"),   String(getFreeSketchSpace() / 1024));

  json_number(F("spiffs_size"),   String(SpiffsTotalBytes() / 1024));
  json_number(F("spiffs_free"),   String(SpiffsFreeSpace() / 1024));
  json_close();
  json_close();

  TXBuffer.endStream();
}

#endif // SHOW_SYSINFO_JSON

#ifdef WEBSERVER_SYSINFO

void handle_sysinfo() {
  # ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_sysinfo"));
  # endif // ifndef BUILD_NO_RAM_TRACKER

  if (!isLoggedIn()) { return; }
  navMenuIndex = MENU_INDEX_TOOLS;
  html_reset_copyTextCounter();
  TXBuffer.startStream();
  sendHeadandTail_stdtemplate(_HEAD);

  addHtml(printWebString);
  addHtml(F("<form>"));

  // the table header
  html_table_class_normal();


  # ifdef WEBSERVER_GITHUB_COPY

  // Not using addFormHeader() to get the copy button on the same header line as 2nd column
  html_TR();
  html_table_header(F("System Info"), 225);
  addHtml(F("<TH>")); // Needed to get the copy button on the same header line.
  addCopyButton(F("copyText"), F("\\n"), F("Copy info to clipboard"));

  TXBuffer.addFlashString((PGM_P)FPSTR(githublogo));
  serve_JS(JSfiles_e::GitHubClipboard);

  # else // ifdef WEBSERVER_GITHUB_COPY
  addFormHeader(F("System Info"));

  # endif // ifdef WEBSERVER_GITHUB_COPY

  handle_sysinfo_basicInfo();

#ifndef WEBSERVER_SYSINFO_MINIMAL
  handle_sysinfo_memory();
#endif

  handle_sysinfo_Network();

# if FEATURE_ETHERNET
  handle_sysinfo_Ethernet();
# endif // if FEATURE_ETHERNET

#ifndef WEBSERVER_SYSINFO_MINIMAL
  handle_sysinfo_WiFiSettings();
#endif

  handle_sysinfo_Firmware();

#ifndef WEBSERVER_SYSINFO_MINIMAL
  handle_sysinfo_SystemStatus();

  handle_sysinfo_NetworkServices();

  handle_sysinfo_ESP_Board();

  handle_sysinfo_Storage();
#endif


  html_end_table();
  html_end_form();
  sendHeadandTail_stdtemplate(_TAIL);
  TXBuffer.endStream();
}

void handle_sysinfo_basicInfo() {
  addRowLabelValue(LabelType::UNIT_NR);

  if (node_time.systemTimePresent())
  {
    addRowLabelValue(LabelType::LOCAL_TIME);
    #if FEATURE_EXT_RTC
    if (Settings.ExtTimeSource() != ExtTimeSource_e::None) {
      addRowLabelValue(LabelType::EXT_RTC_UTC_TIME);
    }
    #endif
    addRowLabelValue(LabelType::TIME_SOURCE);
    addRowLabelValue(LabelType::TIME_WANDER);
  }

  addRowLabel(LabelType::UPTIME);
  {
    addHtml(getExtendedValue(LabelType::UPTIME));
  }

  addRowLabel(LabelType::LOAD_PCT);

  if (wdcounter > 0)
  {
    addHtml(strformat(
      F("%.2f [%%] (LC=%d)"),
      getCPUload(),
      getLoopCountPerSec()));
  }

  static const LabelType::Enum labels[] PROGMEM =
  {
#if FEATURE_INTERNAL_TEMPERATURE
    LabelType::INTERNAL_TEMPERATURE,
#endif

    LabelType::CPU_ECO_MODE,

    LabelType::RESET_REASON,
    LabelType::LAST_TASK_BEFORE_REBOOT,
    LabelType::SW_WD_COUNT,
    LabelType::MAX_LABEL
  };
  addRowLabelValues(labels);

  addRowLabel(F("Boot"));
  {
    addHtml(getLastBootCauseString());
    addHtml(strformat(
      F(" (%d)"), static_cast<uint32_t>(RTC.bootCounter)));
  }
}

#ifndef WEBSERVER_SYSINFO_MINIMAL
void handle_sysinfo_memory() {
  addTableSeparator(F("Memory"), 2, 3);

# ifdef ESP32
  addRowLabelValue(LabelType::HEAP_SIZE);
  addRowLabelValue(LabelType::HEAP_MIN_FREE);
# endif // ifdef ESP32

  int freeMem = ESP.getFreeHeap();
  addRowLabel(LabelType::FREE_MEM);
  {
    addHtmlInt(freeMem);
    addUnit(getFormUnit(LabelType::FREE_MEM));
# ifndef BUILD_NO_RAM_TRACKER
    addHtml(F(" ("));
    addHtmlInt(lowestRAM);
    addHtml(F(" - "));
    addHtml(lowestRAMfunction);
    addHtml(')');
# endif // ifndef BUILD_NO_RAM_TRACKER
  }
# if defined(CORE_POST_2_5_0) || defined(ESP32)
 #  ifndef LIMIT_BUILD_SIZE
  addRowLabelValue(LabelType::HEAP_MAX_FREE_BLOCK);
 #  endif // ifndef LIMIT_BUILD_SIZE
# endif   // if defined(CORE_POST_2_5_0) || defined(ESP32)
# if defined(CORE_POST_2_5_0)
  #  ifndef LIMIT_BUILD_SIZE
  addRowLabelValue(LabelType::HEAP_FRAGMENTATION);
  #  endif // ifndef LIMIT_BUILD_SIZE
  {
    #ifdef USE_SECOND_HEAP
    addRowLabelValue(LabelType::FREE_HEAP_IRAM);
    #endif
  }
# endif // if defined(CORE_POST_2_5_0)


  addRowLabel(LabelType::FREE_STACK);
  {
    addHtmlInt(getCurrentFreeStack());
    addUnit(getFormUnit(LabelType::FREE_STACK));
# ifndef BUILD_NO_RAM_TRACKER
    addHtml(F(" ("));
    addHtmlInt(lowestFreeStack);
    addHtml(F(" - "));
    addHtml(lowestFreeStackfunction);
    addHtml(')');
# endif // ifndef BUILD_NO_RAM_TRACKER
  }

# if defined(ESP32) && defined(BOARD_HAS_PSRAM)

  addRowLabelValue(LabelType::PSRAM_SIZE);
  if (UsePSRAM()) {
    static const LabelType::Enum labels[] PROGMEM =
    {
      LabelType::PSRAM_FREE,
      LabelType::PSRAM_MIN_FREE,
      LabelType::PSRAM_MAX_FREE_BLOCK,
      LabelType::MAX_LABEL
    };
    addRowLabelValues(labels);
  } 
# endif // if defined(ESP32) && defined(BOARD_HAS_PSRAM)
}
#endif

# if FEATURE_ETHERNET
void handle_sysinfo_Ethernet() {
  if (active_network_medium == NetworkMedium_t::Ethernet) {
    addTableSeparator(F("Ethernet"), 2, 3);

    static const LabelType::Enum labels[] PROGMEM =
    {
      LabelType::ETH_CHIP,
      LabelType::ETH_STATE,
      LabelType::ETH_SPEED,
      LabelType::ETH_DUPLEX,
      LabelType::ETH_MAC,
//    LabelType::ETH_IP_ADDRESS_SUBNET,
//    LabelType::ETH_IP_GATEWAY,
//    LabelType::ETH_IP_DNS,

      LabelType::MAX_LABEL
    };

    addRowLabelValues(labels);

  }
}

# endif // if FEATURE_ETHERNET

void handle_sysinfo_Network() {
  addTableSeparator(F("Network"), 2, 3);

  {
    static const LabelType::Enum labels[] PROGMEM =
    {
# if FEATURE_ETHERNET || defined(USES_ESPEASY_NOW)
      LabelType::ETH_WIFI_MODE,
# endif 
      LabelType::IP_CONFIG,
      LabelType::IP_ADDRESS_SUBNET,
#if FEATURE_USE_IPV6
      LabelType::IP6_LOCAL,
      LabelType::IP6_GLOBAL,
//      LabelType::IP6_ALL_ADDRESSES,
#endif
      LabelType::GATEWAY,
      LabelType::CLIENT_IP,
      LabelType::DNS,
      LabelType::ALLOWED_IP_RANGE,
      LabelType::CONNECTED,
      LabelType::NUMBER_RECONNECTS,

      LabelType::MAX_LABEL
    };

    addRowLabelValues(labels);
  }

  addTableSeparator(F("WiFi"), 2, 3, F("Wifi"));

  const bool showWiFiConnectionInfo = !WiFiEventData.WiFiDisconnected();


  addRowLabel(LabelType::WIFI_CONNECTION);
  if (showWiFiConnectionInfo)
  {
    addHtml(toString(getConnectionProtocol()));
    addHtml(F(" (RSSI "));
    addHtmlInt(WiFi.RSSI());
    addHtml(F(" dBm)"));
  } else addHtml('-');

  addRowLabel(LabelType::SSID);
  if (showWiFiConnectionInfo)
  {
    addHtml(WiFi.SSID());
    addHtml(F(" ("));
    addHtml(WiFi.BSSIDstr());
    addHtml(')');
  } else addHtml('-');

  #ifdef ESP32
  const int64_t tsf_time = WiFi_get_TSF_time();
  if (tsf_time > 0) {
    addRowLabel(F("WiFi TSF time"));
    // Split it while printing, so we're not loosing a lot of decimals in the float conversion
    uint32_t tsf_usec{};
    addHtml(secondsToDayHourMinuteSecond(micros_to_sec_usec(tsf_time, tsf_usec)));
    addHtml(strformat(F(".%06u"), tsf_usec));
  }
  #endif

  addRowLabel(getLabel(LabelType::CHANNEL));
  if (showWiFiConnectionInfo) {
    addHtml(getValue(LabelType::CHANNEL));
  } else addHtml('-');

  addRowLabel(getLabel(LabelType::ENCRYPTION_TYPE_STA));
  if (showWiFiConnectionInfo) {
    addHtml(getValue(LabelType::ENCRYPTION_TYPE_STA));
  } else addHtml('-');

  if (active_network_medium == NetworkMedium_t::WIFI)
  {
    addRowLabel(LabelType::LAST_DISCONNECT_REASON);
    addHtml(getValue(LabelType::LAST_DISC_REASON_STR));
    addRowLabelValue(LabelType::WIFI_STORED_SSID1);
    addRowLabelValue(LabelType::WIFI_STORED_SSID2);
  }

  addRowLabelValue(LabelType::STA_MAC);
  addRowLabelValue(LabelType::AP_MAC);
  html_TR();
}

#ifndef WEBSERVER_SYSINFO_MINIMAL
void handle_sysinfo_WiFiSettings() {
  addTableSeparator(F("WiFi Settings"), 2, 3);

  static const LabelType::Enum labels[] PROGMEM =
  {
    LabelType::FORCE_WIFI_BG,
    LabelType::RESTART_WIFI_LOST_CONN,
    LabelType::FORCE_WIFI_NOSLEEP,
# ifdef SUPPORT_ARP
    LabelType::PERIODICAL_GRAT_ARP,
# endif // ifdef SUPPORT_ARP
    LabelType::CONNECTION_FAIL_THRESH,
#if FEATURE_SET_WIFI_TX_PWR
    LabelType::WIFI_TX_MAX_PWR,
    LabelType::WIFI_CUR_TX_PWR,
    LabelType::WIFI_SENS_MARGIN,
    LabelType::WIFI_SEND_AT_MAX_TX_PWR,
#endif
    LabelType::WIFI_NR_EXTRA_SCANS,
#ifdef USES_ESPEASY_NOW
    LabelType::USE_ESPEASY_NOW,
    LabelType::FORCE_ESPEASY_NOW_CHANNEL,
#endif
    LabelType::WIFI_USE_LAST_CONN_FROM_RTC,
#ifndef ESP32
    LabelType::WAIT_WIFI_CONNECT,
#endif
#ifdef ESP32
    LabelType::WIFI_PASSIVE_SCAN,
#endif
    LabelType::HIDDEN_SSID_SLOW_CONNECT,
    LabelType::CONNECT_HIDDEN_SSID,
    LabelType::SDK_WIFI_AUTORECONNECT,
#if FEATURE_USE_IPV6
    LabelType::ENABLE_IPV6,
#endif

    LabelType::MAX_LABEL
  };

  addRowLabelValues(labels);
}
#endif

void handle_sysinfo_Firmware() {
  addTableSeparator(F("Firmware"), 2, 3);

  addRowLabelValue_copy(LabelType::BUILD_DESC);
  addHtml(' ');
  addHtml(F(BUILD_NOTES));

  addRowLabelValue_copy(LabelType::SYSTEM_LIBRARIES);
#ifdef ESP32
  addRowLabelValue_copy(LabelType::ESP_IDF_SDK_VERSION);
#endif
  addRowLabelValue_copy(LabelType::GIT_BUILD);
  addRowLabelValue_copy(LabelType::PLUGIN_COUNT);
  addHtml(' ');
  addHtml(getPluginDescriptionString());

  addRowLabel(F("Build Origin"));
  addHtml(get_build_origin());
  addRowLabelValue_copy(LabelType::BUILD_TIME);
  addRowLabelValue_copy(LabelType::BINARY_FILENAME);
  addRowLabelValue_copy(LabelType::BUILD_PLATFORM);
  addRowLabelValue_copy(LabelType::GIT_HEAD);
  #ifdef CONFIGURATION_CODE
  addRowLabelValue_copy(LabelType::CONFIGURATION_CODE_LBL);
  #endif  // ifdef CONFIGURATION_CODE
}

#ifndef WEBSERVER_SYSINFO_MINIMAL
void handle_sysinfo_SystemStatus() {
  addTableSeparator(F("System Status"), 2, 3);

  static const LabelType::Enum labels[] PROGMEM =
  {
    // Actual Loglevel
    LabelType::SYSLOG_LOG_LEVEL,
    LabelType::SERIAL_LOG_LEVEL,
    LabelType::WEB_LOG_LEVEL,
# if FEATURE_SD
    LabelType::SD_LOG_LEVEL,
# endif // if FEATURE_SD

    LabelType::ENABLE_SERIAL_PORT_CONSOLE,
    LabelType::CONSOLE_SERIAL_PORT,
#if USES_ESPEASY_CONSOLE_FALLBACK_PORT
    LabelType::CONSOLE_FALLBACK_TO_SERIAL0,
    LabelType::CONSOLE_FALLBACK_PORT,
#endif
    LabelType::MAX_LABEL
  };

  addRowLabelValues(labels);
#if FEATURE_CLEAR_I2C_STUCK
  if (Settings.EnableClearHangingI2Cbus()) {
    addRowLabelValue(LabelType::I2C_BUS_STATE);
    addRowLabelValue(LabelType::I2C_BUS_CLEARED_COUNT);
  }
#endif
}
#endif

#ifndef WEBSERVER_SYSINFO_MINIMAL
void handle_sysinfo_NetworkServices() {
  addTableSeparator(F("Network Services"), 2, 3);

  addRowLabel(F("Network Connected"));
  addEnabled(NetworkConnected());

  addRowLabel(F("NTP Initialized"));
  addEnabled(statusNTPInitialized);

  #if FEATURE_MQTT
  if (validControllerIndex(firstEnabledMQTT_ControllerIndex())) {
    addRowLabel(F("MQTT Client Connected"));
    addEnabled(MQTTclient_connected);
  }
  #endif
}
#endif

#ifndef WEBSERVER_SYSINFO_MINIMAL
void handle_sysinfo_ESP_Board() {
  addTableSeparator(F("ESP Board"), 2, 3);


  addRowLabel(LabelType::ESP_CHIP_ID);

  const uint32_t chipID = getChipId();
  addHtml(strformat(
    F("%d (%s)"), 
    chipID, 
    formatToHex(chipID, 6).c_str()));

  static const LabelType::Enum labels[] PROGMEM =
  {
  LabelType::ESP_CHIP_FREQ,
#   ifdef ESP32
  LabelType::ESP_CHIP_XTAL_FREQ,
  LabelType::ESP_CHIP_APB_FREQ,
#   endif // ifdef ESP32

    LabelType::ESP_CHIP_MODEL,
#   if defined(ESP32)
    LabelType::ESP_CHIP_REVISION,
#   endif // if defined(ESP32)
    LabelType::ESP_CHIP_CORES,
    LabelType::BOARD_NAME,
    LabelType::MAX_LABEL
  };

  addRowLabelValues(labels);


#if defined(ESP32)
  addRowLabel(F("ESP Chip Features"));
  addHtml(getChipFeaturesString());
#endif
}

#  endif // ifndef WEBSERVER_SYSINFO_MINIMAL

#  ifndef WEBSERVER_SYSINFO_MINIMAL
void handle_sysinfo_Storage() {
  addTableSeparator(F("Storage"), 2, 3);

  if (getFlashChipId() != 0) {
    addRowLabel(LabelType::FLASH_CHIP_ID);


    // Set to HEX may be something like 0x1640E0.
    // Where manufacturer is 0xE0 and device is 0x4016.
    addHtml(F("Vendor: "));
    addHtml(getValue(LabelType::FLASH_CHIP_VENDOR));

    if (flashChipVendorPuya())
    {
      addHtml(F(" (PUYA"));

      if (puyaSupport()) {
        addHtml(F(", supported"));
      } else {
        addHtml(F(HTML_SYMBOL_WARNING));
      }
      addHtml(')');
    }
    addHtml(F(" Device: "));
    addHtml(getValue(LabelType::FLASH_CHIP_MODEL));
    #ifdef ESP32
    if (getChipFeatures().embeddedFlash) {
      addHtml(F(" (Embedded)"));
    }
    #endif
  }
  const uint32_t realSize = getFlashRealSizeInBytes();
  const uint32_t ideSize  = ESP.getFlashChipSize();

  addRowLabel(LabelType::FLASH_CHIP_REAL_SIZE);
  addHtmlInt(realSize / 1024);
  addHtml(F(" [kB]"));

  addRowLabel(LabelType::FLASH_IDE_SIZE);
  addHtmlInt(ideSize / 1024);
  addHtml(F(" [kB]"));

  addRowLabel(LabelType::FLASH_CHIP_SPEED);
  addHtmlInt(getFlashChipSpeed() / 1000000);
  addHtml(F(" [MHz]"));

  // Please check what is supported for the ESP32
  addRowLabel(LabelType::FLASH_IDE_SPEED);
  addHtmlInt(ESP.getFlashChipSpeed() / 1000000);
  addHtml(F(" [MHz]"));

  addRowLabelValue(LabelType::FLASH_IDE_MODE);

  addRowLabel(LabelType::FLASH_WRITE_COUNT);
  addHtml(strformat(
    F("%d daily / %d cold boot"),
    RTC.flashDayCounter,
    static_cast<int>(RTC.flashCounter)));

  {
    uint32_t maxSketchSize;
    bool     use2step;
    # if defined(ESP8266)
    bool otaEnabled =
    # endif // if defined(ESP8266)
    OTA_possible(maxSketchSize, use2step);

    addRowLabel(LabelType::SKETCH_SIZE);
    addHtml(strformat(
      F("%d [kB] (%d kB not used)"),
      getSketchSize() / 1024,
      (maxSketchSize - getSketchSize()) / 1024));

    addRowLabel(LabelType::MAX_OTA_SKETCH_SIZE);
    addHtml(strformat(
      F("%d [kB] (%d bytes)"),
      maxSketchSize / 1024,
      maxSketchSize));

    # if defined(ESP8266)
    addRowLabel(LabelType::OTA_POSSIBLE);
    addHtml(boolToString(otaEnabled));

    addRowLabel(LabelType::OTA_2STEP);
    addHtml(boolToString(use2step));
    # endif // if defined(ESP8266)
  }

  addRowLabel(LabelType::FS_SIZE);
  addHtml(strformat(
    F("%d [kB] (%d kB free)"),
    SpiffsTotalBytes() / 1024,
    SpiffsFreeSpace() / 1024));

  # ifndef LIMIT_BUILD_SIZE
  addRowLabel(F("Page size"));
  addHtmlInt(SpiffsPagesize());
  addUnit(getFormUnit(LabelType::FREE_MEM)); // FIXME TD-er: Add labeltype

  addRowLabel(F("Block size"));
  addHtmlInt(SpiffsBlocksize());
  addUnit(getFormUnit(LabelType::FREE_MEM)); // FIXME TD-er: Add labeltype

  addRowLabel(F("Number of blocks"));
  addHtmlInt(SpiffsTotalBytes() / SpiffsBlocksize());

  {
  #  if defined(ESP8266)
    fs::FSInfo fs_info;
    ESPEASY_FS.info(fs_info);
    addRowLabel(F("Maximum open files"));
    addHtmlInt(fs_info.maxOpenFiles);

    addRowLabel(F("Maximum path length"));
    addHtmlInt(fs_info.maxPathLength);

  #  endif // if defined(ESP8266)
  }
  # endif // ifndef LIMIT_BUILD_SIZE

#if FEATURE_CHART_STORAGE_LAYOUT

  if (showSettingsFileLayout) {
    addTableSeparator(F("Settings Files"), 2, 3);
    html_TR_TD();
    addHtml(F("Layout Settings File"));
    html_TD();
    getConfig_dat_file_layout();
    html_TR_TD();
    html_TD();
    addHtml(F("(offset / size per item / index)"));

    for (int st = 0; st < static_cast<int>(SettingsType::Enum::SettingsType_MAX); ++st) {
      const SettingsType::Enum settingsType = static_cast<SettingsType::Enum>(st);
      #if !FEATURE_NOTIFIER
      if (settingsType == SettingsType::Enum::NotificationSettings_Type) {
        continue;
      }
      #endif
      html_TR_TD();
      addHtml(SettingsType::getSettingsTypeString(settingsType));
      html_BR();
      addHtml(SettingsType::getSettingsFileName(settingsType));
      html_TD();
      getStorageTableSVG(settingsType);
      delay(1);
    }
  }

  # ifdef ESP32
  addTableSeparator(F("Partitions"), 2, 3,
                    F("https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/partition-tables.html"));

  addRowLabel(F("Data Partition Table"));

  //   TXBuffer += getPartitionTableHeader(F(" - "), F("<BR>"));
  //   TXBuffer += getPartitionTable(ESP_PARTITION_TYPE_DATA, F(" - "), F("<BR>"));
  getPartitionTableSVG(ESP_PARTITION_TYPE_DATA, 0x5856e6);

  addRowLabel(F("App Partition Table"));

  //   TXBuffer += getPartitionTableHeader(F(" - "), F("<BR>"));
  //   TXBuffer += getPartitionTable(ESP_PARTITION_TYPE_APP , F(" - "), F("<BR>"));
  getPartitionTableSVG(ESP_PARTITION_TYPE_APP, 0xab56e6);
  # endif // ifdef ESP32

  #ifdef ESP8266
  addTableSeparator(F("Partitions"), 2, 3);

  addRowLabel(F("Partition Table"));

  getPartitionTableSVG();
  #endif
#endif
}
#endif

#endif    // ifdef WEBSERVER_SYSINFO


#endif

#include "../WebServer/Markup_Buttons.h"

#include "../WebServer/common.h"
#include "../WebServer/HTML_wrappers.h"

#include "../Helpers/StringConverter.h"

#include "../Static/WebStaticData.h"


void addButton(const __FlashStringHelper * url, const __FlashStringHelper * label) {
  addButton(url, label, EMPTY_STRING);
}

void addButton(const __FlashStringHelper * url, const __FlashStringHelper * label, const __FlashStringHelper * classes, bool enabled)
{
  html_add_button_prefix(classes, enabled);
  addHtml(url);
  addHtml('\'', '>');;
  addHtml(label);
  addHtml(F("</a>"));
}

void addButton(const String& url, const String& label)
{
  addButton(url, label, EMPTY_STRING);
}

void addButton(const String& url, const String& label, const String& classes, bool enabled)
{
  html_add_button_prefix(classes, enabled);
  addHtml(url);
  addHtml('\'', '>');;
  addHtml(label);
  addHtml(F("</a>"));
}

void addButtonWithSvg(const String& url, const String& label)
{
  addButtonWithSvg(url, label, EMPTY_STRING, false);
}

void addButtonWithSvg(const String& url, const String& label, const String& svgPath, bool needConfirm) {
  addHtml(F("<a "));
  addHtmlAttribute(F("class"), F("button link"));
  addHtmlAttribute(F("href"),  url);
  #ifndef BUILD_MINIMAL_OTA
  bool hasSVG = svgPath.length() > 0;

  if (hasSVG)
  {
    addHtmlAttribute(F("alt"), label);
  }
  #endif // ifndef BUILD_MINIMAL_OTA

  if (needConfirm) {
    addHtmlAttribute(F("onclick"), F("return confirm(\"Are you sure?\")"));
  }
  addHtml('>');

  #ifndef BUILD_MINIMAL_OTA

  if (hasSVG) {
    addHtml(F("<svg width='24' height='24' viewBox='-1 -1 26 26' style='position: relative; top: 5px;'>"));
    addHtml(svgPath);
    addHtml(F("</svg>"));
  } else
  #endif // ifndef BUILD_MINIMAL_OTA
  {
    addHtml(label);
  }
  addHtml(F("</a>"));
}

void addSaveButton(const String& url, const String& label)
{
#ifdef BUILD_MINIMAL_OTA
  addButtonWithSvg(url, label
                   , EMPTY_STRING
                   , false);
#else // ifdef BUILD_MINIMAL_OTA
  addButtonWithSvg(url,
                   label
                   ,
                   F(
                     "<path d='M19 12v7H5v-7H3v7c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-7h-2zm-6 .67l2.59-2.58L17 11.5l-5 5-5-5 1.41-1.41L11 12.67V3h2v9.67z'  stroke='white' fill='white' ></path>")
                   ,
                   false);
#endif // ifdef BUILD_MINIMAL_OTA
}

void addDeleteButton(const String& url, const String& label)
{
#ifdef BUILD_MINIMAL_OTA
  addButtonWithSvg(url, label
                   , EMPTY_STRING
                   , true);
#else // ifdef BUILD_MINIMAL_OTA
  addButtonWithSvg(url,
                   label
                   ,
                   F(
                     "<path fill='none' d='M0 0h24v24H0V0z'></path><path d='M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM8 9h8v10H8V9zm7.5-5l-1-1h-5l-1 1H5v2h14V4h-3.5z' stroke='white' fill='white' ></path>")
                   ,
                   true);
#endif // ifdef BUILD_MINIMAL_OTA
}

void addWideButton(const __FlashStringHelper * url, const __FlashStringHelper * label) {
  html_add_wide_button_prefix(EMPTY_STRING, true);
  addHtml(url);
  addHtml('\'', '>');;
  addHtml(label);
  addHtml(F("</a>"));
}

void addWideButton(const String& url, const String& label) {
  addWideButton(url, label, EMPTY_STRING, true);
}

void addWideButton(const String& url, const String& label, const String& classes) {
  addWideButton(url, label, classes, true);
}

void addWideButton(const String& url, const String& label, const String& classes, bool enabled)
{
  html_add_wide_button_prefix(classes, enabled);
  addHtml(url);
  addHtml('\'', '>');;
  addHtml(label);
  addHtml(F("</a>"));
}

void addSubmitButton()
{
  addSubmitButton(F("Submit"), F(""));
}

// add submit button with different label and name
void addSubmitButton(const __FlashStringHelper * value, const __FlashStringHelper * name)
{
  addSubmitButton(value, name, F(""));
}

void addSubmitButton(const String& value, const String& name) {
  addSubmitButton(value, name, EMPTY_STRING);
}

void addSubmitButton(const __FlashStringHelper * value, const __FlashStringHelper * name, const __FlashStringHelper * classes)
{
  addSubmitButton(String(value), String(name), String(classes));
}

void addSubmitButton(const String& value, const String& name, const String& classes)
{
  addHtml(F("<input "));
  {
    addHtmlAttribute(F("class"), concat(F("button link "), classes));
  }
  addHtmlAttribute(F("type"),  F("submit"));
  addHtmlAttribute(F("value"), value);

  if (name.length() > 0) {
    addHtmlAttribute(F("name"), name);
  }
  addHtmlAttribute(F("onclick"), F("toasting()"));
  addHtml(F("/><div id='toastmessage'></div>"));
}

// add copy to clipboard button
void addCopyButton(const String& value, const String& delimiter, const String& name)
{
  TXBuffer.addFlashString((PGM_P)FPSTR(jsClipboardCopyPart1));
  addHtml(value);
  TXBuffer.addFlashString((PGM_P)FPSTR(jsClipboardCopyPart2));
  addHtml(delimiter);
  TXBuffer.addFlashString((PGM_P)FPSTR(jsClipboardCopyPart3));

  // Fix HTML
  addHtml(F("<button "));
  addHtmlAttribute(F("class"),   F("button link"));
  addHtmlAttribute(F("onclick"), F("setClipboard()"));
  addHtml('>');
  addHtml(name);
  addHtml(' ', '(');
  html_copyText_marker();
  addHtml(F(")</button>"));
}

#include "../WebServer/HTML_wrappers.h"

#include "../Static/WebStaticData.h"

#include "../WebServer/Markup.h"

#include "../Helpers/StringConverter.h"

#include "../Globals/Settings.h"

// ********************************************************************************
// HTML string re-use to keep the executable smaller
// Flash strings are not checked for duplication.
// ********************************************************************************
void wrap_html_tag(const __FlashStringHelper * tag, const String& text) {
  addHtml('<');
  addHtml(tag);
  addHtml('>');
  addHtml(text);
  addHtml('<', '/');
  addHtml(tag);
  addHtml('>');
}

void wrap_html_tag(const String& tag, const String& text) {
  addHtml(strformat(
    F("<%s>%s</%s>"),
    tag.c_str(),
    text.c_str(),
    tag.c_str()));
}

void wrap_html_tag(char tag, const String& text) {
  addHtml(strformat(
    F("<%c>%s</%c>"),
    tag,
    text.c_str(),
    tag));
}

void html_B(const __FlashStringHelper * text) {
  wrap_html_tag('b', text);
}

void html_B(const String& text) {
  wrap_html_tag('b', text);
}

void html_I(const String& text) {
  wrap_html_tag('i', text);
}

void html_U(const String& text) {
  wrap_html_tag('u', text);
}

void html_TR_TD_highlight() {
  addHtml(F("<TR class=\"highlight\">"));
  html_TD();
}

void html_TR_TD() {
  html_TR();
  html_TD();
}

void html_BR() {
  addHtml(F("<BR>"));
}

void html_TR() {
  addHtml(F("<TR>"));
}

void html_TR_TD_height(int height) {
  html_TR();
  addHtml(strformat(
    F("<TD HEIGHT=\"%d\">"), height));
}

void html_TD() {
  addHtml(F("<TD>"));
}

void html_TD(const __FlashStringHelper * style) {
  addHtml(F("<TD style=\""));
  addHtml(style);
  addHtml(F(";\">"));
}

void html_TD(int td_cnt) {
  for (int i = 0; i < td_cnt; ++i) {
    html_TD();
  }
}

int copyTextCounter = 0;

void html_reset_copyTextCounter() {
  copyTextCounter = 0;
}

void html_copyText_TD() {
  ++copyTextCounter;

  addHtml(strformat(
    F("<TD id='copyText_%d'>"), copyTextCounter));
}

// Add some recognizable token to show which parts will be copied.
void html_copyText_marker() {
  addHtml(F("&#x022C4;")); //   &diam; &diamond; &Diamond; &#x022C4; &#8900;
}

void html_add_estimate_symbol() {
  addHtml(F(" &#8793; ")); //   &#8793;  &#x2259;  &wedgeq;
}

void html_table_class_normal() {
  html_table(F("normal"));
}

void html_table_class_multirow() {
  html_table(F("multirow even"), true);
}

void html_table_class_multirow_noborder() {
  html_table(F("multirow even"), false);
}

void html_table(const __FlashStringHelper * tableclass, bool boxed) {
  html_table(String(tableclass), boxed);
}

void html_table(const String& tableclass, bool boxed) {
  addHtml(F("<table "));
  addHtmlAttribute(F("class"), tableclass);

  if (boxed) {
    addHtmlAttribute(F("border"), F("1px"));
    addHtmlAttribute(F("frame"),  F("box"));
    addHtmlAttribute(F("rules"),  F("all"));
  }
  addHtml('>');
}

void html_table_header(const __FlashStringHelper * label) {
  html_table_header(label, 0);
}

void html_table_header(const String& label) {
  html_table_header(label, 0);
}

void html_table_header(const __FlashStringHelper * label, int width) {
  html_table_header(label, F(""), F(""), width);
}

void html_table_header(const String& label, int width) {
  html_table_header(label, EMPTY_STRING, EMPTY_STRING, width);
}

void html_table_header(const __FlashStringHelper * label, const __FlashStringHelper * helpButton, int width) {
  html_table_header(label, helpButton, F(""), width);
}

void html_table_header(const String& label, const __FlashStringHelper * helpButton, int width) {
  html_table_header(label, helpButton, EMPTY_STRING, width);
}

void html_table_header(const __FlashStringHelper * label, const String& helpButton, int width) {
  html_table_header(label, helpButton, EMPTY_STRING, width);
}

void html_table_header(const String& label, const String& helpButton, int width) {
  html_table_header(label, helpButton, EMPTY_STRING, width);
}

void html_table_header(const __FlashStringHelper * label, const __FlashStringHelper * helpButton, const String& rtdHelpButton, int width) {
  html_table_header(String(label), String(helpButton), rtdHelpButton, width);
}

void html_table_header(const String& label, const __FlashStringHelper * helpButton, const String& rtdHelpButton, int width) {
  html_table_header(label, String(helpButton), rtdHelpButton, width);
}

void html_table_header(const __FlashStringHelper * label, const String& helpButton, const String& rtdHelpButton, int width) {
  html_table_header(String(label), helpButton, rtdHelpButton, width);
}

void html_table_header(const __FlashStringHelper * label, const __FlashStringHelper * helpButton, const __FlashStringHelper * rtdHelpButton, int width) {
  html_table_header(String(label), String(helpButton), String(rtdHelpButton), width);
}

void html_table_header(const String& label, const __FlashStringHelper * helpButton, const __FlashStringHelper * rtdHelpButton, int width) {
  html_table_header(label, String(helpButton), String(rtdHelpButton), width);
}

void html_table_header(const __FlashStringHelper * label, const String& helpButton, const __FlashStringHelper * rtdHelpButton, int width) {
  html_table_header(String(label), helpButton, String(rtdHelpButton), width);
}

void html_table_header(const String& label, const String& helpButton, const String& rtdHelpButton, int width) {
  addHtml(F("<TH"));

  if (width > 0) {
    addHtml(strformat(
      F(" style='width:%dpx;'"), width));
  }
  addHtml('>');
  addHtml(label);

  if (helpButton.length() > 0) {
    addHelpButton(helpButton);
  }

  if (rtdHelpButton.length() > 0) {
    addRTDHelpButton(rtdHelpButton);
  }
  addHtml(F("</TH>"));
}

void html_end_table() {
  addHtml(F("</table>"));
}

void html_end_form() {
  addHtml(F("</form>"));
}

void html_add_button_prefix() {
  html_add_button_prefix(EMPTY_STRING, true);
}

void html_add_button_prefix(const __FlashStringHelper * classes, bool enabled) {
  html_add_button_prefix(String(classes), enabled);
}

void html_add_button_prefix(const String& classes, bool enabled) {
  addHtml(F(" <a class='button link"));

  if (classes.length() > 0) {
    addHtml(' ');
    addHtml(classes);
  }

  if (!enabled) {
    addDisabled();
  }
  addHtml('\'');

  if (!enabled) {
    addDisabled();
  }
  addHtml(F(" href='"));
}

void html_add_wide_button_prefix() {
  html_add_wide_button_prefix(EMPTY_STRING, true);
}

void html_add_wide_button_prefix(const String& classes, bool enabled) {
  html_add_button_prefix(concat(F("wide "), classes), enabled);
}

void html_add_form() {
  addHtml(F("<form name='frmselect' method='post'>"));
}

void html_add_JQuery_script() {
  #ifndef CDN_URL_JQUERY
    #define CDN_URL_JQUERY "https://code.jquery.com/jquery-3.6.4.min.js"
  #endif // ifndef CDN_URL_JQUERY
  addHtml(F("<script src=\"" CDN_URL_JQUERY "\"></script>"));
}

#if FEATURE_CHART_JS
void html_add_ChartJS_script() {
  // To update the CDN link go to: https://www.chartjs.org/docs/latest/getting-started/installation.html
  // - Select a CDN (jsdelivr is fine)
  // - Select the chart.js file (may be called chart.umd.min.js) and copy the url
  // - Replace the url in below script src element, keeping the quotes
  #ifndef CDN_URL_CHART_JS
    #define CDN_URL_CHART_JS "https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"
  #endif // ifndef CDN_URL_CHART_JS

  #ifndef CDN_URL_CHART_JS_ADAPTER_DATE
    #define CDN_URL_CHART_JS_ADAPTER_DATE "https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"
  #endif

  #ifndef CDN_URL_CHART_JS_HAMMERJS
    #define CDN_URL_CHART_JS_HAMMERJS "https://cdn.jsdelivr.net/npm/hammerjs@2.0.8"
  #endif

  #ifndef CDN_URL_CHART_JS_PLUGIN_ZOOM
    #define CDN_URL_CHART_JS_PLUGIN_ZOOM "https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@2.0.1/dist/chartjs-plugin-zoom.min.js"
  #endif


  addHtml(F("<script src=\"" CDN_URL_CHART_JS "\"></script>"));
  addHtml(F("<script src=\"" CDN_URL_CHART_JS_ADAPTER_DATE "\"></script>"));
  addHtml(F("<script src=\"" CDN_URL_CHART_JS_HAMMERJS "\"></script>"));
  addHtml(F("<script src=\"" CDN_URL_CHART_JS_PLUGIN_ZOOM "\"></script>"));
}
#endif // if FEATURE_CHART_JS

#if FEATURE_RULES_EASY_COLOR_CODE
void html_add_Easy_color_code_script() {
  serve_JS(JSfiles_e::EasyColorCode_codemirror);
  serve_JS(JSfiles_e::EasyColorCode_espeasy);
  serve_JS(JSfiles_e::EasyColorCode_cm_plugins);
}
#endif

void html_add_autosubmit_form() {
  addHtml(F("<script><!--\n"
            // "function dept_onchange(frmselect) {frmselect.submit();}"
            "function dept_onchange(e){e.submit()}"
            // "function task_select_onchange(frmselect) {var element = document.getElementById('nosave'); if (typeof(element) != 'undefined' && element != null) {element.disabled = false; element.checked = true;} frmselect.submit();}"
            "function task_select_onchange(e){var n=document.getElementById('nosave');void 0!==n&&null!=n&&(n.disabled=!1,n.checked=!0),e.submit()}"
            // "function rules_set_onchange(rulesselect) {document.getElementById('rules').disabled = true; rulesselect.submit();}"
            "function rules_set_onchange(e){document.getElementById('rules').disabled=!0,e.submit()}"
            "\n//--></script>"));
}

void html_add_script(const __FlashStringHelper * script, bool defer) {
  html_add_script(defer);
  addHtml(script);
  html_add_script_end();
}

void html_add_script(const String& script, bool defer) {
  html_add_script(defer);
  addHtml(script);
  html_add_script_end();
}

void html_add_script(bool defer) {
  html_add_script_arg(F(""), defer);
}

void html_add_script_arg(const __FlashStringHelper * script_arg, bool defer) {
  addHtml(F("<script"));

  addHtml(' ');
  addHtml(script_arg);

  if (defer) {
    addHtml(F(" defer"));
  }
  addHtml('>');
}

void html_add_script_end() {
  addHtml(F("</script>"));
}

// if there is an error-string, add it to the html code with correct formatting
void addHtmlError(const __FlashStringHelper * error) {
  addHtmlError(String(error));
}

void addHtmlError(const String& error) {
  if (error.length() > 0)
  {
    addHtml(F("<div class=\""));

    if (error.startsWith(F("Warn"))) {
      addHtml(F("warning"));
    } else {
      addHtml(F("alert"));
    }
    addHtml(F("\"><span class=\"closebtn\" onclick=\"this.parentElement.style.display='none';\">&times;</span>"));
    addHtml(error);
    addHtml(F("</div>"));
  }
}

void addHtml(const char& char1) {
  TXBuffer += char1;
}

void addHtml(const char& char1, const char& char2) {
  TXBuffer += char1;
  TXBuffer += char2;
}

void addHtml(const __FlashStringHelper * html) {
  TXBuffer.addFlashString((PGM_P)html);
}

void addHtml(const String& html) {
  TXBuffer += html;
}

void addHtml(String&& html) {
  TXBuffer += html;
}

void addHtmlInt(int8_t int_val) {
  addHtml(String(int_val));
}

void addHtmlInt(uint8_t int_val) {
  addHtml(String(int_val));
}

void addHtmlInt(int16_t int_val) {
  addHtml(String(int_val));
}

#if ESP_IDF_VERSION_MAJOR >= 5
#ifndef __riscv
void addHtmlInt(int int_val) {
  addHtml(String(int_val));
}

void addHtmlInt(unsigned int int_val) {
  addHtml(String(int_val));
}
#endif
#endif

void addHtmlInt(int32_t int_val) {
  addHtml(String(int_val));
}

void addHtmlInt(uint32_t int_val) {
  addHtml(String(int_val));
}

void addHtmlInt(int64_t int_val) {
  addHtml(ll2String(int_val));
}

void addHtmlInt(uint64_t int_val) {
  addHtml(ull2String(int_val));
}

void addHtmlFloat(const float& value, unsigned int nrDecimals) {
  addHtml(toString(value, nrDecimals));
}

void addHtmlFloat_NaN_toNull(const float& value, unsigned int nrDecimals) {
  String res;
  if (toValidString(res, value, nrDecimals)) {
    addHtml(res);
  } else {
    addHtml(F("null"));
  }
}


#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
void addHtmlFloat(const double& value, unsigned int nrDecimals) {
  addHtml(doubleToString(value, nrDecimals));
}

void addHtmlFloat_NaN_toNull(const double& value, unsigned int nrDecimals) {
  String res;
  if (doubleToValidString(res, value, nrDecimals)) {
    addHtml(res);
  } else {
    addHtml(F("null"));
  }
}

#endif


void addEncodedHtml(const __FlashStringHelper * html) {
  // FIXME TD-er: What about the function htmlStrongEscape ??
  addEncodedHtml(String(html));
}

void addEncodedHtml(const String& html) {
  // FIXME TD-er: What about the function htmlStrongEscape ??
  String copy(html);

  htmlEscape(copy);
  addHtml(copy);
}

void addHtmlAttribute(char label, int value) {
  addHtmlAttribute(String(label), value);
}

void addHtmlAttribute(char label, float value) {
  addHtmlAttribute(String(label), toString(value, 2));
}

void addHtmlAttribute(const __FlashStringHelper * label, int value) {
  addHtml(' ');
  addHtml(label);
  addHtml('=');
  addHtmlInt(value);
  addHtml(' ');
}

void addHtmlAttribute(const __FlashStringHelper * label, float value) {
  addHtmlAttribute(label, toString(value, 2));
}

void addHtmlAttribute(const String& label, int value) {
  addHtml(' ');
  addHtml(label);
  addHtml('=');
  addHtmlInt(value);
  addHtml(' ');
}

void addHtmlAttribute(const __FlashStringHelper * label, const __FlashStringHelper * value) {
  addHtmlAttribute(label, String(value));
}

void addHtmlAttribute(const __FlashStringHelper * label, const String& value) {
  addHtml(' ');
  addHtml(label);
  addHtml('=', '\'');
  addEncodedHtml(value);
  addHtml('\'', ' ');
}

void addHtmlAttribute(const String& label, const String& value) {
  addHtml(' ');
  addHtml(label);
  addHtml('=', '\'');
  addEncodedHtml(value);
  addHtml('\'', ' ');
}

void addDisabled() {
  addHtml(F(" disabled"));
}

void addHtmlLink(const String& htmlclass, const String& url, const String& label) {
  addHtml(F(" <a "));
  addHtmlAttribute(F("class"),  htmlclass);
  addHtmlAttribute(F("href"),   url);
  addHtmlAttribute(F("target"), F("_blank"));
  addHtml('>');
  addHtml(label);
  addHtml(F("</a>"));
}

void addHtmlDiv(const __FlashStringHelper * htmlclass, const String& content, const String& id, const String& attribute)
{
  addHtmlDiv(String(htmlclass), content, id, attribute);
}


void addHtmlDiv(const String& htmlclass) {
  addHtmlDiv(htmlclass, EMPTY_STRING);
}

void addHtmlDiv(const String& htmlclass, const String& content) {
  addHtmlDiv(htmlclass, content, EMPTY_STRING);
}

void addHtmlDiv(const String& htmlclass, const String& content, const String& id, const String& attribute) {
  addHtml(F(" <div "));
  addHtmlAttribute(F("class"), htmlclass);
  if (id.length() > 0) {
    addHtmlAttribute(F("id"), id);
  }
  if (attribute.length() > 0) {
    addHtml(' ');
    addHtml(attribute);
  }
  addHtml('>');
  addHtml(content);
  addHtml(F("</div>"));
}

void addEnabled(boolean enabled)
{
  addHtml(F("<span class='enabled "));

  if (enabled) {
    addHtml(F("on'>&#10004;"));
  }
  else {
    addHtml(F("off'>&#10060;"));
  }
  addHtml(F("</span>"));
}

void addGpioHtml(int8_t pin) {
  if (pin == -1) { return; }
  addHtml(formatGpioLabel(pin, false));

  if (Settings.isSPI_pin(pin) ||
      Settings.isI2C_pin(pin) ||
      Settings.isEthernetPin(pin) ||
      Settings.isEthernetPinOptional(pin)) {
    addHtml(' ');
    addHtml(F(HTML_SYMBOL_WARNING));
  }
}

void Label_Gpio_toHtml(const __FlashStringHelper *label, const String& gpio_pin_descr) {
  addHtml(label);
  addHtml(':');
  addHtml(F("&nbsp;"));
  addHtml(gpio_pin_descr);
}

#include "../WebServer/ControllerPage.h"


#ifdef WEBSERVER_CONTROLLERS

# include "../WebServer/ESPEasy_WebServer.h"
# include "../WebServer/HTML_wrappers.h"
# include "../WebServer/Markup.h"
# include "../WebServer/Markup_Buttons.h"
# include "../WebServer/Markup_Forms.h"

# include "../DataStructs/ESPEasy_EventStruct.h"

# include "../ESPEasyCore/Controller.h"

# include "../Globals/CPlugins.h"
# include "../Globals/Settings.h"

# if FEATURE_MQTT
#  include "../Globals/MQTT.h"
# endif // if FEATURE_MQTT

# include "../Helpers/_CPlugin_init.h"
# include "../Helpers/_CPlugin_Helper_webform.h"
# include "../Helpers/_Plugin_SensorTypeHelper.h"
# include "../Helpers/ESPEasy_Storage.h"
# include "../Helpers/StringConverter.h"

# include "../Helpers/_CPlugin_Helper_webform.h"
# include "../Helpers/_Plugin_SensorTypeHelper.h"
# include "../Helpers/ESPEasy_Storage.h"
# include "../Helpers/StringConverter.h"

// ********************************************************************************
// Web Interface controller page
// ********************************************************************************
void handle_controllers() {
  # ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_controllers"));
  # endif // ifndef BUILD_NO_RAM_TRACKER

  if (!isLoggedIn()) { return; }
  navMenuIndex = MENU_INDEX_CONTROLLERS;
  TXBuffer.startStream();
  sendHeadandTail_stdtemplate(_HEAD);

  // 'index' value in the URL
  uint8_t controllerindex  = getFormItemInt(F("index"), 0);
  boolean controllerNotSet = controllerindex == 0;
  --controllerindex; // Index in URL is starting from 1, but starting from 0 in the array.

  const int protocol_webarg_value = getFormItemInt(F("protocol"), -1);

  // submitted data
  if ((protocol_webarg_value != -1) && !controllerNotSet)
  {
    const protocolIndex_t protocolIndex = protocol_webarg_value;
    bool mustInit                       = false;
    bool mustCallCpluginSave            = false;
    {
      // Place in a scope to free ControllerSettings memory ASAP
      MakeControllerSettings(ControllerSettings); // -V522

      if (!AllocatedControllerSettings()) {
        addHtmlError(F("Not enough free memory to save settings"));
      } else {
        // Need to make sure every byte between the members is also zero
        // Otherwise the checksum will fail and settings will be saved too often.
        ControllerSettings->reset();

        if (Settings.Protocol[controllerindex] != protocolIndex)
        {
          // Protocol has changed.
          Settings.Protocol[controllerindex] = protocolIndex;

          // there is a protocolIndex selected?
          if (protocolIndex != 0)
          {
            mustInit = true;
            handle_controllers_clearLoadDefaults(controllerindex, *ControllerSettings);
          }
        }

        // subitted same protocolIndex
        else
        {
          // there is a protocolIndex selected
          if (protocolIndex != 0)
          {
            mustInit = true;
            handle_controllers_CopySubmittedSettings(controllerindex, *ControllerSettings);
            mustCallCpluginSave = true;
          }
        }
        addHtmlError(SaveControllerSettings(controllerindex, *ControllerSettings));
        # if FEATURE_MQTT_DISCOVER

        // AutoDiscovery enabled?
        if (ControllerSettings->mqtt_autoDiscovery()
            && Settings.ControllerEnabled[controllerindex]

            // && (ControllerSettings->MqttAutoDiscoveryTrigger[0] != 0)
            && (ControllerSettings->MqttAutoDiscoveryTopic[0] != 0)
            ) {
          // Generate random time-offset in 0.1 sec, range 1..30 seconds
          mqttDiscoveryTimeout = random(10, MQTT_DISCOVERY_MAX_DELAY_0_1_SECONDS);

          if (loglevelActiveFor(LOG_LEVEL_INFO)) {
            addLog(LOG_LEVEL_INFO, strformat(F("MQTT : Start AutoDiscovery on Save. Starting in %.1f sec."), mqttDiscoveryTimeout / 10));
          }
        }
        # endif // if FEATURE_MQTT_DISCOVER
      }
    }

    if (mustCallCpluginSave) {
      // Call CPLUGIN_WEBFORM_SAVE after destructing ControllerSettings object to reduce RAM usage.
      // Controller plugin almost only deals with custom controller settings.
      // Even if they need to save things to the ControllerSettings, then the changes must
      // already be saved first as the CPluginCall does not have the ControllerSettings as argument.
      handle_controllers_CopySubmittedSettings_CPluginCall(controllerindex);
    }
    addHtmlError(SaveSettings());

    if (mustInit) {
      // Init controller plugin using the new settings.
      protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(controllerindex);

      if (validProtocolIndex(ProtocolIndex)) {
        struct EventStruct TempEvent;
        TempEvent.ControllerIndex = controllerindex;
        String dummy;
        CPlugin::Function cfunction =
          Settings.ControllerEnabled[controllerindex] ? CPlugin::Function::CPLUGIN_INIT : CPlugin::Function::CPLUGIN_EXIT;
        CPluginCall(ProtocolIndex, cfunction, &TempEvent, dummy);
      }
    }
  }

  html_add_form();

  if (controllerNotSet)
  {
    handle_controllers_ShowAllControllersTable();
  }
  else
  {
    handle_controllers_ControllerSettingsPage(controllerindex);
  }

  sendHeadandTail_stdtemplate(_TAIL);
  TXBuffer.endStream();
}

// ********************************************************************************
// Selected controller has changed.
// Clear all Controller settings and load some defaults
// ********************************************************************************
void handle_controllers_clearLoadDefaults(uint8_t controllerindex, ControllerSettingsStruct& ControllerSettings)
{
  // Protocol has changed and it was not an empty one.
  // reset (some) default-settings
  protocolIndex_t ProtocolIndex = getProtocolIndex_from_CPluginID(Settings.Protocol[controllerindex]);

  if (!validProtocolIndex(ProtocolIndex)) {
    return;
  }

  const ProtocolStruct& proto = getProtocolStruct(ProtocolIndex);

  ControllerSettings.reset();
# if FEATURE_MQTT_TLS
  ControllerSettings.TLStype(TLS_types::NoTLS);
# endif // if FEATURE_MQTT_TLS
  ControllerSettings.Port = proto.defaultPort;

  // Load some templates from the controller.
  struct EventStruct TempEvent;

  // Hand over the controller settings in the Data pointer, so the controller can set some defaults.
  TempEvent.Data = (uint8_t *)(&ControllerSettings);

  if (proto.usesTemplate) {
    String dummy;
    CPluginCall(ProtocolIndex, CPlugin::Function::CPLUGIN_PROTOCOL_TEMPLATE, &TempEvent, dummy);
  }
  safe_strncpy(ControllerSettings.Subscribe,            TempEvent.String1.c_str(), sizeof(ControllerSettings.Subscribe));
  safe_strncpy(ControllerSettings.Publish,              TempEvent.String2.c_str(), sizeof(ControllerSettings.Publish));
  safe_strncpy(ControllerSettings.MQTTLwtTopic,         TempEvent.String3.c_str(), sizeof(ControllerSettings.MQTTLwtTopic));
  safe_strncpy(ControllerSettings.LWTMessageConnect,    TempEvent.String4.c_str(), sizeof(ControllerSettings.LWTMessageConnect));
  safe_strncpy(ControllerSettings.LWTMessageDisconnect, TempEvent.String5.c_str(), sizeof(ControllerSettings.LWTMessageDisconnect));

  // NOTE: do not enable controller by default, give user a change to enter sensible values first
  Settings.ControllerEnabled[controllerindex] = false;

  // not resetted to default (for convenience)
  // SecuritySettings.ControllerUser[controllerindex]
  // SecuritySettings.ControllerPassword[controllerindex]

  ClearCustomControllerSettings(controllerindex);
}

// ********************************************************************************
// Collect all submitted form data and store in the ControllerSettings
// ********************************************************************************
void handle_controllers_CopySubmittedSettings(uint8_t controllerindex, ControllerSettingsStruct& ControllerSettings)
{
  // copy all settings to controller settings struct
  for (int parameterIdx = 0; parameterIdx <= ControllerSettingsStruct::CONTROLLER_ENABLED; ++parameterIdx) {
    ControllerSettingsStruct::VarType varType = static_cast<ControllerSettingsStruct::VarType>(parameterIdx);
    saveControllerParameterForm(ControllerSettings, controllerindex, varType);
  }
}

void handle_controllers_CopySubmittedSettings_CPluginCall(uint8_t controllerindex) {
  protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(controllerindex);

  if (validProtocolIndex(ProtocolIndex)) {
    struct EventStruct TempEvent;
    TempEvent.ControllerIndex = controllerindex;

    // Call controller plugin to save CustomControllerSettings
    String dummy;
    CPluginCall(ProtocolIndex, CPlugin::Function::CPLUGIN_WEBFORM_SAVE, &TempEvent, dummy);
  }
}

// ********************************************************************************
// Show table with all selected controllers
// ********************************************************************************
void handle_controllers_ShowAllControllersTable()
{
  html_table_class_multirow();
  html_TR();
  html_table_header(F(""),        70);
  html_table_header(F("Nr"),      50);
  html_table_header(F("Enabled"), 100);
  html_table_header(F("Protocol"));
  html_table_header(F("Host"));
  html_table_header(F("Port"));

  MakeControllerSettings(ControllerSettings); // -V522

  if (AllocatedControllerSettings()) {
    for (controllerIndex_t x = 0; x < CONTROLLER_MAX; x++)
    {
      const bool cplugin_set = Settings.Protocol[x] != INVALID_C_PLUGIN_ID;


      LoadControllerSettings(x, *ControllerSettings);
      html_TR_TD();

      if (cplugin_set && !supportedCPluginID(Settings.Protocol[x])) {
        html_add_button_prefix(F("red"), true);
      } else {
        html_add_button_prefix();
      }
      {
        addHtml(F("controllers?index="));
        addHtmlInt(x + 1);
        addHtml(F("'>"));

        if (cplugin_set) {
          addHtml(F("Edit"));
        } else {
          addHtml(F("Add"));
        }
        addHtml(F("</a><TD>"));
        addHtml(getControllerSymbol(x));
      }
      html_TD();

      if (cplugin_set)
      {
        addEnabled(Settings.ControllerEnabled[x]);

        html_TD();
        addHtml(getCPluginNameFromCPluginID(Settings.Protocol[x]));
        html_TD();
        const protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(x);
        {
          String hostDescription;
          CPluginCall(ProtocolIndex, CPlugin::Function::CPLUGIN_WEBFORM_SHOW_HOST_CONFIG, 0, hostDescription);

          if (!hostDescription.isEmpty()) {
            addHtml(hostDescription);
          } else {
            addHtml(ControllerSettings->getHost());
          }
        }

        html_TD();
        const ProtocolStruct& proto = getProtocolStruct(ProtocolIndex);

        if ((INVALID_PROTOCOL_INDEX == ProtocolIndex) || proto.usesPort) {
          addHtmlInt(13 == Settings.Protocol[x] ? Settings.UDPPort : ControllerSettings->Port); // P2P/C013 exception
        }
      }
      else {
        html_TD(3);
      }
    }
  }
  html_end_table();
  html_end_form();
}

// ********************************************************************************
// Show the controller settings page
// ********************************************************************************
void handle_controllers_ControllerSettingsPage(controllerIndex_t controllerindex)
{
  if (!validControllerIndex(controllerindex)) {
    return;
  }

  // Show controller settings page
  html_table_class_normal();
  addFormHeader(F("Controller Settings"));
  addRowLabel(F("Protocol"));
  uint8_t choice = Settings.Protocol[controllerindex];

  addSelector_Head_reloadOnChange(F("protocol"));
  addSelector_Item(F("- Standalone -"), 0, false, false, EMPTY_STRING);

  protocolIndex_t protocolIndex = 0;

  while (validProtocolIndex(protocolIndex))
  {
    const cpluginID_t number = getCPluginID_from_ProtocolIndex(protocolIndex);
    boolean disabled         = false; // !((controllerindex == 0) || !Protocol[x].usesMQTT);
    addSelector_Item(getCPluginNameFromProtocolIndex(protocolIndex),
                     number,
                     choice == number,
                     disabled);
    ++protocolIndex;
  }
  addSelector_Foot(true);

  // addHelpButton(F("EasyProtocols")); // Documentation on Wiki is outdated

  const protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(controllerindex);
  const ProtocolStruct& proto         = getProtocolStruct(ProtocolIndex);

  # ifndef LIMIT_BUILD_SIZE
  addRTDControllerButton(getCPluginID_from_ProtocolIndex(ProtocolIndex));
  # endif // ifndef LIMIT_BUILD_SIZE

  if (Settings.Protocol[controllerindex])
  {
    {
      MakeControllerSettings(ControllerSettings); // -V522

      if (!AllocatedControllerSettings()) {
        addHtmlError(F("Out of memory, cannot load page"));
      } else {
        LoadControllerSettings(controllerindex, *ControllerSettings);

        if (!proto.Custom)
        {
          if (proto.usesHost) {
            addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_USE_DNS);

            if (ControllerSettings->UseDNS)
            {
              addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_HOSTNAME);
            }
            else
            {
              addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_IP);
            }
          }

          if (proto.usesPort) {
            addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_PORT);
          }
          # if FEATURE_MQTT_TLS || FEATURE_HTTP_TLS

          if (proto.usesTLS) {
            addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_MQTT_TLS_TYPE);
            if (proto.usesMQTT) {
              addFormNote(F("Default ports: MQTT: 1883 / MQTT TLS: 8883"));
            } else {
              addFormNote(F("Default ports: HTTP: 80 / HTTPS: 443"));
            }
          }
          # endif // if FEATURE_MQTT_TLS || FEATURE_HTTP_TLS
          # ifdef USES_ESPEASY_NOW

          if (proto.usesMQTT) {
            // FIXME TD-er: Currently only enabled for MQTT protocols, later for more
            addControllerParameterForm(*ControllerSettings, controllerindex,
                                       ControllerSettingsStruct::CONTROLLER_ENABLE_ESPEASY_NOW_FALLBACK);
          }
          # endif // ifdef USES_ESPEASY_NOW

          if (proto.usesQueue) {
            addTableSeparator(F("Controller Queue"), 2, 3);
            addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_MIN_SEND_INTERVAL);
            addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_MAX_QUEUE_DEPTH);
            addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_MAX_RETRIES);
            addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_FULL_QUEUE_ACTION);

            if (proto.allowsExpire) {
              addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_ALLOW_EXPIRE);
            }
            addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_DEDUPLICATE);
          }

          if (proto.usesCheckReply) {
            addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_CHECK_REPLY);
          }

          if (proto.usesTimeout) {
            addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_TIMEOUT);

            if (proto.usesHost) {
              addFormNote(F("Typical timeout: 100...300 msec for local host, >500 msec for internet hosts"));
            }
          }

          if (proto.usesSampleSets) {
            addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_SAMPLE_SET_INITIATOR);
          }

          if (proto.allowLocalSystemTime) {
            addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_USE_LOCAL_SYSTEM_TIME);
          }


          if (proto.useCredentials()) {
            addTableSeparator(F("Credentials"), 2, 3);
          }

          if (proto.useExtendedCredentials()) {
            addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_USE_EXTENDED_CREDENTIALS);
          }

          if (proto.usesAccount)
          {
            addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_USER);
          }

          if (proto.usesPassword)
          {
            addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_PASS);
          }
          # if FEATURE_MQTT

          if (proto.usesMQTT) {
            addTableSeparator(F("MQTT"), 2, 3);

            addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_CLIENT_ID);
            addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_UNIQUE_CLIENT_ID_RECONNECT);
            addRowLabel(F("Current Client ID"));
            addHtml(getMQTTclientID(*ControllerSettings));
            addFormNote(F("Updated on load of this page"));
            addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_RETAINFLAG);
          }
          # endif // if FEATURE_MQTT


          if (proto.usesTemplate
          # if FEATURE_MQTT
              || proto.usesMQTT
          # endif // if FEATURE_MQTT
              )
          {
            addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_SUBSCRIBE);
            addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_PUBLISH);
          }
          # if FEATURE_MQTT

          if (proto.usesMQTT)
          {
            addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_LWT_TOPIC);
            addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_LWT_CONNECT_MESSAGE);
            addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_LWT_DISCONNECT_MESSAGE);
            addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_SEND_LWT);
            addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_WILL_RETAIN);
            addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_CLEAN_SESSION);
            addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_KEEP_ALIVE_TIME);
            #  if FEATURE_MQTT_DISCOVER

            if (proto.mqttAutoDiscover) {
              addTableSeparator(F("MQTT - Auto Discovery"), 2, 3);

              addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_AUTO_DISCOVERY_OPTION);
              addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_AUTO_DISCOVERY_TRIGGER);
              addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_AUTO_DISCOVERY_TOPIC);
              addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_AUTO_DISCOVERY_CONFIG);
              addControllerParameterForm(*ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_RETAINED_DISCOVERY_OPTION);
            }
            #  endif // if FEATURE_MQTT_DISCOVER
          }
          # endif // if FEATURE_MQTT
        }
      }

      // End of scope for ControllerSettings, destruct it to save memory.
    }
    {
      // Load controller specific settings
      struct EventStruct TempEvent;
      TempEvent.ControllerIndex = controllerindex;

      String webformLoadString;
      CPluginCall(ProtocolIndex, CPlugin::Function::CPLUGIN_WEBFORM_LOAD, &TempEvent, webformLoadString);

      if (webformLoadString.length() > 0) {
        addHtmlError(F("Bug in CPlugin::Function::CPLUGIN_WEBFORM_LOAD, should not append to string, use addHtml() instead"));
      }
    }
    {
# if FEATURE_MQTT

      if (proto.usesMQTT) {
        addFormSubHeader(F("Connection Status"));
        addRowLabel(F("MQTT Client Connected"));
        addEnabled(MQTTclient_connected);

#  if FEATURE_MQTT_TLS

        if (proto.usesTLS) {
          addRowLabel(F("Last Error"));
          addHtmlInt(mqtt_tls_last_error);
          addHtml(F(": "));
          addHtml(mqtt_tls_last_errorstr);
/*
      getLastError codes as documented in lib\lib_ssl\bearssl-esp8266\src\t_bearssl_ssl.h
      SSL-level error codes
      |    Receive Fatal Alert
      |    |     Send Fatal Alert
      |    |     |
       0 : 256 : 512 : BR_ERR_OK
       1 : 257 : 513 : BR_ERR_BAD_PARAM           - caller-provided parameter is incorrect
       2 : 258 : 514 : BR_ERR_BAD_STATE           - operation requested by the caller cannot be applied with the current context state (e.g. reading data while outgoing data is waiting to be sent)
       3 : 259 : 515 : BR_ERR_UNSUPPORTED_VERSION - incoming protocol or record version is unsupported
       4 : 260 : 516 : BR_ERR_BAD_VERSION         - incoming record version does not match the expected version
       5 : 261 : 517 : BR_ERR_BAD_LENGTH          - incoming record length is invalid
       6 : 262 : 518 : BR_ERR_TOO_LARGE           - incoming record is too large to be processed, or buffer is too small for the handshake message to send
       7 : 263 : 519 : BR_ERR_BAD_MAC             - decryption found an invalid padding, or the record MAC is not correct
       8 : 264 : 520 : BR_ERR_NO_RANDOM           - no initial entropy was provided, and none can be obtained from the OS
       9 : 265 : 521 : BR_ERR_UNKNOWN_TYPE        - incoming record type is unknown
      10 : 266 : 522 : BR_ERR_UNEXPECTED          - incoming record or message has wrong type with regards to the current engine state
      12 : 268 : 524 : BR_ERR_BAD_CCS             - ChangeCipherSpec message from the peer has invalid contents
      13 : 269 : 525 : BR_ERR_BAD_ALERT           - alert message from the peer has invalid contents (odd length)
      14 : 270 : 526 : BR_ERR_BAD_HANDSHAKE       - incoming handshake message decoding failed
      15 : 271 : 527 : BR_ERR_OVERSIZED_ID        - ServerHello contains a session ID which is larger than 32 bytes
      16 : 272 : 528 : BR_ERR_BAD_CIPHER_SUITE    - server wants to use a cipher suite that we did not claim to support. This is also reported if we tried to advertise a cipher suite that we do not support
      17 : 273 : 529 : BR_ERR_BAD_COMPRESSION     - server wants to use a compression that we did not claim to support
      18 : 274 : 530 : BR_ERR_BAD_FRAGLEN         - server's max fragment length does not match client's
      19 : 275 : 531 : BR_ERR_BAD_SECRENEG        - secure renegotiation failed
      20 : 276 : 532 : BR_ERR_EXTRA_EXTENSION     - server sent an extension type that we did not announce, or used the same extension type several times in a single ServerHello
      21 : 277 : 533 : BR_ERR_BAD_SNI             - invalid Server Name Indication contents (when used by the server, this extension shall be empty)
      22 : 278 : 534 : BR_ERR_BAD_HELLO_DONE      - invalid ServerHelloDone from the server (length is not 0)
      23 : 279 : 535 : BR_ERR_LIMIT_EXCEEDED      - internal limit exceeded (e.g. server's public key is too large)
      24 : 280 : 536 : BR_ERR_BAD_FINISHED        - Finished message from peer does not match the expected value
      25 : 281 : 537 : BR_ERR_RESUME_MISMATCH     - session resumption attempt with distinct version or cipher suite
      26 : 282 : 538 : BR_ERR_INVALID_ALGORITHM   - unsupported or invalid algorithm (ECDHE curve, signature algorithm, hash function)
      27 : 283 : 539 : BR_ERR_BAD_SIGNATURE       - invalid signature (on ServerKeyExchange from server, or in CertificateVerify from client)
      28 : 284 : 540 : BR_ERR_WRONG_KEY_USAGE     - peer's public key does not have the proper type or is not allowed for requested operation
      29 : 285 : 541 : BR_ERR_NO_CLIENT_AUTH      - client did not send a certificate upon request, or the client certificate could not be validated
      31 : 287 : 543 : BR_ERR_IO                  - I/O error or premature close on underlying transport stream. This error code is set only by the simplified I/O API ("br_sslio_*")

      getLastError codes as documented in lib\lib_ssl\bearssl-esp8266\src\t_bearssl_x509.h
      32 : BR_ERR_X509_OK                  - validation was successful; this is not actually an error
      33 : BR_ERR_X509_INVALID_VALUE       - invalid value in an ASN.1 structure
      34 : BR_ERR_X509_TRUNCATED           - truncated certificate
      35 : BR_ERR_X509_EMPTY_CHAIN         - empty certificate chain (no certificate at all)
      36 : BR_ERR_X509_INNER_TRUNC         - decoding error: inner element extends beyond outer element size
      37 : BR_ERR_X509_BAD_TAG_CLASS       - decoding error: unsupported tag class (application or private)
      38 : BR_ERR_X509_BAD_TAG_VALUE       - decoding error: unsupported tag value
      39 : BR_ERR_X509_INDEFINITE_LENGTH   - decoding error: indefinite length
      40 : BR_ERR_X509_EXTRA_ELEMENT       - decoding error: extraneous element
      41 : BR_ERR_X509_UNEXPECTED          - decoding error: unexpected element
      42 : BR_ERR_X509_NOT_CONSTRUCTED     - decoding error: expected constructed element, but is primitive
      43 : BR_ERR_X509_NOT_PRIMITIVE       - decoding error: expected primitive element, but is constructed
      44 : BR_ERR_X509_PARTIAL_BYTE        - decoding error: BIT STRING length is not multiple of 8
      45 : BR_ERR_X509_BAD_BOOLEAN         - decoding error: BOOLEAN value has invalid length
      46 : BR_ERR_X509_OVERFLOW            - decoding error: value is off-limits
      47 : BR_ERR_X509_BAD_DN              - invalid distinguished name
      48 : BR_ERR_X509_BAD_TIME            - invalid date/time representation
      49 : BR_ERR_X509_UNSUPPORTED         - certificate contains unsupported features that cannot be ignored
      50 : BR_ERR_X509_LIMIT_EXCEEDED      - key or signature size exceeds internal limits
      51 : BR_ERR_X509_WRONG_KEY_TYPE      - key type does not match that which was expected
      52 : BR_ERR_X509_BAD_SIGNATURE       - signature is invalid
      53 : BR_ERR_X509_TIME_UNKNOWN        - validation time is unknown
      54 : BR_ERR_X509_EXPIRED             - certificate is expired or not yet valid
      55 : BR_ERR_X509_DN_MISMATCH         - issuer/subject DN mismatch in the chain
      56 : BR_ERR_X509_BAD_SERVER_NAME     - expected server name was not found in the chain
      57 : BR_ERR_X509_CRITICAL_EXTENSION  - unknown critical extension in certificate
      58 : BR_ERR_X509_NOT_CA              - not a CA, or path length constraint violation
      59 : BR_ERR_X509_FORBIDDEN_KEY_USAGE - Key Usage extension prohibits intended usage
      60 : BR_ERR_X509_WEAK_PUBLIC_KEY     - public key found in certificate is too small
      62 : BR_ERR_X509_NOT_TRUSTED         - chain could not be linked to a trust anchor

      getLastError codes as documented in lib\lib_ssl\bearssl-esp8266\src\t_bearssl_ssl.h
       10 : 266 : BR_ALERT_UNEXPECTED_MESSAGE
       20 : 276 : BR_ALERT_BAD_RECORD_MAC
       22 : 278 : BR_ALERT_RECORD_OVERFLOW
       30 : 286 : BR_ALERT_DECOMPRESSION_FAILURE
       40 : 296 : BR_ALERT_HANDSHAKE_FAILURE
       42 : 298 : BR_ALERT_BAD_CERTIFICATE
       43 : 299 : BR_ALERT_UNSUPPORTED_CERTIFICATE
       44 : 300 : BR_ALERT_CERTIFICATE_REVOKED
       45 : 301 : BR_ALERT_CERTIFICATE_EXPIRED
       46 : 302 : BR_ALERT_CERTIFICATE_UNKNOWN
       47 : 303 : BR_ALERT_ILLEGAL_PARAMETER
       48 : 304 : BR_ALERT_UNKNOWN_CA
       49 : 305 : BR_ALERT_ACCESS_DENIED
       50 : 306 : BR_ALERT_DECODE_ERROR
       51 : 307 : BR_ALERT_DECRYPT_ERROR
       70 : 326 : BR_ALERT_PROTOCOL_VERSION
       71 : 327 : BR_ALERT_INSUFFICIENT_SECURITY
       80 : 336 : BR_ALERT_INTERNAL_ERROR
       90 : 346 : BR_ALERT_USER_CANCELED
      100 : 356 : BR_ALERT_NO_RENEGOTIATION
      110 : 366 : BR_ALERT_UNSUPPORTED_EXTENSION
      120 : 376 : BR_ALERT_NO_APPLICATION_PROTOCOL
*/

            #   ifdef ESP32

          if (MQTTclient_connected && (mqtt_tls != nullptr)) {
            MakeControllerSettings(ControllerSettings); // -V522

            if (!AllocatedControllerSettings()) {
              addHtmlError(F("Out of memory, cannot load page"));
            } else {
              LoadControllerSettings(controllerindex, *ControllerSettings);

              // FIXME TD-er: Implement retrieval of certificate

              /*

                 addFormSubHeader(F("Peer Certificate"));

                 {
                 addFormTextArea(
                  F("Certificate Info"),
                  F("certinfo"),
                  mqtt_tls->getPeerCertificateInfo(),
                  -1,
                  -1,
                  -1,
                  true);
                 }
                 {
                 String fingerprint;

                 if (GetTLSfingerprint(fingerprint)) {
                  addFormTextBox(F("Certificate Fingerprint"),
                                  F("fingerprint"),
                                  fingerprint,
                                  64,
                                  true); // ReadOnly
                  addControllerParameterForm(*ControllerSettings, controllerindex,
                                              ControllerSettingsStruct::CONTROLLER_MQTT_TLS_STORE_FINGERPRINT);
                 }
                 }
                 addFormSubHeader(F("Peer Certificate Chain"));
                 {
                 // FIXME TD-er: Must wrap this in divs to be able to fold it by default.
                 const mbedtls_x509_crt *chain;

                 chain = mqtt_tls->getPeerCertificate();

                 int error { 0 };

                 while (chain != nullptr && error == 0) {
                  //                    const bool mustShow = !chain->ca_istrue || chain->next == nullptr;
                  //                    if (mustShow) {
                  String pem, subject;
                  error = ESPEasy_WiFiClientSecure::cert_to_pem(chain, pem, subject);
                  {
                    String label;

                    if (chain->ca_istrue) {
                      label = F("CA ");
                    }
                    label += F("Certificate <tt>");
                    label += subject;
                    label += F("</tt>");
                    addRowLabel(label);
                  }

                  if (error == 0) {
                    addTextArea(
                      F("peerCertInfo"),
                      mqtt_tls->getPeerCertificateInfo(chain),
                      -1,
                      -1,
                      -1,
                      true,
                      false);

                    addTextArea(
                      F("pem"),
                      pem,
                      -1,
                      -1,
                      -1,
                      true,
                      false);
                  } else {
                    addHtmlInt(error);
                  }

                  if (chain->ca_istrue && (chain->next == nullptr)) {
                    // Add checkbox to store CA cert
                    addControllerParameterForm(*ControllerSettings, controllerindex,
                                                ControllerSettingsStruct::CONTROLLER_MQTT_TLS_STORE_CACERT);
                  }

                  //                    }
                  chain = chain->next;
                 }
                 }
               */
            }
          }
            #   endif // ifdef ESP32
        }
#  endif // if FEATURE_MQTT_TLS
      }
# endif // if FEATURE_MQTT
    }

    // Separate enabled checkbox as it doesn't need to use the ControllerSettings.
    // So ControllerSettings object can be destructed before controller specific settings are loaded.
    addControllerEnabledForm(controllerindex);
  }

  addFormSeparator(2);
  html_TR_TD();
  html_TD();
  addButton(F("controllers"), F("Close"));
  addSubmitButton();
  html_end_table();
  html_end_form();
}

#endif // ifdef WEBSERVER_CONTROLLERS

#include "../WebServer/CacheControllerPages.h"

#ifdef USES_C016

# include "../WebServer/ESPEasy_WebServer.h"
# include "../WebServer/AccessControl.h"
# include "../WebServer/HTML_wrappers.h"
# include "../WebServer/JSON.h"
# include "../CustomBuild/ESPEasyLimits.h"
# include "../DataStructs/DeviceStruct.h"
# include "../DataStructs/ESPEasyControllerCache_CSV_dumper.h"
# include "../DataTypes/TaskIndex.h"
# include "../Globals/C016_ControllerCache.h"
# include "../Globals/Cache.h"
# include "../Globals/ESPEasy_time.h"
# include "../Globals/Settings.h"
# include "../Helpers/ESPEasy_math.h"
# include "../Helpers/ESPEasy_Storage.h"
# include "../Helpers/ESPEasy_time_calc.h"
# include "../Helpers/Misc.h"


// ********************************************************************************
// URLs needed for C016_CacheController
// to help dump the content of the binary log files
// ********************************************************************************
void handle_dumpcache() {
  if (!isLoggedIn()) { return; }

  // Filters/export settings
  char separator     = ';';
  bool joinTimestamp = false;
  bool onlySetTasks  = false;


  if (hasArg(F("separator"))) {
    String sep = webArg(F("separator"));

    if (isWrappedWithQuotes(sep)) {
      removeChar(sep, sep[0]);
    }

    if (sep.equalsIgnoreCase(F("Tab"))) { separator = '\t'; }
    else if (sep.equalsIgnoreCase(F("Comma"))) { separator = ','; }
    else if (sep.equalsIgnoreCase(F("Semicolon"))) { separator = ';'; }
  }

  if (hasArg(F("jointimestamp"))) {
    joinTimestamp = true;
  }

  if (hasArg(F("onlysettasks"))) {
    onlySetTasks = true;
  }

  {
    // Send HTTP headers to directly save the dump as a CSV file
    String str =  F("attachment; filename=cachedump_");
    str += Settings.getName();
    str += F("_U");
    str += Settings.Unit;

    if (node_time.systemTimePresent())
    {
      str += '_';
      str += node_time.getDateTimeString('\0', '\0', '\0');
    }
    str += F(".csv");

    sendHeader(F("Content-Disposition"), str);
    TXBuffer.startStream(F("application/octet-stream"), F("*"), 200);
  }


  ESPEasyControllerCache_CSV_dumper dumper(
    joinTimestamp, 
    onlySetTasks, 
    separator, 
    ESPEasyControllerCache_CSV_dumper::Target::CSV_file);

  dumper.generateCSVHeader(true);

  while (dumper.createCSVLine()) {
    dumper.writeCSVLine(true);
  }

  TXBuffer.endStream();
}

void handle_cache_json() {
  if (!isLoggedIn()) { return; }

  // Flush any data still in RTC memory to the cache files.
  C016_flush();

  TXBuffer.startJsonStream();
  addHtml(F("{\"columns\": ["));

  //     addHtml(F("UNIX timestamp;contr. idx;sensortype;taskindex;value count"));
  addHtml(to_json_value(F("UNIX timestamp")));
  addHtml(',');
  addHtml(to_json_value(F("UTC timestamp")));
  addHtml(',');
  addHtml(to_json_value(F("task index")));

  if (hasArg(F("pluginID"))) {
    addHtml(',');
    addHtml(to_json_value(F("plugin ID")));
  }

  for (taskIndex_t i = 0; i < TASKS_MAX; ++i) {
    for (int j = 0; j < VARS_PER_TASK; ++j) {
      String label = getTaskDeviceName(i);
      label += '#';
      label += getTaskValueName(i, j);
      addHtml(',');
      addHtml(to_json_value(label));
    }
  }
  addHtml(F("],\n"));
  addHtml(F("\"files\": ["));
  bool islast    = false;
  int  filenr    = 0;
  int  fileCount = 0;

  while (!islast) {
    const String currentFile = C016_getCacheFileName(filenr, islast);
    ++filenr;

    if (currentFile.length() > 0) {
      if (fileCount != 0) {
        addHtml(',');
      }
      addHtml(to_json_value(currentFile));
      ++fileCount;
    }
  }
  addHtml(F("],\n"));
  addHtml(F("\"pluginID\": ["));

  for (taskIndex_t taskIndex = 0; validTaskIndex(taskIndex); ++taskIndex) {
    if (taskIndex != 0) {
      addHtml(',');
    }
    addHtmlInt(getPluginID_from_TaskIndex(taskIndex).value);
  }
  addHtml(F("],\n"));
  stream_next_json_object_value(F("separator"), F(";"));
  stream_last_json_object_value(F("nrfiles"), fileCount);
  addHtml('\n');
  TXBuffer.endStream();
}

void handle_cache_csv() {
  if (!isLoggedIn()) { return; }
}

#endif // ifdef USES_C016

#include "../WebServer/ConfigPage.h"

#ifdef WEBSERVER_CONFIG

#include "../WebServer/HTML_wrappers.h"
#include "../WebServer/AccessControl.h"
#include "../WebServer/Markup.h"
#include "../WebServer/Markup_Buttons.h"
#include "../WebServer/Markup_Forms.h"
#include "../WebServer/ESPEasy_WebServer.h"

#ifdef USES_ESPEASY_NOW
#include "../DataStructs/MAC_address.h"
#include "../DataStructs/NodeStruct.h"
#endif

#include "../ESPEasyCore/Controller.h"
#include "../ESPEasyCore/ESPEasyNetwork.h"

#include "../Globals/MQTT.h"
#include "../Globals/Nodes.h"
#include "../Globals/SecuritySettings.h"
#include "../Globals/Settings.h"

#include "../Helpers/DeepSleep.h"
#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/Networking.h"
#include "../Helpers/StringConverter.h"


// ********************************************************************************
// Web Interface config page
// ********************************************************************************
void handle_config() {
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("handle_config"));
  #endif

  if (!isLoggedIn()) { return; }

  navMenuIndex = MENU_INDEX_CONFIG;
  TXBuffer.startStream();
  sendHeadandTail_stdtemplate(_HEAD);

  if (web_server.args() != 0)
  {
    String name = webArg(F("name"));
    name.trim();

    Settings.Delay              = getFormItemInt(F("delay"), Settings.Delay);
    Settings.deepSleep_wakeTime = getFormItemInt(F("awaketime"), Settings.deepSleep_wakeTime);
    Settings.Unit = getFormItemInt(F("unit"), Settings.Unit);

    if (strcmp(Settings.Name, name.c_str()) != 0) {
      #ifndef BUILD_MINIMAL_OTA
      addLog(LOG_LEVEL_INFO, F("Unit Name changed."));
      #endif

      if (CPluginCall(CPlugin::Function::CPLUGIN_GOT_INVALID, 0)) { // inform controllers that the old name will be invalid from now on.
#if FEATURE_MQTT
        MQTTDisconnect();                                           // disconnect form MQTT Server if invalid message was sent succesfull.
#endif // if FEATURE_MQTT
      }
#if FEATURE_MQTT
      MQTTclient_should_reconnect = true;
#endif // if FEATURE_MQTT
    }

    // Unit name
    safe_strncpy(Settings.Name, name.c_str(), sizeof(Settings.Name));
    Settings.appendUnitToHostname(isFormItemChecked(F("appendunittohostname")));

    // Password
    copyFormPassword(F("password"), SecuritySettings.Password, sizeof(SecuritySettings.Password));

    // SSID 1
    safe_strncpy(SecuritySettings.WifiSSID, webArg(F("ssid")).c_str(), sizeof(SecuritySettings.WifiSSID));
    copyFormPassword(F("key"), SecuritySettings.WifiKey, sizeof(SecuritySettings.WifiKey));

    // SSID 2
    strncpy_webserver_arg(SecuritySettings.WifiSSID2, F("ssid2"));
    copyFormPassword(F("key2"),  SecuritySettings.WifiKey2,  sizeof(SecuritySettings.WifiKey2));

    // Hidden SSID
    Settings.IncludeHiddenSSID(isFormItemChecked(LabelType::CONNECT_HIDDEN_SSID));
    Settings.HiddenSSID_SlowConnectPerBSSID(isFormItemChecked(LabelType::HIDDEN_SSID_SLOW_CONNECT));

#ifdef ESP32
    Settings.PassiveWiFiScan(isFormItemChecked(LabelType::WIFI_PASSIVE_SCAN));
#endif

    // Access point password.
    copyFormPassword(F("apkey"), SecuritySettings.WifiAPKey, sizeof(SecuritySettings.WifiAPKey));

    // When set you can use the Sensor in AP-Mode without being forced to /setup
    Settings.ApDontForceSetup(isFormItemChecked(F("ApDontForceSetup")));

    // Usually the AP will be started when no WiFi is defined, or the defined one cannot be found. This flag may prevent it.
    Settings.DoNotStartAP(isFormItemChecked(F("DoNotStartAP")));


    // TD-er Read access control from form.
    SecuritySettings.IPblockLevel = getFormItemInt(F("ipblocklevel"));

    switch (SecuritySettings.IPblockLevel) {
      case LOCAL_SUBNET_ALLOWED:
      {
        IPAddress low, high;
        getSubnetRange(low, high);

        for (uint8_t i = 0; i < 4; ++i) {
          SecuritySettings.AllowedIPrangeLow[i]  = low[i];
          SecuritySettings.AllowedIPrangeHigh[i] = high[i];
        }
        break;
      }
      case ONLY_IP_RANGE_ALLOWED:
      case ALL_ALLOWED:

        webArg2ip(F("iprangelow"),  SecuritySettings.AllowedIPrangeLow);
        webArg2ip(F("iprangehigh"), SecuritySettings.AllowedIPrangeHigh);
        break;
    }

    #ifdef USES_ESPEASY_NOW
    for (int peer = 0; peer < ESPEASY_NOW_PEER_MAX; ++peer) {
      String peer_mac = webArg(concat(F("peer"), peer));
      if (peer_mac.length() == 0) {
        peer_mac = F("00:00:00:00:00:00");
      }
      MAC_address mac;
      if (mac.set(peer_mac.c_str())) {
        mac.get(SecuritySettings.EspEasyNowPeerMAC[peer]);
      }
      /*
      String log = F("MAC decoding ");
      log += peer_mac;
      log += F(" => ");
      log += mac.toString();
      addLog(LOG_LEVEL_INFO, log);
      */
    }
    #endif

    Settings.deepSleepOnFail = isFormItemChecked(F("deepsleeponfail"));
    webArg2ip(F("espip"),      Settings.IP);
    webArg2ip(F("espgateway"), Settings.Gateway);
    webArg2ip(F("espsubnet"),  Settings.Subnet);
    webArg2ip(F("espdns"),     Settings.DNS);
#if FEATURE_ETHERNET
    webArg2ip(F("espethip"),      Settings.ETH_IP);
    webArg2ip(F("espethgateway"), Settings.ETH_Gateway);
    webArg2ip(F("espethsubnet"),  Settings.ETH_Subnet);
    webArg2ip(F("espethdns"),     Settings.ETH_DNS);
#endif // if FEATURE_ETHERNET
    #if FEATURE_ALTERNATIVE_CDN_URL
    set_CDN_url_custom(webArg(F("alturl")));
    #endif // if FEATURE_ALTERNATIVE_CDN_URL
    addHtmlError(SaveSettings());
  }

  html_add_form();
  html_table_class_normal();

  addFormHeader(F("Main Settings"));

  Settings.Name[25]             = 0;
  SecuritySettings.Password[25] = 0;
  addFormTextBox(F("Unit Name"), F("name"), Settings.Name, 25);
  addFormNote(concat(F("Hostname: "), NetworkCreateRFCCompliantHostname()));
  addFormNumericBox(F("Unit Number"), F("unit"), Settings.Unit, 0, UNIT_NUMBER_MAX);
  addFormCheckBox(F("Append Unit Number to hostname"), F("appendunittohostname"), Settings.appendUnitToHostname());
  addFormPasswordBox(F("Admin Password"), F("password"), SecuritySettings.Password, 25);

  addFormSubHeader(F("Wifi Settings"));

  addFormTextBox(getLabel(LabelType::SSID), F("ssid"), SecuritySettings.WifiSSID, 31);
  addFormPasswordBox(F("WPA Key"), F("key"), SecuritySettings.WifiKey, 63);
  addFormTextBox(F("Fallback SSID"), F("ssid2"), SecuritySettings.WifiSSID2, 31);
  addFormPasswordBox(F("Fallback WPA Key"), F("key2"), SecuritySettings.WifiKey2, 63);
  addFormNote(F("WPA Key must be at least 8 characters long"));

  addFormCheckBox(LabelType::CONNECT_HIDDEN_SSID,      Settings.IncludeHiddenSSID());

#ifdef ESP32
  addFormCheckBox(LabelType::WIFI_PASSIVE_SCAN, Settings.PassiveWiFiScan());
#endif
  
  addFormCheckBox(LabelType::HIDDEN_SSID_SLOW_CONNECT,      Settings.HiddenSSID_SlowConnectPerBSSID());

  addFormSeparator(2);
  addFormPasswordBox(F("WPA AP Mode Key"), F("apkey"), SecuritySettings.WifiAPKey, 63);
  addFormNote(F("WPA Key must be at least 8 characters long"));

  addFormCheckBox(F("Don't force /setup in AP-Mode"), F("ApDontForceSetup"), Settings.ApDontForceSetup());
  addFormNote(F("When set you can use the Sensor in AP-Mode without being forced to /setup. /setup can still be called."));

  addFormCheckBox(F("Do Not Start AP"), F("DoNotStartAP"), Settings.DoNotStartAP());
  #if FEATURE_ETHERNET
  addFormNote(F("Do not allow to start an AP when unable to connect to configured LAN/WiFi"));
  #else // if FEATURE_ETHERNET
  addFormNote(F("Do not allow to start an AP when configured WiFi cannot be found"));
  #endif // if FEATURE_ETHERNET


  // TD-er add IP access box F("ipblocklevel")
  addFormSubHeader(F("Client IP filtering"));
  {
    IPAddress low, high;
    getIPallowedRange(low, high);
    uint8_t iplow[4];
    uint8_t iphigh[4];

    for (uint8_t i = 0; i < 4; ++i) {
      iplow[i]  = low[i];
      iphigh[i] = high[i];
    }
    addFormIPaccessControlSelect(F("Client IP block level"), F("ipblocklevel"), SecuritySettings.IPblockLevel);
    addFormIPBox(F("Access IP lower range"), F("iprangelow"),  iplow);
    addFormIPBox(F("Access IP upper range"), F("iprangehigh"), iphigh);
  }

  addFormSubHeader(F("WiFi IP Settings"));

  addFormIPBox(F("ESP WiFi IP"),         F("espip"),      Settings.IP);
  addFormIPBox(F("ESP WiFi Gateway"),    F("espgateway"), Settings.Gateway);
  addFormIPBox(F("ESP WiFi Subnetmask"), F("espsubnet"),  Settings.Subnet);
  addFormIPBox(F("ESP WiFi DNS"),        F("espdns"),     Settings.DNS);
  addFormNote(F("Leave empty for DHCP"));

#if FEATURE_ETHERNET
  addFormSubHeader(F("Ethernet IP Settings"));

  addFormIPBox(F("ESP Ethernet IP"),         F("espethip"),      Settings.ETH_IP);
  addFormIPBox(F("ESP Ethernet Gateway"),    F("espethgateway"), Settings.ETH_Gateway);
  addFormIPBox(F("ESP Ethernet Subnetmask"), F("espethsubnet"),  Settings.ETH_Subnet);
  addFormIPBox(F("ESP Ethernet DNS"),        F("espethdns"),     Settings.ETH_DNS);
  addFormNote(F("Leave empty for DHCP"));
#endif // if FEATURE_ETHERNET

#ifdef USES_ESPEASY_NOW
  addFormSubHeader(F("ESPEasy-NOW"));
  for (int peer = 0; peer < ESPEASY_NOW_PEER_MAX; ++peer) {
    addFormMACBox(concat(F("Peer "), peer + 1),
                  concat(F("peer"), peer), 
                  SecuritySettings.EspEasyNowPeerMAC[peer]);

    bool match_STA;
    const NodeStruct* nodeInfo = Nodes.getNodeByMac(SecuritySettings.EspEasyNowPeerMAC[peer], match_STA);
    if (nodeInfo != nullptr)
    {
      String summary = nodeInfo->getSummary();
      summary += match_STA ? F(" (STA)") : F(" (AP)");
      addFormNote(summary);
    }
    
  }
#endif

  addFormSubHeader(F("Sleep Mode"));

  addFormNumericBox(F("Sleep awake time"), F("awaketime"), Settings.deepSleep_wakeTime, 0, 255);
  addUnit(F("sec"));
  addHelpButton(F("SleepMode"));
  addFormNote(F("0 = Sleep Disabled, else time awake from sleep"));

  int dsmax = getDeepSleepMax();
  addFormNumericBox(F("Sleep time"), F("delay"), Settings.Delay, 0, dsmax); // limited by hardware
  {
    addUnit(concat(F("sec (max: "), dsmax) + ')');
  }

  addFormCheckBox(F("Sleep on connection failure"), F("deepsleeponfail"), Settings.deepSleepOnFail);

  addFormSeparator(2);

  #if FEATURE_ALTERNATIVE_CDN_URL
  addFormSubHeader(F("CDN (Content delivery network)"));

  addFormTextBox(F("Custom CDN URL"), F("alturl"), get_CDN_url_custom(), 255);
  addFormNote(concat(F("Leave empty for default CDN url: "), get_CDN_url_prefix()));

  addFormSeparator(2);
  #endif // if FEATURE_ALTERNATIVE_CDN_URL

  html_TR_TD();
  html_TD();
  addSubmitButton();
  html_end_table();
  html_end_form();

  sendHeadandTail_stdtemplate(_TAIL);
  TXBuffer.endStream();
}

#endif // ifdef WEBSERVER_CONFIG
#include "../WebServer/HTML_Print.h"

#include "../Globals/TXBuffer.h"

size_t HTML_Print::write(uint8_t c)
{
  TXBuffer += (char)c;
  return 1;
}

