Skip to content

# Variables & Expressions

## Overview

Reactor has two types of variables: Global and Rule-based. Each variable is given a name, which must be unique within its context (that is, a Rule-based variable cannot have the same name as another variable in the same rule, but a different rule can use the same name).

Global variables are visible to all Rules, Reactions, and other global variables in the system. They are defined on the Expressions page of the Reactor UI.

Rule-based variables are those that are defined on a Rule. They can be referenced in trigger and constraint conditions, and other expressions, belonging to the same rule. They can reference global variables, but they cannot refer to or modify variables belonging to other rules.

Global and Rule-based variables that have expressions defined are expression-driven: their value is determined solely by the result of the expression. They cannot be modified by any other means.

Global and Rule-based variables that don't have expressions associated with them are referred to as expressionless variables. Expressionless variables must be set explicitly by actions, and serve as persistent storage for values (that is, the assigned value survives reboots of the host system and restarts of Reactor). The actions that set their values are the Set Variable action, and making an assignment to them in a Script Action. Refer to these actions for details. They can also be set using an HTTP API endpoint.

It is possible for a Rule-based variable to have the same name as a global variable. Within a Rule, if another expression refers to that name, it will access the Rule-based variable, being local in scope, in preference to the global variable (as it would be in most computer languages). Some users adopt naming conventions like prefixing the names of their global variables to avoid confusion/conflict (e.g. start them with `g_` or simply `g`).

If you delete a variable, any rule, reaction, or other expression that refers to it will not be able to resolve it. You can re-create the variable using the same name after having deleted it, but if that name is used in a rule (directly or by reference through expressions), you will need to disable and re-enable the rule (or reset it) to get the rule to watch the new replacement variable.

How To...

See Expressions with Entities and Attributes (in the How-To section) for an overview of using expressions to fetch and operate on entity data in Reactor.

### Evaluation of Global Variables

The following trigger re-evaluation of Global expression-driven variables:

- Startup of the Reactor engine;
- The value of a Global variable on which the expression depends changes (i.e. if global `B` is defined as `A * A`, then `B` will be re-evaluated when the value of `A` changes);
- An attribute of a dependent entity used in the expression changes — entity dependencies are created by the use of `getEntity()` in the expression;
- The state of a dependent Rule used in the expression changes Rule dependencies are created by the use of `isRuleSet()` or `isRuleEnabled()` in the expression;
- The `alarm()` expression function is used (the expression is re-evaluated when the alarm time expires).

Global expression-driven variables that do not use `alarm()` and do not otherwise have dependencies are, therefore, only evaluated at Engine startup. That means they will not change value after startup, and are, in effect, constants.

### Evaluation of Rule-based Variables

The following trigger re-evaluation of Rule-based expression-driven variables:

- Startup of the parent Rule;
- The value of a Global or Rule-based variable on which the expression depends changes (i.e. if global `B` is defined as `A * A`, then `B` will be re-evaluated when the value of `A` changes);
- An attribute of a dependent entity used in the expression changes — entity dependencies are created by the use of `getEntity()` in the expression;
- The state of a dependent Rule used in the expression changes — Rule dependencies are created by the use of `isRuleSet()` or `isRuleEnabled()` in the expression;
- The Rule is being re-evaluated because its trigger conditions need to be updated; in this case, only variables that do not have one of the above dependencies are evaluated. This re-evaluation of variables occurs prior to evaluating triggers and constraints, so that the updated value of the variable can be used in those conditions.

Note that the handling of variables that do not have dependencies is different for Rule-based vs Global variables. For example, a Global variable driven by the expression `dateparts().weekday` will evaluate at Engine startup and therefore hold the day of the week of that event, even if Reactor runs for days (it is not updated daily or on any other schedule). However, the same expression in a Rule-based variable will be updated every time the Rule's trigger conditions are evaluated.

## Expression Language Syntax

### Conventions

This section has some special conventions because it documents language syntax and library functions, particularly in code snippets (which look `like this`).

The use of less-than/greater-than characters around a word (e.g. `<variable-name>`) denotes a required element that the user must provide.

The use of square brackets around a syntax elements indicates an optional element. For example, the definition of the rounding function `round( number [, precision] )` indicates that the required precision and the comma that precedes it are optional — `round( 123.456 )` and `round( 123.456, 1 )` are both valid uses of the function.

The use of the vertical bar character between elements of a function's description indicates that either/or for those elements. See the `time()` function, which can be called with three different argument list types (i.e. it can be passed a string, an object, or one to six integers).

### Syntax Overview

What it's not...

The expression language is not JavaScript, and definitely not an `eval()` running inside the code. The entire reason for a separate expression language is specifically to avoid this potential security tragedy, and to provide convenience in both syntax and environment that are tuned for IoT applications. Likewise, it is not C, C++, Java, Python, or Lua. And for those of you that may be expecting, or hoping, that is entirely the same as the expression language used in the Reactor plugin for Vera, well... it's not that either.

Reactor's expression syntax is similar to the "infix" expression syntax used by most common languages (C, Java, JavaScript, etc.). The simplest expression is simply a numeric constant, such as `1234`. This is a complete expression, the result value of which is the number 1234. Negative numbers begin with a `-` sign, such as `-1234`. Numbers may have decimal points and decimal digits: `-12.34`. Numbers may also be given in scientific format: `1.234e3` is equal to 1234 (1.234 x 103). Hexadecimal integers may be entered by prefixing with `0x`; for example, `0x20` is decimal 32. Likewise binary integers can be prefixed with `0b`, and octal with `0o`.

Strings are represented as characters surrounded by matching double-quotes ("), single quotes ('), or back-ticks (`). The quote character used to open a string must be used to close the string, and the other quote characters may be used freely within the string. For example, `"This is a 'valid' example"` and `'"this" is' too'`.

Boolean values true and false are represented by the reserved words `true` and `false`, respectively. The reserved word `pi` evaluates to the JavaScript `Math.PI`, approximately 3.14159265.

The reserved word `null` evaluates to the null value (basically means "no value").

Identifiers are names that represent values (variables). An identifier must begin with an upper- or lowercase alphabetic character, and may follow with any combination of alphanumeric characters and underscore. Thus `myLastSignal` is a valid identifier, but `023lastSignal` is not, and nor is `just another name!`.

Functions are identifiers followed by a paren-enclosed list of expressions as its arguments (or empty for no arguments). The maximum value of a series of numbers, for example, can be found using the `max` function like this: `max( 1, -2, pi, lastElement )`.

The expression language includes a set of operators. Multiplication is performed by `*`, so that `3 * 4` yields 12. Division uses `/`, while addition and subtraction use `+` and `-`, respectively, as one might expect. The full list of operators is given below, in order of precedence. Operators with higher precedence are performed before operators with lower precedence, so that expressions like `3 + 4 * 2` yield 11, not 14. The precedence of mathemetical operators follows the Order of Operations we are taught in elementary school. Precedence can be controlled using parentheses, so per the previous example, the result 14 could be arrived at using `(3 + 4) * 2`.

In addition to the mathematical operators, there are relational operators: `==`, `!=`, `>`, `>=`, `<` and `<=` all return true if their operands are equal, not equal, etc. In addition, the two special relational operators `===` and `!==` check equality/inequality not just of value, but of data type, such that `"3" == 3` is true, but `"3" === 3` is false (because the left operand is string type, and the right a number).

Testing Equality of Non-Primitive Types

Note that equality/inequality comparison of arrays or objects (non-primitive types), such as `array1 == array2` does not perform a "deep inspection" of the array/object and is effectively not a valid comparison for most practical purposes. Rather, it determines if the two operands are the same object in memory (as JavaScript does), and thus is most likely false and not truly a relevant comparison. Specifically, the following expressions are expected to be false: `[1,2,3] == [1,2,3]`, `{ abc:1, def:2 } == { abc:1, def:2 }` because although they are equivalent in terms of their contents, they are not, in fact, the same object in memory. The more complex `s=[1,2,3], t=s, s == t` is true, however, because `s` and `t` do refer to the same object in memory.

The boolean operators are `&&` for and and `||` for or, such that `false && true` is false and `false || true` is true. The `!` unary boolean operator negates its right-side operand, so `!true` is false.

The bitwise operators, following "C" (and Java, and JavaScript, and others) are `&` for bitwise AND, `|` for bitwise OR, and `^` for exclusive-OR (XOR).

The array element accessor is square brackets `[]` and should contain the array index. Arrays in Reactor expressions are zero-based, so the first element of an array is `[0]`. If the index given is less than 0, a runtime error occurs. If the index is positive or zero but off the end of the array, null is returned.

The member access operator "dot" (`.`) is used to traverse objects. For example, referring to the power state of an entity may be `entity.attributes.power_switch.state`, which starts with an entity object, drops to the list of attributes within it, and the "power_switch" capability within the attributes, and finally to the "state" value. The right-side operand of the dot operator must be an identifier, so it may not contain special characters. If a member name contains any non-identifier characters, the array access syntax can be used: `entity.attributes['forbidden-name'].value`.

The ternary operator pair `? :` common to C, C++ and Java (and others) is available: `<boolean-expression> ? <true-expression> : <false-expression>`. If the result of the `boolean-expression` given is true, the `true-expression` is evaluated; otherwise, the `false-expression` is evaluated.

The coalesce operators are `??`, `?#`, `?.` and `?[`. Coalesce operators help handle null values in the middle of complex expressions more gracefully. For example, `value ?? 0` will result in the value of the variable `value` if it is not null, but if it is null, will yield 0. The numeric coalesce operator `?#` provides a quick test if the left operand is (or can be) a number (integer or real), and if so returns the numeric value; if not, it returns the right operand (which can be any type). The access coalesce operators are used for object member and array element access: if an identifier `struct` is intended to hold an object, but turns out to be null, a reference to `struct.name` in an expression would throw a runtime evaluation error; using `struct?.name` will instead result in null with no exception thrown. This is convenient because you can carry it `down?.a?.long?.list?.of?.member?.names` without crashing if something is undefined. Likewise if `beans` was intended to be an array but ended up null, the expression `beans[2]` would throw an error, while `beans?[2]` would result in null.

The `in` operator is used to establish if an object contains a specified key (e.g. `key in obj`) or an array contains an element at the given index (e.g. `15 in arr`). It is important to note that this operator works on keys only, not values, and in particular, cannot be used to search an array for a value (i.e. `4 in [ 4, 5, 6 ]` is always false). To find an array element, use the `indexOf()` function. The `first` statement can be used to find a value in an object.

The `..` range operator produces an array containing all integers from the left operand to the right, so `3..6` results in `[3,4,5,6]`. A `for`-style counting loop can be implemented using `each` with the range operator as its operand: `each i in 0..9: <statement>` would execute `<statement>` 10 times.

Multiple expressions can be chained together by separating them with commas The result of a chained expression is the last expression evaluated.

The following is the list of operators supported in order from lowest precedence to highest. Operators on the same line have equal precedence and are evaluated left-associative (from left to right) unless otherwise indicated:

- `,` (chain, right associated, effectively)
- `=` (assignment, right associative)
- `?` (ternary operator first, right-associative)
- `:` (ternary operator second, right-associative)
- `??` (coalesce), `?#` (coalesceNaN) (both right-associative)
- `||`, `or` (logical OR)
- `&&`, `and` (logical AND)
- `|` (bitwise OR)
- `^` (bitwise XOR)
- `&` (bitwise AND)
- `==`, `===`, `!=`, `!==` (equality/inequality, non-associative)
- `<`, `<=`, `>`, `>=` (comparison, non-associative)
- `<<`, `>>` (bit shift)
- `+`, `-`
- `*`, `/`, `%` (mod)
- `**` (power, right-associative)
- `-` (unary minus)
- `!`, `not` (not/negation, right-associative)
- `.`, `?.`, `?[` (member access)

### Data Types

The data types known to the expression language are boolean, number, string, array, object, and the following special type/values (both a type and a value):

- `null`, which basically is used to mean "no value";
- `NaN`, which stands for "Not a Number", which results when a conversion to number fails (e.g. `5 * "hello"` or `int( 'what is this?' )`);
- `Infinity`, which results from division by zero and other similar math failures.

Arrays and objects can be constructed and used on the fly: `[ 5, 99, 23, 17 ]` constructs a four-element array, while `{ name: 'spot', type: 'dog', weight: 33 }` constructs an object.

### Statements

The expression language has a couple of "lightweight statements" that function as a hybrid of a statement and an expression. These are:

#### `each <element-identifier> [, <element-identifier> ] in <array-or-object-expression>: <body-expression>`

The `each` statement will iterate over the given array or object (or expression resulting in an array or object), each time placing an array value or object element in the named variable (and the key or index in the second named variable, if given), and then execute the body expression. The body expression result, if non-`null`, is pushed to an array that forms the `each` expression result. For example, `each num in [ 4,7,33 ]: num * 2` will return an array `[ 8, 14, 66 ]`, while `each v,k in { "alpha": 1, "beta": 2 }: k` will return `["alpha", "beta"]`.

#### `first <element-identifier> [, <element-identifier> ] in <array-or-object> with <test-expression> [ : <result-expression> ]`

The `first` statement will search through the elements of an array or object (top level, no traversal) and return the first value that for which `<test-expression>` is true (or truthy). The result is the value matched, unless the optional `: <result-expression>` clause is given, in which case the result will be that of the expression. Example: `first val,key in devices with val.type=="window": val.name + ' ' + key` will find the first device in an array or map (object) of device objects for which the device object key type is window, and rather than return the device object, return the device name and key as a space-separated string.

#### `if <conditional> then <true-expression> [ elif <conditional> then <true-expression> ]* [ else <false-expression> ] endif`

The `true-` and `false-expressions` may be any expression, including a `do...done` block enclosing multiple expressions. To remove the ambiguity that can arise from using `else if` with a nested `if` block, any number of `elif` subconditions may be included. The optional `else` clause is a catch-all for no other conditions matching.

        t = 2,
        if t === 1 then 'A'
        elif t === 2 then 'B'
        elif t === 3 then 'C'
        else null
        endif
        # Result is "B"

The keywords `elsif` and `elseif` are synonyms for `elif` that you may use "if you're not into the whole brevity thing."

#### `case when <conditional-expr-1>: <true-expression-1> [ when <conditional-expr-n>: <true-expression-n> ]* [ else <default-expression> ] end`

Sometimes `if` statements need to make multiple tests, and the `if` statement and ternary operator can become very difficult to write and follow later. To make things tidier, the `case` statement evaluates a series of `when` clauses; the first `<conditional-expression>` that is `true` will cause the statement to return the value of its matching `<true-expression>`. If none is `true`, the `<default-expression>` result is returned if an `else` clause is present, or `null` otherwise. The `<true-expressions>` and `<default-expression>` may be any expression, including assignments or block statements (even another `case` statement). Example below; lines and spacing for clarity only.

    case
      when tempF < 65: "it's cold in here!"
      when tempF < 76: "we're comfortable"
      when tempF < 85: "it's a bit warm in here!"
      else "we need to cool this place down!"
    end

Note that while the above example shows all `when` clauses testing the value of `tempF`, there is no requirement that the conditionals be consistent or related in this way. It is perfectly acceptable to write `case when sun.isup: "sun is up" when pool.isfull "pool is full" else "read a book" end`, if that is what you need to do.

#### `do <statement-list> done`

When multiple expressions are needed but only a single expression is allowed (such as following the colon (`:`) in an `each` or `first` statement, or for the `then` or `else` in an `if` statement), the `do...done` statement creates an expression block that appears to be a single statement. The result of the `do...done` statement block is the result produced by the last expression in the block.

    /* If the temperature is under 35F, turn on the pool pump and heater (freeze protection) */
    if tempF < 35 then do
        performAction( 'pool>pump', 'power_switch.on' ),
        performAction( 'pool>heater', 'power_switch.on' ),
        performAction( 'pool>heater', 'hvac_heating_unit.set_setpoint', { setpoint: 40 } )
    done

    /* Produce an array of the IDs of battery-powered entities with less than 20% battery remaining
    each id in matchEntities( 'battery_power' ): do
        local entity = getEntity( id ),
        if entity.attributes.battery_power.level < 0.2 then id else null endif
    done

#### `define <functionName>( <args...> ) <expression>`

Defines a function named `<functionName>` that returns the evaluated `<expression>`. Arguments passed to the function will be received as `<args...>`, which must be a comma-separated list of identifiers. Example: `define square(a) a*a` defines a function that returns the square of a single value passed to it received in the variable `a`; the function result is the result of the expression (no `return` statement is required or exists in this syntax). If multiple expressions are required for the implementation of the function, enclose them in a `do ... done` block.

### Functions

Is there a function that...?

I keep adding things as people ask, so let me know if I'm missing what you need.

#### Arithmetic Functions

- `abs( number )` — returns the absolute value of its argument;
- `sign( number )` — returns the sign of its argument: -1 if negative, 0 if zero, 1 if positive;
- `floor( number )` — returns the largest integer less than or equal to its argument;
- `ceil( number )` — returns the next integer greater than or equal to its argument;
- `round( number [ , precision ] )` — rounds `number` to `precision` decimal digits;
- `trunc( number )` — returns the integer portion of its argument (e.g. trunc(-3.4) is -3, where floor(-3.4) is 4);
- `cos/sin/tan/acos/asin/atan/atan2( number )` — trig operations (identical to their JavaScript equivalents);
- `log/exp( number )` — natural logarithm and exponential;
- `pow( base, power )` — raises `base` to the `power`th power (e.g. `pow(10,3)` is 1000);
- `sqrt( number )` — square root (of `number` > 0);
- `random()` — returns a random number greater than or equal to 0 and less than 1;
- `min/max( ... )` — returns the smallest/largest value of its arguments; if an argument is an array, the array is scanned; non-numeric values are ignored, so these functions return null unless at least one number (type) value is found;
- `isNaN( various )` — returns true if the argument is `NaN`, `null`, or if an attempted conversion with `parseInt()/parseFloat()` would result in `NaN` (e.g. `isNaN( 'this is not a number' )` returns true, but `isNaN( '123' )` returns false). Note that this is a little different from JavaScript's `isNaN()`: under JS (as tested in nodejs 16.13.1 and Chrome browser 108.0.5359.98) `isNaN(null)` returns false (i.e. it is a number) while `parseInt(null)` and `parseFloat(null)` both return `NaN`. I think this is a vexing inconsistency, so lexpjs will return true for `isNaN(null)` to agree with `parseInt()/parseFloat()`.
- `isInfinity( value )` &mash; returns true if the argument is `Infinity`, as would result in, for example, division by zero.
- `constrain( number, minimum [, maximum] )` — returns the value given limited by the extrema. That is, if the value is less than the given minimum, the minimum is returned; if a maximum is given and the value is greater, the maximum is returned. Passing null for `minimum` when passing `maximum` results in an upper-bound only constraint (i.e. the function enforces `maximum` and ignores `minimum`).
- `scale( number, fromLow, fromHigh, toLow, toHigh )` — takes the given value (`number`), assumed to be in the range of `fromLow` to `fromHigh`, and rescales it into the range of `toLow` to `toHigh`. For example, `scale(15,10,20,0,100)` returns 50, because 15 is halfway between 10 and 20, and halfway between 0 and 100 is 50.

#### String Handling Functions

- `len( string )` — returns the length of the string;
- `substr( string, start, length )` — returns the portion of the string from the `start`th character for `length` characters;
- `upper/lower( string )` — converts the string to upper/lower-case;
- `match( string, regexp [ , ngroup [ , flags ] ] )` — matches, if possible, the regular expression regexp to the string, and returns the matched string, or `null` if no match; if ngroup is given and the regexp contains groups, the matched part of that group is returned; if flags is `"i"`, a case-insensitive match is done;
- `find( string, regexp [ , flags ] )` — like `match()`, but returns the index of the first character of the match, rather than the matched string, or -1 if no match; the meaning of (optional) flags is the same as for `match()`;
- `replace( string, regexp, replacement [ , flags ] )` — replaces the first substring matched by the regular expression regexp with the replacement string and returns the result; the optional flags (a string) may include "i" for case-insensitive search, and "g" for global replacement (all matches in string are replaced; combined would be "ig"); the `$` is a special character in the replacement string and follows the JavaScript semantics (for `String.replace()`).
- `rtrim/ltrim/trim( string )` — removes whitespace from the right/left/both side(s) of the string;
- `split( string, regexp [, max ] )` — splits the string at the matching regular expression and returns an array (e.g. `split( "1,5,8", "," )` returns `["1","5","8"]`).

- `pad( string, length [, padchar ] )` — returns `string` padded to length `length` characters. If `length` is > 0, padding is done on the right; if `length` < 0, padding is done on the left. If optional `padchar` is specified, it is used to pad the string (default `padchar` is space). If the given `string` is longer than `length`, it is returned unmodified. Examples:

      pad("a", 3)     produces    "a  "
      pad("a", -3)                "  a"
      pad("5", -4, "0")           "0005"
      pad("toolong", -4)          "toolong"

- `quote( string )` — escape any interior characters of the string argument so that the result can be embedded in double-quotes safely (i.e. for a lexpjs/JavaScript/JSON-compatible result). For example, the string `hello "there"` would be returned as `hello \\"there\\"`; and the string `abc<newline>def` (where `<newline>` represents an embedded newline ASCII 10 character) would be `abc\\ndef`.

#### Type Handling Functions

- `int( various )` — attempts conversion of its argument to an integer; returns `NaN` if the argument cannot be converted, otherwise, it returns the integer;
- `float( various )` — attempts conversion to a floating-point value;
- `bool( various )` — attempts conversion to `boolean`; in this expression language, the strings "0", "no", "off" and "false", the empty string, the number 0, and boolean false all result in false; otherwise, the result is true;
- `str( various )` — converts the argument to a string;
- `isnull( various )` — more a test than a conversion, returns true if the argument is `null`.
- `isvalue( various }` — returns true if the argument is not `null` or `NaN`.
- `typeof( various )` — returns the data type of the argument. While JavaScript reports arrays and `null` as objects, lexpjs reports them more specifically as array and null respectively.

#### Date/Time Handling Functions

- `time( [ datetime-string ] | [ dateparts-obj ] | [ year [, month [, day [, hour [, minute [, second ]]]]]] )` — Since all arguments are optional, `time()` returns the current time if none are given. If a single string argument is given, the function will attempt to parse it simplistically. Very complex date/time strings may not parse successfully, but simple strings similar to the locale's default format should work, and ISO-8601 is explicitly supported. If the string contains no time component, it will be assumed to be midnight; if it contains no date component, then the date is assumed to be the current day. If an object is given, it is assumed to be of the same form as that returned by `dateparts()`, and the date is constructed from its available members. If the argument is neither string nor object, `time()` expects numeric arguments and a date/time is constructed using as many parts as are provided (in the order shown). The result is always a Unix Epoch time in milliseconds. See additional notes below.
- `dateparts( [timestamp] )` — returns an object with keys `year`, `month`, `day`, `hour`, `minute`, `second`, `millis`, and `weekday` (0-6, where 0=Sunday) for the given `timestamp`, or the current time if not given.

Important notes with respect to date handling (currently; this will evolve):

- The numbering of months is 1-12 in Reactor (not 0-11 as in JavaScript);
- All time functions operate in the timezone set for the runtime. There are currently no UTC functions.
- When passing a `dateparts`-form object (i.e. an object with keys `year`, `month`, etc.) into `time()`, a missing key is assumed to be 0 except `year` (which is assumed to be the current year), and `month` and `day` which are assumed to be 1. An offset date can be computed by adjusting the values in the object by the offset required. For example, fifteen days before March 1, 2022 can be found with `{ year: 2022, month: 3, day: -14 }` (-14 = 1 - 15). Five hours and 8 minutes before the current time could be written as `t=dateparts(), t.hour=t.hour-5, t.minute=t.minute-8, time(t)`. Using this form of date offset computation (rather than simply subtracting 18,480,000 milliseconds from the current time in milliseconds) accounts for changes in DST, leap seconds, or leap days occurring during the offset interval.

Reactor variables using time functions don't auto-update!

New users to Reactor are surprised when their time expressions don't continuously update, but this is by design. Refer to the Evaluation of... sections at the top of this page for rules on when re-evaluations of variables occur.

#### Array/Object Handling Functions

- `len( array )` — returns the number of elements in the array;
- `keys( object )` — returns, as an array, the keys in the given object;
- `values( object )` — returns, as an array, the values in the given object;
- `clone( various )` — returns a (deep) copy of its argument; this is particularly useful for arrays and objects;
- `join( array, joinstring )` — returns a string with the elements of array converted to strings and joined by joinstring (e.g. `join([4,6,8], ":")` results in the string "4:6:8", while `join([9], ":")` would be simply "9");
- `list( ... )` — returns an array of its arguments; this is legacy syntax (i.e. `list(5,7,9)` is the same as writing `[5,7,9]`, so this function is now obsolete and may be removed later);
- `indexOf( array, value )` — if value is present in array, the index (>=0) is returned; otherwise -1 is returned;
- `count( array )` — returns the number of non-null elements in array;
- `sum( array )` — returns the sum of non-null elements of array; note that only a single argument, which must be an array, is accepted;
- `median( array )` — returns the statistical median of the elements of array;
- `slice( array, start, end )` — returns a new array containing the elements of array from start (zero-based) to, but not including, end;
- `insert( array, pos, newElement )` — inserts newElement into array before pos (zero-based); the array is modified in place and is also returned as the function value;
- `remove( array, pos [ , numRemove ] )` — removes elements from array starting at pos; if numRemove is not given, only the one element at pos is removed, otherwise numRemove elements are removed from the array; the array is modified in place and also returned as the function value;
- `push( array, value [ , maxlen ] )` — appends value at the end of array; if maxlen is given, elements are removed from the head of the array to limit its length to maxlen elements; the array is modified in place and also returned as the function value;
- `unshift( array, value [ , maxlen ] )` — insert value at the beginning of array; if maxlen is given, elements are removed from the end of the array to limit its length to maxlen elements; the array is modified in place and also returned as the function value;
- `pop( array )` — removes the last element of array and returns it; returns `null` if array is empty; the array is modified in place;
- `shift( array )` — removes the first element of array and returns it; returns `null` if array is empty; the array is modified in place;
- `arrayConcat( a, b )` — returns a new array that is the concatenation of a and b; for example, `arrayConcat( [1,2,3], [1,3,5] )` returns `[1,2,3,1,3,5]`;
- `arrayIntersection( a, b )` — returns a new array containing all values in array a that are also in array b; for example, `arrayIntersection( [1,2,3], [1,3,5] )` returns `[1,3]`;
- `arrayDifference( a, b )` — returns a new array containing all values of array a that do not appear in array b; for example, `arrayDifference( [1,2,3], [1,3,5] )` returns `[2]`;
- `arrayExclusive( a, b )` — returns a new array containing all values of the arrays a and b that appear only in either, but not both (this is often referred to as the symmetric difference); for example, `arrayExclusive( [1,2,3], [1,3,5] )` returns `[2,5]`;
- `arrayUnion( a, b )` — returns a new array containing all values of the arrays a and b; for example, `arrayUnion( [1,2,3], [1,3,5] )` returns `[1,2,3,5]`;
- `sort( array [, comparison] )` — sort the given array, returning a new array (the given array is not modified). The given array may contain data of any type. The default sort is a case-insensitive, ascending, (host) locale-aware string sort (so the array is assumed to contain strings, and if it contains any other type the values are coerced to strings prior to comparison). To sort differently (e.g. descending, numeric, etc.), `comparison` can be given as the either the name of a defined function taking two arguments as the values to be compared, or an expression that compares the local variables `$1` and `$2` (defined by the `sort()` function as it runs). In either case, the result must be an integer: 0 if the two values are equal; less than 0 (e.g. -1) if the first value sorts before the second; or greater than zero (e.g. 1) if the first value sorts after the second. The comparison must be stable: given two values, it must return the same result every time it runs. Do not apply randomness or other heuristics to the comparison that may violate this requirement, as this can lead to long runtimes or even infinite loops in the attempt to sort. Example: sort an array of numbers descending (largest values first): `sort( numbers, $1 == $2 ? 0 : ( $1 < $2 ? 1 : -1 ) )`
- `range( start, end [, increment] )` — returns an array of integers counting up from `start` to `end` inclusive. If `end` < `start`, the array elements will count down. If `increment` is not specified, it assumed to be 1 (or -1 if `end` < `start`). Examples: `range(0,5)` returns `[0,1,2,3,4,5]`; `range(5,0)` returns `[5,4,3,2,1,0]`; `range(0,5,2)` returns `[0,2,4]`; `range(5,0,-2)` returns `[5,3,1]`; `range(5,0,2)` returns `[]`, an empty array, because a positive increment on a descending range (`end` < `start`) is impossible. A common use of this function is to iterate over the even-numbered items of some other array: `sum=0, each k in range(0,len(otherArray),2): sum = sum + otherArray[k]`.
- `isArray( various )` — returns true if the argument is an array (of any length);
- `isObject( various )` — returns true if the argument is an object;

#### Conversion Functions

- `hex( num )` — returns the hexadecimal (string) representation of the numeric argument (or NaN if non-numeric); the string is not prefixed (i.e. with `0x`) or padded to any particular length — this us up to the user;
- `oct( num )` — like `hex()` above, but returns the octal representation of its (numeric) argument as a string;
- `bin( num )` — like `hex()` above, but returns the binary representation of its (numeric) argument as a string;
- `toJSON( various )` — returns the argument as a JSON-formatted object (string);
- `parseJSON( json )` — returns the data represented the (parsed) JSON string argument;
- `isJSON( string )` — returns true if the given string is parseable as JSON; false otherwise.
- `btoa( str )` — returns the Base64-encoded representation of the string argument;
- `atob( str )` — returns a string containing the decoded Base64 argument (string).
- `urlencode( string )` — URL-encodes the given string.
- `urldecode( string )` — URL-decodes the given string.

#### Special Reactor Functions

See Expressions with Entities and Attributes (in the How-To section) for an overview of using expressions to fetch and operate on entity data in Reactor.

- `getEntity( id )` — returns data for the entity matching `id`, which may be an entity ID, a canonical entity ID, or an entity name. When using anything other than the canonical ID, the first matching entity found is returned. Searching by entity name can be somewhat canonicalized by preceding the name with the controller ID, as in `"vera>Hall Motion Sensor"`. The result is an object with the entity's data, or null. Expressions of the form `getEntity( "vera>Hall Light Switch" ).attributes.power_switch.state` are a typical form of use. If the data is to be used repeatedly, you may (and should) assign the result to a local variable:

      /* Don't do this... */
      adjusted_temperature =
          getEntity( 'house>device_512' ).attributes.temperature_sensor.value +
          getEntity( 'house>device_512' ).attributes.value_sensor.value

      /* Do this instead... */
      local entity = getEntity( 'house>device_512' ),
      adjusted_temperature = entity.attributes.temperature_sensor.value +
          entity.attributes.value_sensor.value

- `matchEntities( criteria )` — returns an array of canonical IDs of entities matching the criteria specified. The criteria may be either a string containing comma-separated capability names, or an object with any of the following keys: `controller`, `capability`, `group`. When using the object form, the value for each key may be either a string or an array of strings. For example, `{ controller: 'vera', capability: [ 'binary_sensor', 'value_sensor' ] }` would return all entities from the controller with ID `vera` that provide the `binary_sensor` or `value_sensor` capability. The intent of this function is to rapidly produce a reduced list of entities that can then, if necessary, be further filtered using `each` statements. Building on the example:

      sensors = matchEntities( { controller: 'vera',
        capability: [ 'binary_sensor', 'value_sensor' ] } )

      low_battery = each e in sensors:
                      getEntity( e ).attributes.battery_power?.level < 0.5 ? e : null

  The final result of the `low_battery` expression would be the canonical IDs of all of the `sensors` entities found by `matchEntities()` that have the `battery_power` capability and a battery level under 50%. Note that this isn't a particularly good example for battery state tracking. More likely, you'll want to match all devices that have the `battery_power` capability (e.g. `matchEntities( "battery_power" )` ), not just the sensor types shown.

Dynamic Groups

DynamicGroupController and its dynamic groups have been added since `matchEntities()` was originally provided. Dynamic groups are much more efficient at finding and filtering entities than using `matchEntities()` in expressions, and you should try to use them in preference if possible.

- `performAction( entity_id, action, args )` — perform an action on the entity identified by `entity_id`. This function always returns `null` (even if there is an error). The `action` argument must be a string containing the name of an action in a capability supported by the device (e.g. `media_player.mute` or `dimming.set`). The `args` argument must be an object with key/value pairs corresponding to the action's required parameters. Example to set a dimmable light to 25%: `performAction( 'hub>Hallway Light', 'dimming.set', { level: 0.25 } )`
- `isRuleSet( rule_id )` — returns true if the specified rule is set, false if reset, and null if it does not exist.
- `isRuleEnabled( rule_id )` — returns true if the specified rule is enabled, false if disabled, and null if it does not exist.
- `getRule( rule_id )` — returns rule metadata, including its name and status; returns null if the rule does not exist.
- `fileExists( name )` — queries the file system and returns true if the file exists in `storage/userfiles` directory in the Reactor installation; false otherwise.
- `fileRead( name [, options ] )` — reads the named file in the `storage/userfiles` and returns its content. The optional `options` object may contain the following keys: `encoding` to set an encoding (UTF-8 is the default if not otherwise specified); `json`, when boolean true, causes the file contents to be parsed as a JSON object and returned; `lines`, when boolean true, returns the file contents as an array, one line per element. This function returns null on any error.
- `fileWrite( name, data [, options ] )` — writes the named file in `storage/userfiles`. The `data` must be a string; to write any other type, you can convert it to JSON first by wrapping the value/variable in a `toJSON()` call (e.g. `fileWrite( 'test.json', toJSON( obj ) )`). The optional `options` argument must be an object containing any combination of the following keys: `encoding` for the encoding type (UTF-8 is the default); `append`, if boolean true, causes the contents to be appended to the file; `mode` to set the file mode if the file is created (default 0660, read/write for owner and group, none for world).
- `alarm( interval_seconds [, options ] )` — causes a periodic re-evaluation of its global expression. This function can only be used in global expressions and Script conditions in Rule triggers; it will throw an error if used in other contexts. It is meant to be used as part of compound expression, so that the remainder of the expression sets the result value. Example: `alarm( 15 ), time()` will cause the expression to update the current time every 15 seconds. NOTE: small intervals may cause excessive CPU usage or throttling of rules in which the global variable is used. If other events cause the expression to be re-evaluated (such as the update of an entity referred to by `getEntity()` in the expression), the timer interval is not restarted by the `alarm()` call — the interval that starts the timer always finishes unless the timer is explicitly cancelled. The timer can be cancelled by using `alarm(0)` (interval of zero) in the expression. For example, starting the expression with `alarm(0), alarm(interval), ...` guarantees that no matter how often the expression is re-evaluated, the timer restarts its interval each time the re-evaluation occurs. By default, this function will trigger on the given interval. It is possible to synchronize the timer to even multiples of the intervaL, for example, a 60-second interval that triggers at the top of the minute, or a 3600 second interval that triggers at 0 minutes after the hour. The optional `options` argument (an object), and its only supported key currently is `sync`, which when true (e.g. `alarm( 3600, { sync: true } )` will ensure that the timer triggers at the top of the next hour (and if `alarm()` is called again during that evaluation, the next evaluation will occur at the top of the next hour). The return value of the function is a boolean indicating that the timer has successfully completed the interval. Timers used by this call are not preserved across Reactor restarts (e.g. a ten minute `alarm(600)` timer interrupted by a restart causes a new ten minute interval to begin upon restart).

- `strftime( format_string [, time ] )` — Formats `time` to a human-readable form controlled by `format_string`. If `time` is not given or null, the current time is used. The format string implements most of the specifiers and options of the Unix `date` command. The following patterns are supported:

  - `%%` — insert a literal "%" into the string
  - `%a` — inserts the abbreviated day of the week (e.g. "Mon")
  - `%A` — inserts the full day of the week (e.g. "Monday")
  - `%b` — inserts the abbreviated month name (e.g. "Feb")
  - `%B` — inserts the full month name (e.g. "February")
  - `%c` — equivalent to `%a %b %e %T %z %Y`, which produces output in the same form as the Linux `date` command (e.g. "Fri 27 Aug 2021 12:05:24 PM -0400")
  - `%C` — inserts the two-digit century number
  - `%d` — inserts the two-digit day of the month (01-31)
  - `%D` — date, equivalent to `%m/%d/%y`
  - `%e` — inserts the day of the month, space padded (e.g. day 3 is " 3")
  - `%f` — inserts the number of milliseconds (0-999)
  - `%F` — equivalent to `%Y-%m-%d`
  - `%H` — two-digit (24-hour) hour (00-23)
  - `%I` — two-digit (12-hour) hour (01-12)
  - `%j` — day of year (three-digits, zero-filled) (000-366)
  - `%k` — hour (24-hour), space padded (e.g. 4am = " 4", 4pm = "16")
  - `%l` — hour (12-hour), space padded (e.g. 4am = " 4", 4pm = " 4")
  - `%m` — month, two-digit zero filled (01-12)
  - `%M` — minutes, two-digits zero filled (00-59)
  - `%p` — "AM" or "PM"
  - `%P` — "am" or "pm"
  - `%r` — equivalent to `%I:%M:%S %p`
  - `%R` — equivalent to `%H:%M`
  - `%s` — Unix Epoch timestamp (with fractional milliseconds)
  - `%S` — seconds, two-digit zero filled (00-59)
  - `%T` — equivalent to `%H:%M:%S`
  - `%u` — day of week, numeric (1=Monday, 7=Sunday)
  - `%w` — day of week, numeric (0=Sunday, 6=Saturday)
  - `%x` — locale-specific date representation (result depends on locale configuration of the host)
  - `%X` — locale-specific time representation (result depends on locale configuration of the host)
  - `%y` — two-digit year, zero-filled (00-99)
  - `%Y` — four-digit year
  - `%z` — timezone offset (e.g. +/-hhmm, like -0500)

  Additionally, the Unix `date` command's formatting options (`0`, `_`, `-`, `^`) are supported, as well as the width modifier. Examples:

  - `%k` at 4am → " 4" (default with no options, has leading space)
  - `%02k` at 4am → "04" (now same result as `%H`)
  - `%-k` at 4am → "4" (has leading space removed)
  - `%H` at 4am → "04" (default with leading zero)
  - `%_H` at 4am → " 4" (leading zero replaced with space, now same as `%I`)
  - `%8y` in 2021 → "00000021" (nonsensical, but illustrates field width specification with default padding)
  - `%A` → "Friday"
  - `%^A` → "FRIDAY"

  Note that the `strftime()` function is locale-aware, so if you are using a non-English locale, the time "words" (names of months and days of the week, for example) will be in the language defined by the active locale.

- `format( format_string [, arg1 [, arg2 [, ...]]] )` — Formats `format_string` according to its substitutions, inserts `argn` values where indicated. The general form for substitution is `{n}` where n is the (zero-based) argument number (e.g. `{0}` would insert `arg1`'s formatted value). The argument number can be omitted (e.g. `{}`), and the next argument will be used. Optionally, the argument number can be followed by one of the format specifiers shown below, separated from the argument number by a colon (`:`). The format specifier may be preceded by options, given in the following order with any allowed to be omitted: an optional zero-padding flag (`0`), an optional alignment indicator (`<`, `>` or `^` for left aligned, right aligned, or centered, respectively), an optional field width (if not given, the field is as wide as the argument value needs), and for floating-point format specifiers (`e`, `f` and `g`) a dot (`.`) followed by precision (number of digits). For example, "{2:>9.2f}" would format the third argument as a floating point value in a 9-character field, using two digits precision right of the decimal, right justified in the string (padded with spaces to make it 9 characters), so 23.169 would be formatted as "⋅⋅⋅⋅23.17" (for visibility here each "⋅" represents a space in the result string); `{0:06d}` would format a number in the first argument as six digits zero filled (e.g. 123 would be "000123"). The format specifiers available are:

  - `s` — plain string (this is the default format if not given, e.g. `{0}`
  - `q` — format as a quoted string (if the argument contains quotes, they are escaped in the result string)
  - `d` — decimal integer form (argument must be numeric)
  - `b` — binary integer form (argument must be numeric), e.g. value 15 would result in "1111"
  - `o` — octal integer form (argument must be numeric), e.g. 167 would result in "247"
  - `x` — hex, lower-case (argument must be numeric), e.g. 167 would result in "a7"
  - `X` — hex, upper-case (argument must be numeric), e.g. 167 would result in "A7"
  - `f` — fixed-point floating point, e.g. `{0:.2f}` given pi would result in "3.14"
  - `e` — exponential (scientific) form, e.g. `{0:.4e}` given 123,456 would result in "1.2346e+5"
  - `g` — general, switches between `f` and `e` as needed for best readability (estimated)
  - `%` — formats number as a percentage, e.g. 0.15 would format as "15%"

  Examples (spaces shown as "⋅" for clarity only):

  - `format( "Temp is {0}F", 72.33178 )` → `"Temp⋅is⋅72.33178F"`
  - `format( "Temp is {0:.1f}F", 72.33178 )` → `"Temp⋅is⋅72.3F"`
  - `format( "Temp is {0:8.3f}F", 72.33178 )` → `"Temp⋅is⋅⋅⋅72.332F"`
  - `format( "Temp is {0:08.3f}F", 72.33178 )` → `"Temp⋅is⋅0072.332F"`
  - `format( "Temp is {0:<8.3f}F", 72.33178 )` → `"Temp⋅is⋅72.332⋅⋅F"`
  - `format( "In order: {} {} {} {}", "a", "b", "c", "d" )` → `"In order: a b c d"`
  - `format( "Mixed: {2} {} {0} {}", "a", "b", "c", "d" )` → `"Mixed: c d a b"`

- `dumpVars()` — dumps all known variables to the log. This can be handy for troubleshooting your expressions.

- `geodist( lat1, lon1, lat2, lon2 )` — Returns an approximate Great Circle Distance in kilometers between two locations on Earth. To get miles, multiply the result by 0.621371. Latitudes and longitudes must be given in decimal degrees, with negative latitudes being south of the Equator, and negative longitudes being west of the Prime Meridian (i.e. Greenwich).
- `print( ... )` — prints its arguments to the log file. Each argument is separated by a single space. This may be handy for troubleshooting your more complex expressions. Example: `print("count is currently", count)`

#### Reserved Words

As a result of the syntax, the following words are reserved and may not be used as identifiers or function names: `true, false, null, each, in, first, of, with, if, then, else, elif, elsif, elseif, endif, case, when, do, done, define, and, or, not, NaN, Infinity`. Note that keywords and identifiers are case-sensitive, so while `each` is not an acceptable identifier, `Each` or `EACH` would be. The names of all defined functions are also reserved.

## Consequences of Disabling Auto-Evaluation

Auto-evaluation is a feature of Global Variables under which they are updated automatically if they refer to another Global Variable or entity that changes.

DEPRECATED

Disabling auto-evaluation is deprecated and will be removed from a future Reactor release. If you are using this feature and it seems important to you, contact the author ASAP.

Consider a Global Variable named `sensor_updated` that is defined as `getEntity( 'hubitat>76' ).lastupdate`. With auto-evaluation enabled (the default), this global variable will be re-evaluated and updated whenever the specified target of the `getEntity()` function changes. However, if auto-evaluation is turned off for this variable, then it will only be updated when a rule or reaction that references it is run.

Now let's consider what happens if we create another (global) variable called `update_timestamp` with the expression `strftime( "%T", sensor_updated )`. This global variable is now dependent on `sensor_updated`. So we have now created a dependency chain:

        entity               global var                global var
      hubitat>76   ---->   sensor_updated   ---->   update_timestamp
          ^                      ^                        ^
          |                      |                        |
      when this                this                    and so
       changes                changes                 does this

In the default case, with auto-evaluation enabled, a change to the entity will cause `sensor_updated` to be re-evaluated, and if its value changes, then `update_timestamp` will also be re-evaluated.

But, if auto-evaluate is disabled for `sensor_updated`, we break the chain:

      hubitat>76     X     sensor_updated   --->   update_timestamp
          ^          ^-----(auto-eval off)                ^
          |                      ^                        |
          |                      |                        |
      when this               this does             and so neither
       changes                NOT change              does this

In this case, a change to the entity does not update `sensor_updated`, and thus `update_timestamp` will also not be updated. If a rule or reaction later references `sensor_updated`, however, then it will be updated at that moment, and `update_timestamp` will also be updated, because it is dependent on `sensor_updated` (unless, of course, auto-evaluate is also turned off for `update_timestamp` as well).

So keep in mind that turning off auto-evaluation on a global variable doesn't just affect that variable, but will break a chain that affects all values derived from that variable as well.

## The Global Environment

While it's possible to define global functions and constant values in Global Expressions, this may not be the most manageable way to do it because you might have to create a large number of global expressions. Another way to do it is to create an expression in Engine configuration in `reactor.yaml` that defines your constants and functions. For example:

    reactor:
      # other configuration redacted for clarity
      engine:
        enabled: true
        # other configuration redacted for clarity
        global_expression_environment: |
          SITENAME="MyHouse",
          define square(a) a*a

The expression given (called the environment expression) will be evaluated at startup before any global or rule-based variables. Any functions and variables defined by this expression will be available throughout the Reactor environment (i.e. in Rules, substitutions, etc.). The values of variables are not preserved across restarts — they are intended to be set to the same constant value every startup.

The following functions are not available in the environment expression because they can refer to objects and data that are not initialized at the early stage of Engine startup where the environment expression is evaluated. If you need these functions, you need to use a Global Expression for that variable or custom function.

- `getEntity()`
- `matchEntities()`
- `groupMembers()`
- `getRule()`
- `isRuleSet()`
- `isRuleEnabled()`
- `performAction()`
- `alarm()`

Don't Modify Environment-created Variables Elsewhere

The variables defined by the environment expression are intended to be constants (i.e. read-only once defined). Do not modify them in other expressions — currently, the expression language will let you do it, but it is explicitly not supported and may not be possible in a future version of Reactor.

Seeing Environment Values

For debugging and troubleshooting purposes, you can see environment variables by creating a test Global Expression that simply refers to one of them.

Updated: 2025-Nov-16