Skip to content

Expressions

Overview

Reactor has two types of variables: Rule-based and Global. 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).

Rule-based variables are those that are defined on a rule. These variables have evaluation scope limited to the rule; that is, their container variable can be referenced in other variables' expressions within the rule, and they can reference Global Variables, but cannot refer to or modify variables in other rules (i.e. they are local within the scope of the rule that defines them). Rule-based variables are evaluated whenever a dependency of the rule triggers an evaluation of its conditions, and the variables are evaluated prior to the rule's trigger and constraint conditions being evaluated. They are evaluated in the order in which they are shown in the Expressions section of the rule.

Global variables are visible to all rules in the system, and can be referred to by any rule-based variable or any other global variable. Global variables are evaluated a bit differently from local variables. A global variable is evaluated when a rule that refers to the global variable in its own (rule-based) expressions or conditions is evaluated, or when a reaction is run that contains actions with substitutions referring to the global variable. In addition, the normal configuration of global variables is "auto-evaluation," where a global variable is recalculated if it contains references to another global variable or entity that has changed. Because it can be desirable to have some variables only update on demand, auto-evaluation can be turned off for any global variable. See Consequences of Disabling Auto-Evaluation, below.

It is possible for a rule-based variable to have the same name as a global variable. If another expression in the rule 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.

Expression Language Syntax

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 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 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).

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 a comma. 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)
  • : (ternary operator second)
  • ?? (coalesce), ?# (coalesceNaN)
  • ||, 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.

do <statement-list> done

Since the limited syntax of each, case, etc. allow only a single statement to be executed, the do...done statement creates a statement block that appears to be a single statement, thus allowing multiple statements/expressions to be executed in that context. The standard multi-statement result rule applies: the result of the statement block is the result produced by the last expression in the block.

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.

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

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

The syntax guides shown below (which are based on a well-known BNF) denotes optional arguments by enclosing them in [] (square brackets). These characters are not be included when writing your expressions. For example, the BNF dateparts( [time] ) denotes that the time argument is optional and may be omitted, so you could write dateparts( someTimeVariableName ) or just dateparts(), but dateparts( [ someTimeVariableName ] ) would not be correct.

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 powerth 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 return; if a maximum is given and the value is greater, the maximum is returned).
  • 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 startth 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( [time] ) — returns an object with keys year, month, day, hour, minute, second, millis, and weekday (0-6, where 0=Sunday) for the given time, 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.

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);
  • toJSON( various ) — returns the argument as a JSON-formatted object (string);
  • parseJSON( json ) — returns the data represented the (parsed) JSON string argument;
  • 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 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". This function retrieves entities that are otherwise not in context. 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.
  • 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).
  • 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"

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.

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.

Updated: 2024-Sep-30 (24274)