-
Notifications
You must be signed in to change notification settings - Fork 636
RPN Rules
The "RPN Rules" module is an advanced feature that let's you define pretty flexible custom rules to execute actions (mostly changing relay and light statuses) based on different inputs. Rules are defined as a series of commands using Reverse Polish Notation, also called "postfix" notation, where operators come after the operands.
This might look somewhat complicated but it actually has major benefits over the usual "infix" notation:
- Commands (rules) are faster to process
- Rules are simple to understand
- It's easier to implement
First you should familiarize yourself with RPN calculation. Reverse Polish notation (RPN) is a mathematical notation in which operators follow their operands. It does not need any parentheses as long as each operator has a fixed number of operands.
A simple calculation in infix notation might look like this:
( 4 - 2 ) * 5 + 1 =
The same calculation in RPN (postfix) will look like this:
4 2 - 5 * 1 +
It results in a shorter expression since parenthesis are unnecessary. Also the equals sign is not needed since all results are stored in the stack. From the computer point of view is much simpler to evaluate since it doesn't have to look forward for the operands.
Each element in an expression is called "token" (not a standard name). Tokens can be:
- Numbers, always considered as real numbers (floats)
- Operators (+, -, ...)
- Variables (starting with a $ sign)
As I already mentioned, operands (numbers) are pushed into a stack and later operators pop the number of operands from the stack. Let's take a closer look to a simple RPN expression: 4 2 - 5 * 1 +
. The first column shows the next token to process and the third column the status of the stack after processing the token. At the beginning the stack is (usually) empty.
token | action | stack |
---|---|---|
4 | pushes '4' into the stack | 4 |
2 | pushes '2' into the stack | 4 2 |
- | pops 2 values from the stack performs the substraction and pushes the result to the stack |
2 |
5 | pushes '5' into the stack | 2 5 |
* | pops 2 values from the stack performs the multiplication and pushes the result to the stack |
10 |
1 | pushes '1' into the stack | 10 1 |
+ | pops 2 values from the stack performs the sum and pushes the result to the stack |
11 |
Of course you need to know the vocabulary, i.e. the available operators. There are different types of operators:
- Mathematical (+, -, *, ...)
- Stack manipulation (dup, swap, ...)
- Comparison (eq, gt, cmp, ...)
- Logical (and, or, ...)
- Time management (now, hour, minute,...)
- ESPurna related (relay, channel)
See RPNLib README for the full list.
Token | Description | Notes |
---|---|---|
now | Pushes the current epoch time into the stack | Warning: Core 2.3.0 this returns the time in local time zone offset, >= 2.3.0 this returns UTC timestamp |
utc | Pushes the current epoch time into the stack |
Warning: Core >= 2.3.0 this is an alias for now
|
hour | Gets a timestamp from the stack and returns the hour | |
minute | Gets a timestamp from the stack and returns the minute | |
dow | Gets a timestamp from the stack and returns the day of week | Sunday is 1, Monday is 2, etc. |
day | Gets a timestamp from the stack and returns the day of month | |
month | Gets a timestamp from the stack and returns the month | January is 1, February is 2, etc. |
Notice: Depending on Arduino Core version, semantics of
now
andutc
change. With older Core versions,now
returns local timestamp andutc
returns UTC. With newer Core versions, underlying time library changes made it sonow
andutc
now return the same timestamp. To reference UTC time instead of the one in local timezone, please appendutc_
prefix to any of the conversion functions. e.g., instead ofminute
it becomesutc_minute
Token | Description |
---|---|
relay | Gets a value and a relay number from the stack and changes that relay to the given status, status can be 0, 1 or 2 (toggle) |
relay_reset | Similar to relay, but re-schedules any timers attached to the relay (Flood, ON / OFF delays, Pulse etc.) |
Token | Description |
---|---|
black | Turns off all channels (only lights) |
update | Updates channels |
channel | Gets a value and a channel number from the stack and changes that chanel to the given value (from 0 to 255). Does not update the light, you must call "update" at the end. |
Token | Description |
---|---|
showstack | Shows current stack contents in the global debug log |
dbgmsg | Prints the specified string to the global debug log |
Variables are stored in the module context so they can be used from your expressions. Variables are fed from different sources:
- Relay status ($relay0, $relay1,...)
- Lights channel values ($channel0, $channel1,...)
- Sensors ($temperature0, $humidity0,...)
- MQTT (custom names)
By default they are persistent (meaning that once defined they are available for any rule execution in the future) but they do not persist across reboots. This behavior is called "sticky variables" and can be changed from the UI. Non sticky variables get deleted after the next rule execution. This can be useful to avoid dead locks when used with other features like relay pulses.
MQTT variables are a special type of variable that gets its content from MQTT topics. You can define an MQTT and a variable name and every time the topic receives a message the variables will get updated and the rules will be executed. Remember than variable names in the RPN expressions begin with the $ sign.
Rules get executed X milliseconds after the last variable update. The execution delay is 100ms by default but it can be changed from the web UI. This allows for a buffer time so all variables coming from sensors get updated before rules are executed.
All variables changes trigger the rule execution after the delay time. So every time a relay is toggled, every time a sensor reports values or every time one of the MQTT topic gets updated the rules will be executed.
Imagine you have a device with a relay controlling a heater and a temperature sensor. You can implement a simple thermostat with hysteresis using the input variables of the sensor ($temperature0
) and the relay ($relay0
):
$temperature0 18 24 cmp3 1 + [ 1 $relay0 0 ] index 0 relay
Let's split the expression in parts:
sub-expression | description | output |
---|---|---|
$temperature0 18 24 cmp3 | Compares the temperature to the 18-24 range, it will output -1 if temperature is below 18, +1 if it is above 24 and 0 in the middle | -1 to 1 |
1 + | We need to shift the cmp3 result to use the index operator | 0 to 2 |
[ 1 $relay0 0 ] index | We define an index of 3 different values (number of items between [ and ] ) that will be: 1, the current status of the relay (0 or 1) and 0. The sub-expression will return one of them depending on the value in the stack preceding the [
|
0 or 1 |
0 relay | It will set the relay number 0 status according to the value on the stack | (empty) |
This expression will ensure the relay in ON if the temperature is below 18C and OFF if it's above 24C. If the reported temperature is in between 18 and 24 the relay status will not change.
In this case image we have a simple WiFi relay device and a PIR in the same room that reports presence via MQTT to the /livingroom/motion
topic. We first define an MQTT topic for it using motion
as the variable name. Then we can trigger the light if there is motion between 22h and 8h in the morning.
now hour 8 23 cmp3 abs $motion and 0 relay
Again, let's analyze it step by step
sub-expression | description | output |
---|---|---|
now hour | Will return the current hour | 0 to 23 |
8 23 cmp3 abs | Compare the hour in the stack to the 8-23 range and get the absolute value, will output 1 if it's below 8 or above 23, 0 otherwise | 0 or 1 |
$motion and | We do an AND with the motion value, so we will now have a 1 if it's nighttime AND there is motion | 0 or 1 |
0 relay | We use the result value to turn on or off the relay #0 | (empty) |
Since our device will only have a relay, this rule will be executed every minute (triggered by the NTP module) and when there is a new value in the motion topic. Alternatively, if you disable the "stickiness" of the variables, the expression will fail except when the $motion
is defined which will only happen after we receive a message to the motion topic.
Keep in mind that if the result changed the relay status, the relay change will trigger the rule execution again! Try to avoid loops in the rules like, for instance: 1 $relay0 - 0 relay
. This simple expression will turn the relay ON and OFF and ON again and OFF again forever!!
The schedule is different to the previous examples since it will only perform an action at a given time, not every time there is a time or status update like the RPN Rules module. But we can easily emulate this behaviour by using $tick1m
variable that is set every minute:
$tick1m dup minute 0 ne end hour 8 ne end null &tick1m = 1 0 relay
The $tick1m variable contains the timestamp, while the 2nd expression using &tick1m
resets it to null, removing it from variables list after the expression is done. The end
operator takes and argument from the stack and ends the execution if the argument resolves to true. Hence:
sub-expression | description | output |
---|---|---|
$tick1m dup | Check if $tick1m is set and duplicate it's value on the stack | |
minute 0 ne end | Will end the execution if the current minute is not 0 | (empty) |
hour 8 ne end | Will end the execution if the current hour is not 8 | (empty) |
null &tick1m = | Set tick1m variable's value to null, after which the variable will be disallowed to be used by the $tick1m expression | |
1 0 relay | Turn relay 0 to ON, this will only happen at 8:00! |
It is also possible to schedule execution to happen every hour by checking for $tick1h
variable.
RPN allows to start a periodic or one-shot timers (aka RPN runners), that will trigger rule execution when expired.
For example, execute the right part of the expression after timer is done:
5000u every_ms 2 0 relay
Execute the right part of the expression when variable is set to true, but wait 5 seconds first and then set variable to null:
$test end 5000u once_ms null &test = "topic" "message" mqtt_send
Note that 5000u every_ms
defined in multiple will create a single timer.
Some devices, like the Smartlife Mini Smart Socket, have an LED light (sometime RGB) and a power consumption monitor. Wouldn't it be cool that the light would turn more and more red as the power consumption rises?
black $power0 0 1000 0 255 map 0 channel update
This expression maps a power value between 0 and 1000W to a number between 0 and 255 and then it feeds it to channel 0 (usually red). Initially, it sets all channels to 0 (black
) and at the end forces the light driver to update
the color. Nice.
ESPurna has a relay synchronization feature that offers different ways to synchronize relays when there is more than one available, but all of these options perform actions of all the relays, turning them ON or OFF to match the required pattern. What if you have 4 different relays and you want to synchronize relays 0 and 1 together but leave 2 and 3 untied? Easy.
$relay0 1 relay
What if you want them to have opposite value?
1 $relay0 - 1 relay
Now you can control relays 0 and 1 by just changing relay 0. If you want to be able to perform an action on either 0 or 1 and toggle the other as well, you can create one rule for each but remember to disable "Sticky variables" to avoid getting into a loop:
1 $relay0 - 1 relay
1 $relay1 - 0 relay
The module exposes different terminal commands to test and evaluate expressions and variables.
- RPN.OPS will output the list of available operators and the number of required arguments for each of them
- RPN.TEST "<expression>" will execute the expression and output the stack at the end, useful to do partial tests of sub-expressions
- RPN.VARS will output the current defined variables and their values
- RPN.RUNNERS will show the currently running timers
If you're looking for support:
- Issues: this is the most dynamic channel at the moment, you might find an answer to your question by searching open or closed issues.
- Wiki pages: might not be as up-to-date as we all would like (hey, you can also contribute in the documentation!).
- Gitter channel: you have better chances to get fast answers from project contributors or other ESPurna users. (also available with any Matrix client!)
- Issue a question: as a last resort, you can open new question issue on GitHub. Just remember: the more info you provide the more chances you'll have to get an accurate answer.
- Backup the stock firmware
- Flash a pre-built binary image
- Flash a virgin Itead Sonoff device without opening
- Flash TUYA-based device without opening
- Flash Shelly device without opening
- Using PlatformIO
- from Visual Studio Code
- Using Arduino IDE
- Build the Web Interface
- Over-the-air updates
- Two-step updates
- ESPurna OTA Manager
- NoFUSS
- Troubleshooting
- MQTT
- REST API
- Domoticz
- Home Assistant
- InfluxDB
- Prometheus metrics
- Thingspeak
- Alexa
- Google Home
- Architecture
- 3rd Party Plugins
- Coding style
- Pull Requests