diff --git a/docs/source/Controller/C004.rst b/docs/source/Controller/C004.rst index 672f470d7a..1b47087df8 100644 --- a/docs/source/Controller/C004.rst +++ b/docs/source/Controller/C004.rst @@ -20,12 +20,125 @@ GitHub: |C004_github|_ Maintainer: |C004_maintainer| +Description +----------- + +The ThingSpeak controller allows to send data to the ThingSpeak web service so it can be presented using the tools available there. + +Both free and paid accounts are supported, where a free account has some restrictions like the number of 'Channels' that can be created, and the maximum update-frequency. + +Configuration +------------- + +.. image:: C004_ControllerConfiguration.png + +* **Protocol**: The selected Controller Protocol. + +* **Locate Controller**: This should be set to ``Use Hostname``, this will be forced when data is sent via the controller, but not saved. + +* **Controller Hostname**: This should be set to ``api.thingspeak.com``, the only available API entrypoint. No url suffix should be added (at the time of writing this documentation). + +* **Controller Port**: The default port number ``80`` should be used. + +Controller Queue +^^^^^^^^^^^^^^^^ + +* **Minimum Send Interval**: The minimum time to wait before the next set of data can be sent to the controller. This can be set quite high, as the update frequency is not very high, and when sending data in too quick succession, the new data will be ignored. Especially on a free account, only a limited number of updates per day can be sent, so setting this to 600000 msec. (10 minutes) is quite realistic. + +* **Max Queue Depth**: Determines the number of elements that can be stored in the queue before new samples are dismissed. When the receiving server is available, the data elements will be sent, emptying the queue, and making room for new samples again. To avoid sending too many messages at once, causing them to be discarded by ThingSpeak, the queue depth should best be set to 1. At the same time, to avoid samples to be discarded by the controller, the Interval for devices using this controller should also be set rather high, in the 20 to 30 minutes range. + +* **Max Retries**: Setting is not used for this controller. + +* **Full Queue Action**: The controller always uses the ``Ignore New`` strategy when the queue is filled. + +* **Allow Expire**: Not applicable for this controller. + +* **De-duplicate**: When enabled, avoids sending duplicate data, comparing to what's currently in the send-queue. + +* **Check Reply**: The controller always uses the ``Ignore Acknowledgement`` setting. + +* **Client Timeout**: The timeout to allow before the connection is failing. As ThingSpeak is an external internet service, a somewhat longer than the default timeout should be used. 1000 msec. should work in most cases, but on very high latency connections, this value can be increased. + +Credentials +^^^^^^^^^^^ + +* **ThingHTTP Name**: This setting is currently not used for this controller. + +* **API Key**: Enter the Write API Key for the channel the data should be sent to. To use multiple channels, an extra ThingSpeak controller should be configured, so a different API Key can be configured. + +* **Enabled**: To enable the controller this box has to be checked. + +Practical use-cases +------------------- + +As ThingSpeak is a low-frequency external service, only a limited number of updates per day can be sent. For a Free account that's limited to 3 million messages per year, with a minimum interval of 15 seconds, effectively ca. 8000 updates per day. For a paid account it depends on the type of the account used, details and current price information can be found via the `ThingSpeak licensing FAQ `_ + +Multiple sensors to a single channel +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To send out data from multiple ESPEasy tasks (max. 4 values) to a single ThingSpeak Channel (max. 8 fields), the data has to be sent at least 15 seconds apart. To avoid losing data that is sent too soon, it's best to control the task 'activation' from rules. A simple round-robin system will do: + +.. code-block:: none + + On Clock#Time=All,**:** Do // Once every minute + If %v1% = 0 + TaskRun,BME_280_1 // First BME, Idx 1 (field 1..3) + Elseif %v1% = 1 + TaskRun,BME_280_2 // Second BME, Idx 4 (field 4..6) + Elseif %v1% = 2 + TaskRun,DallasAB // 2 Dallas temperature sensors, Idx 7 (field 7..8) + Endif + Let 1,%v1%+1 // Next task + If %v1% > 2 // All done? + Let,1,0 // Reset + Endif + Endon + +All above tasks, ``BME_280_1``, ``BME_280_2`` and ``DallasAB`` should be set to high Interval values, like 600 (every 10 minutes) to avoid them being run unexpectedly. + +For adding more than 8 values, a new ThingSpeak Channel should be created, and because each Channel is using a different Write API Key, an extra ThingSpeak Controller should be configured, and extra tasks set up similarly but for the extra controller. ESPEasy allows up to 3 controllers to be configured, that *can* all 3 be ThingSpeak controllers, if needed. + +Selected values from multiple sensors +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When sending data to a controller via the default task configuration options, all values will be sent to the controller. + +If you want to send only a selection of the values to ThingSpeak, an intermediate Dummy Device should be used to store the values to be sent out, and configured to send the data to the ThingSpeak controller. Multiple sensor values can be collected in the Dummy Device, before it is triggered to send out the data. + +.. code-block:: none + + On BME_280_1#All Do // Single event with all values can best be enabled for the sensor + TaskValueSet,Dummy,1,%eventvalue1% + TaskValueSet,Dummy,2,%eventvalue2% + Endon + + On BME_280_2#All Do + TaskValueSet,Dummy,3,%eventvalue1% + TaskValueSet,Dummy,4,%eventvalue2% + Endon + + On Clock#Time=All,**:** Do // Once every minute + If %v1% = 0 + TaskRun,Dummy // Dummy with BME values, Idx 1 (field 1..4) + Elseif %v1% = 1 + TaskRun,DallasABCD // 4 Dallas temperature sensors, Idx 5 (field 5..8) + Endif + Let 1,%v1%+1 // Next task + If %v1% > 1 // All done? + Let,1,0 // Reset + Endif + Endon + +The Interval for the BMEs can be set to 10..30 seconds, and the Interval for the Dummy task to 0, as it will be triggered from the minute timer. + Change log ---------- .. versionchanged:: 2.0 ... + |added| 2024-05-29 Update documentation and add implementation examples. + |added| Major overhaul for 2.0 release. diff --git a/docs/source/Controller/C004_ControllerConfiguration.png b/docs/source/Controller/C004_ControllerConfiguration.png new file mode 100644 index 0000000000..2be719cc8b Binary files /dev/null and b/docs/source/Controller/C004_ControllerConfiguration.png differ diff --git a/src/_C002.cpp b/src/_C002.cpp index dc9f9f13bd..db832c2fcd 100644 --- a/src/_C002.cpp +++ b/src/_C002.cpp @@ -42,9 +42,9 @@ bool CPlugin_002(CPlugin::Function function, struct EventStruct *event, String& proto.usesExtCreds = true; proto.defaultPort = 1883; proto.usesID = true; - #if FEATURE_MQTT_TLS - proto.usesTLS = true; - #endif + # if FEATURE_MQTT_TLS + proto.usesTLS = true; + # endif // if FEATURE_MQTT_TLS break; } @@ -155,21 +155,18 @@ bool CPlugin_002(CPlugin::Function function, struct EventStruct *event, String& } break; } -# if defined(USES_P088) // || defined(USES_P115) + # if defined(USES_P088) case 88: // Send heatpump IR (P088) if IDX matches - // case 115: // Send heatpump IR (P115) if IDX matches { action = concat(F("heatpumpir,"), svalue1); // svalue1 is like 'gree,1,1,0,22,0,0' break; } -# endif // USES_P088 || USES_P115 + # endif // if defined(USES_P088) default: break; } - const bool validCommand = action.length() > 0; - - if (validCommand) { + if (action.length() != 0) { mustSendEvent = true; // Try plugin and internal @@ -207,19 +204,19 @@ bool CPlugin_002(CPlugin::Function function, struct EventStruct *event, String& if (event->idx != 0) { String json = serializeDomoticzJson(event); -# ifndef BUILD_NO_DEBUG + # ifndef BUILD_NO_DEBUG if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { addLogMove(LOG_LEVEL_DEBUG, concat(F("MQTT : "), json)); } -# endif // ifndef BUILD_NO_DEBUG + # endif // ifndef BUILD_NO_DEBUG String pubname = CPlugin_002_pubname; parseControllerVariables(pubname, event, false); // Publish using move operator, thus pubname and json are empty after this call success = MQTTpublish(event->ControllerIndex, event->TaskIndex, std::move(pubname), std::move(json), CPlugin_002_mqtt_retainFlag); - } // if ixd !=0 + } // if idx !=0 else { addLog(LOG_LEVEL_ERROR, F("MQTT : IDX cannot be zero!")); diff --git a/src/_C004.cpp b/src/_C004.cpp index 5e32ca2eb7..d9903d9b18 100644 --- a/src/_C004.cpp +++ b/src/_C004.cpp @@ -1,136 +1,134 @@ -#include "src/Helpers/_CPlugin_Helper.h" -#ifdef USES_C004 - -// ####################################################################################################### -// ########################### Controller Plugin 004: ThingSpeak ######################################### -// ####################################################################################################### - -# define CPLUGIN_004 -# define CPLUGIN_ID_004 4 -# define CPLUGIN_NAME_004 "ThingSpeak" - -bool CPlugin_004(CPlugin::Function function, struct EventStruct *event, String& string) -{ - bool success = false; - - switch (function) - { - case CPlugin::Function::CPLUGIN_PROTOCOL_ADD: - { - ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_004; - proto.usesMQTT = false; - proto.usesAccount = true; - proto.usesPassword = true; - proto.defaultPort = 80; - proto.usesID = true; - break; - } - - case CPlugin::Function::CPLUGIN_GET_DEVICENAME: - { - string = F(CPLUGIN_NAME_004); - break; - } - - case CPlugin::Function::CPLUGIN_INIT: - { - success = init_c004_delay_queue(event->ControllerIndex); - break; - } - - case CPlugin::Function::CPLUGIN_EXIT: - { - exit_c004_delay_queue(); - break; - } - - case CPlugin::Function::CPLUGIN_GET_PROTOCOL_DISPLAY_NAME: - { - success = true; - - switch (event->idx) { - case ControllerSettingsStruct::CONTROLLER_USER: - string = F("ThingHTTP Name"); - break; - case ControllerSettingsStruct::CONTROLLER_PASS: - string = F("API Key"); - break; - default: - success = false; - break; - } - break; - } - - case CPlugin::Function::CPLUGIN_PROTOCOL_SEND: - { - if (C004_DelayHandler == nullptr) { - break; - } - if (C004_DelayHandler->queueFull(event->ControllerIndex)) { - break; - } - - std::unique_ptr element(new (std::nothrow) C004_queue_element(event)); - - success = C004_DelayHandler->addToQueue(std::move(element)); - Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C004_DELAY_QUEUE, C004_DelayHandler->getNextScheduleTime()); - - break; - } - - case CPlugin::Function::CPLUGIN_FLUSH: - { - process_c004_delay_queue(); - delay(0); - break; - } - - default: - break; - } - return success; -} - -// Uncrustify may change this into multi line, which will result in failed builds -// *INDENT-OFF* -bool do_process_c004_delay_queue(cpluginID_t cpluginID, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { - const C004_queue_element& element = static_cast(element_base); -// *INDENT-ON* - String postDataStr = F("api_key="); - - postDataStr += getControllerPass(element._controller_idx, ControllerSettings); // used for API key - - if (element.sensorType == Sensor_VType::SENSOR_TYPE_STRING) { - postDataStr += F("&status="); - postDataStr += element.txt[0]; // FIXME TD-er: Is this correct? - // See: https://nl.mathworks.com/help/thingspeak/writedata.html - } else { - for (uint8_t x = 0; x < element.valueCount; x++) - { - postDataStr += F("&field"); - postDataStr += element.idx + x; - postDataStr += '='; - postDataStr += element.txt[x]; - } - } - if (!ControllerSettings.UseDNS) { - // Patch the ControllerSettings to make sure we're using a hostname instead of an IP address - ControllerSettings.setHostname(F("api.thingspeak.com")); // PM_CZ: HTTP requests must contain host headers. - ControllerSettings.UseDNS = true; - } - - int httpCode = -1; - send_via_http( - cpluginID, - ControllerSettings, - element._controller_idx, - F("/update"), // uri - F("POST"), - F("Content-Type: application/x-www-form-urlencoded\r\n"), - postDataStr, - httpCode); - return (httpCode >= 100) && (httpCode < 300); -} - -#endif // ifdef USES_C004 +#include "src/Helpers/_CPlugin_Helper.h" +#ifdef USES_C004 + +// ####################################################################################################### +// ########################### Controller Plugin 004: ThingSpeak ######################################### +// ####################################################################################################### + +# define CPLUGIN_004 +# define CPLUGIN_ID_004 4 +# define CPLUGIN_NAME_004 "ThingSpeak" + +bool CPlugin_004(CPlugin::Function function, struct EventStruct *event, String& string) +{ + bool success = false; + + switch (function) + { + case CPlugin::Function::CPLUGIN_PROTOCOL_ADD: + { + ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_004; + proto.usesMQTT = false; + proto.usesAccount = true; + proto.usesPassword = true; + proto.defaultPort = 80; + proto.usesID = true; + break; + } + + case CPlugin::Function::CPLUGIN_GET_DEVICENAME: + { + string = F(CPLUGIN_NAME_004); + break; + } + + case CPlugin::Function::CPLUGIN_INIT: + { + success = init_c004_delay_queue(event->ControllerIndex); + break; + } + + case CPlugin::Function::CPLUGIN_EXIT: + { + exit_c004_delay_queue(); + break; + } + + case CPlugin::Function::CPLUGIN_GET_PROTOCOL_DISPLAY_NAME: + { + success = true; + + switch (event->idx) { + case ControllerSettingsStruct::CONTROLLER_USER: + string = F("ThingHTTP Name"); + break; + case ControllerSettingsStruct::CONTROLLER_PASS: + string = F("API Key"); + break; + default: + success = false; + break; + } + break; + } + + case CPlugin::Function::CPLUGIN_PROTOCOL_SEND: + { + if (C004_DelayHandler == nullptr) { + break; + } + + if (C004_DelayHandler->queueFull(event->ControllerIndex)) { + break; + } + + std::unique_ptr element(new (std::nothrow) C004_queue_element(event)); + + success = C004_DelayHandler->addToQueue(std::move(element)); + Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C004_DELAY_QUEUE, C004_DelayHandler->getNextScheduleTime()); + + break; + } + + case CPlugin::Function::CPLUGIN_FLUSH: + { + process_c004_delay_queue(); + delay(0); + break; + } + + default: + break; + } + return success; +} + +// Uncrustify may change this into multi line, which will result in failed builds +// *INDENT-OFF* +bool do_process_c004_delay_queue(cpluginID_t cpluginID, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { + const C004_queue_element& element = static_cast(element_base); +// *INDENT-ON* +String postDataStr = concat(F("api_key="), + getControllerPass(element._controller_idx, ControllerSettings)); // used for API key + +if (element.sensorType == Sensor_VType::SENSOR_TYPE_STRING) { + postDataStr += concat(F("&status="), element.txt[0]); // FIXME TD-er: Is this correct? + // See: https://nl.mathworks.com/help/thingspeak/writedata.html +} else { + for (uint8_t x = 0; x < element.valueCount; x++) { + postDataStr += strformat(F("&field%d=%s"), + element.idx + x, + element.txt[x].c_str()); + } +} + +if (!ControllerSettings.UseDNS) { + // Patch the ControllerSettings to make sure we're using a hostname instead of an IP address + ControllerSettings.setHostname(F("api.thingspeak.com")); // PM_CZ: HTTP requests must contain host headers. + ControllerSettings.UseDNS = true; +} + +int httpCode = -1; +send_via_http( + cpluginID, + ControllerSettings, + element._controller_idx, + F("/update"), // uri + F("POST"), + F("Content-Type: application/x-www-form-urlencoded\r\n"), + postDataStr, + httpCode); +return (httpCode >= 100) && (httpCode < 300); +} + +#endif // ifdef USES_C004 diff --git a/src/_C005.cpp b/src/_C005.cpp index 3769c077c4..0706a59073 100644 --- a/src/_C005.cpp +++ b/src/_C005.cpp @@ -3,6 +3,7 @@ # include "src/Commands/ExecuteCommand.h" # include "src/Globals/EventQueue.h" +# include "src/Helpers/_CPlugin_Helper_mqtt.h" # include "src/Helpers/PeriodicalActions.h" # include "src/Helpers/StringParser.h" # include "_Plugin_Helper.h" @@ -11,6 +12,15 @@ // ################### Controller Plugin 005: Home Assistant (openHAB) MQTT ############################## // ####################################################################################################### +/** Changelog: + * 2023-08-18 tonhuisman: Clean up source for pull request + * 2023-03-15 tonhuisman: Add processing of topic endpoint /set to issue a TaskValueSet,taskname,taskvalue,payload command for + * topic %sysname%/#/taskname/valuename/set + * Move /cmd and /set handling to helper file for generic MQTT use + * Reformatted source using Uncrustify + * 2023-03 Changelog started + */ + # define CPLUGIN_005 # define CPLUGIN_ID_005 5 # define CPLUGIN_NAME_005 "Home Assistant (openHAB) MQTT" @@ -83,61 +93,10 @@ bool CPlugin_005(CPlugin::Function function, struct EventStruct *event, String& break; } + // String pubname = CPlugin_005_pubname; - String pubname = CPlugin_005_pubname; - const bool contains_valname = pubname.indexOf(F("%valname%")) != -1; - bool mqtt_retainFlag = CPlugin_005_mqtt_retainFlag; - - parseControllerVariables(pubname, event, false); - - 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 (Cache.getTaskDeviceValueName(event->TaskIndex, x).isEmpty()) { - continue; // we skip values with empty labels - } - - String tmppubname = pubname; + success = MQTT_protocol_send(event, CPlugin_005_pubname, CPlugin_005_mqtt_retainFlag); - if (contains_valname) { - parseSingleControllerVariable(tmppubname, event, x, false); - } - String value; - - if (event->sensorType == Sensor_VType::SENSOR_TYPE_STRING) { -# ifndef BUILD_NO_DEBUG - if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - value = event->String2.substring(0, 20); // For the log - } -# endif - } else { - value = formatUserVarNoCheck(event, x); - } -# ifndef BUILD_NO_DEBUG - - if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - addLogMove(LOG_LEVEL_DEBUG, - strformat( - F("MQTT : %s %s"), - 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(), mqtt_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), mqtt_retainFlag)) { - success = true; - } - } - } break; } @@ -160,153 +119,87 @@ bool C005_parse_command(struct EventStruct *event) { // Topic : event->String1 // Message: event->String2 - String cmd; - bool validTopic = false; - const int lastindex = event->String1.lastIndexOf('/'); - const String lastPartTopic = event->String1.substring(lastindex + 1); - const bool has_cmd_arg_index = event->String1.lastIndexOf(F("cmd_arg")) != -1; - - if (equals(lastPartTopic, F("cmd"))) { - // Example: - // Topic: ESP_Easy/Bathroom_pir_env/cmd - // Message: gpio,14,0 - // Full command: gpio,14,0 - - move_special(cmd, String(event->String2)); - - // SP_C005a: string= ;cmd=gpio,12,0 ;taskIndex=12 ;string1=ESPT12/cmd ;string2=gpio,12,0 - validTopic = true; - } else if (has_cmd_arg_index) { - // Example: - // Topic: ESP_Easy/Bathroom_pir_env/cmd_arg1/GPIO/0 - // Message: 14 - // Full command: gpio,14,0 - - uint8_t topic_index = 1; - String topic_folder = parseStringKeepCase(event->String1, topic_index, '/'); - - while (!topic_folder.startsWith(F("cmd_arg")) && !topic_folder.isEmpty()) { - ++topic_index; - topic_folder = parseStringKeepCase(event->String1, topic_index, '/'); - } + bool validTopic = MQTT_handle_topic_commands(event); // default handling of /cmd and /set topics - if (!topic_folder.isEmpty()) { - int32_t cmd_arg_nr = -1; + if (!validTopic) { + String cmd; + const int lastindex = event->String1.lastIndexOf('/'); + const String lastPartTopic = event->String1.substring(lastindex + 1); + const bool has_cmd_arg_index = event->String1.lastIndexOf(F("cmd_arg")) != -1; - if (validIntFromString(topic_folder.substring(7), cmd_arg_nr)) { - int constructed_cmd_arg_nr = 0; - ++topic_index; - topic_folder = parseStringKeepCase(event->String1, topic_index, '/'); - bool msg_added = false; + if (has_cmd_arg_index) { + // Example: + // Topic: ESP_Easy/Bathroom_pir_env/cmd_arg1/GPIO/0 + // Message: 14 + // Full command: gpio,14,0 - while (!topic_folder.isEmpty()) { - if (constructed_cmd_arg_nr != 0) { - cmd += ','; - } + uint8_t topic_index = 1; + String topic_folder = parseStringKeepCase(event->String1, topic_index, '/'); - if (constructed_cmd_arg_nr == cmd_arg_nr) { - cmd += event->String2; - msg_added = true; - } else { - cmd += topic_folder; - ++topic_index; - topic_folder = parseStringKeepCase(event->String1, topic_index, '/'); - } - ++constructed_cmd_arg_nr; - } - - if (!msg_added) { - cmd += ','; - cmd += event->String2; - } - - // addLog(LOG_LEVEL_INFO, concat(F("MQTT cmd: "), cmd)); - - validTopic = true; - } - } - } else { - // Example: - // Topic: ESP_Easy/Bathroom_pir_env/GPIO/14 - // Message: 0 or 1 - // Full command: gpio,14,0 - if (lastindex > 0) { - // Topic has at least one separator - int32_t lastPartTopic_int; - float value_f; - - if (validFloatFromString(event->String2, value_f) && - validIntFromString(lastPartTopic, lastPartTopic_int)) { - const int prevLastindex = event->String1.lastIndexOf('/', lastindex - 1); - - cmd = strformat( - F("%s,%d,%s"), - event->String1.substring(prevLastindex + 1, lastindex).c_str(), - lastPartTopic_int, - event->String2.c_str() // Just use the original format - ); - validTopic = true; + while (!topic_folder.startsWith(F("cmd_arg")) && !topic_folder.isEmpty()) { + ++topic_index; + topic_folder = parseStringKeepCase(event->String1, topic_index, '/'); } - } - } - if (validTopic) { - // 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 - cmd = 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(cmd, 1); - String eventValues = parseStringToEndKeepCase(cmd, 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 = cmd.substring(0, equal_pos); - eventValues = cmd.substring(equal_pos + 1); // Rest of the event, after the '=' char + if (!topic_folder.isEmpty()) { + int32_t cmd_arg_nr = -1; + + if (validIntFromString(topic_folder.substring(7), cmd_arg_nr)) { + int constructed_cmd_arg_nr = 0; + ++topic_index; + topic_folder = parseStringKeepCase(event->String1, topic_index, '/'); + bool msg_added = false; + + while (!topic_folder.isEmpty()) { + if (constructed_cmd_arg_nr != 0) { + cmd += ','; + } + + if (constructed_cmd_arg_nr == cmd_arg_nr) { + cmd += event->String2; + msg_added = true; + } else { + cmd += topic_folder; + ++topic_index; + topic_folder = parseStringKeepCase(event->String1, topic_index, '/'); + } + ++constructed_cmd_arg_nr; } - if (eventValues.startsWith(F(","))) { - // Need to reconstruct the event to get rid of calls like these: - // myevent=,1,2 - eventValues = eventValues.substring(1); + if (!msg_added) { + cmd += ','; + cmd += event->String2; } - // Now reconstruct the complete event - // Without event values: "myEvent" (no '=' char) - // With event values: "myEvent=1,2,3" + // addLog(LOG_LEVEL_INFO, String(F("MQTT cmd: ")) + cmd); - // Re-using the 'cmd' String as that has pre-allocated memory which is - // known to be large enough to hold the entire event. - cmd = eventName; - - if (eventValues.length() > 0) { - // Only append an = if there are eventvalues. - cmd += '='; - cmd += eventValues; - } + validTopic = true; } - - // 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(cmd), true); } } else { - ExecuteCommand_all({ EventValueSource::Enum::VALUE_SOURCE_MQTT, std::move(cmd) }, true); + // Example: + // Topic: ESP_Easy/Bathroom_pir_env/GPIO/14 + // Message: 0 or 1 + // Full command: gpio,14,0 + if (lastindex > 0) { + // Topic has at least one separator + int32_t lastPartTopic_int; + float value_f; + + if (validFloatFromString(event->String2, value_f) && + validIntFromString(lastPartTopic, lastPartTopic_int)) { + const int prevLastindex = event->String1.lastIndexOf('/', lastindex - 1); + cmd = strformat(F("%s,%d,%s"), + event->String1.substring(prevLastindex + 1, lastindex).c_str(), + lastPartTopic_int, + event->String2.c_str()); // Just use the original format + validTopic = true; + } + } + } + + if (validTopic) { + MQTT_execute_command(cmd); } } return validTopic; diff --git a/src/_C006.cpp b/src/_C006.cpp index 9aa07449b3..ea8a5b25a0 100644 --- a/src/_C006.cpp +++ b/src/_C006.cpp @@ -5,9 +5,16 @@ // ########################### Controller Plugin 006: PiDome MQTT ######################################## // ####################################################################################################### +/** Changelog: + * 2023-08-18 tonhuisman: Clean up source for pull request + * 2023-03-15 tonhuisman: Handle setting payload to (Dummy) Devices via topic SysName/TaskName/ValueName/set + * 2023-03 Changelog started + */ +// # include "src/Commands/InternalCommands.h" # include "src/Commands/ExecuteCommand.h" # include "src/ESPEasyCore/Controller.h" # include "src/Globals/Settings.h" +# include "src/Helpers/_CPlugin_Helper_mqtt.h" # include "src/Helpers/Network.h" # include "src/Helpers/PeriodicalActions.h" # include "_Plugin_Helper.h" @@ -69,41 +76,46 @@ bool CPlugin_006(CPlugin::Function function, struct EventStruct *event, String& case CPlugin::Function::CPLUGIN_PROTOCOL_RECV: { - // topic structure /Home/Floor/Location/device//gpio/16 - // Split topic into array - String tmpTopic = event->String1.substring(1); - String topicSplit[10]; - int SlashIndex = tmpTopic.indexOf('/'); - uint8_t count = 0; - - while (SlashIndex > 0 && count < 10 - 1) - { - topicSplit[count] = tmpTopic.substring(0, SlashIndex); - tmpTopic = tmpTopic.substring(SlashIndex + 1); - SlashIndex = tmpTopic.indexOf('/'); - count++; - } - topicSplit[count] = tmpTopic; - - String name = topicSplit[4]; - - if (name.equals(Settings.getName())) - { - String cmd = topicSplit[5]; - cmd += ','; - cmd += topicSplit[6].toInt(); // Par1 - cmd += ','; - - if ((event->String2.equalsIgnoreCase(F("false"))) || - (event->String2.equalsIgnoreCase(F("true")))) + if (!MQTT_handle_topic_commands(event, false)) { // Only handle /set option + // topic structure /Home/Floor/Location/device//gpio/16 + // Split topic into array + String tmpTopic = event->String1.substring(1); + String topicSplit[10]; + int SlashIndex = tmpTopic.indexOf('/'); + uint8_t count = 0; + + while (SlashIndex > 0 && count < 10 - 1) { - cmd += (event->String2.equalsIgnoreCase(F("true"))) ? '1' : '0'; // Par2 + topicSplit[count] = tmpTopic.substring(0, SlashIndex); + tmpTopic = tmpTopic.substring(SlashIndex + 1); + SlashIndex = tmpTopic.indexOf('/'); + count++; } - else + topicSplit[count] = tmpTopic; + + const String name = topicSplit[4]; + + if (name.equals(Settings.Name)) { - cmd += event->String2; // Par2 + String cmd = topicSplit[5]; + cmd += ','; + cmd += topicSplit[6].toInt(); // Par1 + cmd += ','; + const bool isTrue = event->String2.equalsIgnoreCase(F("true")); + + if ((event->String2.equalsIgnoreCase(F("false"))) || + (isTrue)) + { + cmd += isTrue ? '1' : '0'; // Par2 + } + else + { + cmd += event->String2; // Par2 + } + + // ExecuteCommand_all(EventValueSource::Enum::VALUE_SOURCE_MQTT, cmd.c_str()); + MQTT_execute_command(cmd); } - ExecuteCommand_all({EventValueSource::Enum::VALUE_SOURCE_MQTT, std::move(cmd)}, true); } break; } @@ -114,33 +126,8 @@ bool CPlugin_006(CPlugin::Function function, struct EventStruct *event, String& break; } - String pubname = CPlugin_006_pubname; - const bool contains_valname = pubname.indexOf(F("%valname%")) != -1; - bool mqtt_retainFlag = CPlugin_006_mqtt_retainFlag; - - statusLED(true); + success = MQTT_protocol_send(event, CPlugin_006_pubname, CPlugin_006_mqtt_retainFlag); - //LoadTaskSettings(event->TaskIndex); // FIXME TD-er: This can probably be removed - parseControllerVariables(pubname, event, false); - - const uint8_t valueCount = getValueCountForTask(event->TaskIndex); - - for (uint8_t x = 0; x < valueCount; x++) - { - String tmppubname = pubname; - if (contains_valname) { - parseSingleControllerVariable(tmppubname, event, x, false); - } - - // 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(), mqtt_retainFlag)) - success = true; - } else { - if (MQTTpublish(event->ControllerIndex, event->TaskIndex, std::move(tmppubname), formatUserVarNoCheck(event, x), mqtt_retainFlag)) - success = true; - } - } break; } diff --git a/src/_C007.cpp b/src/_C007.cpp index 8d2846cb96..1cf062e58b 100644 --- a/src/_C007.cpp +++ b/src/_C007.cpp @@ -11,6 +11,7 @@ # define CPLUGIN_ID_007 7 # define CPLUGIN_NAME_007 "Emoncms" +# define C007_DEFAULT_URL "/emoncms/input/post.json" bool CPlugin_007(CPlugin::Function function, struct EventStruct *event, String& string) { @@ -26,6 +27,7 @@ bool CPlugin_007(CPlugin::Function function, struct EventStruct *event, String& proto.usesPassword = true; proto.defaultPort = 80; proto.usesID = true; + proto.usesTemplate = true; break; } @@ -35,6 +37,12 @@ bool CPlugin_007(CPlugin::Function function, struct EventStruct *event, String& break; } + case CPlugin::Function::CPLUGIN_PROTOCOL_TEMPLATE: + { + event->String2 = F(C007_DEFAULT_URL); + break; + } + case CPlugin::Function::CPLUGIN_INIT: { success = init_c007_delay_queue(event->ControllerIndex); @@ -52,6 +60,7 @@ bool CPlugin_007(CPlugin::Function function, struct EventStruct *event, String& if (C007_DelayHandler == nullptr) { break; } + if (C007_DelayHandler->queueFull(event->ControllerIndex)) { break; } @@ -93,27 +102,22 @@ bool CPlugin_007(CPlugin::Function function, struct EventStruct *event, String& bool do_process_c007_delay_queue(cpluginID_t cpluginID, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { const C007_queue_element& element = static_cast(element_base); // *INDENT-ON* - String url = F("/emoncms/input/post.json?node="); - - url += Settings.Unit; - url += F("&json="); + if (ControllerSettings.Publish[0] == '\0') { + strcpy_P(ControllerSettings.Publish, PSTR(C007_DEFAULT_URL)); + } + String url = strformat(F("%s?node=%d&json="), ControllerSettings.Publish, Settings.Unit); for (uint8_t i = 0; i < element.valueCount; ++i) { - url += (i == 0) ? '{' : ','; - url += F("field"); - url += element.idx + i; - url += ':'; - url += element.txt[i]; + url += strformat(F("%cfield%d:%s"), (i == 0) ? '{' : ',', element.idx + i, element.txt[i].c_str()); } - url += '}'; - url += F("&apikey="); - url += getControllerPass(element._controller_idx, ControllerSettings); // "0UDNN17RW6XAS2E5" // api key + url += strformat(F("}&apikey=%s"), getControllerPass(element._controller_idx, ControllerSettings).c_str()); // "0UDNN17RW6XAS2E5" // api key + + # ifndef BUILD_NO_DEBUG -#ifndef BUILD_NO_DEBUG - if (Settings.SerialLogLevel >= LOG_LEVEL_DEBUG_MORE) { - serialPrintln(url); + if (loglevelActiveFor(LOG_LEVEL_DEBUG_MORE)) { + addLog(LOG_LEVEL_DEBUG_MORE, url); } -#endif + # endif // ifndef BUILD_NO_DEBUG int httpCode = -1; send_via_http( diff --git a/src/_C014.cpp b/src/_C014.cpp index 95aa7d2754..a934d1ad64 100644 --- a/src/_C014.cpp +++ b/src/_C014.cpp @@ -7,14 +7,20 @@ # 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/PeriodicalActions.h" # include "_Plugin_Helper.h" // ####################################################################################################### -// ################################# Controller Plugin 0014: Homie 3/4 ################################### +// ################################# Controller Plugin 014: Homie 3/4 #################################### // ####################################################################################################### /** Changelog: + * 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 */ @@ -23,8 +29,11 @@ # 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 +# 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" @@ -42,68 +51,54 @@ # 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_CMD_VALUE_NAME "Command" // human readabele name for command value +# 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! +uint8_t msgCounter = 0; // counter for send Messages (currently for information / log only! String CPlugin_014_pubname; bool CPlugin_014_mqtt_retainFlag = false; -/* - // send MQTT Message with complete Topic / Payload - bool CPlugin_014_sendMQTTmsg(String& topic, const char* payload, int& errorCounter) { - bool mqttReturn = MQTTpublish(CPLUGIN_ID_014, INVALID_TASK_INDEX, topic, payload, true); - if (mqttReturn) msgCounter++; - else errorCounter++; - if (loglevelActiveFor(LOG_LEVEL_INFO) && mqttReturn) { - String log = F("C014 : msg T:"); - log += topic; - log += F(" P: "); - log += payload; - addLog(LOG_LEVEL_DEBUG_MORE, log+" success!"); - } - if (loglevelActiveFor(LOG_LEVEL_INFO) && !mqttReturn) { - String log = F("C014 : msg T:"); - log += topic; - log += F(" P: "); - log += payload; - addLog(LOG_LEVEL_ERROR, log+" ERROR!"); - } - return mqttReturn; - } - */ +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) { +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++; } + 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 -#ifndef BUILD_NO_DEBUG if (loglevelActiveFor(LOG_LEVEL_DEBUG_MORE) && mqttReturn) { - String log = F("C014 : T:"); - log += topic; - log += F(" P: "); - log += payload; log += F(" success!"); addLogMove(LOG_LEVEL_DEBUG_MORE, log); } -#endif + # endif // ifndef BUILD_NO_DEBUG if (loglevelActiveFor(LOG_LEVEL_ERROR) && !mqttReturn) { - String log = F("C014 : T:"); - log += topic; - log += F(" P: "); - log += payload; log += F(" ERROR!"); addLogMove(LOG_LEVEL_ERROR, log); } @@ -112,22 +107,21 @@ bool CPlugin_014_sendMQTTdevice(String tmppubname, } // 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) -{ +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, +bool CPlugin_014_sendMQTTnode(String tmppubname, const String& node, const String& value, const String& topic, const String& payload, - int & errorCounter) { + 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 @@ -136,26 +130,20 @@ bool CPlugin_014_sendMQTTnode(String tmppubname, if (mqttReturn) { msgCounter++; } else { errorCounter++; } - #ifndef BUILD_NO_DEBUG + 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) { - String log = F("C014 : V:"); - log += value; - log += F(" T: "); - log += topic; - log += F(" P: "); - log += payload; log += F(" success!"); addLogMove(LOG_LEVEL_DEBUG_MORE, log); } - #endif + # endif // ifndef BUILD_NO_DEBUG if (loglevelActiveFor(LOG_LEVEL_ERROR) && !mqttReturn) { - String log = F("C014 : V:"); - log += value; - log += F(" T: "); - log += topic; - log += F(" P: "); - log += payload; log += F(" ERROR!"); addLogMove(LOG_LEVEL_ERROR, log); } @@ -163,10 +151,11 @@ bool CPlugin_014_sendMQTTnode(String tmppubname, return mqttReturn; } -// and String a comma seperated list -void CPLUGIN_014_addToList(String& valuesList, const String& node) -{ - if (valuesList.length() > 0) { valuesList += ','; } +// and String to a comma separated list +void C014_addToList(String& valuesList, const String& node) { + if (valuesList.length() > 0) { + valuesList += ','; + } valuesList += node; } @@ -215,14 +204,13 @@ bool CPlugin_014(CPlugin::Function function, struct EventStruct *event, String& case CPlugin::Function::CPLUGIN_INTERVAL: { - if (MQTTclient.connected()) - { + if (MQTTclient.connected()) { errorCounter = 0; - pubname = CPLUGIN_014_BASE_TOPIC; // Scheme to form device messages - pubname.replace(F("%sysname%"), Settings.getName()); + pubname = F(CPLUGIN_014_BASE_TOPIC); // Scheme to form device messages + C014_replaceSysname(pubname); -# ifdef CPLUGIN_014_V3 + # 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); @@ -233,14 +221,12 @@ bool CPlugin_014(CPlugin::Function function, struct EventStruct *event, String& 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 + # endif // ifdef CPLUGIN_014_V3 - if (errorCounter > 0) - { + 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); - success = false; } 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. @@ -248,19 +234,16 @@ bool CPlugin_014(CPlugin::Function function, struct EventStruct *event, String& success = true; } - #ifndef BUILD_NO_DEBUG + # ifndef BUILD_NO_DEBUG + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - String log = F("C014 : $stats information sent with "); - - if (errorCounter > 0) { log += errorCounter; } - else { log += F("no"); } - log += F(" errors! ("); - log += msgCounter; - log += F(" messages)"); - msgCounter = 0; - addLogMove(LOG_LEVEL_DEBUG, log); + addLog(LOG_LEVEL_DEBUG, + strformat(F("C014 : $stats information sent with %s errors! (%d messages)"), + errorCounter > 0 ? String(errorCounter).c_str() : "no", + msgCounter)); } - #endif + # endif // ifndef BUILD_NO_DEBUG + msgCounter = 0; } break; } @@ -270,21 +253,20 @@ bool CPlugin_014(CPlugin::Function function, struct EventStruct *event, String& statusLED(true); // send autodiscover header - pubname = CPLUGIN_014_BASE_TOPIC; // Scheme to form device messages - pubname.replace(F("%sysname%"), Settings.getName()); - int deviceCount = 1; // minimum the SYSTEM device exists - int nodeCount = 1; // minimum the cmd node exists + 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 = CPLUGIN_014_BASE_VALUE; // Scheme to form node messages - nodename.replace(F("%sysname%"), Settings.getName()); - 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; // estaimate Units + 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. @@ -294,42 +276,45 @@ bool CPlugin_014(CPlugin::Function function, struct EventStruct *event, String& 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); + 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 + # 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 - # if defined(ESP8266) - CPlugin_014_sendMQTTdevice(pubname, event->TaskIndex, F("$implementation"), F("ESP8266"), errorCounter); - # endif // if defined(ESP8266) - # if defined(ESP32) - CPlugin_014_sendMQTTdevice(pubname, event->TaskIndex, F("$implementation"), F("ESP32"), errorCounter); - # endif // if defined(ESP32) + 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 + # 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 + # 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 + # endif // ifdef CPLUGIN_014_V3 // always send the SYSTEM device with the cmd node - CPLUGIN_014_addToList(nodesList, F(CPLUGIN_014_SYSTEM_DEVICE)); - CPLUGIN_014_addToList(valuesList, F(CPLUGIN_014_CMD_VALUE)); + 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, @@ -370,21 +355,20 @@ bool CPlugin_014(CPlugin::Function function, struct EventStruct *event, String& int gpio = 0; while (gpio <= MAX_GPIO) { - if (Settings.getPinBootState(gpio) != PinBootState::Default_state) // anything but default - { + const PinBootState pinBootState = Settings.getPinBootState(gpio); + + if (pinBootState != PinBootState::Default_state) { // anything but default nodeCount++; - valueName = F(CPLUGIN_014_GPIO_VALUE); - valueName += toString(gpio, 0); - CPLUGIN_014_addToList(valuesList, valueName); + 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); + 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); + CPlugin_014_sendMQTTnode(nodename, F(CPLUGIN_014_SYSTEM_DEVICE), valueName, F("/$datatype"), F("boolean"), errorCounter); - if (Settings.getPinBootState(gpio) != PinBootState::Input) // defined as output - { + 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); @@ -394,36 +378,34 @@ bool CPlugin_014(CPlugin::Function function, struct EventStruct *event, String& } // $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"), F(""), valuesList, errorCounter); - valuesList = F(""); + 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++) - { + for (taskIndex_t x = 0; x < TASKS_MAX; ++x) { const pluginID_t pluginID = Settings.getPluginID_for_task(x); - if (validPluginID_fullcheck(pluginID)) - { + + if (validPluginID_fullcheck(pluginID)) { LoadTaskSettings(x); - deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(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 = F(""); + 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. - { + 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 (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! - CPLUGIN_014_addToList(valuesList, ExtraTaskSettings.TaskDeviceValueNames[varNr]); + 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) @@ -445,10 +427,11 @@ bool CPlugin_014(CPlugin::Function function, struct EventStruct *event, String& errorCounter); // $datatype The data type. See Payloads. Enum: [integer, float, boolean,string, enum, color] - unitName = F(""); + unitName = EMPTY_STRING; switch (Settings.TaskDevicePluginConfig[x][varNr]) { - case 0: valueName = F("integer"); + case 0: + valueName = F("integer"); if ((ExtraTaskSettings.TaskDevicePluginConfig[varNr] != 0) || (ExtraTaskSettings.TaskDevicePluginConfig[varNr + 5] != 0)) { @@ -457,25 +440,29 @@ bool CPlugin_014(CPlugin::Function function, struct EventStruct *event, String& unitName += ExtraTaskSettings.TaskDevicePluginConfig[varNr + valueCount]; } break; - case 1: valueName = F("float"); + case 1: + valueName = F("float"); if ((ExtraTaskSettings.TaskDevicePluginConfig[varNr] != 0) || (ExtraTaskSettings.TaskDevicePluginConfig[varNr + 5] != 0)) { - unitName = ExtraTaskSettings.TaskDevicePluginConfig[varNr]; - unitName += ':'; - unitName += ExtraTaskSettings.TaskDevicePluginConfig[varNr + valueCount]; + 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]; + case 4: + valueName = F("enum"); + unitName = ExtraTaskSettings.TaskDeviceFormula[varNr]; break; - case 5: valueName = F("color"); - unitName = F("rgb"); + case 5: + valueName = F("color"); + unitName = F("rgb"); break; - case 6: valueName = F("color"); - unitName = F("hsv"); + case 6: + valueName = F("color"); + unitName = F("hsv"); break; } CPlugin_014_sendMQTTnode(nodename, @@ -485,13 +472,14 @@ bool CPlugin_014(CPlugin::Function function, struct EventStruct *event, String& valueName, errorCounter); - if (!unitName.isEmpty()) { + if (!unitName.isEmpty()) { CPlugin_014_sendMQTTnode(nodename, deviceName, ExtraTaskSettings.TaskDeviceValueNames[varNr], F("/$format"), unitName, - errorCounter); } + errorCounter); + } nodeCount++; } } @@ -503,16 +491,13 @@ bool CPlugin_014(CPlugin::Function function, struct EventStruct *event, String& // customValues = PluginCall(PLUGIN_WEBFORM_SHOW_VALUES, &TempEvent, customValuesStr); uint8_t customValues = false; - if (!customValues) - { // standard Values - for (uint8_t varNr = 0; varNr < valueCount; varNr++) - { + 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! - { - CPLUGIN_014_addToList(valuesList, ExtraTaskSettings.TaskDeviceValueNames[varNr]); + + 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, @@ -531,6 +516,7 @@ bool CPlugin_014(CPlugin::Function function, struct EventStruct *event, String& 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, @@ -542,48 +528,47 @@ bool CPlugin_014(CPlugin::Function function, struct EventStruct *event, String& nodeCount++; - /* // because values in ESPEasy are unitless lets assueme some units by the value name - (still case sensitive) - if (strstr(ExtraTaskSettings.TaskDeviceValueNames[varNr], "temp") != nullptr ) - { - unitName = F("°C"); - } else if (strstr(ExtraTaskSettings.TaskDeviceValueNames[varNr], "humi") != nullptr ) - { - unitName = F("%"); - } else if (strstr(ExtraTaskSettings.TaskDeviceValueNames[varNr], "press") != nullptr ) - { - unitName = F("Pa"); - } // ToDo: .... and more - - if (unitName != F("")) // 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. Recommended: Yes No - ("") - CPlugin_014_sendMQTTnode(nodename, deviceName, - ExtraTaskSettings.TaskDeviceValueNames[varNr], F("/$unit"), unitName, - errorCounter); - } - unitName = F(""); + /* TODO Fix units? + // because values in ESPEasy are unitless lets assume some units by the value name (still case sensitive) + + if (strstr(ExtraTaskSettings.TaskDeviceValueNames[varNr], "temp") != nullptr) + { + unitName = F("°C"); + } else if (strstr(ExtraTaskSettings.TaskDeviceValueNames[varNr], "humi") != nullptr) + { + unitName = F("%"); + } else if (strstr(ExtraTaskSettings.TaskDeviceValueNames[varNr], "press") != nullptr) + { + unitName = F("Pa"); + } // ToDo: .... and more + + 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.Recommended: Yes No + ("") + CPlugin_014_sendMQTTnode(nodename, deviceName, + ExtraTaskSettings.TaskDeviceValueNames[varNr], F("/$unit"), unitName, + errorCounter); + } + unitName = F(""); */ } } } // end loop throug values } else { // Device has custom Values - #ifndef BUILD_NO_DEBUG + # ifndef BUILD_NO_DEBUG + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - String log = F("C014 : Device has custom values: "); - log += getPluginNameFromDeviceIndex(getDeviceIndex_from_TaskIndex(x)); - log += F(" not implemented!"); - addLogMove(LOG_LEVEL_DEBUG, log); + addLog(LOG_LEVEL_DEBUG, strformat(F("C014 : Device has custom values: %s not implemented!"), + getPluginNameFromDeviceIndex(getDeviceIndex_from_TaskIndex(x)).c_str())); } - #endif + # endif // ifndef BUILD_NO_DEBUG } } - if (!valuesList.isEmpty()) - { + 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, @@ -602,28 +587,27 @@ bool CPlugin_014(CPlugin::Function function, struct EventStruct *event, String& errorCounter); // add device to device list - CPLUGIN_014_addToList(nodesList, deviceName); + 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, + CPlugin_014_sendMQTTnode(nodename, + deviceName, + F("$properties"), + F(""), + valuesList, errorCounter); - valuesList = F(""); + valuesList = EMPTY_STRING; } } else { // device not enabeled - #ifndef BUILD_NO_DEBUG + # ifndef BUILD_NO_DEBUG + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - String log = F("C014 : Device Disabled: "); - log += getPluginNameFromDeviceIndex(getDeviceIndex_from_TaskIndex(x)); - log += F(" not propagated!"); - addLogMove(LOG_LEVEL_DEBUG, log); + addLog(LOG_LEVEL_DEBUG, strformat(F("C014 : Device Disabled: %s not propagated!"), + getPluginNameFromDeviceIndex(getDeviceIndex_from_TaskIndex(x)).c_str())); } - #endif + # endif // ifndef BUILD_NO_DEBUG } } // device configured } // loop through devices @@ -634,12 +618,10 @@ bool CPlugin_014(CPlugin::Function function, struct EventStruct *event, String& CPlugin_014_sendMQTTdevice(pubname, event->TaskIndex, F("$nodes"), nodesList, errorCounter); } - if (errorCounter > 0) - { + 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); - success = false; } 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. @@ -648,18 +630,13 @@ bool CPlugin_014(CPlugin::Function function, struct EventStruct *event, String& } if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("C014 : autodiscover information of "); - log += deviceCount; - log += F(" Devices and "); - log += nodeCount; - log += F(" Nodes sent with "); - - if (errorCounter > 0) { log += errorCounter; } - else { log += F("no"); } - log += F(" errors! ("); - log += msgCounter; - log += F(" messages)"); - addLogMove(LOG_LEVEL_INFO, log); + 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; @@ -675,20 +652,16 @@ bool CPlugin_014(CPlugin::Function function, struct EventStruct *event, String& case CPlugin::Function::CPLUGIN_GOT_INVALID: { - pubname = CPLUGIN_014_BASE_TOPIC; // Scheme to form device messages - pubname.replace(F("%sysname%"), Settings.getName()); + 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 = F("C014 : Device: "); - log += Settings.getName(); - log += F(" got invalid (disconnect"); - - if (success) { log += F("ed)."); } - else { log += F(") failed!"); } + 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; @@ -696,8 +669,8 @@ bool CPlugin_014(CPlugin::Function function, struct EventStruct *event, String& case CPlugin::Function::CPLUGIN_FLUSH: { - pubname = CPLUGIN_014_BASE_TOPIC; // Scheme to form device messages - pubname.replace(F("%sysname%"), Settings.getName()); + 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); @@ -706,168 +679,67 @@ bool CPlugin_014(CPlugin::Function function, struct EventStruct *event, String& case CPlugin::Function::CPLUGIN_PROTOCOL_RECV: { - controllerIndex_t ControllerID = findFirstEnabledControllerWithId(CPLUGIN_ID_014); - bool validTopic = false; + 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 valueNr = 0; - taskIndex_t taskIndex = INVALID_TASK_INDEX; - struct EventStruct TempEvent(event->TaskIndex); - TempEvent.Source = EventValueSource::Enum::VALUE_SOURCE_MQTT; // to trigger the correct acknowledgment - int lastindex = event->String1.lastIndexOf('/'); + int lastindex = event->String1.lastIndexOf('/'); errorCounter = 0; - if (equals(event->String1.substring(lastindex + 1), F("set"))) - { + if (equals(event->String1.substring(lastindex + 1), F("set"))) { pubname = event->String1.substring(0, lastindex); lastindex = pubname.lastIndexOf('/'); - String nodeName = pubname.substring(0, lastindex); - String valueName = pubname.substring(lastindex + 1); + 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 = F("C014 : MQTT received: "); - log += F("/set: N: "); - log += nodeName; - log += F(" V: "); - log += valueName; + log = strformat(F("C014 : MQTT received: /set: N: %s V: %s"), nodeName.c_str(), valueName.c_str()); } - if (nodeName.equals(F(CPLUGIN_014_SYSTEM_DEVICE))) // msg to a system device - { - if (valueName.startsWith(F(CPLUGIN_014_GPIO_VALUE))) // msg to to set gpio values - { - const size_t gpio_value_tag_length = String(F(CPLUGIN_014_GPIO_VALUE)).length(); + 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 - cmd = F("GPIO,"); - cmd += valueName.substring(gpio_value_tag_length).toInt(); // get the GPIO - - if ((equals(event->String2, F("true"))) || (equals(event->String2, '1'))) { cmd += F(",1"); } - else { cmd += F(",0"); } + // 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 (valueName.equals(F(CPLUGIN_014_CMD_VALUE))) // msg to send a command - { + } else if (equals(valueName, F(CPLUGIN_014_CMD_VALUE))) { // msg to send a command cmd = event->String2; validTopic = true; - } else - { - cmd = F("SYSTEM/"); - cmd += valueName; - cmd += F(" unknown!"); - } - } else // msg to a receiving plugin - { - taskIndex = findTaskIndexByName(nodeName); - deviceIndex_t deviceIndex = getDeviceIndex_from_TaskIndex(taskIndex); - taskVarIndex_t taskVarIndex = event->Par2 - 1; - - if (validDeviceIndex(deviceIndex) && validTaskVarIndex(taskVarIndex)) { - const pluginID_t pluginID = getPluginID_from_DeviceIndex(deviceIndex); - constexpr pluginID_t DUMMY_PLUGIN_ID(33); - constexpr pluginID_t HOMIE_RECEIVER_PLUGIN_ID(86); - - if (pluginID == DUMMY_PLUGIN_ID) - { // TaskValueSet,,,, works only with - // new version of P033! - valueNr = findDeviceValueIndexByName(valueName, taskIndex); - - if (valueNr != VARS_PER_TASK) // value Name identified - { - cmd = F("TaskValueSet,"); // Set a Dummy Device Value - cmd += (taskIndex + 1); // set the device Number - cmd += ','; - cmd += (valueNr + 1); // set the value Number - cmd += ','; - cmd += event->String2; // expect float as payload! - validTopic = true; - } - } else if (pluginID == HOMIE_RECEIVER_PLUGIN_ID) { // Plugin 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 (valueNr != VARS_PER_TASK) { - cmd = F("event,"); - cmd += valueName; - cmd += '='; - - if (Settings.TaskDevicePluginConfig[taskIndex][valueNr] == 3) { // Quote Sting 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 - String enumList = ExtraTaskSettings.TaskDeviceFormula[taskVarIndex]; - int i = 1; - - while (!parseString(enumList, i).isEmpty()) { // lookup result in enum List is changed to lowercase - if (parseString(enumList, i).equalsIgnoreCase(event->String2)) { break; } - i++; - } - cmd += i; - cmd += ','; - } - 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) { - parseCommandString(&TempEvent, cmd); - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - log += F(" cmd: "); - log += cmd; - log += F(" OK"); - addLog(LOG_LEVEL_INFO, log); - } - } else { if (loglevelActiveFor(LOG_LEVEL_INFO)) { - log += F(" INVALID MSG"); + 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) { - // in case of event, store to buffer and return... - String command = parseString(cmd, 1); - - if ((equals(command, F("event"))) || (equals(command, F("asyncevent")))) - { - if (Settings.UseRules) { - String newEvent = parseStringToEnd(cmd, 2); - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("C014 : taskIndex:"); - - if (!validTaskIndex(taskIndex)) { - log += F("Invalid"); - } else { - log += taskIndex; - log += F(" valueNr:"); - log += valueNr; - log += F(" valueType:"); - log += Settings.TaskDevicePluginConfig[taskIndex][valueNr]; - } - log += F(" Event: "); - log += newEvent; - addLogMove(LOG_LEVEL_INFO, log); - } - eventQueue.addMove(std::move(newEvent)); - } - } else { // not an event - // FIXME TD-er: Command is not parsed, should we call ExecuteCommand here? - ExecuteCommand_all_config({EventValueSource::Enum::VALUE_SOURCE_MQTT, std::move(cmd)}, true); - } + if (validTopic && !cmdExecuted) { // Don't execute twice + MQTT_execute_command(cmd, true); } } success = validTopic; @@ -880,180 +752,113 @@ bool CPlugin_014(CPlugin::Function function, struct EventStruct *event, String& break; } - String pubname = CPlugin_014_pubname; - const bool contains_valname = pubname.indexOf(F("%valname%")) != -1; - bool mqtt_retainFlag = CPlugin_014_mqtt_retainFlag; - - statusLED(true); - - parseControllerVariables(pubname, event, false); - LoadTaskSettings(event->TaskIndex); - - uint8_t valueCount = getValueCountForTask(event->TaskIndex); - - for (uint8_t x = 0; x < valueCount; x++) - { - String tmppubname = pubname; - String value; - if (contains_valname) { - parseSingleControllerVariable(tmppubname, event, x, false); - } - - // Small optimization so we don't try to copy potentially large strings - if (event->getSensorType() == Sensor_VType::SENSOR_TYPE_STRING) { - if (MQTTpublish(event->ControllerIndex, event->TaskIndex, tmppubname.c_str(), event->String2.c_str(), mqtt_retainFlag)) - success = true; - value = event->String2.substring(0, 20); // For the log - } else { - value = formatUserVarNoCheck(event, x); - if (MQTTpublish(event->ControllerIndex, event->TaskIndex, tmppubname.c_str(), value.c_str(), mqtt_retainFlag)) - success = true; - } + success = MQTT_protocol_send(event, CPlugin_014_pubname, CPlugin_014_mqtt_retainFlag); -#ifndef BUILD_NO_DEBUG - if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - addLogMove(LOG_LEVEL_DEBUG, - strformat(F("C014 : Sent to %s %s"), - tmppubname.c_str(), - value.c_str())); - } -#endif - } 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); - } */ - success = false; + /* 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()) { - String commandName = parseString(string, 1); // could not find a way to get the command out of the event structure. + const String commandName = parseString(string, 1); // could not find a way to get the command out of the event structure. - if (equals(commandName, F("gpio"))) // !ToDo : As gpio is like any other plugin commands should be integrated below! + if (equals(commandName, F(CPLUGIN_014_GPIO_COMMAND))) // !ToDo : As gpio is like any other plugin commands should be integrated + // below! { - int port = event->Par1; // parseString(string, 2).toInt(); - int valueInt = event->Par2; // parseString(string, 3).toInt(); - String valueBool = F("false"); - - if (valueInt == 1) { valueBool = F("true"); } + 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 = CPLUGIN_014_PUBLISH; // ControllerSettings.Publish not used because it can be modified by the user! - topic.replace(F("%sysname%"), Settings.getName()); + 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%"), String(F(CPLUGIN_014_GPIO_VALUE)) + toString(port, 0)); + 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) { - String log = F("C014 : Acknowledged GPIO"); - log += port; - log += F(" value:"); - log += valueBool; - log += F(" ("); - log += valueInt; - log += ')'; log += F(" success!"); addLogMove(LOG_LEVEL_INFO, log); } if (loglevelActiveFor(LOG_LEVEL_ERROR) && !success) { - String log = F("C014 : Acknowledged GPIO"); - log += port; - log += F(" value:"); - log += valueBool; - log += F(" ("); - log += valueInt; - log += ')'; log += F(" ERROR!"); addLogMove(LOG_LEVEL_ERROR, log); } - } else // not gpio - { - taskVarIndex_t taskVarIndex = event->Par2 - 1; + } else { // not gpio + const taskVarIndex_t taskVarIndex = event->Par2 - 1; if (validTaskVarIndex(taskVarIndex)) { userVarIndex_t userVarIndex = event->BaseVarIndex + taskVarIndex; - String topic = CPLUGIN_014_PUBLISH; - topic.replace(F("%sysname%"), Settings.getName()); - int deviceIndex = event->Par1; // parseString(string, 2).toInt(); + 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); - String valueName = ExtraTaskSettings.TaskDeviceValueNames[event->Par2 - 1]; // parseString(string, 3).toInt()-1]; + 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("taskvalueset"))) || (equals(commandName, F("dummyvalueset")))) // should work for both - { - valueStr = formatUserVarNoCheck(event, taskVarIndex); // parseString(string, 4); + 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) { - String log = F("C014 : Acknowledged: "); - log += deviceName; - log += F(" var: "); - log += valueName; - log += F(" topic: "); - log += topic; - log += F(" value: "); - log += valueStr; log += F(" success!"); addLogMove(LOG_LEVEL_INFO, log); } if (loglevelActiveFor(LOG_LEVEL_ERROR) && !success) { - String log = F("C014 : Aacknowledged: "); - log += deviceName; - log += F(" var: "); - log += valueName; - log += F(" topic: "); - log += topic; - log += F(" value: "); - log += valueStr; log += F(" ERROR!"); addLogMove(LOG_LEVEL_ERROR, log); } - } else if (equals(parseString(commandName, 1), F("homievalueset"))) { // acknolages value form P086 Homie Receiver + } 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 + case 0: // PLUGIN_085_VALUE_INTEGER valueInt = static_cast(UserVar[userVarIndex]); valueStr = toString(UserVar[userVarIndex], 0); break; @@ -1062,8 +867,7 @@ bool CPlugin_014(CPlugin::Function function, struct EventStruct *event, String& break; case 2: // PLUGIN_085_VALUE_BOOLEAN - if (UserVar[userVarIndex] == 1) { valueStr = F("true"); } - else { valueStr = F("false"); } + valueStr = boolToString(UserVar[userVarIndex] == 1); break; case 3: // PLUGIN_085_VALUE_STRING // valueStr = ExtraTaskSettings.TaskDeviceFormula[taskVarIndex]; @@ -1084,51 +888,36 @@ bool CPlugin_014(CPlugin::Function function, struct EventStruct *event, String& } 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) { - String log = F("C014 : homie acknowledge: "); - log += deviceName; - log += F(" taskIndex:"); - log += deviceIndex; - log += F(" valueNr:"); - log += event->Par2; - log += F(" valueName:"); - log += valueName; - log += F(" valueType:"); - log += Settings.TaskDevicePluginConfig[deviceIndex - 1][taskVarIndex]; - log += F(" topic:"); - log += topic; - log += F(" valueInt:"); - log += valueInt; - log += F(" valueStr:"); - log += valueStr; - log += F(" 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) { - String log = F("C014 : homie acknowledge: "); - log += deviceName; - log += F(" var: "); - log += valueName; - log += F(" topic: "); - log += topic; - log += F(" value: "); - log += valueStr; - log += F(" failed!"); + 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); - } */ + } 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; } } diff --git a/src/_P086_Homie.ino b/src/_P086_Homie.ino index a999e3d09a..0b0db5270c 100644 --- a/src/_P086_Homie.ino +++ b/src/_P086_Homie.ino @@ -1,340 +1,320 @@ -#include "_Plugin_Helper.h" -#ifdef USES_P086 -//####################################################################################################### -//################################## Plugin 086: Homie receiver########################################## -//####################################################################################################### - - -#define PLUGIN_086 -#define PLUGIN_ID_086 86 -#define PLUGIN_NAME_086 "Generic - Homie receiver" - -// empty default names because settings will be ignored / not used if value name is empty -#define PLUGIN_VALUENAME1_086 "" -#define PLUGIN_VALUENAME2_086 "" -#define PLUGIN_VALUENAME3_086 "" -#define PLUGIN_VALUENAME4_086 "" - -#define PLUGIN_086_VALUE_INTEGER 0 -#define PLUGIN_086_VALUE_FLOAT 1 -#define PLUGIN_086_VALUE_BOOLEAN 2 -#define PLUGIN_086_VALUE_STRING 3 -#define PLUGIN_086_VALUE_ENUM 4 -#define PLUGIN_086_VALUE_RGB 5 -#define PLUGIN_086_VALUE_HSV 6 - -#define PLUGIN_086_VALUE_TYPES 7 -#define PLUGIN_086_VALUE_MAX 4 - -#define PLUGIN_086_DEBUG true - -boolean Plugin_086(uint8_t function, struct EventStruct *event, String& string) -{ - boolean success = false; - - switch (function) - { - - case PLUGIN_DEVICE_ADD: - { - Device[++deviceCount].Number = PLUGIN_ID_086; - Device[deviceCount].Type = DEVICE_TYPE_DUMMY; - Device[deviceCount].VType = Sensor_VType::SENSOR_TYPE_NONE; - Device[deviceCount].Ports = 0; - Device[deviceCount].PullUpOption = false; - Device[deviceCount].InverseLogicOption = false; - Device[deviceCount].FormulaOption = false; - Device[deviceCount].DecimalsOnly = true; - Device[deviceCount].ValueCount = PLUGIN_086_VALUE_MAX; - Device[deviceCount].SendDataOption = false; - Device[deviceCount].TimerOption = false; - Device[deviceCount].GlobalSyncOption = false; - Device[deviceCount].Custom = true; - break; - } - - case PLUGIN_GET_DEVICENAME: - { - string = F(PLUGIN_NAME_086); - break; - } - - case PLUGIN_GET_DEVICEVALUENAMES: - { - strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[0], PSTR(PLUGIN_VALUENAME1_086)); - strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[1], PSTR(PLUGIN_VALUENAME2_086)); - strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[2], PSTR(PLUGIN_VALUENAME3_086)); - strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[3], PSTR(PLUGIN_VALUENAME4_086)); - - break; - } - - case PLUGIN_WEBFORM_LOAD: - { - addFormNote(F("Translation Plugin for controllers able to receive value updates according to the Homie convention.")); - - uint8_t choice = 0; - String labelText; - String keyName; - const __FlashStringHelper * options[PLUGIN_086_VALUE_TYPES] = { - F("integer"), - F("float"), - F("boolean"), - F("string"), - F("enum"), - F("rgb"), - F("hsv") - }; - const int optionValues[PLUGIN_086_VALUE_TYPES] = { - PLUGIN_086_VALUE_INTEGER, - PLUGIN_086_VALUE_FLOAT, - PLUGIN_086_VALUE_BOOLEAN, - PLUGIN_086_VALUE_STRING, - PLUGIN_086_VALUE_ENUM, - PLUGIN_086_VALUE_RGB, - PLUGIN_086_VALUE_HSV - }; - for (int i=0;iTaskIndex, i), NAME_FORMULA_LENGTH_MAX); - labelText = F("Parameter Type"); - keyName = F("valueType"); - keyName += i; - addFormSelector(labelText, keyName, PLUGIN_086_VALUE_TYPES, options, optionValues, choice ); - keyName += F("_min"); - addFormNumericBox(F("Min"),keyName,Cache.getTaskDevicePluginConfig(event->TaskIndex, i)); - keyName = F("valueType"); - keyName += i; - keyName += F("_max"); - addFormNumericBox(F("Max"),keyName,Cache.getTaskDevicePluginConfig(event->TaskIndex, i+PLUGIN_086_VALUE_MAX)); - if (i==0) addFormNote(F("min max values only valid for numeric parameter")); - keyName = F("decimals"); - keyName += i; - addFormNumericBox(F("Decimals"),keyName,Cache.getTaskDeviceValueDecimals(event->TaskIndex, i) ,0,8); - if (i==0) addFormNote(F("Decimal counts for float parameter")); - keyName = F("string"); - keyName += i; - addFormTextBox(F("String or enum"), keyName, Cache.getTaskDeviceFormula(event->TaskIndex, i), NAME_FORMULA_LENGTH_MAX); - if (i==0) addFormNote(F("Default string or enumumeration list (comma seperated).")); - } - success = true; - break; - } - - case PLUGIN_WEBFORM_SAVE: - { - String keyName; - for (int i=0;iPar2 - 1; - const userVarIndex_t userVarIndex = event->BaseVarIndex + taskVarIndex; - if (validTaskIndex(event->TaskIndex) && - validTaskVarIndex(taskVarIndex) && - validUserVarIndex(userVarIndex) && - (event->Par1 == (event->TaskIndex + 1))) {// make sure that this instance is the target - String parameter = parseStringToEndKeepCase(string,4); - String log; -/* if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - log = F("P086 : Acknowledge :"); - log += string; - log += F(" / "); - log += ExtraTaskSettings.TaskDeviceName; - log += F(" / "); - log += ExtraTaskSettings.TaskDeviceValueNames[taskVarIndex]; - 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); - } */ - float floatValue = 0.0f; - String enumList; - int i = 0; - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - log = F("P086 : deviceNr:"); - log += event->TaskIndex + 1; - log += F(" valueNr:"); - log += event->Par2; - log += F(" valueType:"); - log += Settings.TaskDevicePluginConfig[event->TaskIndex][taskVarIndex]; - } - - switch (Settings.TaskDevicePluginConfig[event->TaskIndex][taskVarIndex]) { - case PLUGIN_086_VALUE_INTEGER: - case PLUGIN_086_VALUE_FLOAT: - if (!parameter.isEmpty()) { - if (string2float(parameter,floatValue)) { - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - log += F(" integer/float set to "); - log += floatValue; - addLogMove(LOG_LEVEL_INFO, log); - } - UserVar.setFloat(event->TaskIndex, taskVarIndex, floatValue); - } else { // float conversion failed! - if (loglevelActiveFor(LOG_LEVEL_ERROR)) { - log += F(" parameter:"); - log += parameter; - log += F(" not a float value!"); - addLogMove(LOG_LEVEL_ERROR, log); - } - } - } else { - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - log += F(" value:"); - log += UserVar[userVarIndex]; - addLogMove(LOG_LEVEL_INFO, log); - } - } - break; - - case PLUGIN_086_VALUE_BOOLEAN: - if (parameter=="false") { - floatValue = 0.0f; - } else { - floatValue = 1.0f; - } - UserVar.setFloat(event->TaskIndex, taskVarIndex, floatValue); - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - log += F(" boolean set to "); - log += floatValue; - addLogMove(LOG_LEVEL_INFO, log); - } - break; - - case PLUGIN_086_VALUE_STRING: - //String values not stored to conserve flash memory - //safe_strncpy(ExtraTaskSettings.TaskDeviceFormula[taskVarIndex], parameter.c_str(), sizeof(ExtraTaskSettings.TaskDeviceFormula[taskVarIndex])); - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - log += F(" string set to "); - log += parameter; - addLogMove(LOG_LEVEL_INFO, log); - } - break; - - case PLUGIN_086_VALUE_ENUM: - enumList = Cache.getTaskDeviceFormula(event->TaskIndex, taskVarIndex); - i = 1; - while (!parseString(enumList,i).isEmpty()) { // lookup result in enum List - if (parseString(enumList,i)==parameter) { - floatValue = i; - break; - } - i++; - } - UserVar.setFloat(event->TaskIndex, taskVarIndex, floatValue); - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - log += F(" enum set to "); - log += floatValue; - log += ' '; - log += wrap_braces(parameter); - addLogMove(LOG_LEVEL_INFO, log); - } - break; - - case PLUGIN_086_VALUE_RGB: - //String values not stored to conserve flash memory - //safe_strncpy(ExtraTaskSettings.TaskDeviceFormula[taskVarIndex], parameter.c_str(), sizeof(ExtraTaskSettings.TaskDeviceFormula[taskVarIndex])); - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - log += F(" RGB received "); - log += parameter; - addLogMove(LOG_LEVEL_INFO, log); - } - break; - - case PLUGIN_086_VALUE_HSV: - //String values not stored to conserve flash memory - //safe_strncpy(ExtraTaskSettings.TaskDeviceFormula[taskVarIndex], parameter.c_str(), sizeof(ExtraTaskSettings.TaskDeviceFormula[taskVarIndex])); - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - log += F(" HSV received "); - log += parameter; - addLogMove(LOG_LEVEL_INFO, log); - } - break; - } - success = true; - } - } - } - break; - } - return success; -} -#endif // USES_P086 +#include "_Plugin_Helper.h" +#ifdef USES_P086 + +// ####################################################################################################### +// ################################## Plugin 086: Homie receiver########################################## +// ####################################################################################################### + +/** Changelog: + * 2023-09-10 tonhuisman: Reduce string usage to lower the .bin footprint + * 2023-09-10 tonhuisman: Add changelog, uncrustify source + */ + +# define PLUGIN_086 +# define PLUGIN_ID_086 86 +# define PLUGIN_NAME_086 "Generic - Homie receiver" + +// empty default names because settings will be ignored / not used if value name is empty +# define PLUGIN_VALUENAME1_086 "" + +# define PLUGIN_086_VALUE_INTEGER 0 +# define PLUGIN_086_VALUE_FLOAT 1 +# define PLUGIN_086_VALUE_BOOLEAN 2 +# define PLUGIN_086_VALUE_STRING 3 +# define PLUGIN_086_VALUE_ENUM 4 +# define PLUGIN_086_VALUE_RGB 5 +# define PLUGIN_086_VALUE_HSV 6 + +// Unsupported (yet) value types: +// - Percent +// - DateTime (convert to linuxtime?) +// - Duration + +# define PLUGIN_086_VALUE_MAX VARS_PER_TASK + +# define PLUGIN_086_DEBUG true + +boolean Plugin_086(uint8_t function, struct EventStruct *event, String& string) +{ + boolean success = false; + + switch (function) + { + case PLUGIN_DEVICE_ADD: + { + Device[++deviceCount].Number = PLUGIN_ID_086; + Device[deviceCount].Type = DEVICE_TYPE_DUMMY; + Device[deviceCount].VType = Sensor_VType::SENSOR_TYPE_NONE; + Device[deviceCount].Ports = 0; + Device[deviceCount].DecimalsOnly = true; + Device[deviceCount].ValueCount = PLUGIN_086_VALUE_MAX; + Device[deviceCount].Custom = true; + break; + } + + case PLUGIN_GET_DEVICENAME: + { + string = F(PLUGIN_NAME_086); + break; + } + + case PLUGIN_GET_DEVICEVALUENAMES: + { + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[0], PSTR(PLUGIN_VALUENAME1_086)); + + break; + } + + case PLUGIN_WEBFORM_LOAD: + { + # ifndef BUILD_NO_DEBUG + addFormNote(F("Translation Plugin for controllers able to receive value updates according to the Homie convention.")); + # endif // ifndef BUILD_NO_DEBUG + + const __FlashStringHelper *options[] = { + F("integer"), + F("float"), + F("boolean"), + F("string"), + F("enum"), + F("rgb"), + F("hsv") + }; + const int optionValues[] = { + PLUGIN_086_VALUE_INTEGER, + PLUGIN_086_VALUE_FLOAT, + PLUGIN_086_VALUE_BOOLEAN, + PLUGIN_086_VALUE_STRING, + PLUGIN_086_VALUE_ENUM, + PLUGIN_086_VALUE_RGB, + PLUGIN_086_VALUE_HSV + }; + constexpr int PLUGIN_086_VALUE_TYPES = NR_ELEMENTS(optionValues); + + for (int i = 0; i < PLUGIN_086_VALUE_MAX; ++i) { + addFormSubHeader(concat(F("Function #"), i + 1)); + + if (i == 0) { addFormNote(F("Triggers an event when a ../%taskname%/%event%/set MQTT topic arrives")); } + + addFormTextBox(F("Event Name"), getPluginCustomArgName((i * 10) + 0), + Cache.getTaskDeviceValueName(event->TaskIndex, i), NAME_FORMULA_LENGTH_MAX); + addFormSelector(F("Parameter Type"), getPluginCustomArgName((i * 10) + 1), + PLUGIN_086_VALUE_TYPES, options, optionValues, PCONFIG(i)); + + addFormNumericBox(F("Min"), getPluginCustomArgName((i * 10) + 2), + Cache.getTaskDevicePluginConfig(event->TaskIndex, i)); + addFormNumericBox(F("Max"), getPluginCustomArgName((i * 10) + 3), + Cache.getTaskDevicePluginConfig(event->TaskIndex, i + PLUGIN_086_VALUE_MAX)); + + if (i == 0) { addFormNote(F("min/max values only valid for numeric parameter")); } + + addFormNumericBox(F("Decimals"), getPluginCustomArgName((i * 10) + 4), + Cache.getTaskDeviceValueDecimals(event->TaskIndex, i), 0, 8); + + if (i == 0) { addFormNote(F("Decimal precision for float parameter")); } + + addFormTextBox(F("String or enum"), getPluginCustomArgName((i * 10) + 5), + Cache.getTaskDeviceFormula(event->TaskIndex, i), NAME_FORMULA_LENGTH_MAX); + + if (i == 0) { addFormNote(F("Default string or enumeration list (comma separated)")); } + } + success = true; + break; + } + + case PLUGIN_WEBFORM_SAVE: + { + for (int i = 0; i < PLUGIN_086_VALUE_MAX; ++i) { + strncpy_webserver_arg(ExtraTaskSettings.TaskDeviceValueNames[i], getPluginCustomArgName((i * 10) + 0)); + PCONFIG(i) = getFormItemInt(getPluginCustomArgName((i * 10) + 1)); + ExtraTaskSettings.TaskDevicePluginConfig[i] = getFormItemInt(getPluginCustomArgName((i * 10) + 2)); + ExtraTaskSettings.TaskDevicePluginConfig[i + PLUGIN_086_VALUE_MAX] = getFormItemInt(getPluginCustomArgName((i * 10) + 3)); + ExtraTaskSettings.TaskDeviceValueDecimals[i] = getFormItemInt(getPluginCustomArgName((i * 10) + 4)); + strncpy_webserver_arg(ExtraTaskSettings.TaskDeviceFormula[i], getPluginCustomArgName((i * 10) + 5)); + } + + success = true; + break; + } + + case PLUGIN_INIT: + { + success = true; + break; + } + + case PLUGIN_READ: + { + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + for (uint8_t x = 0; x < PLUGIN_086_VALUE_MAX; ++x) { + addLogMove(LOG_LEVEL_INFO, strformat(F("P086 : Value %d: %s"), x + 1, formatUserVarNoCheck(event->TaskIndex, x).c_str())); + } + } + success = true; + break; + } + + case PLUGIN_WRITE: + { + const String command = parseString(string, 1); + + if (equals(command, F("homievalueset"))) { + const taskVarIndex_t taskVarIndex = event->Par2 - 1; + + if (validTaskIndex(event->TaskIndex) && + validTaskVarIndex(taskVarIndex) && + (event->Par1 == (event->TaskIndex + 1))) { // make sure that this instance is the target + const String parameter = parseStringToEndKeepCase(string, 4); + String log; + + /* + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { + log = F("P086 : Acknowledge :"); + log += string; + log += F(" / "); + log += ExtraTaskSettings.TaskDeviceName; + log += F(" / "); + log += ExtraTaskSettings.TaskDeviceValueNames[taskVarIndex]; + 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); + } */ + float floatValue = 0.0f; + String enumList; + int i = 0; + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + log = strformat(F("P086 : deviceNr: %d valueNr: %d valueType: %d"), + event->TaskIndex + 1, event->Par2, Settings.TaskDevicePluginConfig[event->TaskIndex][taskVarIndex]); + } + + switch (Settings.TaskDevicePluginConfig[event->TaskIndex][taskVarIndex]) { + case PLUGIN_086_VALUE_INTEGER: + case PLUGIN_086_VALUE_FLOAT: + + if (!parameter.isEmpty()) { + if (string2float(parameter, floatValue)) { + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + log += concat(F(" integer/float set to "), floatValue); + addLogMove(LOG_LEVEL_INFO, log); + } + UserVar.setFloat(event->TaskIndex, taskVarIndex, floatValue); + } else { // float conversion failed! + if (loglevelActiveFor(LOG_LEVEL_ERROR)) { + log += strformat(F(" parameter: %s not a float value!"), parameter.c_str()); + addLogMove(LOG_LEVEL_ERROR, log); + } + } + } else { + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + log += concat(F(" value: "), UserVar.getFloat(event->TaskIndex, taskVarIndex)); + addLogMove(LOG_LEVEL_INFO, log); + } + } + break; + + case PLUGIN_086_VALUE_BOOLEAN: + + if (parameter.equalsIgnoreCase(F("false"))) { // This should be a case-sensitive check... + // and also check for "true" + floatValue = 0.0f; + } else { + floatValue = 1.0f; + } + UserVar.setFloat(event->TaskIndex, taskVarIndex, floatValue); + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + log += concat(F(" boolean set to "), floatValue); + addLogMove(LOG_LEVEL_INFO, log); + } + break; + + case PLUGIN_086_VALUE_STRING: + + // String values not stored to conserve flash memory + // safe_strncpy(ExtraTaskSettings.TaskDeviceFormula[taskVarIndex], parameter.c_str(), + // sizeof(ExtraTaskSettings.TaskDeviceFormula[taskVarIndex])); + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + log += concat(F(" string set to "), parameter); + addLogMove(LOG_LEVEL_INFO, log); + } + break; + + case PLUGIN_086_VALUE_ENUM: + { + enumList = Cache.getTaskDeviceFormula(event->TaskIndex, taskVarIndex); + i = 1; + String enumItem = parseStringKeepCase(enumList, i); + + while (!enumItem.isEmpty()) { // lookup result in enum List + if (enumItem.equalsIgnoreCase(parameter)) { // This should be a case-sensitive check... + floatValue = i; + break; + } + i++; + enumItem = parseStringKeepCase(enumList, i); + } + UserVar.setFloat(event->TaskIndex, event->Par2 - 1, floatValue); + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + log += strformat(F(" enum set to %.2f %s"), floatValue, wrap_braces(parameter).c_str()); + addLogMove(LOG_LEVEL_INFO, log); + } + break; + } + case PLUGIN_086_VALUE_RGB: + + // String values not stored to conserve flash memory + // safe_strncpy(ExtraTaskSettings.TaskDeviceFormula[taskVarIndex], parameter.c_str(), + // sizeof(ExtraTaskSettings.TaskDeviceFormula[taskVarIndex])); + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + log += concat(F(" RGB received "), parameter); + addLogMove(LOG_LEVEL_INFO, log); + } + break; + + case PLUGIN_086_VALUE_HSV: + + // String values not stored to conserve flash memory + // safe_strncpy(ExtraTaskSettings.TaskDeviceFormula[taskVarIndex], parameter.c_str(), + // sizeof(ExtraTaskSettings.TaskDeviceFormula[taskVarIndex])); + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + log += concat(F(" HSV received "), parameter); + addLogMove(LOG_LEVEL_INFO, log); + } + break; + } + success = true; + } + } + break; + } + } + return success; +} + +#endif // USES_P086 diff --git a/src/src/Helpers/_CPlugin_Helper_mqtt.cpp b/src/src/Helpers/_CPlugin_Helper_mqtt.cpp new file mode 100644 index 0000000000..18c7be3f85 --- /dev/null +++ b/src/src/Helpers/_CPlugin_Helper_mqtt.cpp @@ -0,0 +1,235 @@ + +#include "../Helpers/_CPlugin_Helper_mqtt.h" + +#if FEATURE_MQTT +# include "../Commands/ExecuteCommand.h" + +/*************************************************************************************** + * 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 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); + const taskVarIndex_t taskVarIndex = event->Par2 - 1; + uint8_t valueNr; + + if (validDeviceIndex(deviceIndex) && validTaskVarIndex(taskVarIndex)) { + const int pluginID = Device[deviceIndex].Number; + + # ifdef USES_P033 + + if ((pluginID == 33) || // Plugin 33 Dummy Device, + // backward compatible behavior: if handleCmd = true then execute TaskValueSet regardless of AllowTaskValueSetAllPlugins + ((handleCmd || Settings.AllowTaskValueSetAllPlugins()) && (pluginID != 86))) { + // TaskValueSet,,,, works only with new version of P033! + valueNr = findDeviceValueIndexByName(valueName, taskIndex); + + if (validTaskVarIndex(valueNr)) { // value Name identified + // 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 + # if defined(USES_P033) && defined(USES_P086) + else + # endif // if defined(USES_P033) && defined(USES_P086) + # ifdef USES_P086 + + if (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) { + 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) { + // 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; + + parseControllerVariables(pubname, event, false); + + 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; + parseSingleControllerVariable(tmppubname, event, x, 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 C%03d : %s %s"), event->ControllerIndex, 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; + } + } + } + return success; +} + +#endif // if FEATURE_MQTT diff --git a/src/src/Helpers/_CPlugin_Helper_mqtt.h b/src/src/Helpers/_CPlugin_Helper_mqtt.h new file mode 100644 index 0000000000..f4f056385b --- /dev/null +++ b/src/src/Helpers/_CPlugin_Helper_mqtt.h @@ -0,0 +1,18 @@ +#ifndef CPLUGIN_HELPER_MQTT_H +#define CPLUGIN_HELPER_MQTT_H + +#if FEATURE_MQTT +# include "../Helpers/_CPlugin_Helper.h" + +bool MQTT_handle_topic_commands(struct EventStruct *event, + bool handleCmd = true, + bool handleSet = true, + bool tryRemoteConfig = false); +void MQTT_execute_command(String& command, + bool tryRemoteConfig = false); +bool MQTT_protocol_send(EventStruct *event, + String pubname, + bool retainFlag); + +#endif // if FEATURE_MQTT +#endif // ifndef CPLUGIN_HELPER_MQTT_H