diff --git a/sonoff/_releasenotes.ino b/sonoff/_releasenotes.ino index 72db773c9..c441078ef 100644 --- a/sonoff/_releasenotes.ino +++ b/sonoff/_releasenotes.ino @@ -1,6 +1,7 @@ /* 5.12.0k - * Prepare for simple rules by enlarging Settings area to now 2048 bytes + * Prepare for simple rules of up to 255 characters by enlarging Settings area to now 2048 bytes * Change Timer parameter name from Power to Action + * Add commands Publish, Rule, RuleTimer and Event. See Wiki about Rule restriction, usage and examples * Fix freeing more code space when emulation is disabled (#1592) * Fix update temperature on DS18x20 drivers (#2328) * Fix compile error when not defined USE_TIMERS (#2400) diff --git a/sonoff/i18n.h b/sonoff/i18n.h index be258ae7e..ce984f1e4 100644 --- a/sonoff/i18n.h +++ b/sonoff/i18n.h @@ -138,28 +138,6 @@ #define D_RSLT_WARNING "WARNING" // Commands sonoff.ino -#define D_CMND_MQTTHOST "MqttHost" -#define D_CMND_MQTTPORT "MqttPort" -#define D_CMND_MQTTRETRY "MqttRetry" -#define D_CMND_STATETEXT "StateText" -#define D_CMND_MQTTFINGERPRINT "MqttFingerprint" -#define D_CMND_MQTTCLIENT "MqttClient" -#define D_CMND_MQTTUSER "MqttUser" -#define D_CMND_MQTTPASSWORD "MqttPassword" -#define D_CMND_FULLTOPIC "FullTopic" -#define D_CMND_PREFIX "Prefix" - #define PRFX_MAX_STRING_LENGTH 5 - #define D_CMND "cmnd" - #define D_STAT "stat" - #define D_TELE "tele" -#define D_CMND_GROUPTOPIC "GroupTopic" -#define D_CMND_TOPIC "Topic" -#define D_CMND_BUTTONTOPIC "ButtonTopic" -#define D_CMND_SWITCHTOPIC "SwitchTopic" -#define D_CMND_BUTTONRETAIN "ButtonRetain" -#define D_CMND_SWITCHRETAIN "SwitchRetain" -#define D_CMND_POWERRETAIN "PowerRetain" -#define D_CMND_SENSORRETAIN "SensorRetain" #define D_CMND_BACKLOG "Backlog" #define D_CMND_DELAY "Delay" #define D_CMND_STATUS "Status" @@ -251,6 +229,31 @@ #define D_CMND_BAUDRATE "Baudrate" #define D_CMND_EXCEPTION "Exception" +// Commands xdrv_00_mqtt.ino +#define D_CMND_MQTTHOST "MqttHost" +#define D_CMND_MQTTPORT "MqttPort" +#define D_CMND_MQTTRETRY "MqttRetry" +#define D_CMND_STATETEXT "StateText" +#define D_CMND_MQTTFINGERPRINT "MqttFingerprint" +#define D_CMND_MQTTCLIENT "MqttClient" +#define D_CMND_MQTTUSER "MqttUser" +#define D_CMND_MQTTPASSWORD "MqttPassword" +#define D_CMND_FULLTOPIC "FullTopic" +#define D_CMND_PREFIX "Prefix" + #define PRFX_MAX_STRING_LENGTH 5 + #define D_CMND "cmnd" + #define D_STAT "stat" + #define D_TELE "tele" +#define D_CMND_GROUPTOPIC "GroupTopic" +#define D_CMND_TOPIC "Topic" +#define D_CMND_BUTTONTOPIC "ButtonTopic" +#define D_CMND_SWITCHTOPIC "SwitchTopic" +#define D_CMND_BUTTONRETAIN "ButtonRetain" +#define D_CMND_SWITCHRETAIN "SwitchRetain" +#define D_CMND_POWERRETAIN "PowerRetain" +#define D_CMND_SENSORRETAIN "SensorRetain" +#define D_CMND_PUBLISH "Publish" + // Commands xdrv_01_light.ino #define D_CMND_CHANNEL "Channel" #define D_CMND_COLOR "Color" diff --git a/sonoff/sonoff.h b/sonoff/sonoff.h index ed1a1c34b..2d8251894 100644 --- a/sonoff/sonoff.h +++ b/sonoff/sonoff.h @@ -166,9 +166,7 @@ enum LichtSubtypes {LST_NONE, LST_SINGLE, LST_COLDWARM, LST_RGB, LST_RGBW, LST_R enum LichtSchemes {LS_POWER, LS_WAKEUP, LS_CYCLEUP, LS_CYCLEDN, LS_RANDOM, LS_MAX}; enum XsnsFunctions {FUNC_INIT, FUNC_LOOP, FUNC_EVERY_50_MSECOND, FUNC_EVERY_SECOND, FUNC_PREP_BEFORE_TELEPERIOD, FUNC_JSON_APPEND, FUNC_WEB_APPEND, FUNC_SAVE_BEFORE_RESTART, - FUNC_COMMAND, FUNC_NTP_INIT, FUNC_NTP_SET, FUNC_CLOCK_TIMER, - FUNC_MQTT_SUBSCRIBE, FUNC_MQTT_INIT, FUNC_MQTT_DATA, FUNC_MQTT_DISCONNECTED, FUNC_MQTT_CONNECTED, - FUNC_SET_POWER, FUNC_SHOW_SENSOR}; + FUNC_COMMAND, FUNC_MQTT_SUBSCRIBE, FUNC_MQTT_INIT, FUNC_MQTT_DATA, FUNC_SET_POWER, FUNC_SHOW_SENSOR}; const uint8_t kDefaultRfCode[9] PROGMEM = { 0x21, 0x16, 0x01, 0x0E, 0x03, 0x48, 0x2E, 0x1A, 0x00 }; diff --git a/sonoff/sonoff.ino b/sonoff/sonoff.ino index d03777048..7e6bc2c97 100644 --- a/sonoff/sonoff.ino +++ b/sonoff/sonoff.ino @@ -1101,7 +1101,7 @@ void MqttDataHandler(char* topic, byte* data, unsigned int data_len) /********************************************************************************************/ -boolean send_button_power(byte key, byte device, byte state) +boolean SendKey(byte key, byte device, byte state) { // key 0 = button_topic // key 1 = switch_topic @@ -1130,13 +1130,18 @@ boolean send_button_power(byte key, byte device, byte state) snprintf_P(mqtt_data, sizeof(mqtt_data), GetStateText(state)); } #ifdef USE_DOMOTICZ - if (!(DomoticzButton(key, device, state, strlen(mqtt_data)))) { + if (!(DomoticzSendKey(key, device, state, strlen(mqtt_data)))) { MqttPublishDirect(stopic, (key) ? Settings.flag.mqtt_switch_retain : Settings.flag.mqtt_button_retain); } #else MqttPublishDirect(stopic, (key) ? Settings.flag.mqtt_switch_retain : Settings.flag.mqtt_button_retain); #endif // USE_DOMOTICZ result = true; +#ifdef USE_RULES + } else { + snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"%s%d\":{\"State\":%d}}"), (key) ? "Switch" : "Button", device, state); + result = RulesProcess(); +#endif // USE_RULES } #ifdef USE_KNX KNX_Send_Button_Power(key, device, state); @@ -1533,7 +1538,7 @@ void ButtonHandler() if (!holdbutton[button_index]) button_pressed = true; // Do not allow within 1 second } if (button_pressed) { - if (!send_button_power(0, button_index +1, POWER_TOGGLE)) { // Execute Toggle command via MQTT if ButtonTopic is set + if (!SendKey(0, button_index +1, POWER_TOGGLE)) { // Execute Toggle command via MQTT if ButtonTopic is set ExecuteCommandPower(button_index +1, POWER_TOGGLE); // Execute Toggle command internally } } @@ -1542,7 +1547,7 @@ void ButtonHandler() if (Settings.flag.button_single) { // Allow only single button press for immediate action snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_APPLICATION D_BUTTON "%d " D_IMMEDIATE), button_index +1); AddLog(LOG_LEVEL_DEBUG); - if (!send_button_power(0, button_index +1, POWER_TOGGLE)) { // Execute Toggle command via MQTT if ButtonTopic is set + if (!SendKey(0, button_index +1, POWER_TOGGLE)) { // Execute Toggle command via MQTT if ButtonTopic is set ExecuteCommandPower(button_index +1, POWER_TOGGLE); // Execute Toggle command internally } } else { @@ -1568,7 +1573,7 @@ void ButtonHandler() if (Settings.flag.button_restrict) { // Button restriction if (holdbutton[button_index] == Settings.param[P_HOLD_TIME] * (STATES / 10)) { // Button hold multipress[button_index] = 0; - send_button_power(0, button_index +1, 3); // Execute Hold command via MQTT if ButtonTopic is set + SendKey(0, button_index +1, 3); // Execute Hold command via MQTT if ButtonTopic is set } } else { if (holdbutton[button_index] == (Settings.param[P_HOLD_TIME] * (STATES / 10)) * hold_time_extent) { // Button held for factor times longer @@ -1594,7 +1599,7 @@ void ButtonHandler() multipress[button_index] = 1; } } - if (single_press && send_button_power(0, button_index + multipress[button_index], POWER_TOGGLE)) { // Execute Toggle command via MQTT if ButtonTopic is set + if (single_press && SendKey(0, button_index + multipress[button_index], POWER_TOGGLE)) { // Execute Toggle command via MQTT if ButtonTopic is set // Success } else { if (multipress[button_index] < 3) { // Single or Double press @@ -1635,7 +1640,7 @@ void SwitchHandler() if (holdwallswitch[i]) { holdwallswitch[i]--; if (0 == holdwallswitch[i]) { - send_button_power(1, i +1, 3); // Execute command via MQTT + SendKey(1, i +1, 3); // Execute command via MQTT } } @@ -1688,7 +1693,7 @@ void SwitchHandler() } if (switchflag < 3) { - if (!send_button_power(1, i +1, switchflag)) { // Execute command via MQTT + if (!SendKey(1, i +1, switchflag)) { // Execute command via MQTT ExecuteCommandPower(i +1, switchflag); // Execute command internally (if i < devices_present) } } diff --git a/sonoff/support.ino b/sonoff/support.ino index 2a2ea7625..6b17f0b75 100644 --- a/sonoff/support.ino +++ b/sonoff/support.ino @@ -1376,12 +1376,14 @@ void RtcSecond() snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_APPLICATION "(" D_UTC_TIME ") %s, (" D_DST_TIME ") %s, (" D_STD_TIME ") %s"), GetTime(0).c_str(), GetTime(2).c_str(), GetTime(3).c_str()); AddLog(LOG_LEVEL_DEBUG); - +#ifdef USE_RULES if (local_time < 1451602800) { // 2016-01-01 - XdrvCall(FUNC_NTP_INIT); + strncpy_P(mqtt_data, PSTR("{\"Time\":{\"Initialized\":1}}"), sizeof(mqtt_data)); } else { - XdrvCall(FUNC_NTP_SET); + strncpy_P(mqtt_data, PSTR("{\"Time\":{\"Set\":1}}"), sizeof(mqtt_data)); } + RulesProcess(); +#endif // USE_RULES } else { ntp_sync_minute++; // Try again in next minute } diff --git a/sonoff/user_config.h b/sonoff/user_config.h index c580a629a..c85a63868 100644 --- a/sonoff/user_config.h +++ b/sonoff/user_config.h @@ -227,6 +227,9 @@ #define USE_TIMERS_WEB // Add timer webpage support (+4k5 code) #define USE_SUNRISE // Add support for Sunrise and sunset tools (+16k) +// -- Rules --------------------------------------- +#define USE_RULES // Add support for rules (+4k4 code) + // -- Internal Analog input ----------------------- #define USE_ADC_VCC // Display Vcc in Power status. Disable for use as Analog input on selected devices diff --git a/sonoff/xdrv_00_mqtt.ino b/sonoff/xdrv_00_mqtt.ino index ca74724c3..fa478b7a7 100644 --- a/sonoff/xdrv_00_mqtt.ino +++ b/sonoff/xdrv_00_mqtt.ino @@ -42,11 +42,11 @@ enum MqttCommands { CMND_MQTTHOST, CMND_MQTTPORT, CMND_MQTTRETRY, CMND_STATETEXT, CMND_MQTTFINGERPRINT, CMND_MQTTCLIENT, - CMND_MQTTUSER, CMND_MQTTPASSWORD, CMND_FULLTOPIC, CMND_PREFIX, CMND_GROUPTOPIC, CMND_TOPIC, + CMND_MQTTUSER, CMND_MQTTPASSWORD, CMND_FULLTOPIC, CMND_PREFIX, CMND_GROUPTOPIC, CMND_TOPIC, CMND_PUBLISH, CMND_BUTTONTOPIC, CMND_SWITCHTOPIC, CMND_BUTTONRETAIN, CMND_SWITCHRETAIN, CMND_POWERRETAIN, CMND_SENSORRETAIN }; const char kMqttCommands[] PROGMEM = D_CMND_MQTTHOST "|" D_CMND_MQTTPORT "|" D_CMND_MQTTRETRY "|" D_CMND_STATETEXT "|" D_CMND_MQTTFINGERPRINT "|" D_CMND_MQTTCLIENT "|" - D_CMND_MQTTUSER "|" D_CMND_MQTTPASSWORD "|" D_CMND_FULLTOPIC "|" D_CMND_PREFIX "|" D_CMND_GROUPTOPIC "|" D_CMND_TOPIC "|" + D_CMND_MQTTUSER "|" D_CMND_MQTTPASSWORD "|" D_CMND_FULLTOPIC "|" D_CMND_PREFIX "|" D_CMND_GROUPTOPIC "|" D_CMND_TOPIC "|" D_CMND_PUBLISH "|" D_CMND_BUTTONTOPIC "|" D_CMND_SWITCHTOPIC "|" D_CMND_BUTTONRETAIN "|" D_CMND_SWITCHRETAIN "|" D_CMND_POWERRETAIN "|" D_CMND_SENSORRETAIN ; uint8_t mqtt_retry_counter = 1; // MQTT connection retry counter @@ -316,8 +316,10 @@ void MqttDisconnected(int state) snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_MQTT D_CONNECT_FAILED_TO " %s:%d, rc %d. " D_RETRY_IN " %d " D_UNIT_SECOND), Settings.mqtt_host, Settings.mqtt_port, state, mqtt_retry_counter); AddLog(LOG_LEVEL_INFO); - - XdrvCall(FUNC_MQTT_DISCONNECTED); +#ifdef USE_RULES + strncpy_P(mqtt_data, PSTR("{\"MQTT\":{\"Disconnected\":1}}"), sizeof(mqtt_data)); + RulesProcess(); +#endif // USE_RULES } void MqttConnected() @@ -369,12 +371,17 @@ void MqttConnected() tele_period = Settings.tele_period -9; } status_update_timer = 2; - +#ifdef USE_RULES + strncpy_P(mqtt_data, PSTR("{\"System\":{\"Boot\":1}}"), sizeof(mqtt_data)); + RulesProcess(); +#endif // USE_RULES XdrvCall(FUNC_MQTT_INIT); } mqtt_initial_connection_state = 0; - - XdrvCall(FUNC_MQTT_CONNECTED); +#ifdef USE_RULES + strncpy_P(mqtt_data, PSTR("{\"MQTT\":{\"Connected\":1}}"), sizeof(mqtt_data)); + RulesProcess(); +#endif // USE_RULES } #ifdef USE_MQTT_TLS @@ -614,6 +621,22 @@ bool MqttCommand() } snprintf_P(mqtt_data, sizeof(mqtt_data), S_JSON_COMMAND_INDEX_SVALUE, command, index, Settings.mqtt_prefix[index -1]); } + else if (CMND_PUBLISH == command_code) { + if (data_len > 0) { + char *mqtt_part = strtok(dataBuf, " "); + if (mqtt_part) { + snprintf(stemp1, sizeof(stemp1), mqtt_part); + mqtt_part = strtok(NULL, " "); + if (mqtt_part) { + snprintf(mqtt_data, sizeof(mqtt_data), mqtt_part); + } else { + mqtt_data[0] = '\0'; + } + MqttPublishDirect(stemp1, false); + snprintf_P(mqtt_data, sizeof(mqtt_data), S_JSON_COMMAND_SVALUE, command, D_JSON_DONE); + } + } + } else if (CMND_GROUPTOPIC == command_code) { if ((data_len > 0) && (data_len < sizeof(Settings.mqtt_grptopic))) { MakeValidMqtt(0, dataBuf); @@ -658,7 +681,7 @@ bool MqttCommand() strlcpy(Settings.button_topic, mqtt_topic, sizeof(Settings.button_topic)); if (!payload) { for(i = 1; i <= MAX_KEYS; i++) { - send_button_power(0, i, 9); // Clear MQTT retain in broker + SendKey(0, i, 9); // Clear MQTT retain in broker } } Settings.flag.mqtt_button_retain = payload; @@ -670,7 +693,7 @@ bool MqttCommand() strlcpy(Settings.button_topic, mqtt_topic, sizeof(Settings.button_topic)); if (!payload) { for(i = 1; i <= MAX_SWITCHES; i++) { - send_button_power(1, i, 9); // Clear MQTT retain in broker + SendKey(1, i, 9); // Clear MQTT retain in broker } } Settings.flag.mqtt_switch_retain = payload; diff --git a/sonoff/xdrv_05_domoticz.ino b/sonoff/xdrv_05_domoticz.ino index ea2b39319..38a979916 100644 --- a/sonoff/xdrv_05_domoticz.ino +++ b/sonoff/xdrv_05_domoticz.ino @@ -265,7 +265,7 @@ boolean DomoticzCommand() return serviced; } -boolean DomoticzButton(byte key, byte device, byte state, byte svalflg) +boolean DomoticzSendKey(byte key, byte device, byte state, byte svalflg) { if ((Settings.domoticz_key_idx[device -1] || Settings.domoticz_switch_idx[device -1]) && (svalflg)) { snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"command\":\"switchlight\",\"idx\":%d,\"switchcmd\":\"%s\"}"), diff --git a/sonoff/xdrv_09_timers.ino b/sonoff/xdrv_09_timers.ino index 35d45ec18..9af486d38 100644 --- a/sonoff/xdrv_09_timers.ino +++ b/sonoff/xdrv_09_timers.ino @@ -224,10 +224,10 @@ void TimerEverySecond() Settings.timer[i].arm = Settings.timer[i].repeat; #ifdef USE_RULES if (3 == Settings.timer[i].power) { // Blink becomes Rule disregarding device and allowing use of Backlog commands - XdrvMailbox.index = i; - XdrvCall(FUNC_CLOCK_TIMER); + snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"Clock\":{\"Timer\":%d}}"), i +1); + RulesProcess(); } else -#endif +#endif // USE_RULES ExecuteCommandPower(Settings.timer[i].device +1, Settings.timer[i].power); } } diff --git a/sonoff/xdrv_10_rules.ino b/sonoff/xdrv_10_rules.ino new file mode 100644 index 000000000..61742f0b6 --- /dev/null +++ b/sonoff/xdrv_10_rules.ino @@ -0,0 +1,443 @@ +/* + xdrv_10_rules.ino - rule support for Sonoff-Tasmota + + Copyright (C) 2018 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 . +*/ + +#ifdef USE_RULES +/*********************************************************************************************\ + * Rules based heavily on ESP Easy implementation + * + * Inspiration: https://github.com/letscontrolit/ESPEasy + * + * Add rules using the following, case insensitive, format: + * on do endon on do 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#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 %eventvalue% endon + * on power1#state=1 do color 001000 endon + * on button1#state do publish cmnd/ring2/power %eventvalue% endon on button2#state do publish cmnd/strip1/power %eventvalue% endon + * on switch1#state do power2 %eventvalue% endon + * + * Notes: + * Spaces after , around and before 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 MAX_RULE_TIMERS 8 + +#define ULONG_MAX 0xffffffffUL + +#define D_CMND_RULE "Rule" +#define D_CMND_RULETIMER "RuleTimer" +#define D_CMND_EVENT "Event" + +#define D_JSON_INITIATED "Initiated" + +enum RulesCommands { CMND_RULE, CMND_RULETIMER, CMND_EVENT }; +const char kRulesCommands[] PROGMEM = D_CMND_RULE "|" D_CMND_RULETIMER "|" D_CMND_EVENT ; + +String rules_event_value; +unsigned long rules_timer[MAX_RULE_TIMERS] = { 0 }; +uint8_t rules_quota = 0; +long rules_power = -1; + +uint32_t rules_triggers = 0; +uint8_t rules_trigger_count = 0; + +/*******************************************************************************************/ + +long TimeDifference(unsigned long prev, unsigned long next) +{ + // Return the time difference as a signed value, taking into account the timers may overflow. + // Returned timediff is between -24.9 days and +24.9 days. + // Returned value is positive when "next" is after "prev" + long signed_diff = 0; + // To cast a value to a signed long, the difference may not exceed half the ULONG_MAX + const unsigned long half_max_unsigned_long = 2147483647u; // = 2^31 -1 + if (next >= prev) { + const unsigned long diff = next - prev; + if (diff <= half_max_unsigned_long) { // Normal situation, just return the difference. + signed_diff = static_cast(diff); // Difference is a positive value. + } else { + // prev has overflow, return a negative difference value + signed_diff = static_cast((ULONG_MAX - next) + prev + 1u); + signed_diff = -1 * signed_diff; + } + } else { + // next < prev + const unsigned long diff = prev - next; + if (diff <= half_max_unsigned_long) { // Normal situation, return a negative difference value + signed_diff = static_cast(diff); + signed_diff = -1 * signed_diff; + } else { + // next has overflow, return a positive difference value + signed_diff = static_cast((ULONG_MAX - prev) + next + 1u); + } + } + return signed_diff; +} + +long TimePassedSince(unsigned long timestamp) +{ + // Compute the number of milliSeconds passed since timestamp given. + // Note: value can be negative if the timestamp has not yet been reached. + return TimeDifference(timestamp, millis()); +} + +bool TimeReached(unsigned long timer) +{ + // Check if a certain timeout has been reached. + const long passed = TimePassedSince(timer); + return (passed >= 0); +} + +/*******************************************************************************************/ + +bool RulesRuleMatch(String &event, String &rule) +{ + // event = {"INA219":{"Voltage":4.494,"Current":0.020,"Power":0.089}} + // event = {"System":{"Boot":1}} + // rule = "INA219#CURRENT>0.100" + + bool match = false; + + // Step1: Analyse rule + int pos = rule.indexOf('#'); + if (pos == -1) return false; // No # sign in rule + + String rule_task = rule.substring(0, pos); // "INA219" or "SYSTEM" + String rule_name = rule.substring(pos +1); // "CURRENT>0.100" or "BOOT" + + char compare = ' '; + pos = rule_name.indexOf(">"); + if (pos > 0) { + compare = '>'; + } else { + pos = rule_name.indexOf("<"); + if (pos > 0) { + compare = '<'; + } else { + pos = rule_name.indexOf("="); + if (pos > 0) { + compare = '='; + } + } + } + + String tmp_value = "none"; + double rule_value = 0; + if (pos > 0) { + tmp_value = rule_name.substring(pos + 1); // "0.100" + rule_value = CharToDouble((char*)tmp_value.c_str()); // 0.1 - This saves 9k code over toFLoat()! + rule_name = rule_name.substring(0, pos); // "CURRENT" + } + + // Step2: Search rule_task and rule_name + StaticJsonBuffer<400> jsonBuf; + JsonObject &root = jsonBuf.parseObject(event); + if (!root.success()) return false; // No valid JSON data + + double value = 0; + const char* str_value = root[rule_task][rule_name]; + +// snprintf_P(log_data, sizeof(log_data), PSTR("RUL: Task %s, Name %s, Value %s, TrigCnt %d, TrigSt %d, Source %s, Json %s"), +// rule_task.c_str(), rule_name.c_str(), tmp_value.c_str(), rules_trigger_count, bitRead(rules_triggers, rules_trigger_count), event.c_str(), (str_value) ? str_value : "none"); +// AddLog(LOG_LEVEL_DEBUG); + + if (!root[rule_task][rule_name].success()) return false; + // No value but rule_name is ok + + rules_event_value = str_value; // Prepare %eventvalue% + + // Step 3: Compare rule (value) + if (str_value) { + value = CharToDouble((char*)str_value); + switch (compare) { + case '>': + if (value > rule_value) match = true; + break; + case '<': + if (value < rule_value) match = true; + break; + case '=': + if (value == rule_value) match = true; + break; + case ' ': + match = true; // Json value but not needed + break; + } + } else match = true; + + if (Settings.flag.rules_once) { + if (match) { // Only allow match state changes + if (!bitRead(rules_triggers, rules_trigger_count)) { + bitSet(rules_triggers, rules_trigger_count); + } else { + match = false; + } + } else { + bitClear(rules_triggers, rules_trigger_count); + } + } + + return match; +} + +/*******************************************************************************************/ + +bool RulesProcess() +{ + bool serviced = false; + + if (!Settings.flag.rules_enabled) return serviced; // Not enabled + if (!strlen(Settings.rules)) return serviced; // No rules + + String event_saved = mqtt_data; + event_saved.toUpperCase(); + String rules = Settings.rules; + + rules_trigger_count = 0; + int plen = 0; + 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("ON ")) return serviced; // Bad syntax - Nothing to start on + + int pevt = rule.indexOf(" DO "); + if (pevt == -1) return serviced; // Bad syntax - Nothing to do + String event_trigger = rule.substring(3, pevt); // "INA219#CURRENT>0.100" + + plen = rule.indexOf(" ENDON"); + if (plen == -1) return serviced; // Bad syntax - No endon + String commands = rules.substring(pevt +4, plen); // "Backlog Dimmer 10;Color 100000" + plen += 6; + +// snprintf_P(log_data, sizeof(log_data), PSTR("RUL: Trigger |%s|, Commands |%s|"), event_trigger.c_str(), commands.c_str()); +// AddLog(LOG_LEVEL_DEBUG); + + rules_event_value = ""; + String event = event_saved; + if (RulesRuleMatch(event, event_trigger)) { + commands.replace(F("%eventvalue%"), rules_event_value); + char command[commands.length() +1]; + snprintf(command, sizeof(command), commands.c_str()); + + snprintf_P(log_data, sizeof(log_data), PSTR("RUL: %s performs \"%s\""), event_trigger.c_str(), command); + AddLog(LOG_LEVEL_INFO); + +// snprintf_P(mqtt_data, sizeof(mqtt_data), S_JSON_COMMAND_SVALUE, D_CMND_RULE, D_JSON_INITIATED); +// MqttPublishPrefixTopic_P(RESULT_OR_STAT, PSTR(D_CMND_RULE)); + + ExecuteCommand(command); + serviced = true; + } + rules_trigger_count++; + } + return serviced; +} + +/*******************************************************************************************/ + +void RulesInit() +{ + if (Settings.rules[0] == '\0') { + Settings.flag.rules_enabled = 0; + Settings.flag.rules_once = 0; + } +} + +void RulesSetPower() +{ + if (Settings.flag.rules_enabled) { + uint16_t new_power = XdrvMailbox.index; + if (rules_power == -1) rules_power = new_power; + uint16_t old_power = rules_power; + rules_power = new_power; + for (byte i = 0; i < devices_present; i++) { + uint8_t new_state = new_power &1; + uint8_t old_state = old_power &1; + if (new_state != old_state) { + snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"Power%d\":{\"State\":%d}}"), i +1, new_state); + RulesProcess(); + } + new_power >>= 1; + old_power >>= 1; + } + } +} + +void RulesEvery50ms() +{ + if (Settings.flag.rules_enabled) { + rules_quota++; + if (rules_quota &1) { // Every 100 ms + mqtt_data[0] = '\0'; + uint16_t tele_period_save = tele_period; + tele_period = 2; // Do not allow HA updates during next function call + XsnsNextCall(FUNC_JSON_APPEND); // ,"INA219":{"Voltage":4.494,"Current":0.020,"Power":0.089} + tele_period = tele_period_save; + if (strlen(mqtt_data)) { + mqtt_data[0] = '{'; // {"INA219":{"Voltage":4.494,"Current":0.020,"Power":0.089} + snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s}"), mqtt_data); + RulesProcess(); + } + } + } +} + +void RulesEverySecond() +{ + if (Settings.flag.rules_enabled) { + for (byte 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 + snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"Rules\":{\"Timer\":%d}}"), i +1); + RulesProcess(); + } + } + } + } +} + +boolean RulesCommand() +{ + char command[CMDSZ]; + boolean serviced = true; + uint8_t index = XdrvMailbox.index; + + int command_code = GetCommandCode(command, sizeof(command), XdrvMailbox.topic, kRulesCommands); + if (CMND_RULE == command_code) { + if ((XdrvMailbox.data_len > 0) && (XdrvMailbox.data_len < sizeof(Settings.rules))) { + if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 6)) { + switch (XdrvMailbox.payload) { + case 0: // Off + case 1: // On + Settings.flag.rules_enabled = XdrvMailbox.payload; + break; + case 2: // Toggle + Settings.flag.rules_enabled ^= 1; + break; + case 4: // Off + case 5: // On + Settings.flag.rules_once = XdrvMailbox.payload &1; + break; + case 6: // Toggle + Settings.flag.rules_once ^= 1; + break; + } + } else { +/* + String uc_data = XdrvMailbox.data; // Do not allow Rule to be used within a rule + uc_data.toUpperCase(); + String uc_command = command; + uc_command += " "; // Distuingish from RuleTimer + uc_command.toUpperCase(); + if (!uc_data.indexOf(uc_command)) strlcpy(Settings.rules, XdrvMailbox.data, sizeof(Settings.rules)); +*/ + strlcpy(Settings.rules, XdrvMailbox.data, sizeof(Settings.rules)); + } + rules_triggers = 0; // Reset once flag + } + snprintf_P (mqtt_data, sizeof(mqtt_data), PSTR("{\"%s\":\"%s\",\"Once\":\"%s\",\"Rules\":\"%s\"}"), command, GetStateText(Settings.flag.rules_enabled), GetStateText(Settings.flag.rules_once), Settings.rules); + } + else if ((CMND_RULETIMER == command_code) && (index > 0) && (index <= MAX_RULE_TIMERS)) { + if (XdrvMailbox.data_len > 0) { + rules_timer[index -1] = (XdrvMailbox.payload > 0) ? millis() + (1000 * XdrvMailbox.payload) : 0; + } + snprintf_P(mqtt_data, sizeof(mqtt_data), S_JSON_COMMAND_INDEX_LVALUE, command, index, (rules_timer[index -1]) ? (rules_timer[index -1] - millis()) / 1000 : 0); + } + else if (CMND_EVENT == command_code) { + if (XdrvMailbox.data_len > 0) { + String event = XdrvMailbox.data; + String parameter = ""; + int pos = event.indexOf('='); + if (pos > 0) { + parameter = event.substring(pos +1); + parameter.trim(); + event = event.substring(0, pos); + } + event.trim(); + snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"Event\":{\"%s\":\"%s\"}}"), event.c_str(), parameter.c_str()); + RulesProcess(); + } + snprintf_P(mqtt_data, sizeof(mqtt_data), S_JSON_COMMAND_SVALUE, command, D_JSON_DONE); + } + else serviced = false; + + return serviced; +} + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +#define XDRV_10 + +boolean Xdrv10(byte function) +{ + boolean result = false; + + switch (function) { + case FUNC_INIT: + RulesInit(); + break; + case FUNC_SET_POWER: + RulesSetPower(); + break; + case FUNC_EVERY_50_MSECOND: + RulesEvery50ms(); + break; + case FUNC_EVERY_SECOND: + RulesEverySecond(); + break; + case FUNC_COMMAND: + result = RulesCommand(); + break; + } + return result; +} + +#endif // USE_RULES \ No newline at end of file diff --git a/sonoff/xdrv_interface.ino b/sonoff/xdrv_interface.ino index 99a1600e8..09f056ece 100644 --- a/sonoff/xdrv_interface.ino +++ b/sonoff/xdrv_interface.ino @@ -182,17 +182,12 @@ boolean XdrvMqttData(char *topicBuf, uint16_t stopicBuf, char *dataBuf, uint16_t * FUNC_LOOP * FUNC_MQTT_SUBSCRIBE * FUNC_MQTT_INIT - * FUNC_MQTT_DISCONNECTED - * FUNC_MQTT_CONNECTED * return FUNC_MQTT_DATA * return FUNC_COMMAND * FUNC_SET_POWER * FUNC_SHOW_SENSOR * FUNC_EVERY_SECOND * FUNC_EVERY_50_MSECOND - * FUNC_NTP_INIT - * FUNC_NTP_SET - * FUNC_CLOCK_TIMER \*********************************************************************************************/ boolean XdrvCall(byte Function)