From c527d4dc995a2e7d4f88abc3ab05b62c82ad1446 Mon Sep 17 00:00:00 2001 From: Laurent <44267323+laurentdong@users.noreply.github.com> Date: Mon, 18 Feb 2019 15:41:40 -0500 Subject: [PATCH 1/7] Support HttpGet command HttpGet command send a HTTP Get request to specified URL and return the response from website or error message if failed. Note: This command support URL encoding, so you do not have to do encode by yourself. For example replacing all " " with %20 is no necessary. Format: httpget With HttpGet command you can do a lot of things. For example: - Retrieve your physical location: httpget http://ipinfo.io/geo Result is a JSON object { "ip": "8.8.8.8", "city": "Mountain View", "region": "California", "country": "US", "loc": "37.3860,-122.0840", "postal": "94035", "phone": "650" } - Control another Sonoff switch directly: httpget http://192.168.1.130/cm?cmnd=power off --- sonoff/i18n.h | 1 + sonoff/sonoff.ino | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/sonoff/i18n.h b/sonoff/i18n.h index 7340a3484..f601b1095 100644 --- a/sonoff/i18n.h +++ b/sonoff/i18n.h @@ -272,6 +272,7 @@ #define D_JSON_GPIO "GPIO" #define D_JSON_FLAG "FLAG" #define D_JSON_BASE "BASE" +#define D_CMND_HTTP_GET "HttpGet" // Commands xdrv_01_mqtt.ino #define D_CMND_MQTTHOST "MqttHost" diff --git a/sonoff/sonoff.ino b/sonoff/sonoff.ino index e54c23260..3ce445a4b 100755 --- a/sonoff/sonoff.ino +++ b/sonoff/sonoff.ino @@ -75,7 +75,7 @@ enum TasmotaCommands { CMND_MODULE, CMND_MODULES, CMND_GPIO, CMND_GPIOS, CMND_PWM, CMND_PWMFREQUENCY, CMND_PWMRANGE, CMND_COUNTER, CMND_COUNTERTYPE, CMND_COUNTERDEBOUNCE, CMND_BUTTONDEBOUNCE, CMND_SWITCHDEBOUNCE, CMND_SLEEP, CMND_UPGRADE, CMND_UPLOAD, CMND_OTAURL, CMND_SERIALLOG, CMND_SYSLOG, CMND_LOGHOST, CMND_LOGPORT, CMND_IPADDRESS, CMND_NTPSERVER, CMND_AP, CMND_SSID, CMND_PASSWORD, CMND_HOSTNAME, - CMND_WIFICONFIG, CMND_FRIENDLYNAME, CMND_SWITCHMODE, CMND_INTERLOCK, CMND_TEMPLATE, + CMND_WIFICONFIG, CMND_FRIENDLYNAME, CMND_SWITCHMODE, CMND_INTERLOCK, CMND_TEMPLATE, CMND_HTTP_GET, CMND_TELEPERIOD, CMND_RESTART, CMND_RESET, CMND_TIMEZONE, CMND_TIMESTD, CMND_TIMEDST, CMND_ALTITUDE, CMND_LEDPOWER, CMND_LEDSTATE, CMND_I2CSCAN, CMND_SERIALSEND, CMND_BAUDRATE, CMND_SERIALDELIMITER, CMND_DRIVER }; const char kTasmotaCommands[] PROGMEM = @@ -85,7 +85,7 @@ const char kTasmotaCommands[] PROGMEM = D_CMND_MODULE "|" D_CMND_MODULES "|" D_CMND_GPIO "|" D_CMND_GPIOS "|" D_CMND_PWM "|" D_CMND_PWMFREQUENCY "|" D_CMND_PWMRANGE "|" D_CMND_COUNTER "|" D_CMND_COUNTERTYPE "|" D_CMND_COUNTERDEBOUNCE "|" D_CMND_BUTTONDEBOUNCE "|" D_CMND_SWITCHDEBOUNCE "|" D_CMND_SLEEP "|" D_CMND_UPGRADE "|" D_CMND_UPLOAD "|" D_CMND_OTAURL "|" D_CMND_SERIALLOG "|" D_CMND_SYSLOG "|" D_CMND_LOGHOST "|" D_CMND_LOGPORT "|" D_CMND_IPADDRESS "|" D_CMND_NTPSERVER "|" D_CMND_AP "|" D_CMND_SSID "|" D_CMND_PASSWORD "|" D_CMND_HOSTNAME "|" - D_CMND_WIFICONFIG "|" D_CMND_FRIENDLYNAME "|" D_CMND_SWITCHMODE "|" D_CMND_INTERLOCK "|" D_CMND_TEMPLATE "|" + D_CMND_WIFICONFIG "|" D_CMND_FRIENDLYNAME "|" D_CMND_SWITCHMODE "|" D_CMND_INTERLOCK "|" D_CMND_TEMPLATE "|" D_CMND_HTTP_GET "|" D_CMND_TELEPERIOD "|" D_CMND_RESTART "|" D_CMND_RESET "|" D_CMND_TIMEZONE "|" D_CMND_TIMESTD "|" D_CMND_TIMEDST "|" D_CMND_ALTITUDE "|" D_CMND_LEDPOWER "|" D_CMND_LEDSTATE "|" D_CMND_I2CSCAN "|" D_CMND_SERIALSEND "|" D_CMND_BAUDRATE "|" D_CMND_SERIALDELIMITER "|" D_CMND_DRIVER; @@ -1018,6 +1018,35 @@ void MqttDataHandler(char* topic, uint8_t* data, unsigned int data_len) } snprintf_P(mqtt_data, sizeof(mqtt_data), S_JSON_COMMAND_NVALUE, command, Settings.pwm_frequency); } + else if (CMND_HTTP_GET == command_code) { + //Test http get + if (data_len > 10 && strncasecmp_P(dataBuf, PSTR("HTTP://"),7) == 0) { + String sUrl = dataBuf; + String sResult = ""; + WiFiClient client; + HTTPClient http; + if (http.begin(client, UrlEncode(dataBuf))) { // HTTP + // start connection and send HTTP header + int httpCode = http.GET(); + + // httpCode will be negative on error + if (httpCode > 0) { + // HTTP header has been send and Server response header has been handled + // file found at server + if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) { + sResult = http.getString(); + } + } else { + //Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str()); + sResult = http.errorToString(httpCode); + } + http.end(); + } else { + sResult = "[HTTP} Unable to connect\n"; + } + snprintf_P(mqtt_data, sizeof(mqtt_data), S_JSON_COMMAND_SVALUE, command, sResult.c_str()); + } + } else if (CMND_PWMRANGE == command_code) { if ((1 == payload) || ((payload > 254) && (payload < 1024))) { Settings.pwm_range = (1 == payload) ? PWM_RANGE : payload; From 2bc84a9aad04762318eddcd53bec83348e7c45e3 Mon Sep 17 00:00:00 2001 From: Laurent <44267323+laurentdong@users.noreply.github.com> Date: Mon, 18 Feb 2019 16:12:14 -0500 Subject: [PATCH 2/7] Optimize RAM usage for Expression --- sonoff/xdrv_10_rules.ino | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/sonoff/xdrv_10_rules.ino b/sonoff/xdrv_10_rules.ino index 9603e549b..1e0c57ae6 100644 --- a/sonoff/xdrv_10_rules.ino +++ b/sonoff/xdrv_10_rules.ino @@ -625,7 +625,7 @@ bool findNextNumber(char * &pNumber, double &value) */ bool findNextVariableValue(char * &pVarname, double &value) { - bool succeed = false; + bool succeed = true; value = 0; String sVarName = ""; while (*pVarname) { @@ -637,32 +637,28 @@ bool findNextVariableValue(char * &pVarname, double &value) } } sVarName.toUpperCase(); - if (sVarName.startsWith("VAR")) { + if (sVarName.startsWith(F("VAR"))) { int index = sVarName.substring(3).toInt(); if (index > 0 && index <= MAX_RULE_VARS) { value = CharToDouble(vars[index -1]); - succeed = true; } - } else if (sVarName.startsWith("MEM")) { + } else if (sVarName.startsWith(F("MEM"))) { int index = sVarName.substring(3).toInt(); if (index > 0 && index <= MAX_RULE_MEMS) { value = CharToDouble(Settings.mems[index -1]); - succeed = true; } - } else if (sVarName.equals("TIME")) { + } else if (sVarName.equals(F("TIME"))) { value = GetMinutesPastMidnight(); - succeed = true; - } else if (sVarName.equals("UPTIME")) { + } else if (sVarName.equals(F("UPTIME"))) { value = GetMinutesUptime(); - succeed = true; #if defined(USE_TIMERS) && defined(USE_SUNRISE) - } else if (sVarName.equals("SUNRISE")) { + } else if (sVarName.equals(F("SUNRISE"))) { value = GetSunMinutes(0); - succeed = true; - } else if (sVarName.equals("SUNSET")) { + } else if (sVarName.equals(F("SUNSET"))) { value = GetSunMinutes(1); - succeed = true; #endif + } else { + succeed = false; } return succeed; From 6f2f0b6feae018ef1414fbbae3bac4e2586d999f Mon Sep 17 00:00:00 2001 From: Laurent <44267323+laurentdong@users.noreply.github.com> Date: Mon, 18 Feb 2019 16:17:32 -0500 Subject: [PATCH 3/7] Add two constant varibles in rules expression Add two variables: - UtcTime The number of seconds that have elapsed since January 1, 1970 (midnight UTC/GMT) - LocalTime Seconds passed after Jan 1, 1970 midnight base on current timezone and daylight saving setting. --- sonoff/support_rtc.ino | 5 +++++ sonoff/xdrv_10_rules.ino | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/sonoff/support_rtc.ino b/sonoff/support_rtc.ino index 5c9f9e2b4..71fe7220c 100644 --- a/sonoff/support_rtc.ino +++ b/sonoff/support_rtc.ino @@ -331,6 +331,11 @@ uint32_t LocalTime(void) return local_time; } +uint32_t UtcTime(void) +{ + return utc_time; +} + uint32_t Midnight(void) { return midnight; diff --git a/sonoff/xdrv_10_rules.ino b/sonoff/xdrv_10_rules.ino index 1e0c57ae6..1dfbb2a58 100644 --- a/sonoff/xdrv_10_rules.ino +++ b/sonoff/xdrv_10_rules.ino @@ -651,6 +651,10 @@ bool findNextVariableValue(char * &pVarname, double &value) value = GetMinutesPastMidnight(); } else if (sVarName.equals(F("UPTIME"))) { value = GetMinutesUptime(); + } else if (sVarName.equals(F("UTCTIME"))) { + value = UtcTime(); + } else if (sVarName.equals(F("LOCALTIME"))) { + value = LocalTime(); #if defined(USE_TIMERS) && defined(USE_SUNRISE) } else if (sVarName.equals(F("SUNRISE"))) { value = GetSunMinutes(0); From 660d0106c23b8bfba0348ef4bd7e07d4636f14aa Mon Sep 17 00:00:00 2001 From: Laurent <44267323+laurentdong@users.noreply.github.com> Date: Mon, 18 Feb 2019 16:18:01 -0500 Subject: [PATCH 4/7] Revert "Support HttpGet command" This reverts commit c527d4dc995a2e7d4f88abc3ab05b62c82ad1446. --- sonoff/i18n.h | 1 - sonoff/sonoff.ino | 33 ++------------------------------- 2 files changed, 2 insertions(+), 32 deletions(-) diff --git a/sonoff/i18n.h b/sonoff/i18n.h index f601b1095..7340a3484 100644 --- a/sonoff/i18n.h +++ b/sonoff/i18n.h @@ -272,7 +272,6 @@ #define D_JSON_GPIO "GPIO" #define D_JSON_FLAG "FLAG" #define D_JSON_BASE "BASE" -#define D_CMND_HTTP_GET "HttpGet" // Commands xdrv_01_mqtt.ino #define D_CMND_MQTTHOST "MqttHost" diff --git a/sonoff/sonoff.ino b/sonoff/sonoff.ino index 3ce445a4b..e54c23260 100755 --- a/sonoff/sonoff.ino +++ b/sonoff/sonoff.ino @@ -75,7 +75,7 @@ enum TasmotaCommands { CMND_MODULE, CMND_MODULES, CMND_GPIO, CMND_GPIOS, CMND_PWM, CMND_PWMFREQUENCY, CMND_PWMRANGE, CMND_COUNTER, CMND_COUNTERTYPE, CMND_COUNTERDEBOUNCE, CMND_BUTTONDEBOUNCE, CMND_SWITCHDEBOUNCE, CMND_SLEEP, CMND_UPGRADE, CMND_UPLOAD, CMND_OTAURL, CMND_SERIALLOG, CMND_SYSLOG, CMND_LOGHOST, CMND_LOGPORT, CMND_IPADDRESS, CMND_NTPSERVER, CMND_AP, CMND_SSID, CMND_PASSWORD, CMND_HOSTNAME, - CMND_WIFICONFIG, CMND_FRIENDLYNAME, CMND_SWITCHMODE, CMND_INTERLOCK, CMND_TEMPLATE, CMND_HTTP_GET, + CMND_WIFICONFIG, CMND_FRIENDLYNAME, CMND_SWITCHMODE, CMND_INTERLOCK, CMND_TEMPLATE, CMND_TELEPERIOD, CMND_RESTART, CMND_RESET, CMND_TIMEZONE, CMND_TIMESTD, CMND_TIMEDST, CMND_ALTITUDE, CMND_LEDPOWER, CMND_LEDSTATE, CMND_I2CSCAN, CMND_SERIALSEND, CMND_BAUDRATE, CMND_SERIALDELIMITER, CMND_DRIVER }; const char kTasmotaCommands[] PROGMEM = @@ -85,7 +85,7 @@ const char kTasmotaCommands[] PROGMEM = D_CMND_MODULE "|" D_CMND_MODULES "|" D_CMND_GPIO "|" D_CMND_GPIOS "|" D_CMND_PWM "|" D_CMND_PWMFREQUENCY "|" D_CMND_PWMRANGE "|" D_CMND_COUNTER "|" D_CMND_COUNTERTYPE "|" D_CMND_COUNTERDEBOUNCE "|" D_CMND_BUTTONDEBOUNCE "|" D_CMND_SWITCHDEBOUNCE "|" D_CMND_SLEEP "|" D_CMND_UPGRADE "|" D_CMND_UPLOAD "|" D_CMND_OTAURL "|" D_CMND_SERIALLOG "|" D_CMND_SYSLOG "|" D_CMND_LOGHOST "|" D_CMND_LOGPORT "|" D_CMND_IPADDRESS "|" D_CMND_NTPSERVER "|" D_CMND_AP "|" D_CMND_SSID "|" D_CMND_PASSWORD "|" D_CMND_HOSTNAME "|" - D_CMND_WIFICONFIG "|" D_CMND_FRIENDLYNAME "|" D_CMND_SWITCHMODE "|" D_CMND_INTERLOCK "|" D_CMND_TEMPLATE "|" D_CMND_HTTP_GET "|" + D_CMND_WIFICONFIG "|" D_CMND_FRIENDLYNAME "|" D_CMND_SWITCHMODE "|" D_CMND_INTERLOCK "|" D_CMND_TEMPLATE "|" D_CMND_TELEPERIOD "|" D_CMND_RESTART "|" D_CMND_RESET "|" D_CMND_TIMEZONE "|" D_CMND_TIMESTD "|" D_CMND_TIMEDST "|" D_CMND_ALTITUDE "|" D_CMND_LEDPOWER "|" D_CMND_LEDSTATE "|" D_CMND_I2CSCAN "|" D_CMND_SERIALSEND "|" D_CMND_BAUDRATE "|" D_CMND_SERIALDELIMITER "|" D_CMND_DRIVER; @@ -1018,35 +1018,6 @@ void MqttDataHandler(char* topic, uint8_t* data, unsigned int data_len) } snprintf_P(mqtt_data, sizeof(mqtt_data), S_JSON_COMMAND_NVALUE, command, Settings.pwm_frequency); } - else if (CMND_HTTP_GET == command_code) { - //Test http get - if (data_len > 10 && strncasecmp_P(dataBuf, PSTR("HTTP://"),7) == 0) { - String sUrl = dataBuf; - String sResult = ""; - WiFiClient client; - HTTPClient http; - if (http.begin(client, UrlEncode(dataBuf))) { // HTTP - // start connection and send HTTP header - int httpCode = http.GET(); - - // httpCode will be negative on error - if (httpCode > 0) { - // HTTP header has been send and Server response header has been handled - // file found at server - if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) { - sResult = http.getString(); - } - } else { - //Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str()); - sResult = http.errorToString(httpCode); - } - http.end(); - } else { - sResult = "[HTTP} Unable to connect\n"; - } - snprintf_P(mqtt_data, sizeof(mqtt_data), S_JSON_COMMAND_SVALUE, command, sResult.c_str()); - } - } else if (CMND_PWMRANGE == command_code) { if ((1 == payload) || ((payload > 254) && (payload < 1024))) { Settings.pwm_range = (1 == payload) ? PWM_RANGE : payload; From 3298048c60676a4890f94b10379d914ca083c5bd Mon Sep 17 00:00:00 2001 From: Laurent <44267323+laurentdong@users.noreply.github.com> Date: Sat, 23 Feb 2019 17:41:06 -0500 Subject: [PATCH 5/7] Update support_rtc.ino --- sonoff/support_rtc.ino | 5 ----- 1 file changed, 5 deletions(-) diff --git a/sonoff/support_rtc.ino b/sonoff/support_rtc.ino index 57f85d559..35f9b61e9 100644 --- a/sonoff/support_rtc.ino +++ b/sonoff/support_rtc.ino @@ -333,11 +333,6 @@ uint32_t LocalTime(void) return local_time; } -uint32_t UtcTime(void) -{ - return utc_time; -} - uint32_t Midnight(void) { return midnight; From dd27ade7efc9d978e9fdee21fbdfa90827b29e06 Mon Sep 17 00:00:00 2001 From: Laurent <44267323+laurentdong@users.noreply.github.com> Date: Sat, 23 Feb 2019 22:33:09 -0500 Subject: [PATCH 6/7] Rules: Trigger Event with MQTT Subscriptions Support subscribe/unsubscribe MQTT topics and trigger specified event with the subscribed MQTT topic. You can subscribe a MQTT topic and assign an event name. Once we received subscribed MQTT message, an event will be automatically triggered. So you can set up a rule with "ON EVENT# DO ..." to do whatever you want based on this MQTT message. The payload is passed as a parameter once the event been triggered. If the payload is in JSON format, you are able to get the value of specified key as parameter. For example, if you have a Tasmota based thermostat and multiple temperature sensors in different place, usually you have to set up a centre home automation system like Domoticz to control the thermostat. Right now, with this new feature, you can write a rule to do this. Two new commands in Rules: 1. Subscribe Subscribe a MQTT topic (with or without key) and assign an event name to it. Command format: Subscribe [, [, ]] This command will subscribe a and give it an event name . The optional parameter is for parse the specified key/value from MQTT message payload with JSON format. In order to parse value from two level JSON data, you can use one dot (".") to split the key into two section. Subscribe command without any parameter will list all topics currently subscribed. 2. Unsubscribe Unsubscribe specified MQTT event. Command format: Unsubscribe [] Unsubscribe a topic subscribed by specify the event name. If no event specified, Unsubscribe all topics subscribed. Examples: 1. Subscribe BkLight, Tasmota/BackyardLight/stat/POWER And define a rule like: Rule1 on event#BkLight=ON do ruletimer4 60 endon 2. Subscribe DnTemp, Tasmota/RoomSensor1/stat/SENSOR, DS18B20.Temperature Define a rule to deal with the MQTT message like {"Time":"2017-02-16T10:13:52", "DS18B20":{"Temperature":20.6}} Rule1 ON EVENT#DnTemp>=21 DO ... ENDON --- sonoff/language/en-GB.h | 1 + sonoff/my_user_config.h | 1 + sonoff/xdrv_02_mqtt.ino | 31 +++++- sonoff/xdrv_10_rules.ino | 215 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 242 insertions(+), 6 deletions(-) diff --git a/sonoff/language/en-GB.h b/sonoff/language/en-GB.h index 794af57fa..e459cd02d 100644 --- a/sonoff/language/en-GB.h +++ b/sonoff/language/en-GB.h @@ -148,6 +148,7 @@ #define D_STOP "Stop" #define D_SUBNET_MASK "Subnet Mask" #define D_SUBSCRIBE_TO "Subscribe to" +#define D_UNSUBSCRIBE_FROM "Unsubscribe from" #define D_SUCCESSFUL "Successful" #define D_SUNRISE "Sunrise" #define D_SUNSET "Sunset" diff --git a/sonoff/my_user_config.h b/sonoff/my_user_config.h index f0cd75cf6..c4ac51eff 100644 --- a/sonoff/my_user_config.h +++ b/sonoff/my_user_config.h @@ -280,6 +280,7 @@ // -- Rules --------------------------------------- #define USE_RULES // Add support for rules (+4k4 code) #define USE_EXPRESSION // Add support for expression evaluation in rules (+3k2 code, +64 bytes mem) + #define SUPPORT_MQTT_EVENT // Support trigger event with MQTT subscriptions (+3k5 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_02_mqtt.ino b/sonoff/xdrv_02_mqtt.ino index fa681e309..8721b42b2 100644 --- a/sonoff/xdrv_02_mqtt.ino +++ b/sonoff/xdrv_02_mqtt.ino @@ -110,12 +110,18 @@ void MqttDisconnect(void) MqttClient.disconnect(); } -void MqttSubscribeLib(char *topic) +void MqttSubscribeLib(const char *topic) { MqttClient.subscribe(topic); MqttClient.loop(); // Solve LmacRxBlk:1 messages } +void MqttUnsubscribeLib(const char *topic) +{ + MqttClient.unsubscribe(topic); + MqttClient.loop(); // Solve LmacRxBlk:1 messages +} + bool MqttPublishLib(const char* topic, bool retained) { bool result = MqttClient.publish(topic, mqtt_data, retained); @@ -148,11 +154,16 @@ void MqttDisconnectedCb(void) MqttDisconnected(MqttClient.State()); // status codes are documented in file mqtt.h as tConnState } -void MqttSubscribeLib(char *topic) +void MqttSubscribeLib(const char *topic) { MqttClient.Subscribe(topic, 0); } +void MqttUnsubscribeLib(const char *topic) +{ + MqttClient.Unsubscribe(topic, 0); +} + bool MqttPublishLib(const char* topic, bool retained) { return MqttClient.Publish(topic, mqtt_data, strlen(mqtt_data), 0, retained); @@ -190,11 +201,16 @@ void MqttMyDataCb(String &topic, String &data) MqttDataHandler((char*)topic.c_str(), (uint8_t*)data.c_str(), data.length()); } -void MqttSubscribeLib(char *topic) +void MqttSubscribeLib(const char *topic) { MqttClient.subscribe(topic, 0); } +void MqttUnsubscribeLib(const char *topic) +{ + MqttClient.unsubscribe(topic, 0); +} + bool MqttPublishLib(const char* topic, bool retained) { return MqttClient.publish(topic, mqtt_data, strlen(mqtt_data), retained, 0); @@ -251,13 +267,20 @@ void MqttRetryCounter(uint8_t value) mqtt_retry_counter = value; } -void MqttSubscribe(char *topic) +void MqttSubscribe(const char *topic) { snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_MQTT D_SUBSCRIBE_TO " %s"), topic); AddLog(LOG_LEVEL_DEBUG); MqttSubscribeLib(topic); } +void MqttUnsubscribe(const char *topic) +{ + snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_MQTT D_UNSUBSCRIBE_FROM " %s"), topic); + AddLog(LOG_LEVEL_DEBUG); + MqttUnsubscribeLib(topic); +} + void MqttPublishDirect(const char* topic, bool retained) { char sretained[CMDSZ]; diff --git a/sonoff/xdrv_10_rules.ino b/sonoff/xdrv_10_rules.ino index 08c6c362b..2a58ea880 100644 --- a/sonoff/xdrv_10_rules.ino +++ b/sonoff/xdrv_10_rules.ino @@ -75,6 +75,8 @@ #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_JSON_INITIATED "Initiated" @@ -105,8 +107,18 @@ const char kCompareOperators[] PROGMEM = "=\0>\0<\0|\0==!=>=<="; #define MAX_EXPRESSION_OPERATOR_PRIORITY 4 #endif // USE_EXPRESSION -enum RulesCommands { CMND_RULE, CMND_RULETIMER, CMND_EVENT, CMND_VAR, CMND_MEM, CMND_ADD, CMND_SUB, CMND_MULT, CMND_SCALE, CMND_CALC_RESOLUTION }; -const char kRulesCommands[] PROGMEM = 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 ; +enum RulesCommands { CMND_RULE, CMND_RULETIMER, CMND_EVENT, CMND_VAR, CMND_MEM, CMND_ADD, CMND_SUB, CMND_MULT, CMND_SCALE, CMND_CALC_RESOLUTION, CMND_SUBSCRIBE, CMND_UNSUBSCRIBE }; +const char kRulesCommands[] PROGMEM = 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 "|" D_CMND_SUBSCRIBE "|" D_CMND_UNSUBSCRIBE ; + +#ifdef SUPPORT_MQTT_EVENT + #include // Import LinkedList library + typedef struct { + String Event; + String Topic; + String Key; + } MQTT_Subscription; + LinkedList subscriptions; +#endif //SUPPORT_MQTT_EVENT String rules_event_value; unsigned long rules_timer[MAX_RULE_TIMERS] = { 0 }; @@ -577,6 +589,192 @@ void RulesTeleperiod(void) rules_teleperiod = 0; } +#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) +{ + bool serviced = false; + if (XdrvMailbox.data_len < 1 || XdrvMailbox.data_len > 128) { + return false; + } + String sTopic = XdrvMailbox.topic; + String sData = XdrvMailbox.data; + //snprintf_P(log_data, sizeof(log_data), PSTR("RUL: MQTT Topic %s, Event %s"), XdrvMailbox.topic, XdrvMailbox.data); + //AddLog(LOG_LEVEL_DEBUG); + MQTT_Subscription event_item; + //Looking for matched topic + for (int index = 0; index < subscriptions.size(); index++) { + event_item = subscriptions.get(index); + + //snprintf_P(log_data, sizeof(log_data), PSTR("RUL: Match MQTT message Topic %s with subscription topic %s"), sTopic.c_str(), event_item.Topic.c_str()); + //AddLog(LOG_LEVEL_DEBUG); + if (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 + StaticJsonBuffer<400> jsonBuf; + JsonObject& jsonData = jsonBuf.parseObject(sData); + String key1 = event_item.Key; + String key2; + if (!jsonData.success()) 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); + if (!jsonData[key1][key2].success()) break; //Failed to get the key/value, ignore this message. + value = (const char *)jsonData[key1][key2]; + } else { + if (!jsonData[key1].success()) break; + value = (const char *)jsonData[key1]; + } + } + value.trim(); + //Create an new event. Cannot directly call RulesProcessEvent(). + snprintf_P(event_data, sizeof(event_data), PSTR("%s=%s"), event_item.Event.c_str(), value.c_str()); + } + } + return serviced; +} + +/********************************************************************************************/ +/* + * Subscribe a MQTT topic (with or without key) and assign an event name to it + * Command Subscribe format: + * Subscribe , [, ] + * This command will subscribe a and give it an event name . + * The optional parameter 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: + * data - A char buffer with all the parameters + * data_len - Length of the parameters + * Return: + * A string include subscribed event, topic and key. + */ +String RulesSubscribe(const char *data, int data_len) +{ + MQTT_Subscription subscription_item; + String events; + if (data_len > 0) { + char parameters[data_len+1]; + memcpy(parameters, data, data_len); + parameters[data_len] = '\0'; + String event_name, topic, key; + + char * pos = strtok(parameters, ","); + if (pos) { + event_name = Trim(pos); + pos = strtok(NULL, ","); + if (pos) { + topic = Trim(pos); + pos = strtok(NULL, ","); + if (pos) { + key = Trim(pos); + } + } + } + //snprintf_P(log_data, sizeof(log_data), PSTR("RUL: Subscribe command with parameters: %s, %s, %s."), event_name.c_str(), topic.c_str(), key.c_str()); + //AddLog(LOG_LEVEL_DEBUG); + event_name.toUpperCase(); + if (event_name.length() > 0 && topic.length() > 0) { + //Search all subscriptions + for (int 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("/#"); + } + } + //snprintf_P(log_data, sizeof(log_data), PSTR("RUL: New topic: %s."), topic.c_str()); + //AddLog(LOG_LEVEL_DEBUG); + //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); + + 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 (int 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 + "; "); + } + } + return events; +} + +/********************************************************************************************/ +/* + * Unsubscribe specified MQTT event. If no event specified, Unsubscribe all. + * Command Unsubscribe format: + * Unsubscribe [] + * Input: + * data - Event name + * data_len - Length of the parameters + * Return: + * list all the events unsubscribed. + */ +String RulesUnsubscribe(const char * data, int data_len) +{ + MQTT_Subscription subscription_item; + String events; + if (data_len > 0) { + for (int index = 0; index < subscriptions.size(); index++) { + subscription_item = subscriptions.get(index); + if (subscription_item.Event.equalsIgnoreCase(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); + } + } + return events; +} +#endif // SUPPORT_MQTT_EVENT + #ifdef USE_EXPRESSION /********************************************************************************************/ /* @@ -1026,6 +1224,14 @@ bool RulesCommand(void) } } snprintf_P(mqtt_data, sizeof(mqtt_data), S_JSON_COMMAND_INDEX_SVALUE, command, index, vars[index -1]); +#ifdef SUPPORT_MQTT_EVENT + } else if (CMND_SUBSCRIBE == command_code) { //MQTT Subscribe command. Subscribe , [, ] + String result = RulesSubscribe(XdrvMailbox.data, XdrvMailbox.data_len); + snprintf_P(mqtt_data, sizeof(mqtt_data), S_JSON_COMMAND_SVALUE, command, result.c_str()); + } else if (CMND_UNSUBSCRIBE == command_code) { //MQTT Un-subscribe command. UnSubscribe + String result = RulesUnsubscribe(XdrvMailbox.data, XdrvMailbox.data_len); + snprintf_P(mqtt_data, sizeof(mqtt_data), S_JSON_COMMAND_SVALUE, command, result.c_str()); +#endif //SUPPORT_MQTT_EVENT } else serviced = false; // Unknown command @@ -1067,6 +1273,11 @@ bool Xdrv10(uint8_t function) case FUNC_RULES_PROCESS: result = RulesProcess(); break; +#ifdef SUPPORT_MQTT_EVENT + case FUNC_MQTT_DATA: + result = RulesMqttData(); + break; +#endif //SUPPORT_MQTT_EVENT } return result; } From 49053f163f9cfbade206b69582c8514f51f9d788 Mon Sep 17 00:00:00 2001 From: Laurent <44267323+laurentdong@users.noreply.github.com> Date: Sun, 24 Feb 2019 10:15:39 -0500 Subject: [PATCH 7/7] Update language files D_UNSUBSCRIBE_FROM --- sonoff/language/bg-BG.h | 1 + sonoff/language/cs-CZ.h | 1 + sonoff/language/de-DE.h | 1 + sonoff/language/el-GR.h | 1 + sonoff/language/es-AR.h | 1 + sonoff/language/fr-FR.h | 1 + sonoff/language/he-HE.h | 1 + sonoff/language/hu-HU.h | 1 + sonoff/language/it-IT.h | 1 + sonoff/language/nl-NL.h | 1 + sonoff/language/pl-PL.h | 1 + sonoff/language/pt-BR.h | 1 + sonoff/language/pt-PT.h | 1 + sonoff/language/ru-RU.h | 1 + sonoff/language/sk-SK.h | 1 + sonoff/language/sv-SE.h | 1 + sonoff/language/tr-TR.h | 1 + sonoff/language/uk-UK.h | 1 + sonoff/language/zh-CN.h | 1 + sonoff/language/zh-TW.h | 1 + 20 files changed, 20 insertions(+) diff --git a/sonoff/language/bg-BG.h b/sonoff/language/bg-BG.h index 626c0b1b8..04cfcf066 100644 --- a/sonoff/language/bg-BG.h +++ b/sonoff/language/bg-BG.h @@ -148,6 +148,7 @@ #define D_STOP "Стоп" #define D_SUBNET_MASK "Маска на подмрежата" #define D_SUBSCRIBE_TO "Записване за" +#define D_UNSUBSCRIBE_FROM "Unsubscribe from" #define D_SUCCESSFUL "Успешно" #define D_SUNRISE "Изгрев" #define D_SUNSET "Залез" diff --git a/sonoff/language/cs-CZ.h b/sonoff/language/cs-CZ.h index e9d17bb8e..e4053ea75 100644 --- a/sonoff/language/cs-CZ.h +++ b/sonoff/language/cs-CZ.h @@ -148,6 +148,7 @@ #define D_STOP "Stop" #define D_SUBNET_MASK "Maska podsítě" #define D_SUBSCRIBE_TO "Přihlaš se do" +#define D_UNSUBSCRIBE_FROM "Unsubscribe from" #define D_SUCCESSFUL "úspěšné." #define D_SUNRISE "Svítání" #define D_SUNSET "Soumrak" diff --git a/sonoff/language/de-DE.h b/sonoff/language/de-DE.h index ed3202919..d41097c0f 100644 --- a/sonoff/language/de-DE.h +++ b/sonoff/language/de-DE.h @@ -148,6 +148,7 @@ #define D_STOP "Stop" #define D_SUBNET_MASK "Subnetzmaske" #define D_SUBSCRIBE_TO "abonniere" +#define D_UNSUBSCRIBE_FROM "Unsubscribe from" #define D_SUCCESSFUL "erfolgreich" #define D_SUNRISE "Sonnenaufgang" #define D_SUNSET "Sonnenuntergang" diff --git a/sonoff/language/el-GR.h b/sonoff/language/el-GR.h index 8e4cf7077..8a3fe75a5 100644 --- a/sonoff/language/el-GR.h +++ b/sonoff/language/el-GR.h @@ -148,6 +148,7 @@ #define D_STOP "Τερματισμός" #define D_SUBNET_MASK "Μάσκα υποδικτύου" #define D_SUBSCRIBE_TO "Εγγραφή στο" +#define D_UNSUBSCRIBE_FROM "Unsubscribe from" #define D_SUCCESSFUL "Επιτυχές" #define D_SUNRISE "Σούρουπο" #define D_SUNSET "Ηλιοβασίλεμα" diff --git a/sonoff/language/es-AR.h b/sonoff/language/es-AR.h index 9dc72cf7c..9f2187bbf 100644 --- a/sonoff/language/es-AR.h +++ b/sonoff/language/es-AR.h @@ -148,6 +148,7 @@ #define D_STOP "Detener" #define D_SUBNET_MASK "Máscara Subred" #define D_SUBSCRIBE_TO "Suscribir a" +#define D_UNSUBSCRIBE_FROM "Unsubscribe from" #define D_SUCCESSFUL "Exitosa" #define D_SUNRISE "Salida del Sol" #define D_SUNSET "Puesta del Sol" diff --git a/sonoff/language/fr-FR.h b/sonoff/language/fr-FR.h index 06ada0874..539094b9f 100644 --- a/sonoff/language/fr-FR.h +++ b/sonoff/language/fr-FR.h @@ -148,6 +148,7 @@ #define D_STOP "Stop" #define D_SUBNET_MASK "Masque sous-réseau" #define D_SUBSCRIBE_TO "Souscrire à" +#define D_UNSUBSCRIBE_FROM "Unsubscribe from" #define D_SUCCESSFUL "Réussi" #define D_SUNRISE "Lever du jour" #define D_SUNSET "Tombée de la nuit" diff --git a/sonoff/language/he-HE.h b/sonoff/language/he-HE.h index 8437e1a05..9f3458889 100644 --- a/sonoff/language/he-HE.h +++ b/sonoff/language/he-HE.h @@ -148,6 +148,7 @@ #define D_STOP "עצירה" #define D_SUBNET_MASK "רשת מסכת משנה" #define D_SUBSCRIBE_TO "הרשם ל" +#define D_UNSUBSCRIBE_FROM "Unsubscribe from" #define D_SUCCESSFUL "הצליח" #define D_SUNRISE "זריחה" #define D_SUNSET "שקיעה" diff --git a/sonoff/language/hu-HU.h b/sonoff/language/hu-HU.h index 393d250e7..649796121 100644 --- a/sonoff/language/hu-HU.h +++ b/sonoff/language/hu-HU.h @@ -148,6 +148,7 @@ #define D_STOP "Leállítás" #define D_SUBNET_MASK "Alhálózati maszk" #define D_SUBSCRIBE_TO "Feliratkozás a(z)" +#define D_UNSUBSCRIBE_FROM "Unsubscribe from" #define D_SUCCESSFUL "Sikeres" #define D_SUNRISE "Napkelte" #define D_SUNSET "Napnyugta" diff --git a/sonoff/language/it-IT.h b/sonoff/language/it-IT.h index 0f35e92b2..4d735426f 100644 --- a/sonoff/language/it-IT.h +++ b/sonoff/language/it-IT.h @@ -148,6 +148,7 @@ #define D_STOP "Stop" #define D_SUBNET_MASK "Maschera sottorete" #define D_SUBSCRIBE_TO "Sottoscrivi a" +#define D_UNSUBSCRIBE_FROM "Unsubscribe from" #define D_SUCCESSFUL "Riuscito" #define D_SUNRISE "Alba" #define D_SUNSET "Tramonto" diff --git a/sonoff/language/nl-NL.h b/sonoff/language/nl-NL.h index b8b159eff..dd3fb1806 100644 --- a/sonoff/language/nl-NL.h +++ b/sonoff/language/nl-NL.h @@ -148,6 +148,7 @@ #define D_STOP "Stop" #define D_SUBNET_MASK "Subnet Masker" #define D_SUBSCRIBE_TO "Abonneer op" +#define D_UNSUBSCRIBE_FROM "Unsubscribe from" #define D_SUCCESSFUL "Gelukt" #define D_SUNRISE "Zonsopgang" #define D_SUNSET "Zonsondergang" diff --git a/sonoff/language/pl-PL.h b/sonoff/language/pl-PL.h index 5d2db7afc..24e2f60ae 100644 --- a/sonoff/language/pl-PL.h +++ b/sonoff/language/pl-PL.h @@ -148,6 +148,7 @@ #define D_STOP "Stop" #define D_SUBNET_MASK "Maska podsieci" #define D_SUBSCRIBE_TO "Subskrybuj do" +#define D_UNSUBSCRIBE_FROM "Unsubscribe from" #define D_SUCCESSFUL "Powodzenie" #define D_SUNRISE "Wschód słońca" #define D_SUNSET "Zachód słońca" diff --git a/sonoff/language/pt-BR.h b/sonoff/language/pt-BR.h index 94d7bea00..3d24e2dbc 100644 --- a/sonoff/language/pt-BR.h +++ b/sonoff/language/pt-BR.h @@ -148,6 +148,7 @@ #define D_STOP "Parar" #define D_SUBNET_MASK "Máscara sub rede" #define D_SUBSCRIBE_TO "Subescrever para" +#define D_UNSUBSCRIBE_FROM "Unsubscribe from" #define D_SUCCESSFUL "Successo" #define D_SUNRISE "Nascer do sol" #define D_SUNSET "Por do sol" diff --git a/sonoff/language/pt-PT.h b/sonoff/language/pt-PT.h index 47dec4ea2..d675efe54 100644 --- a/sonoff/language/pt-PT.h +++ b/sonoff/language/pt-PT.h @@ -148,6 +148,7 @@ #define D_STOP "Parar" #define D_SUBNET_MASK "Mascara sub rede" #define D_SUBSCRIBE_TO "Subescrever para" +#define D_UNSUBSCRIBE_FROM "Unsubscribe from" #define D_SUCCESSFUL "Successo" #define D_SUNRISE "Sunrise" #define D_SUNSET "Sunset" diff --git a/sonoff/language/ru-RU.h b/sonoff/language/ru-RU.h index 038ba6615..50eabbc35 100644 --- a/sonoff/language/ru-RU.h +++ b/sonoff/language/ru-RU.h @@ -148,6 +148,7 @@ #define D_STOP "Стоп" #define D_SUBNET_MASK "Маска Подсети" #define D_SUBSCRIBE_TO "Подписаться на" +#define D_UNSUBSCRIBE_FROM "Unsubscribe from" #define D_SUCCESSFUL "Успешно" #define D_SUNRISE "Sunrise" #define D_SUNSET "Sunset" diff --git a/sonoff/language/sk-SK.h b/sonoff/language/sk-SK.h index ff05b471d..4f741ac51 100644 --- a/sonoff/language/sk-SK.h +++ b/sonoff/language/sk-SK.h @@ -148,6 +148,7 @@ #define D_STOP "Stop" #define D_SUBNET_MASK "Maska podsiete" #define D_SUBSCRIBE_TO "Prihlásiť do" +#define D_UNSUBSCRIBE_FROM "Unsubscribe from" #define D_SUCCESSFUL "úspešné." #define D_SUNRISE "Svitanie" #define D_SUNSET "Súmrak" diff --git a/sonoff/language/sv-SE.h b/sonoff/language/sv-SE.h index 9045b2e24..dd6b31469 100644 --- a/sonoff/language/sv-SE.h +++ b/sonoff/language/sv-SE.h @@ -148,6 +148,7 @@ #define D_STOP "Stoppa" #define D_SUBNET_MASK "Nätmask" #define D_SUBSCRIBE_TO "Prenumera på" +#define D_UNSUBSCRIBE_FROM "Unsubscribe from" #define D_SUCCESSFUL "Lyckat" #define D_SUNRISE "Soluppgång" #define D_SUNSET "Solnedgång" diff --git a/sonoff/language/tr-TR.h b/sonoff/language/tr-TR.h index f971c9ba2..f6f61ec56 100755 --- a/sonoff/language/tr-TR.h +++ b/sonoff/language/tr-TR.h @@ -148,6 +148,7 @@ #define D_STOP "Durdur" #define D_SUBNET_MASK "Altağ Geçidi Maskesi" #define D_SUBSCRIBE_TO "Abone olunan" +#define D_UNSUBSCRIBE_FROM "Unsubscribe from" #define D_SUCCESSFUL "Başarıyla Tamamlandı" #define D_SUNRISE "Gün doğumu" #define D_SUNSET "Gün batımı" diff --git a/sonoff/language/uk-UK.h b/sonoff/language/uk-UK.h index 1557a0d78..71b7665c1 100644 --- a/sonoff/language/uk-UK.h +++ b/sonoff/language/uk-UK.h @@ -148,6 +148,7 @@ #define D_STOP "Стоп" #define D_SUBNET_MASK "Маска Підмережі" #define D_SUBSCRIBE_TO "Підписатись на" +#define D_UNSUBSCRIBE_FROM "Unsubscribe from" #define D_SUCCESSFUL "Успішно" #define D_SUNRISE "Схід сонця" #define D_SUNSET "Захід сонця" diff --git a/sonoff/language/zh-CN.h b/sonoff/language/zh-CN.h index 4e3a7d9fd..dddf34a48 100644 --- a/sonoff/language/zh-CN.h +++ b/sonoff/language/zh-CN.h @@ -148,6 +148,7 @@ #define D_STOP "停止" #define D_SUBNET_MASK "子网掩码" #define D_SUBSCRIBE_TO "订阅" +#define D_UNSUBSCRIBE_FROM "退订" #define D_SUCCESSFUL "成功" #define D_SUNRISE "日出" #define D_SUNSET "日落" diff --git a/sonoff/language/zh-TW.h b/sonoff/language/zh-TW.h index f8526aa5b..ae6d4df07 100644 --- a/sonoff/language/zh-TW.h +++ b/sonoff/language/zh-TW.h @@ -148,6 +148,7 @@ #define D_STOP "停止" #define D_SUBNET_MASK "子網遮罩" #define D_SUBSCRIBE_TO "訂閱" +#define D_UNSUBSCRIBE_FROM "退訂" #define D_SUCCESSFUL "成功" #define D_SUNRISE "Sunrise" #define D_SUNSET "Sunset"