From efbb4e13ac4b334aa5114dda0f85ed2bb75df99b Mon Sep 17 00:00:00 2001 From: Gijs Noorlander Date: Sun, 15 Sep 2019 16:50:49 +0200 Subject: [PATCH] [Rules] Allow transformations on [var#n] and add [int#n] Also add some caches to find task-value names. Fix some minor issue when referring [var#n] variables. These were always rounded to 2 decimals. Also added [int#n] to address the same variables as with [var#n] but rounded to integer values, to be used in rules for more reliable comparing. --- docs/source/Rules/Rules.rst | 28 ++++--- src/ESPEasy-Globals.h | 2 + src/ESPEasyRules.ino | 143 ++++++++++++++++++++++-------------- src/ESPEasyStorage.ino | 1 + src/Misc.ino | 140 ++++++++++++++++++++--------------- 5 files changed, 189 insertions(+), 125 deletions(-) diff --git a/docs/source/Rules/Rules.rst b/docs/source/Rules/Rules.rst index 21db820e8..c3412c375 100644 --- a/docs/source/Rules/Rules.rst +++ b/docs/source/Rules/Rules.rst @@ -239,17 +239,17 @@ remember to add them after the code and always begin with "//": endif //this is another comment endon -Refering values +Referring values --------------- Rules and some plugins can use references to other (dynamic) values within ESPeasy. -The syntax for refering other values is: ``[...#...]`` +The syntax for referring other values is: ``[...#...]`` Sometimes it can be useful to have some extra options, each separated using a '#' like this: ``[...#...#...]`` Reference to a value of a specific task: ``[TaskName#ValueName]`` -Refering a value using some pre-defined format: ``[TaskName#ValueName#transformation#justification]`` +Referring a value using some pre-defined format: ``[TaskName#ValueName#transformation#justification]`` For example, there is a task named "bme280" which has a value named "temperature". @@ -265,36 +265,42 @@ N.B. these references to task values only yield a value when the task is enabled Special task names ------------------ -You must not use the task names ``Plugin`` or ``VAR`` as these hae special meaning. +You must not use the task names ``Plugin``, ``VAR`` ``INT`` as these hae special meaning. ``Plugin`` can be used in a so called ``PLUGIN_REQUEST``, for example: ``[Plugin#GPIO#Pinstate#N]`` to get the pin state of a GPIO pin. -``Var`` is used for internal variables. +``Var`` and ``INT`` are used for internal variables. The variables set with the ``Let`` command will be available in rules -as ``VAR#N`` where ``N`` is 1..16. +as ``VAR#N`` or ``INT#N`` where ``N`` is 1..16. For example: ``Let,10,[VAR#9]`` -Clock, Rules and System etc. are not recommended either since they are used in +N.B. ``INT`` and ``VAR`` use the same variable, only ``INT`` does round them to 0 decimals. +N.B.2 ``INT`` is added in build 20190916. + +``Clock``, ``Rules`` and ``System`` etc. are not recommended either since they are used in event names. Please observe that task names are case insensitive meaning that VAR, var, and Var etc. are all treated the same. + Formatting refered values ------------------------- -When refering another value, some basic formatting can be used. +When referring another value, some basic formatting can be used. -Refering a value using some pre-defined format: ``[TaskName#ValueName#transformation#justification]`` +Referring a value using some pre-defined format: ``[TaskName#ValueName#transformation#justification]`` Transformation ^^^^^^^^^^^^^^ * Transformations are case sensitive. (``M`` differs from ``m``, capital is more verbose) -* Most transformations work on "binary" values (0 or 1) +* Transformations can not be used on "Plugin" calls, like ``[Plugin#GPIO#Pinstate#N]``, since these already use multiple occurences of ``#``. +* Most transformations work on "binary" values (logic values 0 or 1) * A "binary" transformation can be "inverted" by adding a leading ``!``. -* A "binary" value is considered 0 when its string value is "0", otherwise it is an 1. (best to round a value to 0 decimals for this) +* A "binary" value is considered 0 when its string value is "0" or empty, otherwise it is an 1. (float values are rounded) +* A "binary" value can also be used to detect presence of a string, as it is 0 on an empty string or 1 otherwise. Binary transformations: diff --git a/src/ESPEasy-Globals.h b/src/ESPEasy-Globals.h index 9cfeef26b..c79548f71 100644 --- a/src/ESPEasy-Globals.h +++ b/src/ESPEasy-Globals.h @@ -197,6 +197,7 @@ void check_size() { #include "DataStructs/SystemTimerStruct.h" #include "DataStructs/RTCStruct.h" #include "DataStructs/PortStatusStruct.h" +#include "DataStructs/Caches.h" CRCStruct CRCValues; @@ -208,6 +209,7 @@ LogStruct Logging; NotificationStruct Notification[NPLUGIN_MAX]; RTCStruct RTC; DeviceVector Device; +Caches Cache; std::map pluginStats; std::map controllerStats; diff --git a/src/ESPEasyRules.ino b/src/ESPEasyRules.ino index 27c381a94..790f987f5 100644 --- a/src/ESPEasyRules.ino +++ b/src/ESPEasyRules.ino @@ -156,42 +156,83 @@ String rulesProcessingFile(const String& fileName, String& event) { fs::File f = tryOpenFile(fileName, "r+"); SPIFFS_CHECK(f, fileName.c_str()); - String line = ""; - bool match = false; - bool codeBlock = false; - bool isCommand = false; - bool condition[RULES_IF_MAX_NESTING_LEVEL]; - bool ifBranche[RULES_IF_MAX_NESTING_LEVEL]; - byte ifBlock = 0; - byte fakeIfBlock = 0; + String line; + line.reserve(RULES_IF_MAX_NESTING_LEVEL); + bool match = false; + bool codeBlock = false; + bool isCommand = false; + bool condition[RULES_IF_MAX_NESTING_LEVEL]; + bool ifBranche[RULES_IF_MAX_NESTING_LEVEL]; + byte ifBlock = 0; + byte fakeIfBlock = 0; - byte *buf = new byte[RULES_BUFFER_SIZE](); + std::vector buf; + buf.resize(RULES_BUFFER_SIZE); + + bool firstNonSpaceRead = false; + bool commentFound = false; while (f.available()) { - int len = f.read((byte *)buf, RULES_BUFFER_SIZE); + int len = f.read(&buf[0], RULES_BUFFER_SIZE); for (int x = 0; x < len; x++) { int data = buf[x]; SPIFFS_CHECK(data >= 0, fileName.c_str()); - if (data != 10) { - line += char(data); - } else { // if line complete, parse this rule - line.replace("\r", ""); + switch (static_cast(data)) + { + case 10: // "\n" + { + // Line end, parse rule + if (!line.startsWith(F("//")) && (line.length() > 0)) { + parseCompleteNonCommentLine(line, event, log, match, codeBlock, + isCommand, condition, ifBranche, ifBlock, + fakeIfBlock); + backgroundtasks(); + } - if ((line.substring(0, 2) != F("//")) && (line.length() > 0)) { - parseCompleteNonCommentLine(line, event, log, match, codeBlock, - isCommand, condition, ifBranche, ifBlock, - fakeIfBlock); - backgroundtasks(); + // Prepare for new line + line = ""; + firstNonSpaceRead = false; + commentFound = false; + break; } + case 13: // "\r", Just skip this character + break; + case '\t': // tab + case 32: // space + { + // Strip leading spaces. + if (firstNonSpaceRead) { + line += ' '; + } + break; + } + case '/': + { + if (!commentFound) { + line += '/'; - line = ""; + if (line.endsWith("//")) { + // consider the rest of the line a comment + commentFound = true; + } + } + break; + } + default: // Any other character + { + firstNonSpaceRead = true; + + if (!commentFound) { + line += char(data); + } + break; + } } } } - delete[] buf; if (f) { f.close(); @@ -202,6 +243,25 @@ String rulesProcessingFile(const String& fileName, String& event) { return ""; } +void replace_EventValueN_Argv(String& line, const String& argString, unsigned int argc) +{ + String eventvalue; + + eventvalue.reserve(16); + eventvalue = F("%eventvalue"); + eventvalue += argc; + eventvalue += '%'; + String tmpParam; + + if (GetArgv(argString.c_str(), tmpParam, argc)) { + if (argc == 1) { + // For compatibility reasons also replace %eventvalue% + line.replace(F("%eventvalue%"), tmpParam); + } + line.replace(eventvalue, tmpParam); + } +} + void parseCompleteNonCommentLine(String& line, String& event, String& log, bool& match, bool& codeBlock, bool& isCommand, bool condition[], bool ifBranche[], @@ -220,7 +280,8 @@ void parseCompleteNonCommentLine(String& line, String& event, String& log, // "on" (no codeBlock) // This to avoid waisting CPU time... - if (match && !fakeIfBlock) { + // Only process the %eventvalueX% replacements if there is any present. + if (match && !fakeIfBlock && (line.indexOf(F("%eventvalue")) != -1)) { // substitution of %eventvalue% is made here so it can be used on if // statement too if (event.charAt(0) == '!') { @@ -231,38 +292,12 @@ void parseCompleteNonCommentLine(String& line, String& event, String& log, int equalsPos = event.indexOf("="); if (equalsPos > 0) { - String tmpString = event.substring(equalsPos + 1); + // Replace %eventvalueX% with the actual value of the event. + String argString = event.substring(equalsPos + 1); - // line.replace(F("%eventvalue%"), tmpString); // substitute - // %eventvalue% with the actual value from the event - String tmpParam; - - if (GetArgv(tmpString.c_str(), tmpParam, 1)) { - line.replace(F("%eventvalue%"), - tmpParam); // for compatibility issues - line.replace(F("%eventvalue1%"), - tmpParam); // substitute %eventvalue1% in actions with - // the actual value from the event + for (unsigned int argc = 1; argc <= 4; ++argc) { + replace_EventValueN_Argv(line, argString, argc); } - - if (GetArgv(tmpString.c_str(), tmpParam, 2)) { - line.replace(F("%eventvalue2%"), - tmpParam); // substitute %eventvalue2% in actions with - } - - // the actual value from the event - if (GetArgv(tmpString.c_str(), tmpParam, 3)) { - line.replace(F("%eventvalue3%"), - tmpParam); // substitute %eventvalue3% in actions with - } - - // the actual value from the event - if (GetArgv(tmpString.c_str(), tmpParam, 4)) { - line.replace(F("%eventvalue4%"), - tmpParam); // substitute %eventvalue4% in actions with - } - - // the actual value from the event } } } @@ -853,7 +888,7 @@ void createRuleEvents(struct EventStruct *event) { for (byte varNr = 0; varNr < Device[DeviceIndex].ValueCount; varNr++) { String eventString; eventString.reserve(32); // Enough for most use cases, prevent lots of memory allocations. - eventString = getTaskDeviceName(event->TaskIndex); + eventString = getTaskDeviceName(event->TaskIndex); eventString += F("#"); eventString += ExtraTaskSettings.TaskDeviceValueNames[varNr]; eventString += F("="); @@ -865,7 +900,7 @@ void createRuleEvents(struct EventStruct *event) { break; case SENSOR_TYPE_STRING: - // FIXME TD-er: What to add here? length of string? + // FIXME TD-er: What to add here? length of string? break; default: diff --git a/src/ESPEasyStorage.ino b/src/ESPEasyStorage.ino index c845f3624..4ad2f94af 100644 --- a/src/ESPEasyStorage.ino +++ b/src/ESPEasyStorage.ino @@ -788,6 +788,7 @@ String SaveToFile(char *fname, int index, byte *memAddress, int datasize) fs::File f = tryOpenFile(fname, "r+"); if (f) { + Cache.clearAllCaches(); SPIFFS_CHECK(f, fname); SPIFFS_CHECK(f.seek(index, fs::SeekSet), fname); byte *pointerToByteToSave = memAddress; diff --git a/src/Misc.ino b/src/Misc.ino index e73495c28..42ccccd52 100644 --- a/src/Misc.ino +++ b/src/Misc.ino @@ -460,9 +460,9 @@ boolean remoteConfig(struct EventStruct *event, const String& string) if ((configTaskName.length() == 0) || (configCommand.length() == 0)) { return success; // TD-er: Should this be return false? } - int8_t index = getTaskIndexByName(configTaskName); + byte index = findTaskIndexByName(configTaskName); - if (index != -1) + if (index != TASKS_MAX) { event->TaskIndex = index; success = PluginCall(PLUGIN_SET_CONFIG, event, configCommand); @@ -472,21 +472,6 @@ boolean remoteConfig(struct EventStruct *event, const String& string) return success; } -int8_t getTaskIndexByName(const String& TaskNameSearch) -{ - for (byte x = 0; x < TASKS_MAX; x++) - { - LoadTaskSettings(x); - String TaskName = getTaskDeviceName(x); - - if ((TaskName.length() != 0) && (TaskNameSearch.equalsIgnoreCase(TaskName))) - { - return x; - } - } - return -1; -} - /*********************************************************************************************\ Device GPIO name functions to share flash strings \*********************************************************************************************/ @@ -828,6 +813,7 @@ String checkTaskSettings(byte taskIndex) { return F("Warning: Task Device Name is empty. It is adviced to give tasks an unique name"); } } + // Do not use the cached function findTaskIndexByName since that one does rely on the fact names should be unique. for (int i = 0; i < TASKS_MAX; ++i) { if (i != taskIndex && Settings.TaskDeviceEnabled[i]) { LoadTaskSettings(i); @@ -1468,7 +1454,7 @@ String parseTemplate(String& tmpString, byte lineSize) while (findNextDevValNameInString(tmpString, startpos, endpos, deviceName, valueName, format)) { // First copy all upto the start of the [...#...] part to be replaced. newString += tmpString.substring(lastStartpos, startpos); - + if (deviceName.equalsIgnoreCase(F("Plugin"))) { // Handle a plugin request. @@ -1483,18 +1469,29 @@ String parseTemplate(String& tmpString, byte lineSize) if (PluginCall(PLUGIN_REQUEST, 0, command)) { + // Do not call transformValue here. + // The "format" is not empty so must not call the formatter function. newString += command; } } - else if (deviceName.equalsIgnoreCase(F("Var"))) + else if (deviceName.equalsIgnoreCase(F("Var")) || deviceName.equalsIgnoreCase(F("Int"))) { - // Address an internal variable + // Address an internal variable either as float or as int // For example: Let,10,[VAR#9] int varNum; if (validIntFromString(valueName, varNum)) { if ((varNum > 0) && (varNum <= CUSTOM_VARS_MAX)) { - newString += String(customFloatVar[varNum - 1]); + unsigned char nr_decimals = 2; + if (deviceName.equalsIgnoreCase(F("Int"))) { + nr_decimals = 0; + } else if (format.length() != 0) + { + // There is some formatting here, so do not throw away decimals + nr_decimals = 6; + } + String value = String(customFloatVar[varNum - 1], nr_decimals); + transformValue(newString, lineSize, value, format, tmpString); } } } @@ -1506,7 +1503,7 @@ String parseTemplate(String& tmpString, byte lineSize) // For example: "[#getLevel]" byte taskIndex = findTaskIndexByName(deviceName); - if (taskIndex != TASKS_MAX) { + if (taskIndex != TASKS_MAX && Settings.TaskDeviceEnabled[taskIndex]) { byte valueNr = findDeviceValueIndexByName(valueName, taskIndex); if (valueNr != VARS_PER_TASK) { @@ -1526,11 +1523,12 @@ String parseTemplate(String& tmpString, byte lineSize) if (PluginCall(PLUGIN_GET_CONFIG, &TempEvent, tmpName)) { - newString += tmpName; + transformValue(newString, lineSize, tmpName, format, tmpString); } } } } + // Conversion is done (or impossible) for the found "[...#...]" // Continue with the next one. @@ -1562,24 +1560,26 @@ String parseTemplate(String& tmpString, byte lineSize) return newString; } -// Find the first enabled task with given name +// Find the first task with given name // Return TASKS_MAX when not found, else return taskIndex byte findTaskIndexByName(const String& deviceName) { - // FIXME TD-er: Should cache this. + // cache this, since LoadTaskSettings does take some time. + auto result = Cache.taskIndexName.find(deviceName); + if (result != Cache.taskIndexName.end()) { + return result->second; + } for (byte taskIndex = 0; taskIndex < TASKS_MAX; taskIndex++) { - if (Settings.TaskDeviceEnabled[taskIndex]) - { - LoadTaskSettings(taskIndex); - String taskDeviceName = getTaskDeviceName(taskIndex); + LoadTaskSettings(taskIndex); + String taskDeviceName = getTaskDeviceName(taskIndex); - if (taskDeviceName.length() != 0) + if (taskDeviceName.length() != 0) + { + if (deviceName.equalsIgnoreCase(taskDeviceName)) { - if (deviceName.equalsIgnoreCase(taskDeviceName)) - { - return taskIndex; - } + Cache.taskIndexName[deviceName] = taskIndex; + return taskIndex; } } } @@ -1590,13 +1590,18 @@ byte findTaskIndexByName(const String& deviceName) // Return VARS_PER_TASK if none found. byte findDeviceValueIndexByName(const String& valueName, byte taskIndex) { - // FIXME TD-er: Should cache this. + // cache this, since LoadTaskSettings does take some time. + auto result = Cache.taskIndexValueName.find(valueName); + if (result != Cache.taskIndexValueName.end()) { + return result->second; + } LoadTaskSettings(taskIndex); // Probably already loaded, but just to be sure for (byte valueNr = 0; valueNr < VARS_PER_TASK; valueNr++) { if (valueName.equalsIgnoreCase(ExtraTaskSettings.TaskDeviceValueNames[valueNr])) { + Cache.taskIndexValueName[valueName] = valueNr; return valueNr; } } @@ -1677,22 +1682,34 @@ void transformValue( // valueJust="justification" if (valueFormat.length() > 0) //do the checks only if a Format is defined to optimize loop { - const int val = value == "0" ? 0 : 1; //to be used for GPIO status (0 or 1) - const float valFloat = value.toFloat(); - + int logicVal = 0; + float valFloat = 0.0; + if (validFloatFromString(value, valFloat)) + { + //to be used for binary values (0 or 1) + logicVal = static_cast(roundf(valFloat)) == 0 ? 0 : 1; + } else { + if (value.length() > 0) { + logicVal = 1; + } + } String tempValueFormat = valueFormat; - int tempValueFormatLength = tempValueFormat.length(); - const int invertedIndex = tempValueFormat.indexOf('!'); - const int inverted = invertedIndex >= 0 ? 1 : 0; - if (inverted != 0) - tempValueFormat.remove(invertedIndex,1); + { + const int invertedIndex = tempValueFormat.indexOf('!'); + if (invertedIndex != -1) { + // We must invert the value. + logicVal = (logicVal == 0) ? 1 : 0; + // Remove the '!' from the string. + tempValueFormat.remove(invertedIndex,1); + } + } const int rightJustifyIndex = tempValueFormat.indexOf('R'); const bool rightJustify = rightJustifyIndex >= 0 ? 1 : 0; if (rightJustify) tempValueFormat.remove(rightJustifyIndex,1); - tempValueFormatLength = tempValueFormat.length(); //needed because could have been changed after '!' and 'R' removal + const int tempValueFormatLength = tempValueFormat.length(); //Check Transformation syntax if (tempValueFormatLength > 0) @@ -1702,40 +1719,40 @@ void transformValue( case 'V': //value = value without transformations break; case 'O': - value = val == inverted ? F("OFF") : F(" ON"); //(equivalent to XOR operator) + value = logicVal == 0 ? F("OFF") : F(" ON"); //(equivalent to XOR operator) break; case 'C': - value = val == inverted ? F("CLOSE") : F(" OPEN"); + value = logicVal == 0 ? F("CLOSE") : F(" OPEN"); break; case 'M': - value = val == inverted ? F("AUTO") : F(" MAN"); + value = logicVal == 0 ? F("AUTO") : F(" MAN"); break; case 'm': - value = val == inverted ? F("A") : F("M"); + value = logicVal == 0 ? F("A") : F("M"); break; case 'H': - value = val == inverted ? F("COLD") : F(" HOT"); + value = logicVal == 0 ? F("COLD") : F(" HOT"); break; case 'U': - value = val == inverted ? F("DOWN") : F(" UP"); + value = logicVal == 0 ? F("DOWN") : F(" UP"); break; case 'u': - value = val == inverted ? F("D") : F("U"); + value = logicVal == 0 ? F("D") : F("U"); break; case 'Y': - value = val == inverted ? F(" NO") : F("YES"); + value = logicVal == 0 ? F(" NO") : F("YES"); break; case 'y': - value = val == inverted ? F("N") : F("Y"); + value = logicVal == 0 ? F("N") : F("Y"); break; case 'X': - value = val == inverted ? F("O") : F("X"); + value = logicVal == 0 ? F("O") : F("X"); break; case 'I': - value = val == inverted ? F("OUT") : F(" IN"); + value = logicVal == 0 ? F("OUT") : F(" IN"); break; case 'Z' :// return "0" or "1" - value = val == inverted ? "0" : "1"; + value = logicVal == 0 ? "0" : "1"; break; case 'D' ://Dx.y min 'x' digits zero filled & 'y' decimal fixed digits { @@ -1770,10 +1787,13 @@ void transformValue( break; } value = toString(valFloat,y); - int indexDot; - indexDot = value.indexOf('.') > 0 ? value.indexOf('.') : value.length(); - for (byte f = 0; f < (x - indexDot); f++) + int indexDot = value.indexOf('.'); + if (indexDot == -1) { + indexDot = value.length(); + } + for (byte f = 0; f < (x - indexDot); f++) { value = "0" + value; + } break; } case 'F' :// FLOOR (round down) @@ -1874,7 +1894,7 @@ void transformValue( } //end of changes by giig1967g - 2018-04-18 - newString += String(value); + newString += value; { #ifndef BUILD_NO_DEBUG if (loglevelActiveFor(LOG_LEVEL_DEBUG_DEV)) {