#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"), ESPEasy::net::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; } #ifndef BUILD_NO_DEBUG 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) ); } #endif 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(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(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