From ba41a2ada59c5789aeed926a0d3c32a7a9a5cc0a Mon Sep 17 00:00:00 2001 From: gemu2015 Date: Sat, 18 May 2019 09:45:04 +0200 Subject: [PATCH] update scripter --- platformio.ini | 10 +- sonoff/my_user_config.h | 3 + sonoff/scripter.md | 621 ++++++++++ sonoff/sonoff.ino | 12 +- sonoff/support_features.ino | 2 +- sonoff/xdrv_09_timers.ino | 12 +- sonoff/xdrv_10_scripter.ino | 2235 +++++++++++++++++++++++++++++++++++ 7 files changed, 2882 insertions(+), 13 deletions(-) create mode 100644 sonoff/scripter.md create mode 100644 sonoff/xdrv_10_scripter.ino diff --git a/platformio.ini b/platformio.ini index f721a496a..1d80293cb 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,7 +22,7 @@ src_dir = sonoff ;env_default = sonoff-BR ;env_default = sonoff-CN ;env_default = sonoff-CZ -;env_default = sonoff-DE +env_default = sonoff-DE ;env_default = sonoff-ES ;env_default = sonoff-FR ;env_default = sonoff-GR @@ -70,14 +70,14 @@ build_flags = ${esp82xx_defaults.build_flags} platform = espressif8266@~2.1.1 build_flags = ${esp82xx_defaults.build_flags} -Wl,-Teagle.flash.1m.ld -; Code optimization see https://github.com/esp8266/Arduino/issues/5790#issuecomment-475672473 +; Code optimization see https://github.com/esp8266/Arduino/issues/5790#issuecomment-475672473 -O2 -DBEARSSL_SSL_BASIC ; nonos-sdk 22x -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x ; nonos-sdk-pre-v3 ; -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK3 -; lwIP 1.4 +; lwIP 1.4 ; -DPIO_FRAMEWORK_ARDUINO_LWIP_HIGHER_BANDWIDTH ; lwIP 2 - Low Memory ; -DPIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY @@ -96,13 +96,13 @@ build_flags = ${esp82xx_defaults.build_flags} platform = https://github.com/platformio/platform-espressif8266.git#feature/stage build_flags = ${esp82xx_defaults.build_flags} -Wl,-Teagle.flash.1m.ld -; Code optimization see https://github.com/esp8266/Arduino/issues/5790#issuecomment-475672473 +; Code optimization see https://github.com/esp8266/Arduino/issues/5790#issuecomment-475672473 -O2 -DBEARSSL_SSL_BASIC ; nonos-sdk 22x -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x ; nonos-sdk-pre-v3 -; -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK3 +; -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK3 ; lwIP 1.4 ; -DPIO_FRAMEWORK_ARDUINO_LWIP_HIGHER_BANDWIDTH ; lwIP 2 - Low Memory diff --git a/sonoff/my_user_config.h b/sonoff/my_user_config.h index 945261e1c..a7d7a3602 100644 --- a/sonoff/my_user_config.h +++ b/sonoff/my_user_config.h @@ -290,6 +290,9 @@ // -- Rules --------------------------------------- #define USE_RULES // Add support for rules (+4k4 code) +#undef USE_RULES +#define USE_SCRIPT + // #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) diff --git a/sonoff/scripter.md b/sonoff/scripter.md new file mode 100644 index 000000000..5b9ac7840 --- /dev/null +++ b/sonoff/scripter.md @@ -0,0 +1,621 @@ +**Script Language for Tasmota** + +As an alternative to rules. (about 14,2k flash size, variable ram size) + +In submenu Configuration =\> edit script +1535 bytes max script size (uses rules buffer) + +to enable: +\#define USE_SCRIPT +\#undef USE_RULES + + +Up to 50 variables (45 numeric and 5 strings, maybe changed by #define) +Freely definable variable names (all names are intentionally case sensitive) +Nested if,then,else up to a level of 8 +Math operators **+,-,\*,/,%,&,|,^** +all operators may be used in the op= form e.g. **+=** +Left right evaluation with optional brackets +all numbers are float +e.g. temp=hum\*(100/37.5)+temp-(timer\*hum%10) +no spaces allowed between math operations +Comparison operators **==,!=,\>,\>=,<,<=** +**and** , **or** support + +strings support **+** and **+=** operators +string comparison **==,!=** +max string size = 19 chars (default, can be increased or decreased by optional >D parameter) + +**Comments** start with **;** + +**Sections** defined: + +>**\>D ssize** +ssize = optional max stringsize (default=19) +define and init variables here, must be the first section, no other code allowed +**p:**vname specifies permanent vars (the number of permanent vars is limited by tasmota rules space (50 bytes) +numeric var=4 bytes, string var=lenght of string+1) +**t:**vname specifies countdown timers, if >0 they are decremented in seconds until zero is reached. see example below +**i:**vname specifies auto increment counters if >=0 (in seconds) +**m:**vname specifies a median filter variable with 5 entries (for elimination of outliers) +**M:**vname specifies a moving average filter variable with 8 entries (for smoothing data) +(max 5 filters in total m+M) + +>all variable names length taken together may not exceed 256 characters, so keep variable names as short as possible. +memory is dynamically allocated as a result of the D section. +copying a string to a number or reverse is supported + +>**\>B** +executed on BOOT time + +>**\>T** +executed on teleperiod time (**SENSOR** and **STATE**), get tele vars only in this section + +>**\>S** +executed every second + +>**\>E** +executed e.g. on power change and mqtt **RESULT** + +>**\>R** +executed on restart, p vars are saved automatically after this call + + +special variables (read only): + +>**upsecs** = seconds since start +**uptime** = minutes since start +**time** = minutes since midnight +**sunrise** = sunrise minutes since midnight +**sunset** = sunset minutes since midnight +**tper** = teleperiod (may be set also) +**tstamp** = timestamp (local date and time) +**topic** = mqtt topic +**gtopic** = mqtt group topic +**prefixn** = prefix n = 1-3 +**pwr[x]** = tasmota power state (x = 1-N) +**sw[x]** = tasmota switch state (x = 1-N) +>**pin[x]** = gpio pin level (x = 0-16) +**pn[x]** = pin number for sensor code x, 99 if none +**pd[x]** = defined sensor for gpio pin nr x none=999 +**gtmp** = global temperature +**ghum** = global humidity +**gprs** = global pressure +**pow(x y)** = calculates the power of x^y +**med(n x)** = calculates a 5 value median filter of x (2 filters possible n=0,1) +**int(x)** = gets the integer part of x (like floor) +**hn(x)** = converts x (0..255) zu a hex nibble string +**mqtts** = state of mqtt disconnected=0, connected>0 +**wifis** = state of wifi disconnected=0, connected>0 + +>**hours** = hours +**mins** = mins +**secs** = seconds +**day** = day of month +**wday** = day of week +**month** = month +**year** = year + +these variables are cleared after reading true +>**chg[var]** = true if a variables value was changed (numeric vars only) +**upd[var]** = true if a variable was updated +**boot** = true on BOOT +**tinit** = true on time init +**tset** = true on time set +**mqttc** = true on mqtt connect +**mqttd** = true on mqtt disconnect +**wific** = true on wifi connect +**wifid** = true on wifi disconnect + +system vars (for debugging) +>**stack** = stack size +**heap** = heap size +**ram** = used ram size +**slen** = script length +**micros** = running microseconds +**millis** = running milliseconds +**loglvl** = loglevel of script cmds, may be set also + +remarks: +if you define a variable with the same name as a special +variable that special variable is discarded + + +**Tasmota** cmds start with **=\>** +within cmds you can replace text with variables with **%varname%** +a single percent sign must be given as **%%** + +**special** cmds: + +>**=\> print** prints to info log for debugging + +to save code space nearly no error messages are provided. However it is taken care of that at least it should not crash on syntax errors. +if a variable does not exist a **???** is given on commands +if a **SENSOR** or **STATUS** or **RESULT** message or a var does not exist the destination variable is NOT updated. + +2 possibilities for conditionals: +>**if** a==b +**and** x==y +**or** k==i +**then** => do this +**else** => do that +**endif** + +OR + +>**if** a==b +**and** x==y +**or** k==i **{** + => do this +**} else {** + => do that +**}** + +you may NOT mix both methods + +also possible e.g. + +>if var1-var2==var3*var4 +then + +remarks: +the last closing bracket must be on a single line +the condition may not be enclosed in brackets + +>**break** exits a section or terminates a for next loop +**dprecx** sets decimal precision to x (0-9) +**svars** save permanent vars +**delay(x)** pauses x milliseconds (should be as short as possible) +**spin(x m)** set gpio pin x (0-16) to value m (0,1) only the last bit is used, so even values set the pin to zero and uneven values set the pin to 1 +**spinm(x m)** set pin mode gpio pin x (0-16) to mode m (input=0,output=1) + +>**#name** names a subroutine, subroutines are called with **=#name** +**#name(param)** names a subroutines with a parameter is called with **=#name(param)** +subroutines end with the next '#' or '>' line or break, may be nested +params can be numbers or strings and on mismatch are converted + +>**for var from to inc** +**next** +specifies a for next loop, (loop count must not be less then 1) + +>**switch x** +**case a** +**case b** +**ends** +specifies a switch case selector + +**konsole script cmds** +>**script 1 or 0** switch script on or off +**script >cmdline** executes the script cmdline +can be used e.g. to set variables e.g. **script >mintmp=15** +more then one line may be executed seperated by a semicolon e.g. **script >mintmp=15;maxtemp=40** +script itself cant be set because the size would not fit the mqtt buffers + +***example script*** +meant to show some of the possibilities +(actually this code ist too large) + +**\>D** +; define all vars here +p:mintmp=10 (p:means permanent) +p:maxtmp=30 +t:timer1=30 (t:means countdown timer) +t:mt=0 +i:count=0 (i:means auto counter) +hello="hello world" +string="xxx" +url="[192.168.178.86]" +hum=0 +temp=0 +timer=0 +dimmer=0 +sw=0 +rssi=0 +param=0 + +col="" +ocol="" +chan1=0 +chan2=0 +chan3=0 + +ahum=0 +atemp=0 +tcnt=0 +hour=0 +state=1 +m:med5=0 +M:movav=0 + +**\>B** + +string=hello+"how are you?" +=\>print BOOT executed +=\>print %hello% +=\>mp3track 1 + +; list gpio pin definitions +for cnt 0 16 1 +tmp=pd[cnt] +=>print %cnt% = %tmp% +next + +; get gpio pin for relais 1 +tmp=pn[21] +=>print relais 1 is on pin %tmp% + +; pulse relais over raw gpio +spin(tmp 1) +delay(100) +spin(tmp 0) + +; raw pin level +=>print level of gpio1 %pin[1]% + +; pulse over tasmota cmd +=>power 1 +delay(100) +=>power 0 + +**\>T** + +hum=BME280#Humidity +temp=BME280#Temperature +rssi=Wifi#RSSI +string=SleepMode + +; add to median filter +median=temp +; add to moving average filter +movav=hum + +; show filtered results +=>print %median% %movav% + +if chg[rssi]>0 +then =>print rssi changed to %rssi% +endif + +if temp\>30 +and hum\>70 +then =\>print damn hot! +endif + +**\>S** + +; every second but not completely reliable time here +; use upsecs and uptime or best t: for reliable timers + +; call subrountines with parameters +=#sub1("hallo") +=#sub2(999) + +; stop timer after expired +if timer1==0 +then timer1=-1 +=>print timer1 expired +endif + +; auto counter with restart +if count>=10 +then =>print 10 seconds over +count=0 +endif + +if upsecs%5==0 +then =\>print %upsecs% (every 5 seconds) +endif + +; not recommended for reliable timers +timer+=1 +if timer\>=5 +then =\>print 5 seconds over (may be) +timer=0 +endif + +dimmer+=1 +if dimmer\>100 +then dimmer=0 +endif + +=\>dimmer %dimmer% +=\>WebSend %url% dimmer %dimmer% + +; show on display +dprec0 +=\>displaytext [c1l1f1s2p20] dimmer=%dimmer% + +=\>print %upsecs% %uptime% %time% %sunrise% %sunset% %tstamp% + +if time\>sunset +and time< sunrise +then +; night time +if pwr[1]==0 +then =\>power1 1 +endif +else +; day time +if pwr[1]\>0 +then =\>power1 0 +endif +endif + +; clr display on boot +if boot\>0 +then =\>displaytext [z] +endif + +; frost warning +if temp<0 +and mt<=0 +then =#sendmail("frost alert") +; alarm only every 5 minutes +mt=300 +=>mp3track 2 +endif + +; var has been updated +if upd[hello]>0 +then =>print %hello% +endif + +; send to Thingspeak every 60 seconds +; average data in between +if upsecs%60==0 +then +ahum/=tcnt +atemp/=tcnt +=>Websend [184.106.153.149:80]/update?key=PYUZMVWCICBW492&field1=%atemp%&field2=%ahum% +tcnt=0 +atemp=0 +ahum=0 +else +ahum+=hum +atemp+=temp +tcnt+=1 +endif + +hour=int(time/60) +if chg[hour]>0 +then +; exactly every hour +=>print full hour reached +endif + +if time>5 { +=>print more then 5 minutes after midnight +} else { +=>print less then 5 minutes after midnight +} + + +; publish abs hum every teleperiod time +if mqtts>0 +and upsecs%tper==0 +then +; calc abs humidity +tmp=pow(2.718281828 (17.67\*temp)/(temp+243.5)) +tmp=(6.112\*tmp\*hum\*18.01534)/((273.15+temp)\*8.31447215) +; publish median filtered value +=>Publish tele/%topic%/SENSOR {"Script":{"abshum":%med(0 tmp)%}} +endif + +;switch case state machine +switch state +case 1 +=>print state=%state% , start +state+=1 +case 2 +=>print state=%state% +state+=1 +case 3 +=>print state=%state% , reset +state=1 +ends + + +; subroutines +\#sub1(string) +=>print sub1: %string% +\#sub2(param) +=>print sub2: %param% + +\#sendmail(string) +=>sendmail [smtp.gmail.com:465:user:passwd:::alarm] %string% + +**\>E** +=\>print event executed! + + +; check if switch changed state +sw=sw[1] +if chg[sw]>0 +then =\>power1 %sw% +endif + +hello="event occured" + +; check for Color change (Color is a string) +col=Color +; color change needs 2 string vars +if col!=ocol +then ocol=col +=>print color changed %col% +endif + +; or check change of color channels +chan1=Channel[1] +chan2=Channel[2] +chan3=Channel[3] + +if chg[chan1]>0 +or chg[chan2]>0 +or chg[chan3]>0 +then => color has changed +endif + +; compose color string for red +col=hn(255)+hn(0)+hn(0) +=>color %col% + +**\>R** +=\>print restarting now + +**a real example** +epaper 29 with sgp30 and bme280 +some vars are set from iobroker +DisplayText substituted to save script space +\>D +hum=0 +temp=0 +press=0 +ahum=0 +tvoc=0 +eco2=0 +zwz=0 +wr1=0 +wr2=0 +wr3=0 +otmp=0 +pwl=0 +tmp=0 +DT="DisplayText" +; preset units in case they are not available +punit="hPa" +tunit="C" + +\>B +;reset auto draw +=>%DT% [zD0] +;clr display and draw a frame +=>%DT% [x0y20h296x0y40h296] + +\>T +; get tele vars +temp=BME280#Temperature +hum=BME280#Humidity +press=BME280#Pressure +tvoc=SGP30#TVOC +eco2=SGP30#eCO2 +ahum=SGP30#aHumidity +tunit=TempUnit +punit=PressureUnit + +\>S +// update display every teleperiod time +if upsecs%tper==0 +then +dprec2 +=>%DT% [f1p7x0y5]%temp% %tunit% +=>%DT% [p5x70y5]%hum% %%[x250y5t] +=>%DT% [p11x140y5]%press% %punit% +=>%DT% [p10x30y25]TVOC: %tvoc% ppb +=>%DT% [p10x160y25]eCO2: %eco2% ppm +=>%DT% [p10c26l5]ahum: %ahum% g^m3 + +dprec0 +=>%DT% [p25c1l5]WR 1 (Dach) : %wr1% W +=>%DT% [p25c1l6]WR 2 (Garage): %-wr3% W +=>%DT% [p25c1l7]WR 3 (Garten): %-wr2% W +=>%DT% [p25c1l8]Aussentemperatur: %otmp% C +=>%DT% [x170y95r120:30f2p6x185y100] %pwl% %% +; now update screen +=>%DT% [d] +endif + + +\>E + +\>R + +**another real example** +ILI 9488 color LCD Display shows various energy graphs +display switches on and off with proximity sensor +BMP280 and vl5310x +some vars are set from iobroker + +**>D** +temp=0 +press=0 +zwz=0 +wr1=0 +wr2=0 +wr3=0 +otmp=0 +pwl=0 +tmp=0 +dist=0 +DT="DisplayText" +punit="hPa" +tunit="C" +hour=0 + +**>B** +=>%DT% [z] + +// define 2 graphs, 2. has 3 tracks +=>%DT% [zCi1G2656:5:20:400:80:1440:-5000:5000:3Ci7f3x410y20]+5000 W[x410y95]-5000 W [Ci7f1x70y3] Zweirichtungsz~80hler - 24 Stunden +=>%DT% [Ci1G2657:5:120:400:80:1440:0:5000:3Ci7f3x410y120]+5000 W[x410y195]0 W [Ci7f1x70y103] Wechselrichter 1-3 - 24 Stunden +=>%DT% [Ci1G2658:5:120:400:80:1440:0:5000:16][Ci1G2659:5:120:400:80:1440:0:5000:5] +=>%DT% [f1s1b0:260:260:100:50:2:11:4:2:Rel 1:b1:370:260:100:50:2:11:4:2:Dsp off:] +=>mp3volume 100 +=>mp3track 4 + +**>T** +; get some tele vars +temp=BMP280#Temperature +press=BMP280#Pressure +tunit=TempUnit +punit=PressureUnit +dist=VL53L0X#Distance + +; check proximity sensor to switch display on/off +; to prevent burn in +if dist>300 +then +if pwr[2]>0 +then +=>power2 0 +endif +else +if pwr[2]==0 +then +=>power2 1 +endif +endif + + +**>S** +; update graph every teleperiod +if upsecs%tper==0 +then +dprec2 +=>%DT% [f1Ci3x40y260w30Ci1] +=>%DT% [Ci7x120y220t] +=>%DT% [Ci7x180y220T] +=>%DT% [Ci7p8x120y240]%temp% %tunit% +=>%DT% [Ci7x120y260]%press% %punit% +=>%DT% [Ci7x120y280]%dist% mm +dprec0 +=>%DT% [g0:%zwz%g1:%wr1%g2:%-wr2%g3:%-wr3%] +if zwz>0 +then +=>%DT% [p-8x410y55Ci2Bi0]%zwz% W +else +=>%DT% [p-8x410y55Ci3Bi0]%zwz% W +endif +=>%DT% [p-8x410y140Ci3Bi0]%wr1% W +=>%DT% [p-8x410y155Ci16Bi0]%-wr2% W +=>%DT% [p-8x410y170Ci5Bi0]%-wr3% W +endif + +; chime every full hour +hour=int(time/60) +if chg[hour]>0 +then =>mp3track 4 +endif + +**>E** + +**>R** diff --git a/sonoff/sonoff.ino b/sonoff/sonoff.ino index 6e1260c78..9517755ee 100755 --- a/sonoff/sonoff.ino +++ b/sonoff/sonoff.ino @@ -1468,7 +1468,12 @@ void MqttDataHandler(char* topic, uint8_t* data, unsigned int data_len) Response_P(PSTR("{\"" D_JSON_COMMAND "\":\"" D_JSON_UNKNOWN "\"}")); type = (char*)topicBuf; } - if (mqtt_data[0] != '\0') { MqttPublishPrefixTopic_P(RESULT_OR_STAT, type); } + if (mqtt_data[0] != '\0') { + MqttPublishPrefixTopic_P(RESULT_OR_STAT, type); +#ifdef USE_SCRIPT + XdrvRulesProcess(); +#endif + } fallback_topic_flag = false; } @@ -1835,6 +1840,9 @@ void MqttPublishTeleState(void) mqtt_data[0] = '\0'; MqttShowState(); MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_STATE), MQTT_TELE_RETAIN); +#ifdef USE_SCRIPT + RulesTeleperiod(); // Allow rule based HA messages +#endif // USE_SCRIPT } bool MqttShowSensor(void) @@ -1919,7 +1927,7 @@ void PerformEverySecond(void) mqtt_data[0] = '\0'; if (MqttShowSensor()) { MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR), Settings.flag.mqtt_sensor_retain); -#ifdef USE_RULES +#if defined(USE_RULES) || defined(USE_SCRIPT) RulesTeleperiod(); // Allow rule based HA messages #endif // USE_RULES } diff --git a/sonoff/support_features.ino b/sonoff/support_features.ino index c97f77c9d..1802f93b4 100644 --- a/sonoff/support_features.ino +++ b/sonoff/support_features.ino @@ -103,7 +103,7 @@ void GetFeatures(void) #ifdef USE_TIMERS_WEB feature_drv1 |= 0x04000000; // xdrv_09_timers.ino #endif -#ifdef USE_RULES +#if defined(USE_RULES) || defined(USE_SCRIPT) feature_drv1 |= 0x08000000; // xdrv_10_rules.ino #endif #ifdef USE_KNX diff --git a/sonoff/xdrv_09_timers.ino b/sonoff/xdrv_09_timers.ino index dcd18c6f1..3f838ed7a 100644 --- a/sonoff/xdrv_09_timers.ino +++ b/sonoff/xdrv_09_timers.ino @@ -285,7 +285,7 @@ void TimerEverySecond(void) if (time == set_time) { if (xtimer.days & days) { Settings.timer[i].arm = xtimer.repeat; -#ifdef USE_RULES +#if defined(USE_RULES) || defined(USE_SCRIPT) if (3 == xtimer.power) { // Blink becomes Rule disregarding device and allowing use of Backlog commands Response_P(PSTR("{\"Clock\":{\"Timer\":%d}}"), i +1); XdrvRulesProcess(); @@ -359,7 +359,8 @@ bool TimerCommand(void) Settings.timer[index -1].data = Settings.timer[XdrvMailbox.payload -1].data; // Copy timer } } else { -#ifndef USE_RULES +//#ifndef USE_RULES +#if defined(USE_RULES)==0 && defined(USE_SCRIPT)==0 if (devices_present) { #endif StaticJsonBuffer<256> jsonBuffer; @@ -437,7 +438,8 @@ bool TimerCommand(void) index++; } -#ifndef USE_RULES +//#ifndef USE_RULES +#if defined(USE_RULES)==0 && defined(USE_SCRIPT)==0 } else { Response_P(PSTR("{\"" D_CMND_TIMER "%d\":\"" D_JSON_TIMER_NO_DEVICE "\"}"), index); // No outputs defined so nothing to control error = 1; @@ -630,7 +632,7 @@ const char HTTP_TIMER_SCRIPT5[] PROGMEM = "if(%d>0){" // Create Output and Action drop down boxes "eb('oa').innerHTML=\"" D_TIMER_OUTPUT " " D_TIMER_ACTION " \";" "o=qs('#p1');ce('" D_OFF "',o);ce('" D_ON "',o);ce('" D_TOGGLE "',o);" // Create offset direction select options -#ifdef USE_RULES +#if defined(USE_RULES) || defined(USE_SCRIPT) "ce('" D_RULE "',o);" #else "ce('" D_BLINK "',o);" @@ -768,7 +770,7 @@ bool Xdrv09(uint8_t function) #ifdef USE_WEBSERVER #ifdef USE_TIMERS_WEB case FUNC_WEB_ADD_BUTTON: -#ifdef USE_RULES +#if defined(USE_RULES) || defined(USE_SCRIPT) WSContentSend_P(HTTP_BTN_MENU_TIMER); #else if (devices_present) { WSContentSend_P(HTTP_BTN_MENU_TIMER); } diff --git a/sonoff/xdrv_10_scripter.ino b/sonoff/xdrv_10_scripter.ino new file mode 100644 index 000000000..72e6b6190 --- /dev/null +++ b/sonoff/xdrv_10_scripter.ino @@ -0,0 +1,2235 @@ +/* + xdrv_10_scripter.ino - script support for Sonoff-Tasmota + + Copyright (C) 2019 Gerhard Mutz 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 . +*/ + +// for doku see up to date doku in file scripter.md + +// uses about 14,2 k of flash +// more stack could be needed for sendmail => -D CONT_STACKSIZE=4800 = +0.8k stack -0.8k heap +// + +/* to doo +optimize code for space + +// remarks +goal is fast execution time, minimal use of ram and intuitive syntax +therefore => +case sensitive cmds and vars (lowercase uses time and code) +no math hierarchy (costs ram and execution time, better group with brackets, anyhow better readable for beginners) +(will probably make math hierarchy an ifdefed option) +keywords if then else endif, or, and are better readable for beginners (others may use {}) +*/ +#ifdef USE_SCRIPT +#ifndef USE_RULES + +#define XDRV_10 10 + +#define SCRIPT_DEBUG 0 + +#define MAXVARS 50 +#define MAXNVARS 45 +#define MAXSVARS 5 +#define MAXFILT 5 +#define SCRIPT_SVARSIZE 20 +#define SCRIPT_MAXSSIZE 48 +#define SCRIPT_EOL '\n' +#define SCRIPT_FLOAT_PRECISION 2 +#define SCRIPT_MAXPERM (MAX_RULE_MEMS*10)-4/sizeof(float) + + +enum {OPER_EQU=1,OPER_PLS,OPER_MIN,OPER_MUL,OPER_DIV,OPER_PLSEQU,OPER_MINEQU,OPER_MULEQU,OPER_DIVEQU,OPER_EQUEQU,OPER_NOTEQU,OPER_GRTEQU,OPER_LOWEQU,OPER_GRT,OPER_LOW,OPER_PERC,OPER_XOR,OPER_AND,OPER_OR,OPER_ANDEQU,OPER_OREQU,OPER_XOREQU,OPER_PERCEQU}; +enum {SCRIPT_LOGLEVEL=1,SCRIPT_TELEPERIOD}; + +typedef union { + uint8_t data; + struct { + uint8_t is_string : 1; // string or number + uint8_t is_permanent : 1; + uint8_t is_timer : 1; + uint8_t is_autoinc : 1; + uint8_t changed : 1; + uint8_t settable : 1; + uint8_t is_filter : 1; + uint8_t constant : 1; + }; +} SCRIPT_TYPE; + +struct T_INDEX { + uint8_t index; + SCRIPT_TYPE bits; +}; + +struct M_FILT { + uint8_t numvals; + uint8_t index; + float maccu; + float rbuff[1]; +}; + +// global memory +struct SCRIPT_MEM { + float *fvars; // number var pointer + float *s_fvars; // shadow var pointer + struct T_INDEX *type; // type and index pointer + struct M_FILT *mfilt; + char *glob_vnp; // var name pointer + uint8_t *vnp_offset; + char *glob_snp; // string vars pointer + char *scriptptr; + uint8_t numvars; + void *script_mem; + uint16_t script_mem_size; + uint8_t script_dprec; + uint8_t var_not_found; + uint8_t glob_error; + uint8_t max_ssize; + uint8_t script_loglevel; +} glob_script_mem; + +uint8_t tasm_cmd_activ=0; + +uint32_t script_lastmillis; + +char *GetNumericResult(char *lp,uint8_t lastop,float *fp,JsonObject *jo); + +void ScriptEverySecond(void) { + + if (bitRead(Settings.rule_enabled, 0)) { + struct T_INDEX *vtp=glob_script_mem.type; + float delta=(millis()-script_lastmillis)/1000; + script_lastmillis=millis(); + for (uint8_t count=0; count0) { + // decrement + *fp-=delta; + if (*fp<0) *fp=0; + } + } + if (vtp[count].bits.is_autoinc) { + // increments timers + float *fp=&glob_script_mem.fvars[vtp[count].index]; + if (*fp>=0) { + *fp+=delta; + } + } + } + Run_Scripter(">S",2,0); + } +} + +void RulesTeleperiod(void) { + if (bitRead(Settings.rule_enabled, 0)) Run_Scripter(">T",2, mqtt_data); +} + +#define SCRIPT_SKIP_SPACES while (*lp==' ' || *lp=='\t') lp++; +#define SCRIPT_SKIP_EOL while (*lp==SCRIPT_EOL) lp++; + +// allocates all variable and presets them +int16_t Init_Scripter(char *script) { + // scan lines for >DEF + uint16_t lines=0,nvars=0,svars=0,vars=0; + char *lp=script; + char vnames[MAXVARS*10]; + char *vnames_p=vnames; + char *vnp[MAXVARS]; + char **vnp_p=vnp; + char strings[MAXSVARS*SCRIPT_MAXSSIZE]; + struct M_FILT mfilt[MAXFILT]; + + char *strings_p=strings; + char *snp[MAXSVARS]; + char **snp_p=snp; + uint8_t numperm=0,numflt=0,count; + + glob_script_mem.max_ssize=SCRIPT_SVARSIZE; + glob_script_mem.scriptptr=0; + if (!*script) return -999; + + float fvalues[MAXVARS]; + struct T_INDEX vtypes[MAXVARS]; + char init=0; + while (1) { + // check line + // skip leading spaces + SCRIPT_SKIP_SPACES + // skip empty line + if (*lp=='\n' || *lp=='\r') goto next_line; + // skip comment + if (*lp==';') goto next_line; + if (init) { + // init section + if (*lp=='>') { + init=0; + break; + } + char *op=strchr(lp,'='); + if (op) { + vtypes[vars].bits.data=0; + // found variable definition + if (*lp=='p' && *(lp+1)==':') { + lp+=2; + if (numpermMAXFILT) { + return -6; + } + } else { + vtypes[vars].bits.is_filter=0; + } + *vnp_p++=vnames_p; + while (lpMAXNVARS) { + return -1; + } + } else { + // string vars + op++; + *snp_p++=strings_p; + while (*op!='\"') { + if (*op==SCRIPT_EOL) break; + *strings_p++=*op++; + } + *strings_p++=0; + vtypes[vars].bits.is_string=1; + vtypes[vars].index=svars; + svars++; + if (svars>MAXSVARS) { + return -2; + } + } + vars++; + if (vars>MAXVARS) { + return -3; + } + } + } else { + if (!strncmp(lp,">D",2)) { + lp+=2; + SCRIPT_SKIP_SPACES + if (isdigit(*lp)) { + uint8_t ssize=atoi(lp)+1; + if (ssize<10 || ssize>SCRIPT_MAXSSIZE) ssize=SCRIPT_MAXSSIZE; + glob_script_mem.max_ssize=ssize; + } + init=1; + } + } + // next line + next_line: + lp = strchr(lp, SCRIPT_EOL); + if (!lp) break; + lp++; + } + + uint16_t fsize=0; + for (count=0; countnumvals=mfilt[count].numvals; + mp+=sizeof(struct M_FILT)+((mfilt[count].numvals&0x7f)-1)*sizeof(float); + } + + glob_script_mem.numvars=vars; + glob_script_mem.script_dprec=SCRIPT_FLOAT_PRECISION; + glob_script_mem.script_loglevel=LOG_LEVEL_INFO; + + +#if SCRIPT_DEBUG>2 + struct T_INDEX *dvtp=glob_script_mem.type; + for (uint8_t count=0; count0 + ClaimSerial(); + SetSerialBaudrate(9600); +#endif + + // store start of actual program here + glob_script_mem.scriptptr=lp-1; + return 0; + +} + +#define NUM_RES 0xfe +#define STR_RES 0xfd +#define VAR_NV 0xff + +#define NTYPE 0 +#define STYPE 0x80 + + +//Settings.seriallog_level +//Settings.weblog_level + +float Get_MFilter(uint8_t index) { + uint8_t *mp=(uint8_t*)glob_script_mem.mfilt; + for (uint8_t count=0; countnumvals&0x80) { + // moving average + return mflp->maccu/(mflp->numvals&0x7f); + } else { + // median, sort array + float tbuff[mflp->numvals],tmp; + uint8_t flag; + memmove(tbuff,mflp->rbuff,sizeof(tbuff)); + for (uint8_t ocnt=0; ocntnumvals; ocnt++) { + flag=0; + for (uint8_t count=0; countnumvals-1; count++) { + if (tbuff[count]>tbuff[count+1]) { + tmp=tbuff[count]; + tbuff[count]=tbuff[count+1]; + tbuff[count+1]=tmp; + flag=1; + } + } + if (!flag) break; + } + return mflp->rbuff[mflp->numvals/2]; + } + } + mp+=sizeof(struct M_FILT)+((mflp->numvals&0x7f)-1)*sizeof(float); + } + return 0; +} + +void Set_MFilter(uint8_t index, float invar) { + uint8_t *mp=(uint8_t*)glob_script_mem.mfilt; + for (uint8_t count=0; countnumvals&0x80) { + // moving average + mflp->maccu-=mflp->rbuff[mflp->index]; + mflp->maccu+=invar; + mflp->rbuff[mflp->index]=invar; + mflp->index++; + if (mflp->index>=(mflp->numvals&0x7f)) mflp->index=0; + } else { + // median + mflp->rbuff[mflp->index]=invar; + mflp->index++; + if (mflp->index>=mflp->numvals) mflp->index=0; + } + break; + } + mp+=sizeof(struct M_FILT)+((mflp->numvals&0x7f)-1)*sizeof(float); + } +} + +#define MEDIAN_SIZE 5 +#define MEDIAN_FILTER_NUM 2 + +struct MEDIAN_FILTER { +float buffer[MEDIAN_SIZE]; +int8_t index; +} script_mf[MEDIAN_FILTER_NUM]; + +float DoMedian5(uint8_t index, float in) { + + if (index>=MEDIAN_FILTER_NUM) index=0; + + struct MEDIAN_FILTER* mf=&script_mf[index]; + + float tbuff[MEDIAN_SIZE],tmp; + uint8_t flag; + mf->buffer[mf->index]=in; + mf->index++; + if (mf->index>=MEDIAN_SIZE) mf->index=0; + // sort list and take median + memmove(tbuff,mf->buffer,sizeof(tbuff)); + for (uint8_t ocnt=0; ocnttbuff[count+1]) { + tmp=tbuff[count]; + tbuff[count]=tbuff[count+1]; + tbuff[count+1]=tmp; + flag=1; + } + } + if (!flag) break; + } + return tbuff[MEDIAN_SIZE/2]; +} + + +// vtype => ff=nothing found, fe=constant number,fd = constant string else bit 7 => 80 = string, 0 = number +// no flash strings here for performance reasons!!! +char *isvar(char *lp, uint8_t *vtype,struct T_INDEX *tind,float *fp,char *sp,JsonObject *jo) { + uint16_t count,len=0; + uint8_t nres=0; + char vname[32]; + float fvar=0; + tind->index=0; + tind->bits.data=0; + + if (isdigit(*lp) || (*lp=='-' && isdigit(*(lp+1))) || *lp=='.') { + // isnumber + if (fp) *fp=CharToDouble(lp); + if (*lp=='-') lp++; + while (isdigit(*lp) || *lp=='.') { + if (*lp==0 || *lp==SCRIPT_EOL) break; + lp++; + } + tind->bits.constant=1; + tind->bits.is_string=0; + *vtype=NUM_RES; + return lp; + } + if (*lp=='"') { + lp++; + while (*lp!='"') { + if (*lp==0 || *lp==SCRIPT_EOL) break; + if (sp) *sp++=*lp; + lp++; + } + if (sp) *sp=0; + *vtype=STR_RES; + tind->bits.constant=1; + tind->bits.is_string=1; + return lp+1; + } + + if (*lp=='-') { + // inverted var + nres=1; + lp++; + } + + const char *term="\n\r ])=+-/*%>index=VAR_NV; + glob_script_mem.var_not_found=1; + return lp; + } + + struct T_INDEX *vtp=glob_script_mem.type; + for (count=0; countindex=count; // overwrite with global var index + if (vtp[count].bits.is_string==0) { + *vtype=NTYPE|index; + if (vtp[count].bits.is_filter) { + fvar=Get_MFilter(index); + } else { + fvar=glob_script_mem.fvars[index]; + } + if (nres) fvar=-fvar; + if (fp) *fp=fvar; + } else { + *vtype=STYPE|index; + if (sp) strlcpy(sp,glob_script_mem.glob_snp+(index*glob_script_mem.max_ssize),SCRIPT_MAXSSIZE); + } + return lp+len; + } + } + } + + if (jo) { + // look for json input + const char* str_value; + uint8_t aindex; + String vn; + char *ja=strchr(vname,'['); + if (ja) { + // json array + *ja=0; + ja++; + // fetch array index + float fvar; + GetNumericResult(ja,OPER_EQU,&fvar,0); + aindex=fvar; + if (aindex<1 || aindex>6) aindex=1; + aindex--; + } + if (jo->success()) { + char *subtype=strchr(vname,'#'); + if (subtype) { + *subtype=0; + subtype++; + } + vn=vname; + str_value = (*jo)[vn]; + if ((*jo)[vn].success()) { + if (subtype) { + JsonObject &jobj1=(*jo)[vn]; + if (jobj1.success()) { + vn=subtype; + jo=&jobj1; + str_value = (*jo)[vn]; + if ((*jo)[vn].success()) { + goto skip; + } + } else { + goto chknext; + } + } + skip: + if (ja) { + // json array + str_value = (*jo)[vn][aindex]; + } + if (str_value && *str_value) { + if ((*jo).is(vn)) { + if (!strncmp(str_value,"ON",2)) { + if (fp) *fp=1; + } else if (!strncmp(str_value,"OFF",3)) { + if (fp) *fp=0; + } else { + *vtype=STR_RES; + tind->bits.constant=1; + tind->bits.is_string=1; + if (sp) strlcpy(sp,str_value,SCRIPT_MAXSSIZE); + return lp+len; + } + } else { + if (fp) *fp=CharToDouble((char*)str_value); + *vtype=NUM_RES; + tind->bits.constant=1; + tind->bits.is_string=0; + return lp+len; + } + } + } + } + } + +chknext: + switch (vname[0]) { + case 'b': + if (!strncmp(vname,"boot",4)) { + if (rules_flag.system_boot) { + rules_flag.system_boot=0; + fvar=1; + } + goto exit; + } + break; + case 'c': + if (!strncmp(vname,"chg[",4)) { + // var changed + struct T_INDEX ind; + uint8_t vtype; + isvar(vname+4,&vtype,&ind,0,0,0); + if (!ind.bits.constant) { + uint8_t index=glob_script_mem.type[ind.index].index; + if (glob_script_mem.fvars[index]!=glob_script_mem.s_fvars[index]) { + // var has changed + glob_script_mem.s_fvars[index]=glob_script_mem.fvars[index]; + fvar=1; + len++; + goto exit; + } else { + fvar=0; + len++; + goto exit; + } + } + } + break; + case 'd': + if (!strncmp(vname,"day",3)) { + fvar=RtcTime.day_of_month; + goto exit; + } + break; + case 'g': + if (!strncmp(vname,"gtmp",4)) { + fvar=global_temperature; + goto exit; + } + if (!strncmp(vname,"ghum",4)) { + fvar=global_humidity; + goto exit; + } + if (!strncmp(vname,"gprs",4)) { + fvar=global_pressure; + goto exit; + } + if (!strncmp(vname,"gtopic",6)) { + if (sp) strlcpy(sp,Settings.mqtt_grptopic,glob_script_mem.max_ssize); + goto strexit; + } + break; + case 'h': + if (!strncmp(vname,"hours",5)) { + fvar=RtcTime.hour; + goto exit; + } + if (!strncmp(vname,"heap",4)) { + fvar=ESP.getFreeHeap(); + goto exit; + } + if (!strncmp(vname,"hn(",3)) { + lp=GetNumericResult(lp+3,OPER_EQU,&fvar,0); + if (fvar<0 || fvar>255) fvar=0; + lp++; + len=0; + if (sp) { + sprintf(sp,"%02x",(uint8_t)fvar); + } + goto strexit; + } + break; + case 'i': + if (!strncmp(vname,"int(",4)) { + lp=GetNumericResult(lp+4,OPER_EQU,&fvar,0); + fvar=floor(fvar); + lp++; + len=0; + goto exit; + } + break; + case 'l': + if (!strncmp(vname,"loglvl",6)) { + fvar=glob_script_mem.script_loglevel; + tind->index=SCRIPT_LOGLEVEL; + exit_settable: + if (fp) *fp=fvar; + *vtype=NTYPE; + tind->bits.settable=1; + tind->bits.is_string=0; + return lp+len; + } + break; + case 'm': + if (!strncmp(vname,"med(",4)) { + float fvar1; + lp=GetNumericResult(lp+4,OPER_EQU,&fvar1,0); + SCRIPT_SKIP_SPACES + // arg2 + float fvar2; + lp=GetNumericResult(lp,OPER_EQU,&fvar2,0); + fvar=DoMedian5(fvar1,fvar2); + lp++; + len=0; + goto exit; + } + if (!strncmp(vname,"micros",6)) { + fvar=micros(); + goto exit; + } + if (!strncmp(vname,"millis",6)) { + fvar=millis(); + goto exit; + } + if (!strncmp(vname,"mins",4)) { + fvar=RtcTime.minute; + goto exit; + } + if (!strncmp(vname,"month",5)) { + fvar=RtcTime.month; + goto exit; + } + if (!strncmp(vname,"mqttc",5)) { + if (rules_flag.mqtt_connected) { + rules_flag.mqtt_connected=0; + fvar=1; + } + goto exit; + } + if (!strncmp(vname,"mqttd",5)) { + if (rules_flag.mqtt_disconnected) { + rules_flag.mqtt_disconnected=0; + fvar=1; + } + goto exit; + } + if (!strncmp(vname,"mqtts",5)) { + fvar=!global_state.mqtt_down; + goto exit; + } + break; + case 'p': + if (!strncmp(vname,"pin[",4)) { + // raw pin level + GetNumericResult(vname+4,OPER_EQU,&fvar,0); + fvar=digitalRead((uint8_t)fvar); + // skip ] bracket + len++; + goto exit; + } + if (!strncmp(vname,"pn[",3)) { + GetNumericResult(vname+3,OPER_EQU,&fvar,0); + fvar=pin[(uint8_t)fvar]; + // skip ] bracket + len++; + goto exit; + } + if (!strncmp(vname,"pd[",3)) { + GetNumericResult(vname+3,OPER_EQU,&fvar,0); + uint8_t gpiopin=fvar; + for (uint8_t i=0;iindex=SCRIPT_TELEPERIOD; + goto exit_settable; + } + if (!strncmp(vname,"tinit",5)) { + if (rules_flag.time_init) { + rules_flag.time_init=0; + fvar=1; + } + goto exit; + } + if (!strncmp(vname,"tset",4)) { + if (rules_flag.time_set) { + rules_flag.time_set=0; + fvar=1; + } + goto exit; + } + if (!strncmp(vname,"tstamp",6)) { + if (sp) strlcpy(sp,GetDateAndTime(DT_LOCAL).c_str(),glob_script_mem.max_ssize); + goto strexit; + } + if (!strncmp(vname,"topic",5)) { + if (sp) strlcpy(sp,Settings.mqtt_topic,glob_script_mem.max_ssize); + goto strexit; + } + break; + case 'u': + if (!strncmp(vname,"uptime",6)) { + fvar=MinutesUptime(); + goto exit; + } + if (!strncmp(vname,"upsecs",6)) { + fvar=uptime; + goto exit; + } + if (!strncmp(vname,"upd[",4)) { + // var was updated + struct T_INDEX ind; + uint8_t vtype; + isvar(vname+4,&vtype,&ind,0,0,0); + if (!ind.bits.constant) { + if (!ind.bits.changed) { + fvar=0; + len++; + goto exit; + } else { + glob_script_mem.type[ind.index].bits.changed=0; + fvar=1; + len++; + goto exit; + } + } + goto notfound; + } + break; + case 'w': + if (!strncmp(vname,"wday",4)) { + fvar=RtcTime.day_of_week; + goto exit; + } + if (!strncmp(vname,"wific",5)) { + if (rules_flag.wifi_connected) { + rules_flag.wifi_connected=0; + fvar=1; + } + goto exit; + } + if (!strncmp(vname,"wifid",5)) { + if (rules_flag.wifi_disconnected) { + rules_flag.wifi_disconnected=0; + fvar=1; + } + goto exit; + } + if (!strncmp(vname,"wifis",5)) { + fvar=!global_state.wifi_down; + goto exit; + } + break; + case 'y': + if (!strncmp(vname,"year",4)) { + fvar=RtcTime.year; + goto exit; + } + break; + default: + break; + } + // nothing valid found +notfound: + if (fp) *fp=0; + *vtype=VAR_NV; + tind->index=VAR_NV; + glob_script_mem.var_not_found=1; + return lp; + // return constant numbers +exit: + if (fp) *fp=fvar; + *vtype=NUM_RES; + tind->bits.constant=1; + tind->bits.is_string=0; + return lp+len; + // return constant strings +strexit: + *vtype=STYPE; + tind->bits.constant=1; + tind->bits.is_string=1; + return lp+len; +} + + + +char *getop(char *lp, uint8_t *operand) { + switch (*lp) { + case '=': + if (*(lp+1)=='=') { + *operand=OPER_EQUEQU; + return lp+2; + } else { + *operand=OPER_EQU; + return lp+1; + } + break; + case '+': + if (*(lp+1)=='=') { + *operand=OPER_PLSEQU; + return lp+2; + } else { + *operand=OPER_PLS; + return lp+1; + } + break; + case '-': + if (*(lp+1)=='=') { + *operand=OPER_MINEQU; + return lp+2; + } else { + *operand=OPER_MIN; + return lp+1; + } + break; + case '*': + if (*(lp+1)=='=') { + *operand=OPER_MULEQU; + return lp+2; + } else { + *operand=OPER_MUL; + return lp+1; + } + break; + case '/': + if (*(lp+1)=='=') { + *operand=OPER_DIVEQU; + return lp+2; + } else { + *operand=OPER_DIV; + return lp+1; + } + break; + case '!': + if (*(lp+1)=='=') { + *operand=OPER_NOTEQU; + return lp+2; + } + break; + case '>': + if (*(lp+1)=='=') { + *operand=OPER_GRTEQU; + return lp+2; + } else { + *operand=OPER_GRT; + return lp+1; + + } + break; + case '<': + if (*(lp+1)=='=') { + *operand=OPER_LOWEQU; + return lp+2; + } else { + *operand=OPER_LOW; + return lp+1; + } + break; + case '%': + if (*(lp+1)=='=') { + *operand=OPER_PERCEQU; + return lp+2; + } else { + *operand=OPER_PERC; + return lp+1; + } + break; + case '^': + if (*(lp+1)=='=') { + *operand=OPER_XOREQU; + return lp+2; + } else { + *operand=OPER_XOR; + return lp+1; + } + break; + case '&': + if (*(lp+1)=='=') { + *operand=OPER_ANDEQU; + return lp+2; + } else { + *operand=OPER_AND; + return lp+1; + } + break; + case '|': + if (*(lp+1)=='=') { + *operand=OPER_OREQU; + return lp+2; + } else { + *operand=OPER_OR; + return lp+1; + } + break; + } + *operand=0; + return lp; +} + + +#if defined(ARDUINO_ESP8266_RELEASE_2_3_0) || defined(ARDUINO_ESP8266_RELEASE_2_4_0) || defined(ARDUINO_ESP8266_RELEASE_2_4_1) +// All version before core 2.4.2 +// https://github.com/esp8266/Arduino/issues/2557 +extern "C" { +#include + extern cont_t g_cont; +} +uint16_t GetStack(void) { + register uint32_t *sp asm("a1"); + return (4 * (sp - g_pcont.stack)); +} + +#else +extern "C" { +#include + extern cont_t* g_pcont; +} +uint16_t GetStack(void) { + register uint32_t *sp asm("a1"); + return (4 * (sp - g_pcont->stack)); +} +#endif + +char *GetStringResult(char *lp,uint8_t lastop,char *cp,JsonObject *jo) { + uint8_t operand=0; + uint8_t vtype; + char *slp; + struct T_INDEX ind; + char str[SCRIPT_MAXSSIZE],str1[SCRIPT_MAXSSIZE]; + while (1) { + lp=isvar(lp,&vtype,&ind,0,str1,jo); + switch (lastop) { + case OPER_EQU: + strlcpy(str,str1,sizeof(str)); + break; + case OPER_PLS: + strlcat(str,str1,sizeof(str)); + break; + } + slp=lp; + lp=getop(lp,&operand); + switch (operand) { + case OPER_EQUEQU: + case OPER_NOTEQU: + case OPER_LOW: + case OPER_LOWEQU: + case OPER_GRT: + case OPER_GRTEQU: + lp=slp; + strcpy(cp,str); + return lp; + break; + default: + break; + } + lastop=operand; + if (!operand) { + strcpy(cp,str); + return lp; + } + } +} + +char *GetNumericResult(char *lp,uint8_t lastop,float *fp,JsonObject *jo) { +uint8_t operand=0; +float fvar1,fvar; +char *slp; +uint8_t vtype; +struct T_INDEX ind; + while (1) { + // get 1. value + if (*lp=='(') { + lp++; + lp=GetNumericResult(lp,OPER_EQU,&fvar1,jo); + lp++; + //if (*lp==')') lp++; + } else { + lp=isvar(lp,&vtype,&ind,&fvar1,0,jo); + if (vtype!=NUM_RES && vtype&STYPE) { + // string type + glob_script_mem.glob_error=1; + } + } + switch (lastop) { + case OPER_EQU: + fvar=fvar1; + break; + case OPER_PLS: + fvar+=fvar1; + break; + case OPER_MIN: + fvar-=fvar1; + break; + case OPER_MUL: + fvar*=fvar1; + break; + case OPER_DIV: + fvar/=fvar1; + break; + case OPER_PERC: + fvar=fmod(fvar,fvar1); + break; + case OPER_XOR: + fvar=(uint32_t)fvar^(uint32_t)fvar1; + break; + case OPER_AND: + fvar=(uint32_t)fvar&(uint32_t)fvar1; + break; + case OPER_OR: + fvar=(uint32_t)fvar|(uint32_t)fvar1; + break; + default: + break; + } + slp=lp; + lp=getop(lp,&operand); + switch (operand) { + case OPER_EQUEQU: + case OPER_NOTEQU: + case OPER_LOW: + case OPER_LOWEQU: + case OPER_GRT: + case OPER_GRTEQU: + lp=slp; + *fp=fvar; + return lp; + break; + default: + break; + } + lastop=operand; + if (!operand) { + *fp=fvar; + return lp; + } + } +} + + +// replace vars in cmd %var% +void Replace_Cmd_Vars(char *srcbuf,char *dstbuf,uint16_t dstsize) { + char *cp; + uint16_t count; + uint8_t vtype; + float fvar; + cp=srcbuf; + struct T_INDEX ind; + char string[SCRIPT_MAXSSIZE]; + for (count=0;count=sizeof(str)) len=len>=sizeof(str); + strlcpy(str,cp,len); + toSLog(str); +} + +void toLogEOL(const char *s1,const char *str) { + if (!str) return; + uint8_t index=0; + char *cp=log_data; + strcpy(cp,s1); + cp+=strlen(s1); + while (*str) { + if (*str==SCRIPT_EOL) break; + *cp++=*str++; + } + *cp=0; + AddLog(LOG_LEVEL_INFO); +} + + +void toSLog(const char *str) { + if (!str) return; +#if SCRIPT_DEBUG>0 + while (*str) { + Serial.write(*str); + str++; + } +#endif +} + +#define IF_NEST 8 +// execute section of scripter +int16_t Run_Scripter(const char *type, uint8_t tlen, char *js) { + uint8_t vtype=0,sindex,xflg,floop=0,globvindex; + struct T_INDEX ind; + uint8_t operand,lastop,numeric=1,if_state[IF_NEST],if_result[IF_NEST],and_or,ifstck=0,s_ifstck=0; + if_state[ifstck]=0; + if_result[ifstck]=0; + char cmpstr[SCRIPT_MAXSSIZE]; + + if (tasm_cmd_activ) return 0; + + + float *dfvar,*cv_count,cv_max,cv_inc; + char *cv_ptr; + float fvar=0,fvar1,sysvar,swvar; + uint8_t section=0,sysv_type=0,swflg=0; + + if (!glob_script_mem.scriptptr) { + return -99; + } + + DynamicJsonBuffer jsonBuffer; // on heap + JsonObject &jobj=jsonBuffer.parseObject(js); + JsonObject *jo; + if (js) jo=&jobj; + else jo=0; + + char *lp=glob_script_mem.scriptptr; + + while (1) { + // check line + // skip leading spaces + startline: + SCRIPT_SKIP_SPACES + // skip empty line + SCRIPT_SKIP_EOL + // skip comment + if (*lp==';') goto next_line; + if (!*lp) break; + + if (section) { + // we are in section + if (*lp=='>') { + section=0; + break; + } + if (*lp=='#') { + section=0; + break; + } + glob_script_mem.var_not_found=0; + +#if SCRIPT_DEBUG>0 + char tbuff[128]; + sprintf(tbuff,"stack=%d,state=%d,cmpres=%d line: ",ifstck,if_state[ifstck],if_result[ifstck]); + toLogEOL(tbuff,lp); +#endif + + if (!strncmp(lp,"if",2)) { + lp+=2; + if (if_state[ifstck]>0) { + if (ifstck=2) { + lp+=5; + if_state[ifstck]=0; + if (ifstck>0) { + ifstck--; + } + s_ifstck=ifstck; // >>>>> + goto next_line; + } else if (!strncmp(lp,"or",2) && if_state[ifstck]==1) { + lp+=2; + and_or=1; + } else if (!strncmp(lp,"and",3) && if_state[ifstck]==1) { + lp+=3; + and_or=2; + } + + if (*lp=='{' && if_state[ifstck]==1) { + lp+=1; // then + if_state[ifstck]=2; + } else if (*lp=='{' && if_state[ifstck]==3) { + lp+=1; // after else + //if_state[ifstck]=3; + } else if (*lp=='}' && if_state[ifstck]>=2) { + lp++; // must check for else + char *slp=lp; + uint8_t iselse=0; + for (uint8_t count=0; count<8;count++) { + if (*lp=='}') { + // must be endif + break; + } + if (!strncmp(lp,"else",4)) { + // is before else, no endif + if_state[ifstck]=3; + lp+=4; + iselse=1; + break; + } + lp++; + } + if (!iselse) { + lp=slp; + // endif + if_state[ifstck]=0; + if (ifstck>0) { + ifstck--; + } + s_ifstck=ifstck; // >>>>> + } + } + + if (!strncmp(lp,"for",3)) { + // start for next loop, fetch 3 params + // simple implementation, zero loop count not supported + lp+=3; + SCRIPT_SKIP_SPACES + lp=isvar(lp,&vtype,&ind,0,0,0); + if ((vtype!=VAR_NV) && (vtype&STYPE)==0) { + // numeric var + uint8_t index=glob_script_mem.type[ind.index].index; + cv_count=&glob_script_mem.fvars[index]; + SCRIPT_SKIP_SPACES + lp=GetNumericResult(lp,OPER_EQU,cv_count,0); + SCRIPT_SKIP_SPACES + lp=GetNumericResult(lp,OPER_EQU,&cv_max,0); + SCRIPT_SKIP_SPACES + lp=GetNumericResult(lp,OPER_EQU,&cv_inc,0); + SCRIPT_SKIP_EOL + cv_ptr=lp; + floop=1; + } else { + // error + toLogEOL("for error",lp); + } + } else if (!strncmp(lp,"next",4) && floop>0) { + // for next loop + *cv_count+=cv_inc; + if (*cv_count<=cv_max) { + lp=cv_ptr; + } else { + lp+=4; + floop=0; + } + } + + if (!strncmp(lp,"switch",6)) { + lp+=6; + SCRIPT_SKIP_SPACES + lp=GetNumericResult(lp,OPER_EQU,&swvar,0); + swflg=1; + } else if (!strncmp(lp,"case",4) && swflg>0) { + lp+=4; + SCRIPT_SKIP_SPACES + float cvar; + lp=GetNumericResult(lp,OPER_EQU,&cvar,0); + if (swvar!=cvar) { + swflg=2; + } else { + swflg=1; + } + } else if (!strncmp(lp,"ends",4) && swflg>0) { + lp+=4; + swflg=0; + } + + if (swflg==2) goto next_line; + + + SCRIPT_SKIP_SPACES + //SCRIPT_SKIP_EOL + if (*lp==SCRIPT_EOL) { + goto next_line; + } + //toLogN(lp,16); + if (if_state[s_ifstck]==3 && if_result[s_ifstck]) goto next_line; + if (if_state[s_ifstck]==2 && !if_result[s_ifstck]) goto next_line; + + s_ifstck=ifstck; + + if (!strncmp(lp,"break",5)) { + if (floop) { + // should break loop + floop=0; + } else { + section=0; + } + break; + } else if (!strncmp(lp,"dprec",5)) { + lp+=5; + // number precision + glob_script_mem.script_dprec=atoi(lp); + goto next_line; + } else if (!strncmp(lp,"delay(",6)) { + lp+=5; + // delay + lp=GetNumericResult(lp,OPER_EQU,&fvar,0); + delay(fvar); + goto next_line; + } else if (!strncmp(lp,"spinm(",6)) { + lp+=6; + // set pin mode + lp=GetNumericResult(lp,OPER_EQU,&fvar,0); + int8_t pinnr=fvar; + SCRIPT_SKIP_SPACES + lp=GetNumericResult(lp,OPER_EQU,&fvar,0); + int8_t mode=fvar; + pinMode(pinnr,mode&1); + goto next_line; + } else if (!strncmp(lp,"spin(",5)) { + lp+=5; + // set pin mode + lp=GetNumericResult(lp,OPER_EQU,&fvar,0); + int8_t pinnr=fvar; + SCRIPT_SKIP_SPACES + lp=GetNumericResult(lp,OPER_EQU,&fvar,0); + int8_t mode=fvar; + digitalWrite(pinnr,mode&1); + goto next_line; + } else if (!strncmp(lp,"svars(",5)) { + lp+=5; + // save vars + Scripter_save_pvars(); + goto next_line; + } + + else if (!strncmp(lp,"=>",2)) { + // execute cmd + lp+=2; + SCRIPT_SKIP_SPACES + #define SCRIPT_CMDMEM 512 + char *cmdmem=(char*)malloc(SCRIPT_CMDMEM); + if (cmdmem) { + char *cmd=cmdmem; + short count; + for (count=0; countfvar1); + break; + case OPER_GRTEQU: + res=(*dfvar>=fvar1); + break; + default: + // error + break; + } + + if (!and_or) { + if_result[s_ifstck]=res; + } else if (and_or==1) { + if_result[s_ifstck]|=res; + } else { + if_result[s_ifstck]&=res; + } +#if SCRIPT_DEBUG>0 + char tbuff[128]; + sprintf(tbuff,"p1=%d,p2=%d,cmpres=%d line: ",(int32_t)*dfvar,(int32_t)fvar1,if_result[s_ifstck]); + toLogEOL(tbuff,lp); +#endif + + } else { + // compare string + char str[SCRIPT_MAXSSIZE]; + lp=GetStringResult(lp,OPER_EQU,str,0); + if (lastop==OPER_EQUEQU || lastop==OPER_NOTEQU) { + uint8_t res=0; + res=strcmp(cmpstr,str); + if (lastop==OPER_EQUEQU) res=!res; + if (!and_or) { + if_result[s_ifstck]=res; + } else if (and_or==1) { + if_result[s_ifstck]|=res; + } else { + if_result[s_ifstck]&=res; + } + } + } + SCRIPT_SKIP_SPACES + if (*lp=='{' && if_state[ifstck]==1) { + lp+=1; // then + if_state[ifstck]=2; + } + goto next_line; + } else { + if (numeric) { + char *slp=lp; + glob_script_mem.glob_error=0; + lp=GetNumericResult(lp,OPER_EQU,&fvar,jo); + if (glob_script_mem.glob_error==1) { + // mismatch was string, not number + // get the string and convert to number + lp=isvar(slp,&vtype,&ind,0,cmpstr,jo); + fvar=CharToDouble(cmpstr); + } + switch (lastop) { + case OPER_EQU: + if (glob_script_mem.var_not_found) { + if (!js) toLog("var not found\n"); + goto next_line; + } + *dfvar=fvar; + break; + case OPER_PLSEQU: + *dfvar+=fvar; + break; + case OPER_MINEQU: + *dfvar-=fvar; + break; + case OPER_MULEQU: + *dfvar*=fvar; + break; + case OPER_DIVEQU: + *dfvar/=fvar; + break; + case OPER_PERCEQU: + *dfvar=fmod(*dfvar,fvar); + break; + case OPER_ANDEQU: + *dfvar=(uint32_t)*dfvar&(uint32_t)fvar; + break; + case OPER_OREQU: + *dfvar=(uint32_t)*dfvar|(uint32_t)fvar; + break; + case OPER_XOREQU: + *dfvar=(uint32_t)*dfvar^(uint32_t)fvar; + break; + default: + // error + break; + } + // var was changed + glob_script_mem.type[globvindex].bits.changed=1; + if (glob_script_mem.type[globvindex].bits.is_filter) { + Set_MFilter(glob_script_mem.type[globvindex].index,*dfvar); + } + + if (sysv_type) { + switch (sysv_type) { + case SCRIPT_LOGLEVEL: + glob_script_mem.script_loglevel=*dfvar; + break; + case SCRIPT_TELEPERIOD: + if (*dfvar<10) *dfvar=10; + if (*dfvar>300) *dfvar=300; + Settings.tele_period=*dfvar; + break; + } + sysv_type=0; + } + + } else { + // string result + char str[SCRIPT_MAXSSIZE]; + char *slp=lp; + lp=GetStringResult(lp,OPER_EQU,str,jo); + if (!js && glob_script_mem.var_not_found) { + // mismatch + lp=GetNumericResult(slp,OPER_EQU,&fvar,0); + dtostrfd(fvar,6,str); + glob_script_mem.var_not_found=0; + } + + if (!glob_script_mem.var_not_found) { + // var was changed + glob_script_mem.type[globvindex].bits.changed=1; + if (lastop==OPER_EQU) { + strlcpy(glob_script_mem.glob_snp+(sindex*glob_script_mem.max_ssize),str,glob_script_mem.max_ssize); + } else if (lastop==OPER_PLSEQU) { + strlcat(glob_script_mem.glob_snp+(sindex*glob_script_mem.max_ssize),str,glob_script_mem.max_ssize); + } + } + } + SCRIPT_SKIP_SPACES + if (*lp=='{' && if_state[ifstck]==3) { + lp+=1; // else + //if_state[ifstck]=3; + } + goto next_line; + } + } else { + // decode line + if (*lp=='>' && tlen==1) { + // called from cmdline + lp++; + section=1; + goto startline; + } + if (!strncmp(lp,type,tlen)) { + // found section + section=1; + // check for subroutine + if (*type=='#') { + // check for parameter + type+=tlen; + if (*type=='(') { + float fparam; + numeric=1; + glob_script_mem.glob_error=0; + GetNumericResult((char*)type,OPER_EQU,&fparam,0); + if (glob_script_mem.glob_error==1) { + // was string, not number + numeric=0; + // get the string + GetStringResult((char*)type+1,OPER_EQU,cmpstr,0); + } + lp+=tlen; + if (*lp=='(') { + // fetch destination + lp++; + lp=isvar(lp,&vtype,&ind,0,0,0); + if (vtype!=VAR_NV) { + // found variable as result + uint8_t index=glob_script_mem.type[ind.index].index; + if ((vtype&STYPE)==0) { + // numeric result + dfvar=&glob_script_mem.fvars[index]; + if (numeric) { + *dfvar=fparam; + } else { + // mismatch + *dfvar=CharToDouble(cmpstr); + } + } else { + // string result + sindex=index; + if (!numeric) { + strlcpy(glob_script_mem.glob_snp+(sindex*glob_script_mem.max_ssize),cmpstr,glob_script_mem.max_ssize); + } else { + // mismatch + dtostrfd(fparam,6,glob_script_mem.glob_snp+(sindex*glob_script_mem.max_ssize)); + } + } + } + } + } + } + } + } + // next line + next_line: + if (*lp==SCRIPT_EOL) { + lp++; + } else { + lp = strchr(lp, SCRIPT_EOL); + if (!lp) break; + lp++; + } + } +} + +uint8_t script_xsns_index = 0; + + +void ScripterEvery100ms(void) +{ + if (Settings.rule_enabled && (uptime > 4)) { + mqtt_data[0] = '\0'; + uint16_t script_tele_period_save = tele_period; + tele_period = 2; + XsnsNextCall(FUNC_JSON_APPEND, script_xsns_index); + tele_period = script_tele_period_save; + if (strlen(mqtt_data)) { + mqtt_data[0] = '{'; + snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s}"), mqtt_data); + Run_Scripter(">T",2, mqtt_data); + } + } +} + +//mems[MAX_RULE_MEMS] is 50 bytes in 6.5 +// can hold 11 floats or floats + strings +// should report overflow later +void Scripter_save_pvars(void) { + uint32_t lptr=(uint32_t)Settings.mems[0]; + int16_t mlen=0; + lptr&=0xfffffffc; + float *fp=(float*)lptr; + fp++; + mlen+=sizeof(float); + struct T_INDEX *vtp=glob_script_mem.type; + for (uint8_t count=0; countMAX_RULE_MEMS*10) { + vtp[count].bits.is_permanent=0; + return; + } + *fp++=glob_script_mem.fvars[index]; + } + } + char *cp=(char*)fp; + for (uint8_t count=0; countMAX_RULE_MEMS*10) { + vtp[count].bits.is_permanent=0; + return; + } + strcpy(cp,sp); + cp+=slen+1; + } + } +} + +// works only with webserver +#ifdef USE_WEBSERVER + +#define WEB_HANDLE_SCRIPT "s10" +#define D_CONFIGURE_SCRIPT "Edit script" +#define D_RULEVARS "edit script" + +const char S_CONFIGURE_SCRIPT[] PROGMEM = D_CONFIGURE_SCRIPT; + +const char HTTP_BTN_MENU_RULES[] PROGMEM = + "

"; + + +const char HTTP_FORM_SCRIPT[] PROGMEM = + "
 " D_RULEVARS " " + "
"; + +const char HTTP_FORM_SCRIPT1[] PROGMEM = + "
script enable
" + "
"; + +void HandleScriptConfiguration(void) +{ + if (!HttpCheckPriviledgedAccess()) { return; } + + AddLog_P(LOG_LEVEL_DEBUG, S_LOG_HTTP, S_CONFIGURE_SCRIPT); + + if (WebServer->hasArg("save")) { + ScriptSaveSettings(); + HandleConfiguration(); + return; + } + + WSContentStart_P(S_CONFIGURE_SCRIPT); + WSContentSendStyle(); + WSContentSend_P(HTTP_FORM_SCRIPT); + WSContentSend_P(HTTP_FORM_SCRIPT1,1,1,bitRead(Settings.rule_enabled,0) ? " checked" : "",1,1,MAX_RULE_SIZE*3); + + // script is to larg for WSContentSend_P + if (Settings.rules[0][0]) { + _WSContentSend(Settings.rules[0]); + } + WSContentSend_P(HTTP_FORM_SCRIPT1b); + WSContentSend_P(HTTP_FORM_END); + WSContentSpaceButton(BUTTON_CONFIGURATION); + WSContentStop(); + } + + +void strrepl_inplace(char *str, const char *a, const char *b) { + for (char *cursor=str; (cursor=strstr(cursor,a)) != NULL;) { + memmove(cursor+strlen(b),cursor+strlen(a),strlen(cursor)-strlen(a)+1); + for (int i=0; b[i]!='\0'; i++) { + cursor[i] = b[i]; + } + cursor += strlen(b); + } +} + +#define MAX_SCRIPT_SIZE MAX_RULE_SIZE*3 + +void ScriptSaveSettings(void) { + + if (WebServer->hasArg("c1")) { + bitWrite(Settings.rule_enabled,0,1); + } else { + bitWrite(Settings.rule_enabled,0,0); + } + + String str = WebServer->arg("t1"); + + if (*str.c_str()) { +#if 1 + strrepl_inplace((char*)str.c_str(),"\r\n","\n"); + strrepl_inplace((char*)str.c_str(),"\r","\n"); +#else + str.replace("\r\n","\n"); + str.replace("\r","\n"); +#endif + strlcpy(Settings.rules[0],str.c_str(), MAX_RULE_SIZE*3); + } + + if (glob_script_mem.script_mem) { + Scripter_save_pvars(); + free(glob_script_mem.script_mem); + glob_script_mem.script_mem=0; + glob_script_mem.script_mem_size=0; + } + + if (bitRead(Settings.rule_enabled, 0)) { + int16_t res=Init_Scripter(Settings.rules[0]); + if (res) { + snprintf_P(log_data, sizeof(log_data), PSTR("script init error: %d"),res); + AddLog(LOG_LEVEL_INFO); + return; + } + Run_Scripter(">B",2,0); + } +} + +#endif + +void execute_script(char *script) { +char *svd_sp=glob_script_mem.scriptptr; + strcat(script,"\n#"); + glob_script_mem.scriptptr=script; + Run_Scripter(">",1,0); + glob_script_mem.scriptptr=svd_sp; + Scripter_save_pvars(); +} + +enum ScriptCommands { CMND_SCRIPT }; +const char kScriptCommands[] PROGMEM = "Script"; + +bool ScriptCommand(void) { + char command[CMDSZ]; + bool serviced = true; + uint8_t index = XdrvMailbox.index; + + int command_code = GetCommandCode(command, sizeof(command), XdrvMailbox.topic, kScriptCommands); + if (-1 == command_code) { + serviced = false; // Unknown command + } + else if ((CMND_SCRIPT == command_code) && (index > 0)) { + + if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload < 2)) { + switch (XdrvMailbox.payload) { + case 0: // Off + case 1: // On + bitWrite(Settings.rule_enabled, index -1, XdrvMailbox.payload); + } + } else { + if ('>' == XdrvMailbox.data[0]) { + // execute script + for (uint8_t count=0; countB",2,0); + break; + case FUNC_EVERY_100_MSECOND: + ScripterEvery100ms(); + break; + case FUNC_EVERY_SECOND: + ScriptEverySecond(); + break; + case FUNC_COMMAND: + result = ScriptCommand(); + break; + case FUNC_SET_POWER: + case FUNC_RULES_PROCESS: + if (bitRead(Settings.rule_enabled, 0)) Run_Scripter(">E",2,mqtt_data); + break; +#ifdef USE_WEBSERVER + case FUNC_WEB_ADD_BUTTON: + WSContentSend_P(HTTP_BTN_MENU_RULES); + break; + case FUNC_WEB_ADD_HANDLER: + WebServer->on("/" WEB_HANDLE_SCRIPT, HandleScriptConfiguration); + break; +#endif // USE_WEBSERVER + case FUNC_SAVE_BEFORE_RESTART: + if (bitRead(Settings.rule_enabled, 0)) { + Run_Scripter(">R",2,0); + Scripter_save_pvars(); + } + break; + } + return result; +} + +#endif // not use RULES +#endif // USE_SCRIPT