Tasmota/tasmota/tasmota_xdrv_driver/xdrv_10_rules.ino

2513 lines
90 KiB
C++

/*
xdrv_10_rules.ino - rule support for Tasmota
Copyright (C) 2021 ESP Easy Group and Theo Arends
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifdef USE_RULES
#ifndef USE_SCRIPT
/*********************************************************************************************\
* Rules based heavily on ESP Easy implementation
*
* Inspiration: https://github.com/letscontrolit/ESPEasy
*
* Add rules using the following, case insensitive, format:
* on <trigger1> do <commands> endon on <trigger2> do <commands> endon ..
*
* Examples:
* on System#Boot do Color 001000 endon
* on INA219#Current>0.100 do Dimmer 10 endon
* on INA219#Current>0.100 do Backlog Dimmer 10;Color 10,0,0 endon
* on INA219#Current>0.100 do Backlog Dimmer 10;Color 100000 endon on System#Boot do color 001000 endon
* on ds18b20#temperature>23 do power off endon on ds18b20#temperature<22 do power on endon
* on mqtt#connected do color 000010 endon
* on mqtt#disconnected do color 00100C endon
* on time#initialized do color 001000 endon
* on time#initialized>120 do color 001000 endon
* on time#set do color 001008 endon
* on clock#timer=3 do color 080800 endon
* on rules#timer=1 do color 080800 endon
* on mqtt#connected do color 000010 endon on mqtt#disconnected do color 001010 endon on time#initialized do color 001000 endon on time#set do backlog color 000810;ruletimer1 10 endon on rules#timer=1 do color 080800 endon
* on event#anyname do color 100000 endon
* on event#anyname do color %value% endon
* on power1#state=1 do color 001000 endon
* on button1#state do publish cmnd/ring2/power %value% endon on button2#state do publish cmnd/strip1/power %value% endon
* on switch1#state do power2 %value% endon
* on analog#a0div10 do publish cmnd/ring2/dimmer %value% endon
* on loadavg<50 do power 2 endon
* on Time#Initialized do Backlog var1 0;event checktime=%time% endon on event#checktime>%timer1% do var1 1 endon on event#checktime>=%timer2% do var1 0 endon * on event#checktime do Power1 %var1% endon
*
* Notes:
* Spaces after <on>, around <do> and before <endon> are mandatory
* System#Boot is initiated after MQTT is connected due to command handling preparation
* Control rule triggering with command:
* Rule 0 = Rules disabled (Off)
* Rule 1 = Rules enabled (On)
* Rule 2 = Toggle rules state
* Rule 4 = Perform commands as long as trigger is met (Once OFF)
* Rule 5 = Perform commands once until trigger is not met (Once ON)
* Rule 6 = Toggle Once state
* Execute an event like:
* Event anyname=001000
* Set a RuleTimer to 100 seconds like:
* RuleTimer2 100
\*********************************************************************************************/
#define XDRV_10 10
#ifndef RULE_MAX_EVENTSZ
#define RULE_MAX_EVENTSZ 100
#endif
#ifndef RULE_MAX_MQTT_EVENTSZ
#define RULE_MAX_MQTT_EVENTSZ 256
#endif
//#define DEBUG_RULES
#include <unishox.h>
#define D_CMND_RULE "Rule"
#define D_CMND_RULETIMER "RuleTimer"
#define D_CMND_EVENT "Event"
#define D_CMND_VAR "Var"
#define D_CMND_MEM "Mem"
#define D_CMND_ADD "Add"
#define D_CMND_SUB "Sub"
#define D_CMND_MULT "Mult"
#define D_CMND_SCALE "Scale"
#define D_CMND_CALC_RESOLUTION "CalcRes"
#define D_CMND_SUBSCRIBE "Subscribe"
#define D_CMND_UNSUBSCRIBE "Unsubscribe"
#define D_CMND_IF "If"
#define D_JSON_INITIATED "Initiated"
#define COMPARE_OPERATOR_NONE -1
#define COMPARE_OPERATOR_EQUAL 0
#define COMPARE_OPERATOR_BIGGER 1
#define COMPARE_OPERATOR_SMALLER 2
#define COMPARE_OPERATOR_EXACT_DIVISION 3
#define COMPARE_OPERATOR_NUMBER_EQUAL 4
#define COMPARE_OPERATOR_NOT_EQUAL 5
#define COMPARE_OPERATOR_BIGGER_EQUAL 6
#define COMPARE_OPERATOR_SMALLER_EQUAL 7
#define COMPARE_OPERATOR_STRING_ENDS_WITH 8
#define COMPARE_OPERATOR_STRING_STARTS_WITH 9
#define COMPARE_OPERATOR_STRING_CONTAINS 10
#define COMPARE_OPERATOR_STRING_NOT_EQUAL 11
#define COMPARE_OPERATOR_STRING_NOT_CONTAINS 12
#define MAXIMUM_COMPARE_OPERATOR COMPARE_OPERATOR_STRING_NOT_CONTAINS
const char kCompareOperators[] PROGMEM = "=\0>\0<\0|\0==!=>=<=$>$<$|$!$^";
#ifdef USE_EXPRESSION
#include <LinkedList.h> // Import LinkedList library
const char kExpressionOperators[] PROGMEM = "+-*/%^\0";
#define EXPRESSION_OPERATOR_ADD 0
#define EXPRESSION_OPERATOR_SUBTRACT 1
#define EXPRESSION_OPERATOR_MULTIPLY 2
#define EXPRESSION_OPERATOR_DIVIDEDBY 3
#define EXPRESSION_OPERATOR_MODULO 4
#define EXPRESSION_OPERATOR_POWER 5
const uint8_t kExpressionOperatorsPriorities[] PROGMEM = {1, 1, 2, 2, 3, 4};
#define MAX_EXPRESSION_OPERATOR_PRIORITY 4
#define LOGIC_OPERATOR_AND 1
#define LOGIC_OPERATOR_OR 2
#define IF_BLOCK_INVALID -1
#define IF_BLOCK_ANY 0
#define IF_BLOCK_ELSEIF 1
#define IF_BLOCK_ELSE 2
#define IF_BLOCK_ENDIF 3
#endif // USE_EXPRESSION
// Define to indicate that rules are always enabled
#ifdef USE_BERRY
#define BERRY_RULES 1
#else
#define BERRY_RULES 0
#endif
const char kRulesCommands[] PROGMEM = "|" // No prefix
D_CMND_RULE "|" D_CMND_RULETIMER "|" D_CMND_EVENT "|" D_CMND_VAR "|" D_CMND_MEM "|"
D_CMND_ADD "|" D_CMND_SUB "|" D_CMND_MULT "|" D_CMND_SCALE "|" D_CMND_CALC_RESOLUTION
#ifdef SUPPORT_MQTT_EVENT
"|" D_CMND_SUBSCRIBE "|" D_CMND_UNSUBSCRIBE
#endif
#ifdef SUPPORT_IF_STATEMENT
"|" D_CMND_IF
#endif
;
void (* const RulesCommand[])(void) PROGMEM = {
&CmndRule, &CmndRuleTimer, &CmndEvent, &CmndVariable, &CmndMemory,
&CmndAddition, &CmndSubtract, &CmndMultiply, &CmndScale, &CmndCalcResolution
#ifdef SUPPORT_MQTT_EVENT
, &CmndSubscribe, &CmndUnsubscribe
#endif
#ifdef SUPPORT_IF_STATEMENT
, &CmndIf
#endif
};
#ifdef SUPPORT_MQTT_EVENT
#include <LinkedList.h> // Import LinkedList library
typedef struct {
String Event;
String Topic;
String Key;
} MQTT_Subscription;
LinkedList<MQTT_Subscription> subscriptions;
#endif // SUPPORT_MQTT_EVENT
struct RULES {
String event_value;
unsigned long timer[MAX_RULE_TIMERS] = { 0 };
uint32_t triggers[MAX_RULE_SETS] = { 0 };
uint8_t trigger_count[MAX_RULE_SETS] = { 0 };
long new_power = -1;
long old_power = -1;
long old_dimm = -1;
uint16_t last_minute = 60;
uint16_t vars_event = 0; // Bitmask supporting MAX_RULE_VARS bits
uint16_t mems_event = 0; // Bitmask supporting MAX_RULE_MEMS bits
bool teleperiod = false;
bool busy = false;
bool no_execute = false; // Don't actually execute rule commands
char event_data[RULE_MAX_EVENTSZ];
} Rules;
char rules_vars[MAX_RULE_VARS][33] = {{ 0 }};
#if (MAX_RULE_VARS>16)
#error MAX_RULE_VARS is bigger than 16
#endif
#if (MAX_RULE_MEMS>16)
#error MAX_RULE_MEMS is bigger than 16
#endif
/*******************************************************************************************/
/*
* Add Unishox compression to Rules
*
* New compression for Rules, depends on SetOption93
*
* To avoid memory corruption when downgrading, the format is as follows:
* - If `SetOption93 0`
* Rule[x][] = 511 char max NULL terminated string (512 with trailing NULL)
* Rule[x][0] = 0 if the Rule<x> is empty
* New: in case the string is empty we also enforce:
* Rule[x][1] = 0 (i.e. we have two conseutive NULLs)
*
* - If `SetOption93 1`
* If the rule is smaller than 511, it is stored uncompressed. Rule[x][0] is not null.
* If the rule is empty, Rule[x][0] = 0 and Rule[x][1] = 0;
* If the rule is bigger than 511, it is stored compressed
* The first byte of each Rule is always NULL.
* Rule[x][0] = 0, if firmware is downgraded, the rule will be considered as empty
*
* The second byte contains the size of uncompressed rule in 8-bytes blocks (i.e. (len+7)/8 )
* Maximum rule size is 2KB (2048 bytes per rule), although there is little chances compression ratio will go down to 75%
* Rule[x][1] = size uncompressed in dwords. If zero, the rule is empty.
*
* The remaining bytes contain the compressed rule, NULL terminated
*/
/*******************************************************************************************/
#ifdef USE_UNISHOX_COMPRESSION
// Statically allocate one String per rule
String k_rules[MAX_RULE_SETS] = { String(), String(), String() }; // Strings are created empty
// Unishox compressor; // singleton
#endif // USE_UNISHOX_COMPRESSION
// Returns whether the rule is uncompressed, which means the first byte is not NULL
inline bool IsRuleUncompressed(uint32_t idx) {
#ifdef USE_UNISHOX_COMPRESSION
return Settings->rules[idx][0] ? true : false; // first byte not NULL, the rule is not empty and not compressed
#else
return true;
#endif
}
// Returns whether the rule is empty, which requires two consecutive NULL
inline bool IsRuleEmpty(uint32_t idx) {
#ifdef USE_UNISHOX_COMPRESSION
return (Settings->rules[idx][0] == 0) && (Settings->rules[idx][1] == 0) ? true : false;
#else
return (Settings->rules[idx][0] == 0) ? true : false;
#endif
}
// Returns the approximate (+3-0) length of the rule, not counting the trailing NULL
size_t GetRuleLen(uint32_t idx) {
// no need to use #ifdef USE_UNISHOX_COMPRESSION, the compiler will optimize since first test is always true
if (IsRuleUncompressed(idx)) {
return strlen(Settings->rules[idx]);
} else { // either empty or compressed
return Settings->rules[idx][1] * 8; // cheap calculation, but not byte accurate (may overshoot by 7)
}
}
// Returns the actual Flash storage for the Rule, including trailing NULL
size_t GetRuleLenStorage(uint32_t idx) {
#ifdef USE_UNISHOX_COMPRESSION
if (Settings->rules[idx][0] || !Settings->rules[idx][1]) { // if first byte is non-NULL it is uncompressed, if second byte is NULL, then it's either uncompressed or empty
return 1 + strlen(Settings->rules[idx]); // uncompressed or empty
} else {
return 2 + strlen(&Settings->rules[idx][1]); // skip first byte and get len of the compressed rule
}
#else
return 1 + strlen(Settings->rules[idx]);
#endif
}
#ifdef USE_UNISHOX_COMPRESSION
// internal function, do the actual decompression
void GetRule_decompress(String &rule, const char *rule_head) {
size_t buf_len = 1 + *rule_head * 8; // the first byte contains size of buffer for uncompressed rule / 8, buf_len may overshoot by 7
rule_head++; // advance to the actual compressed buffer
rule = Decompress(rule_head, buf_len);
}
#endif // USE_UNISHOX_COMPRESSION
//
// Read rule in memory, uncompress if needed
//
// Returns: String() object containing a copy of the rule (rule processing is destructive and will change the String)
String GetRule(uint32_t idx) {
if (IsRuleUncompressed(idx)) {
return String(Settings->rules[idx]);
} else {
#ifdef USE_UNISHOX_COMPRESSION // we still do #ifdef to make sure we don't link unnecessary code
String rule("");
if (Settings->rules[idx][1] == 0) { return rule; } // the rule is empty
// If the cache is empty, we need to decompress from Settings
if (0 == k_rules[idx].length() ) {
GetRule_decompress(rule, &Settings->rules[idx][1]);
if (!Settings->flag4.compress_rules_cpu) {
k_rules[idx] = rule; // keep a copy for next time
}
} else {
// we have a valid copy
rule = k_rules[idx];
}
return rule;
#endif
}
return ""; // Fix GCC10 warning
}
#ifdef USE_UNISHOX_COMPRESSION
// internal function, comrpess rule and store a cached version uncompressed (except if SetOption94 1)
// If out == nullptr, we are in dry-run mode, so don't keep rule in cache
int32_t SetRule_compress(uint32_t idx, const char *in, size_t in_len, char *out, size_t out_len) {
int32_t len_compressed;
len_compressed = compressor.unishox_compress(in, in_len, out, out_len);
if (len_compressed >= 0) { // negative means compression failed because of buffer too small, we leave the rule untouched
// check if we need to store in cache
k_rules[idx] = (const char*) nullptr; // Assign the String to nullptr, clears previous string and disallocate internal buffers of String object
if ((!Settings->flag4.compress_rules_cpu) && out) { // if out == nullptr, don't store cache
// keep copy in cache
k_rules[idx] = in;
}
}
return len_compressed;
}
#endif // USE_UNISHOX_COMPRESSION
// Returns:
// >= 0 : the actual stored size
// <0 : not enough space
int32_t SetRule(uint32_t idx, const char *content, bool append = false) {
if (nullptr == content) { content = ""; } // if nullptr, use empty string
size_t len_in = strlen(content);
bool needsCompress = false;
size_t offset = 0;
if (len_in >= MAX_RULE_SIZE) { // if input is more than 512, it will not fit uncompressed
needsCompress = true;
}
if (append) {
if (IsRuleUncompressed(idx) || IsRuleEmpty(idx)) { // if already uncompressed (so below 512) and append mode, check if it still fits uncompressed
offset = strlen(Settings->rules[idx]);
if (len_in + offset >= MAX_RULE_SIZE) {
needsCompress = true;
}
} else {
needsCompress = true; // we append to a non-empty compressed rule, so it won't fit uncompressed
}
}
if (!needsCompress) { // the rule fits uncompressed, so just copy it
// strlcpy(Settings->rules[idx] + offset, content, sizeof(Settings->rules[idx]));
strlcpy(Settings->rules[idx] + offset, content, sizeof(Settings->rules[idx]) - offset);
if (0 == Settings->rules[idx][0]) {
Settings->rules[idx][1] = 0;
}
#ifdef USE_UNISHOX_COMPRESSION
if (0 != len_in + offset) {
// do a dry-run compression to display how much it would be compressed
int32_t len_compressed, len_uncompressed;
len_uncompressed = strlen(Settings->rules[idx]);
len_compressed = compressor.unishox_compress(Settings->rules[idx], len_uncompressed, nullptr /* dry-run */, MAX_RULE_SIZE + 8);
AddLog(LOG_LEVEL_INFO, PSTR("RUL: Stored uncompressed, would compress from %d to %d (-%d%%)"), len_uncompressed, len_compressed, 100 - changeUIntScale(len_compressed, 0, len_uncompressed, 0, 100));
}
#endif // USE_UNISHOX_COMPRESSION
return len_in + offset;
} else {
#ifdef USE_UNISHOX_COMPRESSION
int32_t len_compressed;
// allocate temp buffer so we don't nuke the rule if it's too big to fit
char *buf_out = (char*) malloc(MAX_RULE_SIZE + 8); // take some margin
if (!buf_out) { return -1; } // fail if couldn't allocate
// compress
if (append) {
String content_append = GetRule(idx); // get original Rule and decompress it if needed
content_append += content; // concat new content
len_in = content_append.length(); // adjust length
len_compressed = SetRule_compress(idx, content_append.c_str(), len_in, buf_out, MAX_RULE_SIZE + 8);
} else {
len_compressed = SetRule_compress(idx, content, len_in, buf_out, MAX_RULE_SIZE + 8);
}
if ((len_compressed >= 0) && (len_compressed < MAX_RULE_SIZE - 2)) {
// size is ok, copy to Settings
Settings->rules[idx][0] = 0; // clear first byte to mark as compressed
Settings->rules[idx][1] = (len_in + 7) / 8; // store original length in first bytes (4 bytes chuks)
memcpy(&Settings->rules[idx][2], buf_out, len_compressed);
Settings->rules[idx][len_compressed + 2] = 0; // add NULL termination
AddLog(LOG_LEVEL_INFO, PSTR("RUL: Compressed from %d to %d (-%d%%)"), len_in, len_compressed, 100 - changeUIntScale(len_compressed, 0, len_in, 0, 100));
// AddLog(LOG_LEVEL_INFO, PSTR("RUL: First bytes: %02X%02X%02X%02X"), Settings->rules[idx][0], Settings->rules[idx][1], Settings->rules[idx][2], Settings->rules[idx][3]);
// AddLog(LOG_LEVEL_INFO, PSTR("RUL: GetRuleLenStorage = %d"), GetRuleLenStorage(idx));
} else {
len_compressed = -1; // failed
// clear rule cache, so it will be reloaded from Settings
k_rules[idx] = (const char *) nullptr;
}
free(buf_out);
return len_compressed;
#else // USE_UNISHOX_COMPRESSION
return -1; // the rule does not fit and we can't compress
#endif // USE_UNISHOX_COMPRESSION
}
}
/*******************************************************************************************/
bool RulesRuleMatch(uint8_t rule_set, String &event, String &rule, bool stop_all_rules)
{
// event = {"INA219":{"Voltage":4.494,"Current":0.020,"Power":0.089}}
// event = {"System":{"Boot":1}}
// rule = "INA219#CURRENT>0.100"
bool match = false;
char stemp[10];
// Step1: Analyse rule
String rule_expr = rule; // "TELE-INA219#CURRENT>0.100"
if (Rules.teleperiod) {
int ppos = rule_expr.indexOf(F("TELE-")); // "TELE-INA219#CURRENT>0.100" or "INA219#CURRENT>0.100"
if (ppos == -1) { return false; } // No pre-amble in rule
rule_expr = rule.substring(5); // "INA219#CURRENT>0.100" or "SYSTEM#BOOT"
}
String rule_name, rule_param;
int8_t compareOperator = parseCompareExpression(rule_expr, rule_name, rule_param); // Parse the compare expression.Return operator and the left, right part of expression
// rule_name = "INA219#CURRENT"
// rule_param = "0.100" or "%VAR1%"
#ifdef DEBUG_RULES
AddLog(LOG_LEVEL_DEBUG, PSTR("RUL-RM1: Teleperiod %d, Expr %s, Name %s, Param %s"), Rules.teleperiod, rule_expr.c_str(), rule_name.c_str(), rule_param.c_str());
#endif
char rule_svalue[80] = { 0 };
float rule_value = 0;
if (compareOperator != COMPARE_OPERATOR_NONE) {
for (uint32_t i = 0; i < MAX_RULE_VARS; i++) {
snprintf_P(stemp, sizeof(stemp), PSTR("%%VAR%d%%"), i +1);
if (rule_param.startsWith(stemp)) {
rule_param = rules_vars[i];
break;
}
}
for (uint32_t i = 0; i < MAX_RULE_MEMS; i++) {
snprintf_P(stemp, sizeof(stemp), PSTR("%%MEM%d%%"), i +1);
if (rule_param.startsWith(stemp)) {
rule_param = SettingsText(SET_MEM1 + i);
break;
}
}
if (rule_param.startsWith(F("%TIME%"))) {
rule_param = String(MinutesPastMidnight());
}
if (rule_param.startsWith(F("%UPTIME%"))) {
rule_param = String(MinutesUptime());
}
if (rule_param.startsWith(F("%TIMESTAMP%"))) {
rule_param = GetDateAndTime(DT_LOCAL).c_str();
}
#if defined(USE_TIMERS)
if (rule_param.startsWith(F("%TIMER"))) {
uint32_t index = rule_param.substring(6).toInt();
if ((index > 0) && (index <= MAX_TIMERS)) {
snprintf_P(stemp, sizeof(stemp), PSTR("%%TIMER%d%%"), index);
if (rule_param.startsWith(stemp)) {
rule_param = String(TimerGetTimeOfDay(index -1));
}
}
}
#if defined(USE_SUNRISE)
if (rule_param.startsWith(F("%SUNRISE%"))) {
rule_param = String(SunMinutes(0));
}
if (rule_param.startsWith(F("%SUNSET%"))) {
rule_param = String(SunMinutes(1));
}
#endif // USE_SUNRISE
#endif // USE_TIMERS
#if defined(USE_LIGHT)
char scolor[LIGHT_COLOR_SIZE];
if (rule_param.startsWith(F("%COLOR%"))) {
rule_param = LightGetColor(scolor);
}
#endif
// #ifdef USE_ZIGBEE
// if (rule_param.startsWith(F("%ZBDEVICE%"))) {
// snprintf_P(stemp, sizeof(stemp), PSTR("0x%04X"), Z_GetLastDevice());
// rule_param = String(stemp);
// }
// if (rule_param.startsWith(F("%ZBGROUP%"))) {
// rule_param = String(Z_GetLastGroup());
// }
// if (rule_param.startsWith(F("%ZBCLUSTER%"))) {
// rule_param = String(Z_GetLastCluster());
// }
// if (rule_param.startsWith(F("%ZBENDPOINT%"))) {
// rule_param = String(Z_GetLastEndpoint());
// }
// #endif
rule_param.toUpperCase();
strlcpy(rule_svalue, rule_param.c_str(), sizeof(rule_svalue));
int temp_value = GetStateNumber(rule_svalue);
if (temp_value > -1) {
rule_value = temp_value;
} else {
rule_value = CharToFloat((char*)rule_svalue); // 0.1 - This saves 9k code over toFLoat()!
}
}
// Step2: Search rule_name
int pos;
int rule_name_idx = 0;
if ((pos = rule_name.indexOf(F("["))) > 0) { // "SUBTYPE1#CURRENT[1]"
rule_name_idx = rule_name.substring(pos +1).toInt();
if ((rule_name_idx < 1) || (rule_name_idx > 6)) { // Allow indexes 1 to 6
rule_name_idx = 1;
}
rule_name = rule_name.substring(0, pos); // "SUBTYPE1#CURRENT"
}
String buf = event; // Copy the string into a new buffer that will be modified
// Do not do below replace as it will replace escaped quote too.
// buf.replace("\\"," "); // "Disable" any escaped control character
//AddLog(LOG_LEVEL_DEBUG, PSTR("RUL-RM2: RulesRuleMatch |%s|"), buf.c_str());
JsonParser parser((char*)buf.c_str());
JsonParserObject obj = parser.getRootObject();
if (!obj) {
// AddLog(LOG_LEVEL_DEBUG, PSTR("RUL: Event too long (%d)"), event.length());
AddLog(LOG_LEVEL_DEBUG, PSTR("RUL: No valid JSON (%s)"), buf.c_str());
return false; // No valid JSON data
}
String subtype;
uint32_t i = 0;
while ((pos = rule_name.indexOf(F("#"))) > 0) { // "SUBTYPE1#SUBTYPE2#CURRENT"
subtype = rule_name.substring(0, pos);
obj = obj[subtype.c_str()].getObject();
if (!obj) { return false; } // not found
rule_name = rule_name.substring(pos +1);
if (i++ > 10) { return false; } // Abandon possible loop
yield();
}
JsonParserToken val = obj[rule_name.c_str()];
if (!val) { return false; } // last level not found
const char* str_value;
if (rule_name_idx) {
if (val.isArray()) {
str_value = (val.getArray())[rule_name_idx -1].getStr();
} else {
str_value = val.getStr();
}
} else {
str_value = val.getStr(); // "CURRENT"
}
#ifdef DEBUG_RULES
AddLog(LOG_LEVEL_DEBUG, PSTR("RUL-RM3: Name %s, Value |%s|, TrigCnt %d, TrigSt %d, Source %s, Json |%s|"),
rule_name.c_str(), rule_svalue, Rules.trigger_count[rule_set], bitRead(Rules.triggers[rule_set],
Rules.trigger_count[rule_set]), event.c_str(), (str_value[0] != '\0') ? str_value : "none");
#endif
Rules.event_value = str_value; // Prepare %value%
// Step 3: Compare rule (value)
float value = 0;
if (str_value) {
value = CharToFloat((char*)str_value);
int int_value = int(value);
int int_rule_value = int(rule_value);
String str_str_value = String(str_value);
switch (compareOperator) {
case COMPARE_OPERATOR_EXACT_DIVISION:
match = (int_rule_value && (int_value % int_rule_value) == 0);
break;
case COMPARE_OPERATOR_EQUAL:
match = (!strcasecmp(str_value, rule_svalue)); // Compare strings - this also works for hexadecimals
break;
case COMPARE_OPERATOR_BIGGER:
match = (value > rule_value);
break;
case COMPARE_OPERATOR_SMALLER:
match = (value < rule_value);
break;
case COMPARE_OPERATOR_NUMBER_EQUAL:
match = (value == rule_value);
break;
case COMPARE_OPERATOR_NOT_EQUAL:
match = (value != rule_value);
break;
case COMPARE_OPERATOR_BIGGER_EQUAL:
match = (value >= rule_value);
break;
case COMPARE_OPERATOR_SMALLER_EQUAL:
match = (value <= rule_value);
break;
case COMPARE_OPERATOR_STRING_ENDS_WITH:
match = str_str_value.endsWith(rule_svalue);
break;
case COMPARE_OPERATOR_STRING_STARTS_WITH:
match = str_str_value.startsWith(rule_svalue);
break;
case COMPARE_OPERATOR_STRING_CONTAINS:
match = (str_str_value.indexOf(rule_svalue) >= 0);
break;
case COMPARE_OPERATOR_STRING_NOT_EQUAL:
match = (0!=strcasecmp(str_value, rule_svalue)); // Compare strings - this also works for hexadecimals
break;
case COMPARE_OPERATOR_STRING_NOT_CONTAINS:
match = (str_str_value.indexOf(rule_svalue) < 0);
break;
default:
match = true;
}
} else match = true;
if (stop_all_rules) { match = false; }
//AddLog(LOG_LEVEL_DEBUG, PSTR("RUL-RM4: Match 1 %d, Triggers %08X, TriggerCount %d"), match, Rules.triggers[rule_set], Rules.trigger_count[rule_set]);
if (bitRead(Settings->rule_once, rule_set)) {
if (match) { // Only allow match state changes
if (!bitRead(Rules.triggers[rule_set], Rules.trigger_count[rule_set])) {
bitSet(Rules.triggers[rule_set], Rules.trigger_count[rule_set]);
} else {
match = false;
}
} else {
bitClear(Rules.triggers[rule_set], Rules.trigger_count[rule_set]);
}
}
//AddLog(LOG_LEVEL_DEBUG, PSTR("RUL-RM5: Match 2 %d, Triggers %08X, TriggerCount %d"), match, Rules.triggers[rule_set], Rules.trigger_count[rule_set]);
return match;
}
/********************************************************************************************/
/*
* Parse a comparison expression.
* Get 3 parts - left expression, compare operator and right expression.
* Input:
* expr - A comparison expression like VAR1 >= MEM1 + 10
* leftExpr - Used to accept returned left parts of expression
* rightExpr - Used to accept returned right parts of expression
* Output:
* leftExpr - Left parts of expression
* rightExpr - Right parts of expression
* Return:
* compare operator
* COMPARE_OPERATOR_NONE - failed
*/
int8_t parseCompareExpression(String &expr, String &leftExpr, String &rightExpr)
{
char compare_operator[3];
int8_t compare = COMPARE_OPERATOR_NONE;
leftExpr = expr;
int position;
for (int8_t i = MAXIMUM_COMPARE_OPERATOR; i >= 0; i--) {
snprintf_P(compare_operator, sizeof(compare_operator), kCompareOperators + (i *2));
if ((position = expr.indexOf(compare_operator)) > 0) {
compare = i;
leftExpr = expr.substring(0, position);
leftExpr.trim();
rightExpr = expr.substring(position + strlen(compare_operator));
rightExpr.trim();
break;
}
}
return compare;
}
void RulesVarReplace(String &commands, const String &sfind, const String &replace)
{
// String ufind = sfind;
// ufind.toUpperCase();
// char *find = (char*)ufind.c_str();
char *find = (char*)sfind.c_str();
uint32_t flen = strlen(find);
String ucommand = commands;
ucommand.toUpperCase();
char *read_from = (char*)ucommand.c_str();
char *write_to = (char*)commands.c_str();
char *found_at;
while ((found_at = strstr(read_from, find)) != nullptr) {
write_to += (found_at - read_from);
memmove_P(write_to, find, flen); // Make variable Uppercase
write_to += flen;
read_from = found_at + flen;
}
commands.replace(find, replace);
}
/*******************************************************************************************/
bool RuleSetProcess(uint8_t rule_set, String &event_saved)
{
bool serviced = false;
char stemp[10];
delay(0); // Prohibit possible loop software watchdog
#ifdef DEBUG_RULES
AddLog(LOG_LEVEL_DEBUG, PSTR("RUL-RP1: Event = %s, Rule = %s"), event_saved.c_str(), Settings->rules[rule_set]);
#endif
String rules = GetRule(rule_set);
Rules.trigger_count[rule_set] = 0;
int plen = 0;
int plen2 = 0;
bool stop_all_rules = false;
while (true) {
rules = rules.substring(plen); // Select relative to last rule
rules.trim();
if (!rules.length()) { return serviced; } // No more rules
String rule = rules;
rule.toUpperCase(); // "ON INA219#CURRENT>0.100 DO BACKLOG DIMMER 10;COLOR 100000 ENDON"
if (!rule.startsWith(F("ON "))) { return serviced; } // Bad syntax - Nothing to start on
int pevt = rule.indexOf(F(" DO "));
if (pevt == -1) { return serviced; } // Bad syntax - Nothing to do
String event_trigger = rule.substring(3, pevt); // "INA219#CURRENT>0.100"
event_trigger.trim();
plen = rule.indexOf(F(" ENDON"));
plen2 = rule.indexOf(F(" BREAK"));
if ((plen == -1) && (plen2 == -1)) { return serviced; } // Bad syntax - No ENDON neither BREAK
if (plen == -1) { plen = 9999; }
if (plen2 == -1) { plen2 = 9999; }
plen = tmin(plen, plen2);
String commands = rules.substring(pevt +4, plen); // "Backlog Dimmer 10;Color 100000"
Rules.event_value = "";
String event = event_saved;
#ifdef DEBUG_RULES
AddLog(LOG_LEVEL_DEBUG, PSTR("RUL-RP2: Event |%s|, Rule |%s|, Command(s) |%s|"), event.c_str(), event_trigger.c_str(), commands.c_str());
#endif
if (!event_trigger.startsWith(F("FILE#")) && RulesRuleMatch(rule_set, event, event_trigger, stop_all_rules)) {
if (Rules.no_execute) return true;
if (plen == plen2) { stop_all_rules = true; } // If BREAK was used on a triggered rule, Stop execution of this rule set
commands.trim();
String ucommand = commands;
ucommand.toUpperCase();
// if (!ucommand.startsWith("BACKLOG")) { commands = "backlog " + commands; } // Always use Backlog to prevent power race exception
// Use Backlog with event to prevent rule event loop exception unless IF is used which uses an implicit backlog
if ((ucommand.indexOf(F("IF ")) == -1) &&
(ucommand.indexOf(F("EVENT ")) != -1) &&
(ucommand.indexOf(F("BACKLOG")) == -1)) {
commands = String(F("backlog ")) + commands;
}
RulesVarReplace(commands, F("%VALUE%"), Rules.event_value);
for (uint32_t i = 0; i < MAX_RULE_VARS; i++) {
snprintf_P(stemp, sizeof(stemp), PSTR("%%VAR%d%%"), i +1);
RulesVarReplace(commands, stemp, rules_vars[i]);
}
for (uint32_t i = 0; i < MAX_RULE_MEMS; i++) {
snprintf_P(stemp, sizeof(stemp), PSTR("%%MEM%d%%"), i +1);
RulesVarReplace(commands, stemp, SettingsText(SET_MEM1 +i));
}
for (uint32_t i = 0; i < TasmotaGlobal.devices_present; i++) {
snprintf_P(stemp, sizeof(stemp), PSTR("%%POWER%d%%"), i +1);
RulesVarReplace(commands, stemp, String(bitRead(TasmotaGlobal.power, i)));
}
for (uint32_t i = 0; i < MAX_SWITCHES_SET; i++) {
if (SwitchUsed(i)) {
snprintf_P(stemp, sizeof(stemp), PSTR("%%SWITCH%d%%"), i +1);
RulesVarReplace(commands, stemp, String(SwitchState(i)));
}
}
RulesVarReplace(commands, F("%TIME%"), String(MinutesPastMidnight()));
RulesVarReplace(commands, F("%UTCTIME%"), String(UtcTime()));
RulesVarReplace(commands, F("%UPTIME%"), String(MinutesUptime()));
RulesVarReplace(commands, F("%TIMESTAMP%"), GetDateAndTime(DT_LOCAL));
RulesVarReplace(commands, F("%TOPIC%"), TasmotaGlobal.mqtt_topic);
snprintf_P(stemp, sizeof(stemp), PSTR("%06X"), ESP_getChipId());
RulesVarReplace(commands, F("%DEVICEID%"), stemp);
RulesVarReplace(commands, F("%MACADDR%"), NetworkUniqueId());
#if defined(USE_TIMERS)
for (uint32_t i = 0; i < MAX_TIMERS; i++) {
snprintf_P(stemp, sizeof(stemp), PSTR("%%TIMER%d%%"), i +1);
RulesVarReplace(commands, stemp, String(TimerGetTimeOfDay(i)));
}
#if defined(USE_SUNRISE)
RulesVarReplace(commands, F("%SUNRISE%"), String(SunMinutes(0)));
RulesVarReplace(commands, F("%SUNSET%"), String(SunMinutes(1)));
#endif // USE_SUNRISE
#endif // USE_TIMERS
#if defined(USE_LIGHT)
char scolor[LIGHT_COLOR_SIZE];
RulesVarReplace(commands, F("%COLOR%"), LightGetColor(scolor));
#endif
#ifdef USE_ZIGBEE
snprintf_P(stemp, sizeof(stemp), PSTR("0x%04X"), Z_GetLastDevice());
RulesVarReplace(commands, F("%ZBDEVICE%"), String(stemp));
RulesVarReplace(commands, F("%ZBGROUP%"), String(Z_GetLastGroup()));
RulesVarReplace(commands, F("%ZBCLUSTER%"), String(Z_GetLastCluster()));
RulesVarReplace(commands, F("%ZBENDPOINT%"), String(Z_GetLastEndpoint()));
#endif
char command[commands.length() +1];
strlcpy(command, commands.c_str(), sizeof(command));
AddLog(LOG_LEVEL_INFO, PSTR("RUL: %s performs \"%s\""), event_trigger.c_str(), command);
// Response_P(S_JSON_COMMAND_SVALUE, D_CMND_RULE, D_JSON_INITIATED);
// MqttPublishPrefixTopic_P(RESULT_OR_STAT, PSTR(D_CMND_RULE));
#ifdef SUPPORT_IF_STATEMENT
char *pCmd = command;
RulesPreprocessCommand(pCmd); // Do pre-process for IF statement
#endif
ExecuteCommand(command, SRC_RULE);
serviced = true;
}
plen += 6;
Rules.trigger_count[rule_set]++;
}
return serviced;
}
/*******************************************************************************************/
String RuleLoadFile(const char* fname) {
/* Read a string from rule space data between 'ON FILE#<fname> DO ' and ' ENDON' like:
rule3 on file#calib.dat do {"rms":{"current_a":3166385,"voltage_a":-767262},"freq":0} endon
NOTE: String may not contain word 'ENDON'!!
*/
String filename = F("ON FILE#");
filename += fname;
filename += F(" DO ");
// filename.toUpperCase();
for (uint32_t i = 0; i < MAX_RULE_SETS; i++) {
if (!GetRuleLen(i)) { continue; }
String rules = GetRule(i);
rules.toUpperCase();
int start = rules.indexOf(filename);
if (start == -1) { continue; }
start += filename.length();
int end = rules.indexOf(F(" ENDON"), start);
if (end == -1) { continue; }
rules = GetRule(i);
return rules.substring(start, end); // {"rms":{"current_a":3166385,"voltage_a":-767262},"freq":0}
}
// AddLog(LOG_LEVEL_DEBUG, PSTR("RUL: File '%s' not found or empty"), fname);
return "";
}
/*******************************************************************************************/
bool RulesProcessEvent(const char *json_event)
{
#ifdef USE_BERRY
// events are passed to Berry before Rules engine
callBerryRule(json_event, Rules.teleperiod);
#endif
if (Rules.busy) { return false; }
Rules.busy = true;
bool serviced = false;
SHOW_FREE_MEM(PSTR("RulesProcessEvent"));
#ifdef DEBUG_RULES
AddLog(LOG_LEVEL_DEBUG, PSTR("RUL: ProcessEvent |%s|"), json_event);
#endif
String event_saved = json_event;
// json_event = {"INA219":{"Voltage":4.494,"Current":0.020,"Power":0.089}}
// json_event = {"System":{"Boot":1}}
// json_event = {"SerialReceived":"on"} - invalid but will be expanded to {"SerialReceived":{"Data":"on"}}
char *p = strchr(json_event, ':');
if ((p != NULL) && !(strchr(++p, ':'))) { // Find second colon
event_saved.replace(F(":"), F(":{\"Data\":"));
event_saved += F("}");
// event_saved = {"SerialReceived":{"Data":"on"}}
}
event_saved.toUpperCase();
#ifdef DEBUG_RULES
AddLog(LOG_LEVEL_DEBUG, PSTR("RUL: Event |%s|"), event_saved.c_str());
#endif
for (uint32_t i = 0; i < MAX_RULE_SETS; i++) {
if (GetRuleLen(i) && bitRead(Settings->rule_enabled, i)) {
if (RuleSetProcess(i, event_saved)) { serviced = true; }
}
}
Rules.busy = false;
return serviced;
}
bool RulesProcess(void) {
return RulesProcessEvent(XdrvMailbox.data);
}
void RulesInit(void)
{
// indicates scripter not enabled
bitWrite(Settings->rule_once, 7, 0);
// and indicates scripter do not use compress
bitWrite(Settings->rule_once, 6, 0);
TasmotaGlobal.rules_flag.data = 0;
for (uint32_t i = 0; i < MAX_RULE_SETS; i++) {
if (0 == GetRuleLen(i)) {
bitWrite(Settings->rule_enabled, i, 0);
bitWrite(Settings->rule_once, i, 0);
}
}
Rules.teleperiod = false;
}
void RulesEvery50ms(void)
{
if ((Settings->rule_enabled || BERRY_RULES) && !Rules.busy) { // Any rule enabled
char json_event[RULE_MAX_EVENTSZ +16]; // Add 16 chars for {"Event": .. }
if (-1 == Rules.new_power) { Rules.new_power = TasmotaGlobal.power; }
if (Rules.new_power != Rules.old_power) {
if (Rules.old_power != -1) {
for (uint32_t i = 0; i < TasmotaGlobal.devices_present; i++) {
uint8_t new_state = (Rules.new_power >> i) &1;
if (new_state != ((Rules.old_power >> i) &1)) {
snprintf_P(json_event, sizeof(json_event), PSTR("{\"Power%d\":{\"State\":%d}}"), i +1, new_state);
RulesProcessEvent(json_event);
}
}
} else {
// Boot time POWER OUTPUTS (Relays) Status
for (uint32_t i = 0; i < TasmotaGlobal.devices_present; i++) {
uint8_t new_state = (Rules.new_power >> i) &1;
snprintf_P(json_event, sizeof(json_event), PSTR("{\"Power%d\":{\"Boot\":%d}}"), i +1, new_state);
RulesProcessEvent(json_event);
}
// Boot time SWITCHES Status
for (uint32_t i = 0; i < MAX_SWITCHES_SET; i++) {
if (SwitchUsed(i)) {
snprintf_P(json_event, sizeof(json_event), PSTR("{\"%s\":{\"Boot\":%d}}"), GetSwitchText(i).c_str(), (SwitchState(i)));
RulesProcessEvent(json_event);
}
}
}
Rules.old_power = Rules.new_power;
}
else if (Rules.old_dimm != Settings->light_dimmer) {
if (Rules.old_dimm != -1) {
snprintf_P(json_event, sizeof(json_event), PSTR("{\"Dimmer\":{\"State\":%d}}"), Settings->light_dimmer);
} else {
// Boot time DIMMER VALUE
snprintf_P(json_event, sizeof(json_event), PSTR("{\"Dimmer\":{\"Boot\":%d}}"), Settings->light_dimmer);
}
RulesProcessEvent(json_event);
Rules.old_dimm = Settings->light_dimmer;
}
else if (Rules.event_data[0]) {
char *event;
char *parameter;
event = strtok_r(Rules.event_data, "=", &parameter); // Rules.event_data = fanspeed=10
if (event) {
event = Trim(event);
if (parameter) {
parameter = Trim(parameter);
} else {
parameter = event + strlen(event); // '\0'
}
bool quotes = (parameter[0] != '{');
snprintf_P(json_event, sizeof(json_event), PSTR("{\"Event\":{\"%s\":%s%s%s}}"), event, (quotes)?"\"":"", parameter, (quotes)?"\"":"");
Rules.event_data[0] ='\0';
RulesProcessEvent(json_event);
} else {
Rules.event_data[0] ='\0';
}
}
else if (Rules.vars_event || Rules.mems_event){
if (Rules.vars_event) {
for (uint32_t i = 0; i < MAX_RULE_VARS; i++) {
if (bitRead(Rules.vars_event, i)) {
bitClear(Rules.vars_event, i);
snprintf_P(json_event, sizeof(json_event), PSTR("{\"Var%d\":{\"State\":\"%s\"}}"), i+1, rules_vars[i]);
RulesProcessEvent(json_event);
break;
}
}
}
if (Rules.mems_event) {
for (uint32_t i = 0; i < MAX_RULE_MEMS; i++) {
if (bitRead(Rules.mems_event, i)) {
bitClear(Rules.mems_event, i);
snprintf_P(json_event, sizeof(json_event), PSTR("{\"Mem%d\":{\"State\":\"%s\"}}"), i+1, SettingsText(SET_MEM1 +i));
RulesProcessEvent(json_event);
break;
}
}
}
}
else if (TasmotaGlobal.rules_flag.data) {
json_event[0] = '\0';
if (TasmotaGlobal.rules_flag.system_init) {
TasmotaGlobal.rules_flag.system_init = 0;
strncpy_P(json_event, PSTR("{\"System\":{\"Init\":1}}"), sizeof(json_event));
}
else if (TasmotaGlobal.rules_flag.system_boot) {
TasmotaGlobal.rules_flag.system_boot = 0;
strncpy_P(json_event, PSTR("{\"System\":{\"Boot\":1}}"), sizeof(json_event));
}
else if (TasmotaGlobal.rules_flag.time_init) {
TasmotaGlobal.rules_flag.time_init = 0;
snprintf_P(json_event, sizeof(json_event), PSTR("{\"Time\":{\"Initialized\":%d}}"), MinutesPastMidnight());
}
else if (TasmotaGlobal.rules_flag.time_set) {
TasmotaGlobal.rules_flag.time_set = 0;
snprintf_P(json_event, sizeof(json_event), PSTR("{\"Time\":{\"Set\":%d}}"), MinutesPastMidnight());
}
else if (TasmotaGlobal.rules_flag.mqtt_connected) {
TasmotaGlobal.rules_flag.mqtt_connected = 0;
strncpy_P(json_event, PSTR("{\"MQTT\":{\"Connected\":1}}"), sizeof(json_event));
}
else if (TasmotaGlobal.rules_flag.mqtt_disconnected) {
TasmotaGlobal.rules_flag.mqtt_disconnected = 0;
strncpy_P(json_event, PSTR("{\"MQTT\":{\"Disconnected\":1}}"), sizeof(json_event));
}
else if (TasmotaGlobal.rules_flag.wifi_connected) {
TasmotaGlobal.rules_flag.wifi_connected = 0;
strncpy_P(json_event, PSTR("{\"WIFI\":{\"Connected\":1}}"), sizeof(json_event));
}
else if (TasmotaGlobal.rules_flag.wifi_disconnected) {
TasmotaGlobal.rules_flag.wifi_disconnected = 0;
strncpy_P(json_event, PSTR("{\"WIFI\":{\"Disconnected\":1}}"), sizeof(json_event));
}
#if defined(ESP32) && CONFIG_IDF_TARGET_ESP32 && defined(USE_ETHERNET)
else if (TasmotaGlobal.rules_flag.eth_connected) {
TasmotaGlobal.rules_flag.eth_connected = 0;
strncpy_P(json_event, PSTR("{\"ETH\":{\"Connected\":1}}"), sizeof(json_event));
}
else if (TasmotaGlobal.rules_flag.eth_disconnected) {
TasmotaGlobal.rules_flag.eth_disconnected = 0;
strncpy_P(json_event, PSTR("{\"ETH\":{\"Disconnected\":1}}"), sizeof(json_event));
}
#endif // USE_ETHERNET
else if (TasmotaGlobal.rules_flag.http_init) {
TasmotaGlobal.rules_flag.http_init = 0;
strncpy_P(json_event, PSTR("{\"HTTP\":{\"Initialized\":1}}"), sizeof(json_event));
}
#ifdef USE_SHUTTER
else if (TasmotaGlobal.rules_flag.shutter_moved) {
TasmotaGlobal.rules_flag.shutter_moved = 0;
strncpy_P(json_event, PSTR("{\"SHUTTER\":{\"Moved\":1}}"), sizeof(json_event));
}
else if (TasmotaGlobal.rules_flag.shutter_moving) {
TasmotaGlobal.rules_flag.shutter_moving = 0;
strncpy_P(json_event, PSTR("{\"SHUTTER\":{\"Moving\":1}}"), sizeof(json_event));
}
#endif // USE_SHUTTER
if (json_event[0]) {
RulesProcessEvent(json_event); // Only service one event within 50mS
}
}
}
}
void RulesEvery100ms(void) {
static uint8_t xsns_index = 0;
if ((Settings->rule_enabled || BERRY_RULES) && !Rules.busy && (TasmotaGlobal.uptime > 4)) { // Any rule enabled and allow 4 seconds start-up time for sensors (#3811)
ResponseClear();
int tele_period_save = TasmotaGlobal.tele_period;
TasmotaGlobal.tele_period = 2; // Do not allow HA updates during next function call
XsnsNextCall(FUNC_JSON_APPEND, xsns_index); // ,"INA219":{"Voltage":4.494,"Current":0.020,"Power":0.089}
TasmotaGlobal.tele_period = tele_period_save;
if (ResponseLength()) {
ResponseJsonStart(); // {"INA219":{"Voltage":4.494,"Current":0.020,"Power":0.089}
ResponseJsonEnd();
RulesProcessEvent(ResponseData());
}
}
}
void RulesEverySecond(void)
{
char json_event[120];
if ((Settings->rule_enabled || BERRY_RULES) && !Rules.busy) { // Any rule enabled
if (RtcTime.valid) {
if ((TasmotaGlobal.uptime > 60) && (RtcTime.minute != Rules.last_minute)) { // Execute from one minute after restart every minute only once
Rules.last_minute = RtcTime.minute;
snprintf_P(json_event, sizeof(json_event), PSTR("{\"Time\":{\"Minute\":%d}}"), MinutesPastMidnight());
RulesProcessEvent(json_event);
}
}
}
for (uint32_t i = 0; i < MAX_RULE_TIMERS; i++) {
if (Rules.timer[i] != 0L) { // Timer active?
if (TimeReached(Rules.timer[i])) { // Timer finished?
Rules.timer[i] = 0L; // Turn off this timer
if ((Settings->rule_enabled || BERRY_RULES) && !Rules.busy) { // Any rule enabled
snprintf_P(json_event, sizeof(json_event), PSTR("{\"Rules\":{\"Timer\":%d}}"), i +1);
RulesProcessEvent(json_event);
}
}
}
}
}
void RulesSaveBeforeRestart(void)
{
if ((Settings->rule_enabled || BERRY_RULES) && !Rules.busy) { // Any rule enabled
char json_event[32];
strncpy_P(json_event, PSTR("{\"System\":{\"Save\":1}}"), sizeof(json_event));
RulesProcessEvent(json_event);
}
}
void RulesSetPower(void)
{
Rules.new_power = XdrvMailbox.index;
}
#ifdef SUPPORT_MQTT_EVENT
/********************************************************************************************/
/*
* Rules: Process received MQTT message.
* If the message is in our subscription list, trigger an event with the value parsed from MQTT data
* Input:
* void - We are going to access XdrvMailbox data directly.
* Return:
* true - The message is consumed.
* false - The message is not in our list.
*/
bool RulesMqttData(void) {
if ((XdrvMailbox.data_len < 1) || (XdrvMailbox.data_len > RULE_MAX_MQTT_EVENTSZ)) {
return false;
}
bool serviced = false;
String sTopic = XdrvMailbox.topic;
String buData = XdrvMailbox.data;
//AddLog(LOG_LEVEL_DEBUG, PSTR("RUL: MQTT Topic %s, Event %s"), XdrvMailbox.topic, XdrvMailbox.data);
MQTT_Subscription event_item;
//Looking for matched topic
char json_event[RULE_MAX_MQTT_EVENTSZ +32]; // Add chars for {"Event":{"<item.Event>": .. }
for (uint32_t index = 0; index < subscriptions.size(); index++) {
String sData = buData;
event_item = subscriptions.get(index);
//AddLog(LOG_LEVEL_DEBUG, PSTR("RUL: Match MQTT message Topic %s with subscription topic %s"), sTopic.c_str(), event_item.Topic.c_str());
if ((sTopic == event_item.Topic) || sTopic.startsWith(event_item.Topic+"/")) {
//This topic is subscribed by us, so serve it
serviced = true;
String value;
if (event_item.Key.length() == 0) { //If did not specify Key
value = sData;
} else { //If specified Key, need to parse Key/Value from JSON data
JsonParser parser((char*)sData.c_str());
JsonParserObject jsonData = parser.getRootObject();
String key1 = event_item.Key;
String key2;
if (!jsonData) break; //Failed to parse JSON data, ignore this message.
int dot;
if ((dot = key1.indexOf('.')) > 0) {
key2 = key1.substring(dot+1);
key1 = key1.substring(0, dot);
JsonParserToken value_tok = jsonData[key1.c_str()].getObject()[key2.c_str()];
if (!value_tok) break; //Failed to get the key/value, ignore this message.
value = value_tok.getStr();
// if (!jsonData[key1][key2].success()) break; //Failed to get the key/value, ignore this message.
// value = (const char *)jsonData[key1][key2];
} else {
JsonParserToken value_tok = jsonData[key1.c_str()];
if (!value_tok) break; //Failed to get the key/value, ignore this message.
value = value_tok.getStr();
// if (!jsonData[key1].success()) break;
// value = (const char *)jsonData[key1];
}
}
value.trim();
/*
//Create an new event. Cannot directly call RulesProcessEvent().
snprintf_P(Rules.event_data, sizeof(Rules.event_data), PSTR("%s=%s"), event_item.Event.c_str(), value.c_str());
// 20230107 Superseded by the following code
*/
bool quotes = (value[0] != '{');
snprintf_P(json_event, sizeof(json_event), PSTR("{\"Event\":{\"%s\":%s%s%s}}"), event_item.Event.c_str(), (quotes)?"\"":"", value.c_str(), (quotes)?"\"":"");
RulesProcessEvent(json_event);
}
}
return serviced;
}
/********************************************************************************************/
/*
* Subscribe a MQTT topic (with or without key) and assign an event name to it
* Command Subscribe format:
* Subscribe <event_name>, <topic> [, <key>]
* This command will subscribe a <topic> and give it an event name <event_name>.
* The optional parameter <key> is for parse the specified key/value from MQTT message
* payload with JSON format.
* Subscribe
* Subscribe command without any parameter will list all topics currently subscribed.
* Input:
* XdrvMailbox.data - A char buffer with all the parameters
* XdrvMailbox.data_len - Length of the parameters
* Return:
* A string include subscribed event, topic and key.
*/
void CmndSubscribe(void)
{
MQTT_Subscription subscription_item;
String events;
if (XdrvMailbox.data_len > 0) {
char parameters[XdrvMailbox.data_len+1];
memcpy(parameters, XdrvMailbox.data, XdrvMailbox.data_len);
parameters[XdrvMailbox.data_len] = '\0';
String event_name, topic, key;
char * pos = strtok(parameters, ",");
if (pos) {
event_name = Trim(pos);
pos = strtok(nullptr, ",");
if (pos) {
topic = Trim(pos);
pos = strtok(nullptr, ",");
if (pos) {
key = Trim(pos);
}
}
}
//AddLog(LOG_LEVEL_DEBUG, PSTR("RUL: Subscribe command with parameters: %s, %s, %s."), event_name.c_str(), topic.c_str(), key.c_str());
event_name.toUpperCase();
if (event_name.length() > 0 && topic.length() > 0) {
//Search all subscriptions
for (uint32_t index=0; index < subscriptions.size(); index++) {
if (subscriptions.get(index).Event.equals(event_name)) {
//If find exists one, remove it.
String stopic = subscriptions.get(index).Topic + "/#";
MqttUnsubscribe(stopic.c_str());
subscriptions.remove(index);
break;
}
}
//Add "/#" to the topic
if (!topic.endsWith("#")) {
if (topic.endsWith("/")) {
topic.concat("#");
} else {
topic.concat("/#");
}
}
//AddLog(LOG_LEVEL_DEBUG, PSTR("RUL: New topic: %s."), topic.c_str());
//MQTT Subscribe
subscription_item.Event = event_name;
subscription_item.Topic = topic.substring(0, topic.length() - 2); //Remove "/#" so easy to match
subscription_item.Key = key;
subscriptions.add(subscription_item);
if (2 == XdrvMailbox.index) {
topic = subscription_item.Topic; // Do not append "/#""
}
MqttSubscribe(topic.c_str());
events.concat(event_name + "," + topic
+ (key.length()>0 ? "," : "")
+ key);
} else {
events = D_JSON_WRONG_PARAMETERS;
}
} else {
//If did not specify the event name, list all subscribed event
for (uint32_t index=0; index < subscriptions.size(); index++) {
subscription_item = subscriptions.get(index);
events.concat(subscription_item.Event + "," + subscription_item.Topic
+ (subscription_item.Key.length()>0 ? "," : "")
+ subscription_item.Key + "; ");
}
}
ResponseCmndChar(events.c_str());
}
/********************************************************************************************/
/*
* Unsubscribe specified MQTT event. If no event specified, Unsubscribe all.
* Command Unsubscribe format:
* UnSubscribe [<event_name>]
* Input:
* XdrvMailbox.data - Event name
* XdrvMailbox.data_len - Length of the parameters
* Return:
* list all the events unsubscribed.
*/
void CmndUnsubscribe(void)
{
MQTT_Subscription subscription_item;
String events;
if (XdrvMailbox.data_len > 0) {
for (uint32_t index = 0; index < subscriptions.size(); index++) {
subscription_item = subscriptions.get(index);
if (subscription_item.Event.equalsIgnoreCase(XdrvMailbox.data)) {
String stopic = subscription_item.Topic + "/#";
MqttUnsubscribe(stopic.c_str());
events = subscription_item.Event;
subscriptions.remove(index);
break;
}
}
} else {
// If did not specify the event name, unsubscribe all event
String stopic;
while (subscriptions.size() > 0) {
events.concat(subscriptions.get(0).Event + "; ");
stopic = subscriptions.get(0).Topic + "/#";
MqttUnsubscribe(stopic.c_str());
subscriptions.remove(0);
}
}
ResponseCmndChar(events.c_str());
}
#endif // SUPPORT_MQTT_EVENT
#ifdef USE_EXPRESSION
/********************************************************************************************/
/*
* Looking for matched bracket - ")"
* Search buffer from current loction, skip all nested bracket pairs, find the matched close bracket.
* Input:
* pStart - Point to a char buffer start with "("
* Output:
* N/A
* Return:
* position of matched close bracket
*/
char * findClosureBracket(char * pStart)
{
char * pointer = pStart + 1;
//Look for the matched closure parenthesis.")"
bool bFindClosures = false;
uint8_t matchClosures = 1;
while (*pointer)
{
if (*pointer == ')') {
matchClosures--;
if (matchClosures == 0) {
bFindClosures = true;
break;
}
} else if (*pointer == '(') {
matchClosures++;
}
pointer++;
}
if (bFindClosures) {
return pointer;
} else {
return nullptr;
}
}
/********************************************************************************************/
/*
* Parse a number value
* Input:
* pNumber - A char pointer point to a digit started string (guaranteed)
* value - Reference a float variable used to accept the result
* Output:
* pNumber - Pointer forward to next character after the number
* value - float type, the result value
* Return:
* true - succeed
* false - failed
*/
bool findNextNumber(char * &pNumber, float &value)
{
bool bSucceed = false;
String sNumber = "";
if (*pNumber == '-') {
sNumber = "-";
pNumber++;
}
while (*pNumber) {
if (isdigit(*pNumber) || (*pNumber == '.')) {
sNumber += *pNumber;
pNumber++;
} else {
break;
}
}
if (sNumber.length() > 0) {
value = CharToFloat(sNumber.c_str());
bSucceed = true;
}
return bSucceed;
}
/********************************************************************************************/
/*
* Parse a variable (like VAR1, MEM3) and get its value (float type)
* Input:
* pVarname - A char pointer point to a variable name string
* value - Reference a float variable used to accept the result
* Output:
* pVarname - Pointer forward to next character after the variable
* value - float type, the result value
* Return:
* true - succeed
* false - failed
*/
bool findNextVariableValue(char * &pVarname, float &value)
{
bool succeed = true;
value = 0;
String sVarName = "";
while (*pVarname) {
if (isalpha(*pVarname) || isdigit(*pVarname)) {
sVarName.concat(*pVarname);
pVarname++;
} else {
break;
}
}
sVarName.toUpperCase();
if (sVarName.startsWith(F("VAR"))) {
int index = sVarName.substring(3).toInt();
if (index > 0 && index <= MAX_RULE_VARS) {
value = CharToFloat(rules_vars[index -1]);
}
} else if (sVarName.startsWith(F("MEM"))) {
int index = sVarName.substring(3).toInt();
if (index > 0 && index <= MAX_RULE_MEMS) {
value = CharToFloat(SettingsText(SET_MEM1 + index -1));
}
} else if (sVarName.equals(F("TIME"))) {
value = MinutesPastMidnight();
} else if (sVarName.equals(F("UPTIME"))) {
value = MinutesUptime();
} else if (sVarName.equals(F("UTCTIME"))) {
value = UtcTime();
} else if (sVarName.equals(F("LOCALTIME"))) {
value = LocalTime();
#if defined(USE_TIMERS)
} else if (sVarName.startsWith(F("TIMER"))) {
uint32_t index = sVarName.substring(5).toInt();
if (index > 0 && index <= MAX_TIMERS) {
value = TimerGetTimeOfDay(index -1);
}
#if defined(USE_SUNRISE)
} else if (sVarName.equals(F("SUNRISE"))) {
value = SunMinutes(0);
} else if (sVarName.equals(F("SUNSET"))) {
value = SunMinutes(1);
#endif // USE_SUNRISE
#endif // USE_TIMERS
// #ifdef USE_ZIGBEE
// // } else if (sVarName.equals(F("ZBDEVICE"))) {
// // value = Z_GetLastDevice();
// } else if (sVarName.equals(F("ZBGROUP"))) {
// value = Z_GetLastGroup();
// } else if (sVarName.equals(F("ZBCLUSTER"))) {
// value = Z_GetLastCluster();
// } else if (sVarName.equals(F("ZBENDPOINT"))) {
// value = Z_GetLastEndpoint();
// #endif
} else {
succeed = false;
}
return succeed;
}
/********************************************************************************************/
/*
* Find next object in expression and evaluate it
* An object could be:
* - A float number start with a digit or minus, like 0.787, -3
* - A variable name, like VAR1, MEM3
* - An expression enclosed with a pair of round brackets, (.....)
* Input:
* pointer - A char pointer point to a place of the expression string
* value - Reference a float variable used to accept the result
* Output:
* pointer - Pointer forward to next character after next object
* value - float type, the result value
* Return:
* true - succeed
* false - failed
*/
bool findNextObjectValue(char * &pointer, float &value)
{
bool bSucceed = false;
while (*pointer)
{
if (isspace(*pointer)) { //Skip leading spaces
pointer++;
continue;
}
if (isdigit(*pointer) || (*pointer) == '-') { //This object is a number
bSucceed = findNextNumber(pointer, value);
break;
} else if (isalpha(*pointer)) { //Should be a variable like VAR12, MEM1
bSucceed = findNextVariableValue(pointer, value);
break;
} else if (*pointer == '(') { //It is a sub expression bracketed with ()
char * closureBracket = findClosureBracket(pointer); //Get the position of closure bracket ")"
if (closureBracket != nullptr) {
value = evaluateExpression(pointer+1, closureBracket - pointer - 1);
pointer = closureBracket + 1;
bSucceed = true;
}
break;
} else { //No number, no variable, no expression, then invalid object.
break;
}
}
return bSucceed;
}
/********************************************************************************************/
/*
* Find next operator in expression
* An operator could be: +, - , * , / , %, ^
* Input:
* pointer - A char pointer point to a place of the expression string
* op - Reference to a variable used to accept the result
* Output:
* pointer - Pointer forward to next character after next operator
* op - The operator. 0, 1, 2, 3, 4, 5
* Return:
* true - succeed
* false - failed
*/
bool findNextOperator(char * &pointer, int8_t &op)
{
bool bSucceed = false;
while (*pointer)
{
if (isspace(*pointer)) { //Skip leading spaces
pointer++;
continue;
}
op = EXPRESSION_OPERATOR_ADD;
const char *pch = kExpressionOperators;
char ch;
while ((ch = pgm_read_byte(pch++)) != '\0') {
if (ch == *pointer) {
bSucceed = true;
pointer++;
break;
}
op++;
}
break;
}
return bSucceed;
}
/********************************************************************************************/
/*
* Calculate a simple expression composed by 2 value and 1 operator, like 2 * 3
* Input:
* pointer - A char pointer point to a place of the expression string
* value - Reference a float variable used to accept the result
* Output:
* pointer - Pointer forward to next character after next object
* value - float type, the result value
* Return:
* true - succeed
* false - failed
*/
float calculateTwoValues(float v1, float v2, uint8_t op)
{
switch (op)
{
case EXPRESSION_OPERATOR_ADD:
return v1 + v2;
case EXPRESSION_OPERATOR_SUBTRACT:
return v1 - v2;
case EXPRESSION_OPERATOR_MULTIPLY:
return v1 * v2;
case EXPRESSION_OPERATOR_DIVIDEDBY:
return (0 == v2) ? 0 : (v1 / v2);
case EXPRESSION_OPERATOR_MODULO:
return (0 == v2) ? 0 : (int(v1) % int(v2));
case EXPRESSION_OPERATOR_POWER:
return FastPrecisePow(v1, v2);
}
return 0;
}
/********************************************************************************************/
/*
* Parse and evaluate an expression.
* For example: "10 * ( MEM2 + 1) / 2"
* Right now, only support operators listed here: (order by priority)
* Priority 4: ^ (power)
* Priority 3: % (modulo, always get integer result)
* Priority 2: *, /
* Priority 1: +, -
* Input:
* expression - The expression to be evaluated
* len - Length of the expression
* Return:
* float - result.
* 0 - if the expression is invalid
* An example:
* MEM1 = 3, MEM2 = 6, VAR2 = 15, VAR10 = 80
* At beginning, the expression might be complicated like: 3.14 * (MEM1 * (10 + VAR2 ^2) - 100) % 10 + VAR10 / (2 + MEM2)
* We are going to scan the whole expression, evaluate each object.
* Finally we will have a value list:.
* Order Object Value
* 0 3.14 3.14
* 1 (MEM1 * (10 + VAR2 ^2) - 100) 605
* 2 10 10
* 3 VAR10 80
* 4 (2 + MEM2) 8
* And an operator list:
* Order Operator Priority
* 0 * 2
* 1 % 3
* 2 + 1
* 3 / 2
*/
float evaluateExpression(const char * expression, unsigned int len)
{
char expbuf[len + 1];
memcpy(expbuf, expression, len);
expbuf[len] = '\0';
char * scan_pointer = expbuf;
LinkedList<float> object_values;
LinkedList<int8_t> operators;
int8_t op;
float va;
//Find and add the value of first object
if (findNextObjectValue(scan_pointer, va)) {
object_values.add(va);
} else {
return 0;
}
while (*scan_pointer)
{
if (findNextOperator(scan_pointer, op)
&& *scan_pointer
&& findNextObjectValue(scan_pointer, va))
{
operators.add(op);
object_values.add(va);
} else {
//No operator followed or no more object after this operator, we done.
break;
}
}
//Going to evaluate the whole expression
//Calculate by order of operator priorities. Looking for all operators with specified priority (from High to Low)
for (int32_t priority = MAX_EXPRESSION_OPERATOR_PRIORITY; priority>0; priority--) {
int index = 0;
while (index < operators.size()) {
if (priority == pgm_read_byte(kExpressionOperatorsPriorities + operators.get(index))) { //need to calculate the operator first
//get current object value and remove the next object with current operator
va = calculateTwoValues(object_values.get(index), object_values.remove(index + 1), operators.remove(index));
//Replace the current value with the result
object_values.set(index, va);
} else {
index++;
}
}
}
return object_values.get(0);
}
#endif // USE_EXPRESSION
#ifdef SUPPORT_IF_STATEMENT
void CmndIf(void)
{
if (XdrvMailbox.data_len > 0) {
char parameters[XdrvMailbox.data_len+1];
memcpy(parameters, XdrvMailbox.data, XdrvMailbox.data_len);
parameters[XdrvMailbox.data_len] = '\0';
ProcessIfStatement(parameters);
}
ResponseCmndDone();
}
/********************************************************************************************/
/*
* Evaluate a comparison expression.
* Get the logic value of expression, true or false
* Input:
* expression - A comparison expression like VAR1 >= MEM1 + 10
* len - Length of expression
* Output:
* N/A
* Return:
* logic value of comparison expression
*/
bool evaluateComparisonExpression(const char *expression, int len)
{
bool bResult = true;
char expbuf[len + 1];
memcpy(expbuf, expression, len);
expbuf[len] = '\0';
String compare_expression = expbuf;
String leftExpr, rightExpr;
int8_t compareOp = parseCompareExpression(compare_expression, leftExpr, rightExpr);
double leftValue = evaluateExpression(leftExpr.c_str(), leftExpr.length());
double rightValue = evaluateExpression(rightExpr.c_str(), rightExpr.length());
switch (compareOp) {
case COMPARE_OPERATOR_EXACT_DIVISION:
bResult = (rightValue != 0 && leftValue == int(leftValue)
&& rightValue == int(rightValue) && (int(leftValue) % int(rightValue)) == 0);
break;
case COMPARE_OPERATOR_EQUAL:
bResult = leftExpr.equalsIgnoreCase(rightExpr); // Compare strings - this also works for hexadecimals
break;
case COMPARE_OPERATOR_BIGGER:
bResult = (leftValue > rightValue);
break;
case COMPARE_OPERATOR_SMALLER:
bResult = (leftValue < rightValue);
break;
case COMPARE_OPERATOR_NUMBER_EQUAL:
bResult = (leftValue == rightValue);
break;
case COMPARE_OPERATOR_NOT_EQUAL:
bResult = (leftValue != rightValue);
break;
case COMPARE_OPERATOR_BIGGER_EQUAL:
bResult = (leftValue >= rightValue);
break;
case COMPARE_OPERATOR_SMALLER_EQUAL:
bResult = (leftValue <= rightValue);
break;
case COMPARE_OPERATOR_STRING_ENDS_WITH:
bResult = leftExpr.endsWith(rightExpr);
break;
case COMPARE_OPERATOR_STRING_STARTS_WITH:
bResult = leftExpr.startsWith(rightExpr);
break;
case COMPARE_OPERATOR_STRING_CONTAINS:
bResult = (leftExpr.indexOf(rightExpr) >= 0);
break;
case COMPARE_OPERATOR_STRING_NOT_EQUAL:
bResult = !leftExpr.equalsIgnoreCase(rightExpr); // Compare strings - this also works for hexadecimals
break;
case COMPARE_OPERATOR_STRING_NOT_CONTAINS:
bResult = (leftExpr.indexOf(rightExpr) < 0);
break;
}
return bResult;
}
/********************************************************************************************/
/*
* Looking for a logical operator, either "AND" or "OR"
* A logical operator is expected at this moment. If we find something else, this function will fail.
* Input:
* pointer - Point to a char buffer
* op - Used to accpet the logical operator type
* Output:
* Pointer - pointer will forward to next character after the logical operator.
* op - The logical operator type we found
* Return:
* true - succeed
* false - failed
*/
bool findNextLogicOperator(char * &pointer, int8_t &op)
{
bool bSucceed = false;
while (*pointer && isspace(*pointer)) {
//Skip spaces
pointer++;
}
if (*pointer) {
if (strncasecmp_P(pointer, PSTR("AND "), 4) == 0) {
op = LOGIC_OPERATOR_AND;
pointer += 4;
bSucceed = true;
} else if (strncasecmp_P(pointer, PSTR("OR "), 3) == 0) {
op = LOGIC_OPERATOR_OR;
pointer += 3;
bSucceed = true;
}
}
return bSucceed;
}
/********************************************************************************************/
/*
* Find next logical object and get its value
* A logical object could be:
* - A comparison expression.
* - A logical expression bracketed with a pair of parenthesis.
* Input:
* pointer - A char pointer point to a start of logical object
* value - Used to accept the result value
* Output:
* pointer - Pointer forward to next character after the object
* value - boolean type, the value of the logical object.
* Return:
* true - succeed
* false - failed
*/
bool findNextLogicObjectValue(char * &pointer, bool &value)
{
bool bSucceed = false;
while (*pointer && isspace(*pointer)) {
//Skip leading spaces
pointer++;
}
char * pExpr = pointer;
while (*pointer) {
if (isalpha(*pointer)
&& (strncasecmp_P(pointer, PSTR("AND "), 4) == 0
|| strncasecmp_P(pointer, PSTR("OR "), 3) == 0))
{ //We have a logic operator, should stop
value = evaluateComparisonExpression(pExpr, pointer - pExpr);
bSucceed = true;
break;
} else if (*pointer == '(') { //It is a sub expression bracketed with ()
char * closureBracket = findClosureBracket(pointer); //Get the position of closure bracket ")"
if (closureBracket != nullptr) {
value = evaluateLogicalExpression(pointer+1, closureBracket - pointer - 1);
pointer = closureBracket + 1;
bSucceed = true;
}
break;
}
pointer++;
}
if (!bSucceed && pointer > pExpr) {
//The whole buffer is an comparison expression
value = evaluateComparisonExpression(pExpr, pointer - pExpr);
bSucceed = true;
}
return bSucceed;
}
/********************************************************************************************/
/*
* Evaluate a logical expression
* Logic expression is constructed with multiple comparison expressions and logical
* operators between them. For example: Mem1==0 AND (time > sunrise + 60).
* Parenthesis are allowed to change the priority of logical operators.
* Input:
* expression - A logical expression
* len - Length of the expression
* Output:
* N/A
* Return:
* boolean - the value of logical expression
*/
bool evaluateLogicalExpression(const char * expression, int len)
{
//Make a copy first
char expbuff[len + 1];
memcpy(expbuff, expression, len);
expbuff[len] = '\0';
//AddLog(LOG_LEVEL_DEBUG, PSTR("EvalLogic: |%s|"), expbuff);
char * pointer = expbuff;
LinkedList<bool> values;
LinkedList<int8_t> logicOperators;
//Find first comparison expression
bool bValue;
if (findNextLogicObjectValue(pointer, bValue)) {
values.add(bValue);
} else {
return false;
}
int8_t op;
while (*pointer) {
if (findNextLogicOperator(pointer, op)
&& (*pointer) && findNextLogicObjectValue(pointer, bValue))
{
logicOperators.add(op);
values.add(bValue);
} else {
break;
}
}
//Calculate all "AND" first
int index = 0;
while (index < logicOperators.size()) {
if (logicOperators.get(index) == LOGIC_OPERATOR_AND) {
values.set(index, values.get(index) && values.get(index+1));
values.remove(index + 1);
logicOperators.remove(index);
} else {
index++;
}
}
//Then, calculate all "OR"
index = 0;
while (index < logicOperators.size()) {
if (logicOperators.get(index) == LOGIC_OPERATOR_OR) {
values.set(index, values.get(index) || values.get(index+1));
values.remove(index + 1);
logicOperators.remove(index);
} else {
index++;
}
}
return values.get(0);
}
/********************************************************************************************/
/*
* This function search in a buffer to find out an IF block start from current position
* Note: All the tokens found during the searching will be changed to NULL terminated string.
* Please make a copy before call this function if you still need it.
* Input:
* pointer - Point to a NULL end string buffer with the commands
* lenWord - Accept the length of block end word
* block_type - The block type you are looking for.
* Output:
* pointer - pointer point to the end of if block.
* lenWord - The length of block end word ("ENDIF", "ELSEIF", "ELSE")
* Return:
* The block type we find.
* IF_BLOCK_INVALID - Failed.
*/
int8_t findIfBlock(char * &pointer, int &lenWord, int8_t block_type)
{
int8_t foundBlock = IF_BLOCK_INVALID;
//First break into words delimited by space or ";"
const char * word;
while (*pointer) {
if (!isalpha(*pointer)) {
pointer++;
continue;
}
word = pointer;
while (*pointer && isalpha(*pointer)) {
pointer++;
}
lenWord = pointer - word;
if (2 == lenWord && 0 == strncasecmp_P(word, PSTR("IF"), 2)) {
//if we find a new "IF" that means this is nested if block
//Try to finish this nested if block
if (findIfBlock(pointer, lenWord, IF_BLOCK_ENDIF) != IF_BLOCK_ENDIF) {
//If failed, we done.
break;
}
} else if ( (IF_BLOCK_ENDIF == block_type || IF_BLOCK_ANY == block_type)
&& (5 == lenWord) && (0 == strncasecmp_P(word, PSTR("ENDIF"), 5)))
{
//Find an "ENDIF"
foundBlock = IF_BLOCK_ENDIF;
break;
} else if ( (IF_BLOCK_ELSEIF == block_type || IF_BLOCK_ANY == block_type)
&& (6 == lenWord) && (0 == strncasecmp_P(word, PSTR("ELSEIF"), 6)))
{
//Find an "ELSEIF"
foundBlock = IF_BLOCK_ELSEIF;
break;
} else if ( (IF_BLOCK_ELSE == block_type || IF_BLOCK_ANY == block_type)
&& (4 == lenWord) && (0 == strncasecmp_P(word, PSTR("ELSE"), 4)))
{
//Find an "ELSE"
foundBlock = IF_BLOCK_ELSE;
break;
}
}
return foundBlock;
}
/********************************************************************************************/
/*
* This function is used to execute a commands block in if statement when one of the condition is true.
* Input:
* commands - A char buffer include (but not limited) the commands block need to execute
* len - Length of the commands block
* Output:
N/A
* Return:
* void
*/
void ExecuteCommandBlock(const char * commands, int len)
{
char cmdbuff[len + 1]; //apply enough space
memcpy(cmdbuff, commands, len);
cmdbuff[len] = '\0';
//AddLog(LOG_LEVEL_DEBUG, PSTR("ExecCmd: |%s|"), cmdbuff);
char oneCommand[len + 1]; //To put one command
int insertPosition = 0; //When insert into backlog, we should do it by 0, 1, 2 ...
char * pos = cmdbuff;
int lenEndBlock = 0;
while (*pos) {
if (isspace(*pos) || '\x1e' == *pos || ';' == *pos) {
pos++;
continue;
}
if (strncasecmp_P(pos, PSTR("BACKLOG "), 8) == 0) {
//Skip "BACKLOG " and set not first command flag. So all followed command will be send to backlog
pos += 8;
continue;
}
if (strncasecmp_P(pos, PSTR("IF "), 3) == 0) {
//Has a nested IF statement
//Find the matched ENDIF
char *pEndif = pos + 3; //Skip "IF "
if (IF_BLOCK_ENDIF != findIfBlock(pEndif, lenEndBlock, IF_BLOCK_ENDIF)) {
//Cannot find matched endif, stop execution.
break;
}
//We has the whole IF statement, copy to oneCommand
memcpy(oneCommand, pos, pEndif - pos);
oneCommand[pEndif - pos] = '\0';
pos = pEndif;
} else { //Normal command
//Looking for the command end single - '\x1e'
char *pEndOfCommand = strpbrk(pos, "\x1e;");
if (NULL == pEndOfCommand) {
pEndOfCommand = pos + strlen(pos);
}
memcpy(oneCommand, pos, pEndOfCommand - pos);
oneCommand[pEndOfCommand - pos] = '\0';
pos = pEndOfCommand;
}
//Start to process current command we found
//Going to insert the command into backlog
String sCurrentCommand = oneCommand;
sCurrentCommand.trim();
/*
if (sCurrentCommand.length() > 0
&& backlog.size() < MAX_BACKLOG && !TasmotaGlobal.backlog_mutex)
{
//Insert into backlog
TasmotaGlobal.backlog_mutex = true;
backlog.add(insertPosition, sCurrentCommand);
TasmotaGlobal.backlog_mutex = false;
insertPosition++;
}
*/
if ((sCurrentCommand.length() > 0) && (backlog.size() < MAX_BACKLOG)) {
//Insert into backlog
backlog.add(insertPosition, sCurrentCommand);
insertPosition++;
}
}
return;
}
/********************************************************************************************/
/*
* Execute IF statement. This is the place to run a "IF ..." command.
* Input:
* statements - The IF statement we are going to process
* Output:
N/A
* Return:
* void
*/
void ProcessIfStatement(const char* statements)
{
String conditionExpression;
int len = strlen(statements);
char statbuff[len + 1];
memcpy(statbuff, statements, len + 1);
char *pos = statbuff;
int lenEndBlock = 0;
while (true) { //Each loop process one IF (or ELSEIF) block
//Find and test the condition expression followed the IF or ELSEIF
//Search for the open bracket first
while (*pos && *pos != '(') {
pos++;
}
if (0 == *pos) { break; }
char * posEnd = findClosureBracket(pos);
if (true == evaluateLogicalExpression(pos + 1, posEnd - (pos + 1))) {
//Looking for matched "ELSEIF", "ELSE" or "ENDIF", then Execute this block
char * cmdBlockStart = posEnd + 1;
char * cmdBlockEnd = cmdBlockStart;
int8_t nextBlock = findIfBlock(cmdBlockEnd, lenEndBlock, IF_BLOCK_ANY);
if (IF_BLOCK_INVALID == nextBlock) {
//Failed
break;
}
ExecuteCommandBlock(cmdBlockStart, cmdBlockEnd - cmdBlockStart - lenEndBlock);
pos = cmdBlockEnd;
break;
} else { //Does not match the IF condition, going to check elseif and else
pos = posEnd + 1;
int8_t nextBlock = findIfBlock(pos, lenEndBlock, IF_BLOCK_ANY);
if (IF_BLOCK_ELSEIF == nextBlock) {
//Continue process next ELSEIF block like IF
continue;
} else if (IF_BLOCK_ELSE == nextBlock) {
//Looking for matched "ENDIF" then execute this block
char * cmdBlockEnd = pos;
int8_t nextBlock = findIfBlock(cmdBlockEnd, lenEndBlock, IF_BLOCK_ENDIF);
if (IF_BLOCK_ENDIF != nextBlock) {
//Failed
break;
}
ExecuteCommandBlock(pos, cmdBlockEnd - pos - lenEndBlock);
break;
} else { // IF_BLOCK_ENDIF == nextBlock
//We done
break;
}
}
}
}
/********************************************************************************************/
/*
* This function is called in Rules event handler to process any command between DO ... ENDON (BREAK)
* - Do escape (convert ";" into "\x1e") for all IF statements.
* Input:
* commands - The commands block need to execute
* Output:
N/A
* Return:
* void
*/
void RulesPreprocessCommand(char *pCommands)
{
char * cmd = pCommands;
int lenEndBlock = 0;
while (*cmd) {
//Skip all ";" and space between two commands
if (';' == *cmd || isspace(*cmd)) {
cmd++;
}
else if (strncasecmp_P(cmd, PSTR("IF "), 3) == 0) { //found IF block
//We are going to look for matched "ENDIF"
char * pIfStart = cmd;
char * pIfEnd = pIfStart + 3; //Skip "IF "
//int pIfStart = cmd - command; //"IF" statement block start at position (relative to command start)
if (IF_BLOCK_ENDIF == findIfBlock(pIfEnd, lenEndBlock, IF_BLOCK_ENDIF)) {
//Found the ENDIF
cmd = pIfEnd; //Will continue process from here
//Escapte from ";" to "\x1e".
//By remove all ";" in IF statement block, we can prevent backlog command cut the whole block as multiple commands
while (pIfStart < pIfEnd) {
if (';' == *pIfStart)
*pIfStart = '\x1e';
pIfStart++;
}
}
else { //Did not find the matched ENDIF, stop processing
break;
}
}
else { //Other commands, skip it
while (*cmd && ';' != *cmd) {
cmd++;
}
}
}
return;
}
#endif //SUPPORT_IF_STATEMENT
/*********************************************************************************************\
* Commands
\*********************************************************************************************/
void CmndRule(void)
{
if (0 == XdrvMailbox.index) {
char data = '\0';
if (XdrvMailbox.data_len > 0) { // Allow show all if 0
if (!((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 10))) {
if ('"' == XdrvMailbox.data[0]) {
data = '"'; // Save data as XdrvMailbox.data is destroyed
} else {
XdrvMailbox.data_len = 0; // Discard any additional text
}
}
}
for (uint32_t i = 1; i <= MAX_RULE_SETS; i++) {
XdrvMailbox.index = i;
XdrvMailbox.data[0] = data; // Only 0 or "
CmndRule();
MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_STAT, XdrvMailbox.command);
}
ResponseClear(); // Disable further processing
return;
}
uint8_t index = XdrvMailbox.index;
if ((index > 0) && (index <= MAX_RULE_SETS)) {
// if ((XdrvMailbox.data_len > 0) && (XdrvMailbox.data_len < sizeof(Settings->rules[index -1]))) { // TODO postpone size calculation
if (XdrvMailbox.data_len > 0) { // TODO postpone size calculation
if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 10)) {
switch (XdrvMailbox.payload) {
case 0: // Off
case 1: // On
bitWrite(Settings->rule_enabled, index -1, XdrvMailbox.payload);
break;
case 2: // Toggle
bitWrite(Settings->rule_enabled, index -1, bitRead(Settings->rule_enabled, index -1) ^1);
break;
case 4: // Off
case 5: // On
bitWrite(Settings->rule_once, index -1, XdrvMailbox.payload &1);
break;
case 6: // Toggle
bitWrite(Settings->rule_once, index -1, bitRead(Settings->rule_once, index -1) ^1);
break;
case 8: // Off
case 9: // On
bitWrite(Settings->rule_stop, index -1, XdrvMailbox.payload &1);
break;
case 10: // Toggle
bitWrite(Settings->rule_stop, index -1, bitRead(Settings->rule_stop, index -1) ^1);
break;
}
} else {
bool append = false;
if ('+' == XdrvMailbox.data[0]) {
XdrvMailbox.data[0] = ' '; // Remove + and make sure at least one space is inserted
append = true;
}
int32_t res = SetRule(index - 1, ('"' == XdrvMailbox.data[0]) ? "" : XdrvMailbox.data, append);
if (res < 0) {
AddLog(LOG_LEVEL_ERROR, PSTR("RUL: Not enough space"));
}
}
Rules.triggers[index -1] = 0; // Reset once flag
}
String rule = GetRule(index - 1);
size_t rule_len = rule.length();
if (rule_len > MAX_RULE_SIZE - 3) {
size_t start_index = 0; // start from 0
while (start_index < rule_len) { // until we reached end of rule
size_t last_index = start_index + MAX_RULE_SIZE - 3; // set max length to what would fit uncompressed, i.e. MAX_RULE_SIZE - 3 (first NULL + length + last NULL)
if (last_index < rule_len) { // if we didn't reach the end, try to shorten to last space character
int32_t next_index = rule.lastIndexOf(" ", last_index);
if (next_index > start_index) { // if space was found and is not before start_index (i.e. we are progressing)
last_index = next_index; // shrink to the last space
} // otherwise it means there are no spaces, we need to cut somewhere even if the result cannot be entered back
} else {
last_index = rule_len; // until the end of the rule
}
AddLog(LOG_LEVEL_INFO, PSTR("RUL: Rule%d %s%s"),
index, 0 == start_index ? PSTR("") : PSTR("+"),
rule.substring(start_index, last_index).c_str());
start_index = last_index + 1;
}
// we need to split the rule in chunks
// rule = rule.substring(0, MAX_RULE_SIZE);
// rule += F("...");
}
Response_P(PSTR("{\"%s%d\":{\"State\":\"%s\",\"Once\":\"%s\",\"StopOnError\":\"%s\",\"Length\":%d,\"Free\":%d,\"Rules\":\"%s\"}}"),
XdrvMailbox.command, index, GetStateText(bitRead(Settings->rule_enabled, index -1)), GetStateText(bitRead(Settings->rule_once, index -1)),
GetStateText(bitRead(Settings->rule_stop, index -1)),
rule_len, MAX_RULE_SIZE - GetRuleLenStorage(index - 1),
EscapeJSONString(rule.c_str()).c_str());
}
}
void CmndRuleTimer(void)
{
if (XdrvMailbox.index > MAX_RULE_TIMERS) { return; }
uint32_t i = XdrvMailbox.index;
uint32_t max_i = XdrvMailbox.index;
if (0 == i) {
i = 1;
max_i = MAX_RULE_TIMERS;
}
#ifdef USE_EXPRESSION
float timer_set = evaluateExpression(XdrvMailbox.data, XdrvMailbox.data_len);
timer_set = (timer_set > 0) ? millis() + (1000 * timer_set) : 0;
#else
uint32_t timer_set = (XdrvMailbox.payload > 0) ? millis() + (1000 * XdrvMailbox.payload) : 0;
#endif // USE_EXPRESSION
if (XdrvMailbox.data_len > 0) {
for ( ; i <= max_i ; ++i ) {
Rules.timer[i -1] = timer_set;
}
}
ResponseClear();
for (i = 0; i < MAX_RULE_TIMERS; i++) {
ResponseAppend_P(PSTR("%c\"T%d\":%d"), (i) ? ',' : '{', i +1, (Rules.timer[i]) ? (Rules.timer[i] - millis()) / 1000 : 0);
}
ResponseJsonEnd();
}
void CmndEvent(void)
{
if (XdrvMailbox.data_len > 0) {
strlcpy(Rules.event_data, XdrvMailbox.data, sizeof(Rules.event_data));
#ifdef USE_DEVICE_GROUPS
if (!XdrvMailbox.grpflg) SendDeviceGroupMessage(1, DGR_MSGTYP_UPDATE, DGR_ITEM_EVENT, XdrvMailbox.data);
#endif // USE_DEVICE_GROUPS
}
if (XdrvMailbox.command) ResponseCmndDone();
}
void CmndVariable(void)
{
if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= MAX_RULE_VARS)) {
if (!XdrvMailbox.usridx) {
ResponseClear();
for (uint32_t i = 0; i < MAX_RULE_VARS; i++) {
ResponseAppend_P(PSTR("%c\"Var%d\":\"%s\""), (i) ? ',' : '{', i +1, rules_vars[i]);
}
ResponseJsonEnd();
} else {
if (XdrvMailbox.data_len > 0) {
#ifdef USE_EXPRESSION
if (XdrvMailbox.data[0] == '=') { // Spaces already been skipped in data
dtostrfd(evaluateExpression(XdrvMailbox.data + 1, XdrvMailbox.data_len - 1), Settings->flag2.calc_resolution, rules_vars[XdrvMailbox.index -1]);
} else {
strlcpy(rules_vars[XdrvMailbox.index -1], ('"' == XdrvMailbox.data[0]) ? "" : XdrvMailbox.data, sizeof(rules_vars[XdrvMailbox.index -1]));
}
#else
strlcpy(rules_vars[XdrvMailbox.index -1], ('"' == XdrvMailbox.data[0]) ? "" : XdrvMailbox.data, sizeof(rules_vars[XdrvMailbox.index -1]));
#endif // USE_EXPRESSION
bitSet(Rules.vars_event, XdrvMailbox.index -1);
}
ResponseCmndIdxChar(rules_vars[XdrvMailbox.index -1]);
}
}
}
void CmndMemory(void)
{
if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= MAX_RULE_MEMS)) {
if (!XdrvMailbox.usridx) {
ResponseCmndAll(SET_MEM1, MAX_RULE_MEMS);
} else {
if (XdrvMailbox.data_len > 0) {
#ifdef USE_EXPRESSION
if (XdrvMailbox.data[0] == '=') { // Spaces already been skipped in data
char rules_mem[FLOATSZ];
dtostrfd(evaluateExpression(XdrvMailbox.data + 1, XdrvMailbox.data_len - 1), Settings->flag2.calc_resolution, rules_mem);
SettingsUpdateText(SET_MEM1 + XdrvMailbox.index -1, rules_mem);
} else {
SettingsUpdateText(SET_MEM1 + XdrvMailbox.index -1, ('"' == XdrvMailbox.data[0]) ? "" : XdrvMailbox.data);
}
#else
SettingsUpdateText(SET_MEM1 + XdrvMailbox.index -1, ('"' == XdrvMailbox.data[0]) ? "" : XdrvMailbox.data);
#endif // USE_EXPRESSION
bitSet(Rules.mems_event, XdrvMailbox.index -1);
}
ResponseCmndIdxChar(SettingsText(SET_MEM1 + XdrvMailbox.index -1));
}
}
}
void CmndCalcResolution(void)
{
if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 7)) {
Settings->flag2.calc_resolution = XdrvMailbox.payload;
}
ResponseCmndNumber(Settings->flag2.calc_resolution);
}
void CmndAddition(void)
{
if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= MAX_RULE_VARS)) {
if (XdrvMailbox.data_len > 0) {
float tempvar = CharToFloat(rules_vars[XdrvMailbox.index -1]) + CharToFloat(XdrvMailbox.data);
dtostrfd(tempvar, Settings->flag2.calc_resolution, rules_vars[XdrvMailbox.index -1]);
bitSet(Rules.vars_event, XdrvMailbox.index -1);
}
ResponseCmndIdxChar(rules_vars[XdrvMailbox.index -1]);
}
}
void CmndSubtract(void)
{
if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= MAX_RULE_VARS)) {
if (XdrvMailbox.data_len > 0) {
float tempvar = CharToFloat(rules_vars[XdrvMailbox.index -1]) - CharToFloat(XdrvMailbox.data);
dtostrfd(tempvar, Settings->flag2.calc_resolution, rules_vars[XdrvMailbox.index -1]);
bitSet(Rules.vars_event, XdrvMailbox.index -1);
}
ResponseCmndIdxChar(rules_vars[XdrvMailbox.index -1]);
}
}
void CmndMultiply(void)
{
if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= MAX_RULE_VARS)) {
if (XdrvMailbox.data_len > 0) {
float tempvar = CharToFloat(rules_vars[XdrvMailbox.index -1]) * CharToFloat(XdrvMailbox.data);
dtostrfd(tempvar, Settings->flag2.calc_resolution, rules_vars[XdrvMailbox.index -1]);
bitSet(Rules.vars_event, XdrvMailbox.index -1);
}
ResponseCmndIdxChar(rules_vars[XdrvMailbox.index -1]);
}
}
void CmndScale(void)
{
if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= MAX_RULE_VARS)) {
if (XdrvMailbox.data_len > 0) {
if (ArgC() == 5) { // Process parameter entry
char argument[XdrvMailbox.data_len];
float valueIN = CharToFloat(ArgV(argument, 1));
float fromLow = CharToFloat(ArgV(argument, 2));
float fromHigh = CharToFloat(ArgV(argument, 3));
float toLow = CharToFloat(ArgV(argument, 4));
float toHigh = CharToFloat(ArgV(argument, 5));
float value = map_double(valueIN, fromLow, fromHigh, toLow, toHigh);
dtostrfd(value, Settings->flag2.calc_resolution, rules_vars[XdrvMailbox.index -1]);
bitSet(Rules.vars_event, XdrvMailbox.index -1);
} else {
ResponseCmndIdxError();
return;
}
}
ResponseCmndIdxChar(rules_vars[XdrvMailbox.index -1]);
}
}
float map_double(float x, float in_min, float in_max, float out_min, float out_max)
{
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
/*********************************************************************************************\
* Interface
\*********************************************************************************************/
bool Xdrv10(uint32_t function)
{
bool result = false;
switch (function) {
case FUNC_EVERY_50_MSECOND:
RulesEvery50ms();
break;
case FUNC_EVERY_100_MSECOND:
RulesEvery100ms();
break;
case FUNC_EVERY_SECOND:
RulesEverySecond();
break;
case FUNC_SET_POWER:
RulesSetPower();
break;
case FUNC_COMMAND:
result = DecodeCommand(kRulesCommands, RulesCommand);
break;
case FUNC_RULES_PROCESS:
result = RulesProcess();
break;
case FUNC_TELEPERIOD_RULES_PROCESS:
Rules.teleperiod = true;
result = RulesProcess();
Rules.teleperiod = false;
break;
case FUNC_SAVE_BEFORE_RESTART:
RulesSaveBeforeRestart();
break;
#ifdef SUPPORT_MQTT_EVENT
case FUNC_MQTT_DATA:
result = RulesMqttData();
break;
#endif // SUPPORT_MQTT_EVENT
case FUNC_PRE_INIT:
RulesInit();
break;
}
return result;
}
#endif // Do not USE_SCRIPT
#endif // USE_RULES