[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.
This commit is contained in:
Gijs Noorlander
2019-09-25 00:44:44 +02:00
parent 7f62bea00c
commit efbb4e13ac
5 changed files with 189 additions and 125 deletions
+17 -11
View File
@@ -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:
+2
View File
@@ -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<int, TimingStats> pluginStats;
std::map<int, TimingStats> controllerStats;
+89 -54
View File
@@ -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<byte> 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<char>(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:
+1
View File
@@ -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;
+80 -60
View File
@@ -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: "[<taskname>#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<int>(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)) {