#include "src/Helpers/_CPlugin_Helper.h"
#ifdef USES_C014

# include "src/Commands/ExecuteCommand.h"
# include "src/DataTypes/NodeTypeID.h"
# include "src/Globals/Device.h"
# include "src/Globals/MQTT.h"
# include "src/Globals/Plugins.h"
# include "src/Globals/Statistics.h"
# include "src/Helpers/_CPlugin_Helper_mqtt.h"
# include "src/Helpers/ESPEasy_UnitOfMeasure.h"
# include "src/Helpers/PeriodicalActions.h"
# include "_Plugin_Helper.h"

// #######################################################################################################
// ################################# Controller Plugin 014: Homie 3/4 ####################################
// #######################################################################################################

/** Changelog:
 * 2025-08-23 tonhuisman: Add 10/sec call to poll background connection process while not connected
 * 2025-06-18 tonhuisman: Add support for Unit of Measure attribute, when available in the build, sent in AutoDiscovery
 *                        Enable sending Derived values when available
 * 2024-03-02 tonhuisman: Fix using parseSystemVariables() for processing %sysname%. Might still break the same configurations,
 *                        logging improvements
 * 2023-10-30 tonhuisman: Fix using getHostname() instead of getName() for %sysname%. This might break some configurations!
 *                        minor improvements
 * 2023-08-18 tonhuisman: Clean up source to improve resource usage
 * 2023-03-15 tonhuisman: Replace use of deprecated DummyValueSet with TaskValueSet
 * 2023-03 Changelog started
 */

# define CPLUGIN_014
# define CPLUGIN_ID_014              14

// Define which Homie version to use
# if !defined(CPLUGIN_014_V3) && !defined(CPLUGIN_014_V4)

// #define CPLUGIN_014_V3
#  define CPLUGIN_014_V4
# endif // if !defined(CPLUGIN_014_V3) && !defined(CPLUGIN_014_V4)

# ifdef CPLUGIN_014_V3
  #  define CPLUGIN_014_HOMIE_VERSION   "3.0.0"
  #  define CPLUGIN_NAME_014            "Homie MQTT (Version 3.0.1)"
# endif // ifdef CPLUGIN_014_V3
# ifdef CPLUGIN_014_V4
  #  define CPLUGIN_014_HOMIE_VERSION   "4.0.0"
  #  define CPLUGIN_NAME_014            "Homie MQTT (Version 4.0.0 dev)"
# endif // ifdef CPLUGIN_014_V4

// subscribe and publish schemes should not be changed by the user. This will probably break the homie convention. Do @ your own risk;)
# define CPLUGIN_014_SUBSCRIBE       "homie/%sysname%/+/+/set" // only subscribe to /set topics to reduce load by receiving all retained
                                                               // messages
# define CPLUGIN_014_PUBLISH         "homie/%sysname%/%tskname%/%valname%"

# define CPLUGIN_014_BASE_TOPIC      "homie/%sysname%/#"
# define CPLUGIN_014_BASE_VALUE      "homie/%sysname%/%device%/%node%/%property%"
# define CPLUGIN_014_INTERVAL        "90"                  // to prevent timeout !ToDo set by lowest plugin interval
# define CPLUGIN_014_SYSTEM_DEVICE   "SYSTEM"              // name for system device Plugin for cmd and GIO values
# define CPLUGIN_014_CMD_VALUE       "cmd"                 // name for command value
# define CPLUGIN_014_GPIO_VALUE      "gpio"                // name for gpio value i.e. "gpio1"
# define CPLUGIN_014_GPIO_VALUE_LEN  4                     // length of GPIO to avoid creating a String to get the length
# define CPLUGIN_014_CMD_VALUE_NAME  "Command"             // human readabele name for command value

# define CPLUGIN_014_GPIO_COMMAND          "gpio"          // name for gpio command
# define CPLUGIN_014_TASKVALUESET_COMMAND  "taskvalueset"  // name for taskvalueset command
# define CPLUGIN_014_HOMIEVALUESET_COMMAND "homievalueset" // name for homievalueset command

uint8_t msgCounter = 0;                                    // counter for send Messages (currently for information / log only!

String CPlugin_014_pubname;
bool   CPlugin_014_mqtt_retainFlag = false;

void C014_replaceSysname(String& var) {
  parseSystemVariables(var, false); // Used to be getName(), but that doesn't include the UnitNr when configured
}

bool CPlugin_014_sendMQTTdevice(String                     tmppubname,
                                taskIndex_t                taskIndex,
                                const __FlashStringHelper *topic,
                                const String             & payload,
                                int                      & errorCounter) {
  tmppubname.replace(F("#"), topic);
  bool mqttReturn = MQTTpublish(CPLUGIN_ID_014, taskIndex, tmppubname.c_str(), payload.c_str(), true);

  if (mqttReturn) {
    msgCounter++;
  } else {
    errorCounter++;
  }

  String log;

  if (loglevelActiveFor(LOG_LEVEL_ERROR)) { // Also true for LOG_LEVEL_DEBUG_MORE
    log = strformat(F("C014 : T:%s P: %s"), String(topic).c_str(), payload.c_str());
  }
  # ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG_MORE) && mqttReturn) {
    log += F(" success!");
    addLogMove(LOG_LEVEL_DEBUG_MORE, log);
  }
  # endif // ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_ERROR) && !mqttReturn) {
    log += F(" ERROR!");
    addLogMove(LOG_LEVEL_ERROR, log);
  }
  processMQTTdelayQueue();
  return mqttReturn;
}

// send MQTT Message with CPLUGIN_014_BASE_TOPIC Topic scheme / Payload
bool CPlugin_014_sendMQTTdevice(const String             & tmppubname,
                                taskIndex_t                taskIndex,
                                const __FlashStringHelper *topic,
                                const __FlashStringHelper *payload,
                                int                      & errorCounter) {
  return CPlugin_014_sendMQTTdevice(tmppubname, taskIndex, topic, String(payload), errorCounter);
}

// send MQTT Message with CPLUGIN_014_BASE_VALUE Topic scheme / Payload
bool CPlugin_014_sendMQTTnode(String        tmppubname,
                              const String& node,
                              const String& value,
                              const String& topic,
                              const String& payload,
                              int         & errorCounter) {
  tmppubname.replace(F("%device%"),    node);
  tmppubname.replace(F("%node%"),      value);
  tmppubname.replace(F("/%property%"), topic); // leading forward slash required to send "homie/device/value" topics
  bool mqttReturn = MQTTpublish(CPLUGIN_ID_014, INVALID_TASK_INDEX, tmppubname.c_str(), payload.c_str(), true);

  if (mqttReturn) { msgCounter++; }
  else { errorCounter++; }

  String log;

  if (loglevelActiveFor(LOG_LEVEL_ERROR)) { // Also true for LOG_LEVEL_DEBUG_MORE
    log = strformat(F("C014 : V:%s T: %s P: %s"), value.c_str(), topic.c_str(), payload.c_str());
  }
  # ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG_MORE) && mqttReturn) {
    log += F(" success!");
    addLogMove(LOG_LEVEL_DEBUG_MORE, log);
  }
  # endif // ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_ERROR) && !mqttReturn) {
    log += F(" ERROR!");
    addLogMove(LOG_LEVEL_ERROR, log);
  }
  processMQTTdelayQueue();
  return mqttReturn;
}

// and String to a comma separated list
void C014_addToList(String& valuesList, const String& node) {
  if (valuesList.length() > 0) {
    valuesList += ',';
  }
  valuesList += node;
}

bool CPlugin_014(CPlugin::Function function, struct EventStruct *event, String& string)
{
  bool   success      = false;
  int    errorCounter = 0;
  String pubname;
  String tmppubname;

  switch (function)
  {
    case CPlugin::Function::CPLUGIN_PROTOCOL_ADD:
    {
      ProtocolStruct& proto = getProtocolStruct(event->idx); //      = CPLUGIN_ID_014;
      proto.usesMQTT     = true;
      proto.usesTemplate = true;
      proto.usesAccount  = true;
      proto.usesPassword = true;
      proto.usesExtCreds = true;
      proto.defaultPort  = 1883;
      proto.usesID       = false;
      # if FEATURE_MQTT_TLS
      proto.usesTLS = true;
      # endif // if FEATURE_MQTT_TLS
      # if FEATURE_STRING_VARIABLES
      proto.allowSendDerived = true;
      # endif // if FEATURE_STRING_VARIABLES
      break;
    }

    case CPlugin::Function::CPLUGIN_GET_DEVICENAME:
    {
      string = F(CPLUGIN_NAME_014);
      break;
    }

    case CPlugin::Function::CPLUGIN_INIT:
    {
      success = init_mqtt_delay_queue(event->ControllerIndex, CPlugin_014_pubname, CPlugin_014_mqtt_retainFlag);
      break;
    }

    case CPlugin::Function::CPLUGIN_EXIT:
    {
      exit_mqtt_delay_queue();
      break;
    }

    case CPlugin::Function::CPLUGIN_INTERVAL:
    {
      if (MQTTclient.connected()) {
        errorCounter = 0;

        pubname = F(CPLUGIN_014_BASE_TOPIC); // Scheme to form device messages
        C014_replaceSysname(pubname);

        # ifdef CPLUGIN_014_V3

        // $stats/uptime	Device → Controller	Time elapsed in seconds since the boot of the device	Yes	Yes
        CPlugin_014_sendMQTTdevice(pubname, event->TaskIndex, F("$stats/uptime"), toString(getUptimeMinutes() * 60, 0), errorCounter);

        // $stats/signal	Device → Controller	Signal strength in %	Yes	No
        float RssI = WiFi.RSSI();
        RssI = isnan(RssI) ? -100.0f : RssI;
        RssI = min(max(2 * (RssI + 100.0f), 0.0f), 100.0f);

        CPlugin_014_sendMQTTdevice(pubname, event->TaskIndex, F("$stats/signal"), toString(RssI, 1), errorCounter);
        # endif // ifdef CPLUGIN_014_V3

        if (errorCounter > 0) {
          // alert: this is the state the device is when connected to the MQTT broker, but something wrong is happening. E.g. a sensor is
          // not providing data and needs human intervention. You have to send this message when something is wrong.
          CPlugin_014_sendMQTTdevice(pubname, event->TaskIndex, F("$state"), F("alert"), errorCounter);
        } else {
          // ready: this is the state the device is in when it is connected to the MQTT broker, has sent all Homie messages and is ready to
          // operate. You have to send this message after all other announcements message have been sent.
          CPlugin_014_sendMQTTdevice(pubname, event->TaskIndex, F("$state"), F("ready"), errorCounter);
          success = true;
        }

        # ifndef BUILD_NO_DEBUG

        if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
          addLog(LOG_LEVEL_DEBUG,
                 strformat(F("C014 : $stats information sent with %s errors! (%d messages)"),
                           errorCounter > 0 ? String(errorCounter).c_str() : "no",
                           msgCounter));
        }
        # endif // ifndef BUILD_NO_DEBUG
        msgCounter = 0;
      }
      break;
    }

    case CPlugin::Function::CPLUGIN_GOT_CONNECTED: //// call after connected to mqtt server to publich device autodicover features
    {
      statusLED(true);

      // send autodiscover header
      pubname = F(CPLUGIN_014_BASE_TOPIC);           // Scheme to form device messages
      C014_replaceSysname(pubname);
      int deviceCount = 1;                           // minimum the SYSTEM device exists
      int nodeCount   = 1;                           // minimum the cmd node exists
      errorCounter = 0;

      if (lastBootCause != BOOT_CAUSE_DEEP_SLEEP) {  // skip sending autodiscover data when returning from deep sleep
        String nodename = F(CPLUGIN_014_BASE_VALUE); // Scheme to form node messages
        C014_replaceSysname(nodename);
        String nodesList;                            // build comma separated List for nodes
        String valuesList;                           // build comma separated List for values
        String deviceName;                           // current Device Name nr:name
        String valueName;                            // current Value Name
        String unitName;                             // estimate Units

        // init: this is the state the device is in when it is connected to the MQTT broker, but has not yet sent all Homie messages and is
        // not yet ready to operate. This is the first message that must that must be sent.
        CPlugin_014_sendMQTTdevice(pubname, event->TaskIndex, F("$state"), F("init"),                    errorCounter);

        // $homie	Device → Controller	Version of the Homie convention the device conforms to	Yes	Yes
        CPlugin_014_sendMQTTdevice(pubname, event->TaskIndex, F("$homie"), F(CPLUGIN_014_HOMIE_VERSION), errorCounter);

        // $name	Device → Controller	Friendly name of the device	Yes	Yes
        CPlugin_014_sendMQTTdevice(pubname, event->TaskIndex, F("$name"),  Settings.getName(),           errorCounter);

        // $localip	Device → Controller	IP of the device on the local network	Yes	Yes
        # ifdef CPLUGIN_014_V3
        CPlugin_014_sendMQTTdevice(pubname, event->TaskIndex, F("$localip"), formatIP(NetworkLocalIP()), errorCounter);

        // $mac	Device → Controller	Mac address of the device network interface. The format MUST be of the type A1:B2:C3:D4:E5:F6	Yes	Yes
        CPlugin_014_sendMQTTdevice(pubname, event->TaskIndex, F("$mac"),     NetworkMacAddress(),        errorCounter);

        // $implementation	Device → Controller	An identifier for the Homie implementation (example esp8266)	Yes	Yes
        CPlugin_014_sendMQTTdevice(pubname, event->TaskIndex, F("$implementation"),
                                   #  if defined(ESP8266)
                                   F("ESP8266"),
                                   #  endif // if defined(ESP8266)
                                   #  if defined(ESP32)
                                   F("ESP32"),
                                   #  endif // if defined(ESP32)
                                   errorCounter);

        // $fw/version	Device → Controller	Version of the firmware running on the device	Yes	Yes
        CPlugin_014_sendMQTTdevice(pubname, event->TaskIndex, F("$fw/version"), toString(Settings.Build, 0),
                                   errorCounter);

        #  if FEATURE_ESPEASY_P2P

        // $fw/name	Device → Controller	Name of the firmware running on the device. Allowed characters are the same as the device ID	Yes	Yes
        CPlugin_014_sendMQTTdevice(pubname, event->TaskIndex, F("$fw/name"), getNodeTypeDisplayString(NODE_TYPE_ID),
                                   errorCounter);
        #  endif // if FEATURE_ESPEASY_P2P

        // $stats/interval	Device → Controller	Interval in seconds at which the device refreshes its $stats/+: See next section for
        // details about statistical attributes	Yes	Yes
        CPlugin_014_sendMQTTdevice(pubname, event->TaskIndex, F("$stats/interval"), F(CPLUGIN_014_INTERVAL),
                                   errorCounter);
        # endif // ifdef CPLUGIN_014_V3

        // always send the SYSTEM device with the cmd node
        C014_addToList(nodesList,  F(CPLUGIN_014_SYSTEM_DEVICE));
        C014_addToList(valuesList, F(CPLUGIN_014_CMD_VALUE));

        // $name	Device → Controller	Friendly name of the Node	Yes	Yes
        CPlugin_014_sendMQTTnode(nodename,
                                 F(CPLUGIN_014_SYSTEM_DEVICE),
                                 F("$name"),
                                 F(""),
                                 F(CPLUGIN_014_SYSTEM_DEVICE),
                                 errorCounter);

        // $name	Device → Controller	Friendly name of the property.	Any String	Yes	No ("")
        CPlugin_014_sendMQTTnode(nodename,
                                 F(CPLUGIN_014_SYSTEM_DEVICE),
                                 F(CPLUGIN_014_CMD_VALUE),
                                 F("/$name"),
                                 F(CPLUGIN_014_CMD_VALUE_NAME),
                                 errorCounter);

        // $datatype	The data type. See Payloads.	Enum: [integer, float, boolean, string, enum, color]
        CPlugin_014_sendMQTTnode(nodename,
                                 F(CPLUGIN_014_SYSTEM_DEVICE),
                                 F(CPLUGIN_014_CMD_VALUE),
                                 F("/$datatype"),
                                 F("string"),
                                 errorCounter);

        // $settable	Device → Controller	Specifies whether the property is settable (true) or readonly (false)	true or false	Yes	No
        // (false)
        CPlugin_014_sendMQTTnode(nodename,
                                 F(CPLUGIN_014_SYSTEM_DEVICE),
                                 F(CPLUGIN_014_CMD_VALUE),
                                 F("/$settable"),
                                 F("true"),
                                 errorCounter);

        // enum all devices

        // FIRST Standard GPIO tasks
        int gpio = 0;

        while (gpio <= MAX_GPIO) {
          const PinBootState pinBootState = Settings.getPinBootState(gpio);

          if (pinBootState != PinBootState::Default_state) { // anything but default
            nodeCount++;
            valueName = concat(F(CPLUGIN_014_GPIO_VALUE), gpio);
            C014_addToList(valuesList, valueName);

            // $name	Device → Controller	Friendly name of the property.	Any String	Yes	No ("")
            CPlugin_014_sendMQTTnode(nodename, F(CPLUGIN_014_SYSTEM_DEVICE), valueName, F("/$name"),     valueName,    errorCounter);

            // $datatype	The data type. See Payloads.	Enum: [integer, float, boolean,string, enum, color]
            CPlugin_014_sendMQTTnode(nodename, F(CPLUGIN_014_SYSTEM_DEVICE), valueName, F("/$datatype"), F("boolean"), errorCounter);

            if (pinBootState != PinBootState::Input) { // defined as output
              // $settable	Device → Controller	Specifies whether the property is settable (true) or readonly (false)	true or
              // false	Yes	No (false)
              CPlugin_014_sendMQTTnode(nodename, F(CPLUGIN_014_SYSTEM_DEVICE), valueName, F("/$settable"), F("true"), errorCounter);
            }
          }
          ++gpio;
        }

        // $properties	Device → Controller	Properties the node exposes, with format id separated by a , if there are multiple nodes.	Yes	Yes
        CPlugin_014_sendMQTTnode(nodename, F(CPLUGIN_014_SYSTEM_DEVICE), F("$properties"), EMPTY_STRING, valuesList, errorCounter);
        valuesList = EMPTY_STRING;
        deviceCount++;

        // SECOND Plugins
        for (taskIndex_t x = 0; x < TASKS_MAX; ++x) {
          const pluginID_t pluginID = Settings.getPluginID_for_task(x);

          if (validPluginID_fullcheck(pluginID)) {
            LoadTaskSettings(x);
            const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(x);

            deviceName = getTaskDeviceName(x);

            if (validDeviceIndex(DeviceIndex) && Settings.TaskDeviceEnabled[x]) { // Device is enabled so send information
                                                                                  // device enabled
              valuesList = EMPTY_STRING;

              const uint8_t valueCount = getValueCountForTask(x);

              if (!Device[DeviceIndex].SendDataOption) { // check if device is not sending data = assume that it can receive.
                constexpr pluginID_t HOMIE_RECEIVER_PLUGIN_ID(86);

                if (pluginID == HOMIE_RECEIVER_PLUGIN_ID) {
                  for (uint8_t varNr = 0; varNr < valueCount; ++varNr) {
                    if (validPluginID_fullcheck(Settings.getPluginID_for_task(x))) {
                      if (ExtraTaskSettings.TaskDeviceValueNames[varNr][0] != 0) { // do not send if Value Name is empty!
                        C014_addToList(valuesList, ExtraTaskSettings.TaskDeviceValueNames[varNr]);

                        // $settable	Device → Controller	Specifies whether the property is settable (true) or readonly (false)	true
                        // or false	Yes	No (false)
                        CPlugin_014_sendMQTTnode(nodename,
                                                 deviceName,
                                                 ExtraTaskSettings.TaskDeviceValueNames[varNr],
                                                 F("/$settable"),
                                                 F("true"),
                                                 errorCounter);

                        // $name	Device → Controller	Friendly name of the property.	Any String	Yes	No ("")
                        valueName  = F("Homie Receiver: ");
                        valueName += ExtraTaskSettings.TaskDeviceValueNames[varNr];
                        CPlugin_014_sendMQTTnode(nodename,
                                                 deviceName,
                                                 ExtraTaskSettings.TaskDeviceValueNames[varNr],
                                                 F("/$name"),
                                                 valueName,
                                                 errorCounter);

                        // $datatype	The data type. See Payloads.	Enum: [integer, float, boolean,string, enum, color]
                        unitName.clear();

                        switch (Settings.TaskDevicePluginConfig[x][varNr]) {
                          case 0:
                            valueName = F("integer");

                            if ((ExtraTaskSettings.TaskDevicePluginConfig[varNr] != 0) ||
                                (ExtraTaskSettings.TaskDevicePluginConfig[varNr + 5] != 0)) {
                              unitName  = ExtraTaskSettings.TaskDevicePluginConfig[varNr];
                              unitName += ':';
                              unitName += ExtraTaskSettings.TaskDevicePluginConfig[varNr + valueCount];
                            }
                            break;
                          case 1:
                            valueName = F("float");

                            if ((ExtraTaskSettings.TaskDevicePluginConfig[varNr] != 0) ||
                                (ExtraTaskSettings.TaskDevicePluginConfig[varNr + 5] != 0)) {
                              unitName = strformat(F("%d:%d"),
                                                   ExtraTaskSettings.TaskDevicePluginConfig[varNr],
                                                   ExtraTaskSettings.TaskDevicePluginConfig[varNr + valueCount]);
                            }
                            break;
                          case 2: valueName = F("boolean"); break;
                          case 3: valueName = F("string"); break;
                          case 4:
                            valueName = F("enum");
                            unitName  = ExtraTaskSettings.TaskDeviceFormula[varNr];
                            break;
                          case 5:
                            valueName = F("color");
                            unitName  = F("rgb");
                            break;
                          case 6:
                            valueName = F("color");
                            unitName  = F("hsv");
                            break;
                        }
                        CPlugin_014_sendMQTTnode(nodename,
                                                 deviceName,
                                                 ExtraTaskSettings.TaskDeviceValueNames[varNr],
                                                 F("/$datatype"),
                                                 valueName,
                                                 errorCounter);

                        if (!unitName.isEmpty()) {
                          CPlugin_014_sendMQTTnode(nodename,
                                                   deviceName,
                                                   ExtraTaskSettings.TaskDeviceValueNames[varNr],
                                                   F("/$format"),
                                                   unitName,
                                                   errorCounter);
                        }
                        nodeCount++;
                      }
                    }
                  }
                }
              } else {
                // ignore cutom values for now! Assume all Values are standard float.
                // String customValuesStr;
                // customValues = PluginCall(PLUGIN_WEBFORM_SHOW_VALUES, &TempEvent, customValuesStr);
                uint8_t customValues = false;

                if (!customValues) { // standard Values
                  for (uint8_t varNr = 0; varNr < valueCount; ++varNr) {
                    const pluginID_t pluginID = Settings.getPluginID_for_task(x);

                    if (validPluginID_fullcheck(pluginID)) {
                      if (ExtraTaskSettings.TaskDeviceValueNames[varNr][0] != 0) { // do not send if Value Name is empty!
                        C014_addToList(valuesList, ExtraTaskSettings.TaskDeviceValueNames[varNr]);

                        // $name	Device → Controller	Friendly name of the property.	Any String	Yes	No ("")
                        CPlugin_014_sendMQTTnode(nodename,
                                                 deviceName,
                                                 ExtraTaskSettings.TaskDeviceValueNames[varNr],
                                                 F("/$name"),
                                                 ExtraTaskSettings.TaskDeviceValueNames[varNr],
                                                 errorCounter);

                        // $datatype	The data type. See Payloads.	Enum: [integer, float, boolean,string, enum, color]
                        CPlugin_014_sendMQTTnode(nodename,
                                                 deviceName,
                                                 ExtraTaskSettings.TaskDeviceValueNames[varNr],
                                                 F("/$datatype"),
                                                 F("float"),
                                                 errorCounter);

                        constexpr pluginID_t DUMMY_PLUGIN_ID(33);

                        if (pluginID == DUMMY_PLUGIN_ID) { // Dummy Device can send AND receive Data
                          CPlugin_014_sendMQTTnode(nodename,
                                                   deviceName,
                                                   ExtraTaskSettings.TaskDeviceValueNames[varNr],
                                                   F("/$settable"),
                                                   F("true"),
                                                   errorCounter);
                        }

                        nodeCount++;

                        # if FEATURE_TASKVALUE_UNIT_OF_MEASURE

                        // Use configured Unit of Measure setting per Value (using Homeassistant supported 100+ values)
                        unitName = toUnitOfMeasureName(ExtraTaskSettings.getTaskVarUnitOfMeasure(varNr));

                        if (!unitName.isEmpty()) // found a unit match
                        {
                          // $unit	Device → Controller	A string containing the unit of this property. You
                          // are not limited to the recommended values, although they are the only well known ones
                          // that will have to be recognized by any Homie consumer.
                          CPlugin_014_sendMQTTnode(nodename,
                                                   deviceName,
                                                   ExtraTaskSettings.TaskDeviceValueNames[varNr],
                                                   F("/$unit"),
                                                   unitName,
                                                   errorCounter);
                        }
                        unitName.clear();
                        # endif // if FEATURE_TASKVALUE_UNIT_OF_MEASURE
                      }
                    }
                  } // end loop throug regular values

                  // Discovery for Derived values, if sending is enabled
                  # if FEATURE_STRING_VARIABLES

                  if (Settings.SendDerivedTaskValues(x, event->ControllerIndex)) {
                    String taskNameLC(deviceName);
                    taskNameLC.toLowerCase();
                    String postfix;
                    const String search = getDerivedValueSearchAndPostfix(deviceName, 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 vType;
                        const String vname2 = getDerivedValueNameUomAndVType(deviceName, valueName, uom, vType);

                        if (!vname2.isEmpty()) {
                          valueName = vname2;
                        }
                        C014_addToList(valuesList, valueName);

                        // $name	Device → Controller	Friendly name of the property.	Any String	Yes	No ("")
                        CPlugin_014_sendMQTTnode(nodename,
                                                 deviceName,
                                                 valueName,
                                                 F("/$name"),
                                                 valueName,
                                                 errorCounter);

                        // $datatype	The data type. See Payloads.	Enum: [integer, float, boolean,string, enum, color]
                        CPlugin_014_sendMQTTnode(nodename,
                                                 deviceName,
                                                 valueName,
                                                 F("/$datatype"),
                                                 F("float"),
                                                 errorCounter);

                        if (!uom.isEmpty()) {
                          CPlugin_014_sendMQTTnode(nodename,
                                                   deviceName,
                                                   valueName,
                                                   F("/$unit"),
                                                   uom,
                                                   errorCounter);
                        }

                        ++varNr;
                      }
                      else if (it->first.substring(0, search.length()).compareTo(search) > 0) {
                        break;
                      }
                      ++it;
                    }
                  }
                  # endif // if FEATURE_STRING_VARIABLES
                } else { // Device has custom Values
                  # ifndef BUILD_NO_DEBUG

                  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
                    addLog(LOG_LEVEL_DEBUG, strformat(F("C014 : Device has custom values: %s not implemented!"),
                                                      getPluginNameFromDeviceIndex(getDeviceIndex_from_TaskIndex(x)).c_str()));
                  }
                  # endif // ifndef BUILD_NO_DEBUG
                }
              }

              if (!valuesList.isEmpty()) {
                // only add device to list if it has nodes!
                // $name	Device → Controller	Friendly name of the Node	Yes	Yes
                CPlugin_014_sendMQTTnode(nodename,
                                         deviceName,
                                         F("$name"),
                                         F(""),
                                         getTaskDeviceName(x),
                                         errorCounter);

                // $type	Device → Controller	Type of the node	Yes	Yes
                CPlugin_014_sendMQTTnode(nodename,
                                         deviceName,
                                         F("$type"),
                                         F(""),
                                         getPluginNameFromDeviceIndex(DeviceIndex),
                                         errorCounter);

                // add device to device list
                C014_addToList(nodesList, deviceName);
                deviceCount++;

                // $properties	Device → Controller	Properties the node exposes, with format id separated by a , if there are multiple
                // nodes.	Yes	Yes
                CPlugin_014_sendMQTTnode(nodename,
                                         deviceName,
                                         F("$properties"),
                                         F(""),
                                         valuesList,
                                         errorCounter);
                valuesList = EMPTY_STRING;
              }
            } else { // device not enabeled
              # ifndef BUILD_NO_DEBUG

              if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
                addLog(LOG_LEVEL_DEBUG, strformat(F("C014 : Device Disabled: %s not propagated!"),
                                                  getPluginNameFromDeviceIndex(getDeviceIndex_from_TaskIndex(x)).c_str()));
              }
              # endif // ifndef BUILD_NO_DEBUG
            }
          } // device configured
        }   // loop through devices

        // and finally ...
        // $nodes	Device → Controller	Nodes the device exposes, with format id separated by a , if there are multiple nodes. To
        // make a node an array, append [] to the ID.	Yes	Yes
        CPlugin_014_sendMQTTdevice(pubname, event->TaskIndex, F("$nodes"), nodesList, errorCounter);
      }

      if (errorCounter > 0) {
        // alert: this is the state the device is when connected to the MQTT broker, but something wrong is happening. E.g. a sensor is not
        // providing data and needs human intervention. You have to send this message when something is wrong.
        CPlugin_014_sendMQTTdevice(pubname, event->TaskIndex, F("$state"), F("alert"), errorCounter);
      } else {
        // ready: this is the state the device is in when it is connected to the MQTT broker, has sent all Homie messages and is ready to
        // operate. You have to send this message after all other announcements message have been sent.
        CPlugin_014_sendMQTTdevice(pubname, event->TaskIndex, F("$state"), F("ready"), errorCounter);
        success = true;
      }

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        addLog(LOG_LEVEL_INFO,
               strformat(F("C014 : autodiscover information of %d Devices and %d Nodes sent with %s errors! (%d messages)"),
                         deviceCount,
                         nodeCount,
                         errorCounter > 0 ? String(errorCounter).c_str() : "no",
                         msgCounter)
               );
      }
      msgCounter   = 0;
      errorCounter = 0;
      break;
    }

    case CPlugin::Function::CPLUGIN_PROTOCOL_TEMPLATE:
    {
      event->String1 = F(CPLUGIN_014_SUBSCRIBE);
      event->String2 = F(CPLUGIN_014_PUBLISH);
      break;
    }

    case CPlugin::Function::CPLUGIN_GOT_INVALID:
    {
      pubname = F(CPLUGIN_014_BASE_TOPIC); // Scheme to form device messages
      C014_replaceSysname(pubname);

      // disconnected: this is the state the device is in when it is cleanly disconnected from the MQTT broker. You must send this message
      // before cleanly disconnecting
      success = CPlugin_014_sendMQTTdevice(pubname, event->TaskIndex, F("$state"), F("disconnected"), errorCounter);

      if (loglevelActiveFor(LOG_LEVEL_INFO)) {
        String log = strformat(F("C014 : Device: %s got invalid (disconnect%s"),
                               Settings.getHostname().c_str(), String(success ? F("ed).") : F(") failed!")).c_str());
        addLogMove(LOG_LEVEL_INFO, log);
      }
      break;
    }

    case CPlugin::Function::CPLUGIN_FLUSH:
    {
      pubname = F(CPLUGIN_014_BASE_TOPIC); // Scheme to form device messages
      C014_replaceSysname(pubname);

      // sleeping: this is the state the device is in when the device is sleeping. You have to send this message before sleeping.
      success = CPlugin_014_sendMQTTdevice(pubname, event->TaskIndex, F("$state"), F("sleeping"), errorCounter);
      break;
    }

    case CPlugin::Function::CPLUGIN_PROTOCOL_RECV:
    {
      const controllerIndex_t ControllerID = findFirstEnabledControllerWithId(CPLUGIN_ID_014);
      bool validTopic                      = false;
      bool cmdExecuted                     = false;

      if (!validControllerIndex(ControllerID)) {
        // Controller is not enabled.
        break;
      } else {
        String cmd;
        int    lastindex = event->String1.lastIndexOf('/');
        errorCounter = 0;

        if (equals(event->String1.substring(lastindex + 1), F("set"))) {
          pubname   = event->String1.substring(0, lastindex);
          lastindex = pubname.lastIndexOf('/');
          String nodeName        = pubname.substring(0, lastindex);
          const String valueName = pubname.substring(lastindex + 1);
          lastindex = nodeName.lastIndexOf('/');
          nodeName  = nodeName.substring(lastindex + 1);

          String log;

          if (loglevelActiveFor(LOG_LEVEL_INFO)) {
            log = strformat(F("C014 : MQTT received: /set: N: %s V: %s"), nodeName.c_str(), valueName.c_str());
          }

          if (equals(nodeName, F(CPLUGIN_014_SYSTEM_DEVICE))) {                    // msg to a system device
            if (valueName.startsWith(F(CPLUGIN_014_GPIO_VALUE))) {                 // msg to to set gpio values
              constexpr size_t gpio_value_tag_length = CPLUGIN_014_GPIO_VALUE_LEN; // now uses fixed length or constexpr

              // get the GPIO
              // Homie spec says state should be 'true' or 'false'...
              cmd = strformat(F("GPIO,%d,%c"),
                              valueName.substring(gpio_value_tag_length).toInt(),
                              (equals(event->String2, F("true")) || equals(event->String2, '1')) ? '1' : '0');
              validTopic = true;
            } else if (equals(valueName, F(CPLUGIN_014_CMD_VALUE))) { // msg to send a command
              cmd        = event->String2;
              validTopic = true;
            } else {
              cmd = strformat(F("SYSTEM/%s unknown!"), valueName.c_str());
            }
          } else {              // msg to a receiving plugin
            // Only handle /set case that supports P033 Dummy device and P086 Homie receiver
            cmdExecuted = MQTT_handle_topic_commands(event, false, true);
            validTopic  = true; // Avoid error messages
          }

          if (validTopic) {
            if (loglevelActiveFor(LOG_LEVEL_INFO)) {
              log += strformat(F(" cmd: %s OK"), cmd.c_str());
              addLog(LOG_LEVEL_INFO, log);
            }
          } else if (loglevelActiveFor(LOG_LEVEL_INFO)) {
            log += F(" INVALID MSG");
            addLog(LOG_LEVEL_INFO, log);
          }
        }

        if (validTopic && !cmdExecuted) { // Don't execute twice
          MQTT_execute_command(cmd, true);
        }
      }
      success = validTopic;
      break;
    }

    case CPlugin::Function::CPLUGIN_PROTOCOL_SEND:
    {
      if (MQTT_queueFull(event->ControllerIndex)) {
        break;
      }

      const bool taskRetained = Settings.SendRetainedTaskValues(event->TaskIndex, event->ControllerIndex);

      success = MQTT_protocol_send(event, CPlugin_014_pubname, CPlugin_014_mqtt_retainFlag || taskRetained);

      break;
    }

    case CPlugin::Function::CPLUGIN_ACKNOWLEDGE:
    {
      /* if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
         String log = F("CPLUGIN_ACKNOWLEDGE: ");
         log += string;
         log += F(" / ");
         log += getTaskDeviceName(event->TaskIndex);
         log += F(" / ");
         log += ExtraTaskSettings.TaskDeviceValueNames[event->Par2-1];
         log += F(" sensorType:");
         log += event->sensorType;
         log += F(" Source:");
         log += event->Source;
         log += F(" idx:");
         log += event->idx;
         log += F(" S1:");
         log += event->String1;
         log += F(" S2:");
         log += event->String2;
         log += F(" S3:");
         log += event->String3;
         log += F(" S4:");
         log += event->String4;
         log += F(" S5:");
         log += event->String5;
         log += F(" P1:");
         log += event->Par1;
         log += F(" P2:");
         log += event->Par2;
         log += F(" P3:");
         log += event->Par3;
         log += F(" P4:");
         log += event->Par4;
         log += F(" P5:");
         log += event->Par5;
         addLog(LOG_LEVEL_DEBUG, log);
         } */

      if (!string.isEmpty()) {
        const String commandName = parseString(string, 1);    // could not find a way to get the command out of the event structure.

        if (equals(commandName, F(CPLUGIN_014_GPIO_COMMAND))) // !ToDo : As gpio is like any other plugin commands should be integrated
                                                              // below!
        {
          const int port         = event->Par1;               // parseString(string, 2).toInt();
          const int valueInt     = event->Par2;               // parseString(string, 3).toInt();
          const String valueBool = boolToString(valueInt == 1);

          String topic = F(CPLUGIN_014_PUBLISH);              // ControllerSettings.Publish not used because it can be modified by the user!
          C014_replaceSysname(topic);
          topic.replace(F("%tskname%"), F(CPLUGIN_014_SYSTEM_DEVICE));
          topic.replace(F("%valname%"), concat(F(CPLUGIN_014_GPIO_VALUE), port));

          success = MQTTpublish(CPLUGIN_ID_014, INVALID_TASK_INDEX, topic.c_str(), valueBool.c_str(), false);

          String log = strformat(F("C014 : Acknowledged GPIO%d value:%s (%d)"),
                                 port, valueBool.c_str(), valueInt);

          if (loglevelActiveFor(LOG_LEVEL_INFO) && success) {
            log += F(" success!");
            addLogMove(LOG_LEVEL_INFO, log);
          }

          if (loglevelActiveFor(LOG_LEVEL_ERROR) && !success) {
            log += F(" ERROR!");
            addLogMove(LOG_LEVEL_ERROR, log);
          }
        } else { // not gpio
          const taskVarIndex_t taskVarIndex = event->Par2 - 1;

          if (validTaskVarIndex(taskVarIndex)) {
            userVarIndex_t userVarIndex = event->BaseVarIndex + taskVarIndex;
            String topic                = F(CPLUGIN_014_PUBLISH);
            C014_replaceSysname(topic);
            const int deviceIndex = event->Par1; // parseString(string, 2).toInt();
            LoadTaskSettings(deviceIndex - 1);
            const String deviceName = getTaskDeviceName(event->TaskIndex);
            topic.replace(F("%tskname%"), deviceName);
            const String valueName = ExtraTaskSettings.TaskDeviceValueNames[taskVarIndex]; // parseString(string, 3).toInt()-1];
            topic.replace(F("%valname%"), valueName);
            String valueStr;
            int    valueInt = 0;

            if (equals(commandName, F(CPLUGIN_014_TASKVALUESET_COMMAND))) { // removed dummyvalueset command some time ago...
              valueStr = formatUserVarNoCheck(event, taskVarIndex);         // parseString(string, 4);
              success  = MQTTpublish(CPLUGIN_ID_014, INVALID_TASK_INDEX, topic.c_str(), valueStr.c_str(), false);

              String log = strformat(F("C014 : Acknowledged: %s var: %s topic: %s value: %s"),
                                     deviceName.c_str(), valueName.c_str(), topic.c_str(), valueStr.c_str());

              if (loglevelActiveFor(LOG_LEVEL_INFO) && success) {
                log += F(" success!");
                addLogMove(LOG_LEVEL_INFO, log);
              }

              if (loglevelActiveFor(LOG_LEVEL_ERROR) && !success) {
                log += F(" ERROR!");
                addLogMove(LOG_LEVEL_ERROR, log);
              }
            } else if (equals(commandName, F(CPLUGIN_014_HOMIEVALUESET_COMMAND))) { // acknowledges value form P086 Homie Receiver
              switch (Settings.TaskDevicePluginConfig[deviceIndex - 1][taskVarIndex]) {
                case 0:                                                             // PLUGIN_085_VALUE_INTEGER
                  valueInt = static_cast<int>(UserVar[userVarIndex]);
                  valueStr = toString(UserVar[userVarIndex], 0);
                  break;
                case 1: // PLUGIN_085_VALUE_FLOAT
                  valueStr = formatUserVarNoCheck(event, taskVarIndex);
                  break;
                case 2: // PLUGIN_085_VALUE_BOOLEAN

                  valueStr = boolToString(UserVar[userVarIndex] == 1);
                  break;
                case 3: // PLUGIN_085_VALUE_STRING
                  // valueStr = ExtraTaskSettings.TaskDeviceFormula[taskVarIndex];
                  valueStr = parseStringToEndKeepCase(string, 4);
                  break;
                case 4: // PLUGIN_085_VALUE_ENUM
                  valueInt = static_cast<int>(UserVar[userVarIndex]);
                  valueStr = parseStringKeepCase(ExtraTaskSettings.TaskDeviceFormula[taskVarIndex], valueInt);
                  break;
                case 5: // PLUGIN_085_VALUE_RGB
                  // valueStr = ExtraTaskSettings.TaskDeviceFormula[taskVarIndex];
                  valueStr = parseStringToEnd(string, 4);
                  break;
                case 6: // PLUGIN_085_VALUE_HSV
                  // valueStr = ExtraTaskSettings.TaskDeviceFormula[taskVarIndex];
                  valueStr = parseStringToEnd(string, 4);
                  break;
              }
              success = MQTTpublish(CPLUGIN_ID_014, INVALID_TASK_INDEX, topic.c_str(), valueStr.c_str(), false);

              String log = concat(F("C014 : homie acknowledge: "), deviceName);

              if (loglevelActiveFor(LOG_LEVEL_INFO) && success) {
                log += strformat(F(" taskIndex:%d valueNr:%d valueName:%s valueType:%d topic:%s valueInt:%d valueStr:%s success!"),
                                 deviceIndex,
                                 event->Par2,
                                 valueName.c_str(),
                                 Settings.TaskDevicePluginConfig[deviceIndex - 1][taskVarIndex],
                                 topic.c_str(),
                                 valueInt,
                                 valueStr.c_str());
                addLogMove(LOG_LEVEL_INFO, log);
              }

              if (loglevelActiveFor(LOG_LEVEL_ERROR) && !success) {
                log += strformat(F(" var: %s topic: %s value: %s ERROR!"), // was: failed!
                                 valueName.c_str(), topic.c_str(), valueStr.c_str());
                addLogMove(LOG_LEVEL_ERROR, log);
              }
            } else { // Acknowledge not implemented yet
              /* if (loglevelActiveFor(LOG_LEVEL_INFO)) {
                 String log = F("C014 : Plugin acknowledged: ");
                 log+=function;
                 log+=F(" / ");
                 log+=commandName;
                 log+=F(" cmd: ");
                 log+=string;
                 log+=F(" not implemented!");
                 addLog(LOG_LEVEL_ERROR, log);
                 } */
              success = false;
            }
          }
        }
      }
      break;
    }

    # if FEATURE_MQTT_CONNECT_BACKGROUND
    case CPlugin::Function::CPLUGIN_TEN_PER_SECOND:
    {
      MQTTConnectInBackground(CONTROLLER_MAX, true); // Report state
      break;
    }
    # endif // if FEATURE_MQTT_CONNECT_BACKGROUND

    default:
      break;
  }

  return success;
}

#endif // ifdef USES_C014
