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)