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 complexs=[1,2,3], t=s, s == t
is true, however, becauses
andt
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"
orint( '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 )
— roundsnumber
toprecision
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 )
— raisesbase
to thepower
th power (e.g.pow(10,3)
is 1000);sqrt( number )
— square root (ofnumber
> 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 isNaN
,null
, or if an attempted conversion withparseInt()/parseFloat()
would result inNaN
(e.g.isNaN( 'this is not a number' )
returns true, butisNaN( '123' )
returns false). Note that this is a little different from JavaScript'sisNaN()
: 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) whileparseInt(null)
andparseFloat(null)
both returnNaN
. I think this is a vexing inconsistency, so lexpjs will return true forisNaN(null)
to agree withparseInt()/parseFloat()
.isInfinity( value )
&mash; returns true if the argument isInfinity
, 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 offromLow
tofromHigh
, and rescales it into the range oftoLow
totoHigh
. 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 thestart
th character forlength
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, ornull
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 ] )
— likematch()
, 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 formatch()
;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 (forString.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 ] )
— returnsstring
padded to lengthlength
characters. Iflength
is > 0, padding is done on the right; iflength
< 0, padding is done on the left. If optionalpadchar
is specified, it is used to pad the string (defaultpadchar
is space). If the givenstring
is longer thanlength
, 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 stringhello "there"
would be returned ashello \\"there\\"
; and the stringabc<newline>def
(where<newline>
represents an embedded newline ASCII 10 character) would beabc\\ndef
.
Type Handling Functions
int( various )
— attempts conversion of its argument to an integer; returnsNaN
if the argument cannot be converted, otherwise, it returns the integer;float( various )
— attempts conversion to a floating-point value;bool( various )
— attempts conversion toboolean
; 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 isnull
.isvalue( various }
— returns true if the argument is notnull
orNaN
.typeof( various )
— returns the data type of the argument. While JavaScript reports arrays andnull
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 bydateparts()
, 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 keysyear
,month
,day
,hour
,minute
,second
,millis
, andweekday
(0-6, where 0=Sunday) for the giventime
, 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 keysyear
,month
, etc.) intotime()
, a missing key is assumed to be 0 exceptyear
(which is assumed to be the current year), andmonth
andday
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 ast=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", whilejoin([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; returnsnull
if array is empty; the array is modified in place;shift( array )
— removes the first element of array and returns it; returnsnull
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 thesort()
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 fromstart
toend
inclusive. Ifend
<start
, the array elements will count down. Ifincrement
is not specified, it assumed to be 1 (or -1 ifend
<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 matchingid
, 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 formgetEntity( "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 IDvera
that provide thebinary_sensor
orvalue_sensor
capability. The intent of this function is to rapidly produce a reduced list of entities that can then, if necessary, be further filtered usingeach
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 thesensors
entities found bymatchEntities()
that have thebattery_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 thebattery_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 byentity_id
. This function always returnsnull
(even if there is an error). Theaction
argument must be a string containing the name of an action in a capability supported by the device (e.g.media_player.mute
ordimming.set
). Theargs
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 instorage/userfiles
directory in the Reactor installation; false otherwise.fileRead( name [, options ] )
— reads the named file in thestorage/userfiles
and returns its content. The optionaloptions
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 instorage/userfiles
. Thedata
must be a string; to write any other type, you can convert it to JSON first by wrapping the value/variable in atoJSON()
call (e.g.fileWrite( 'test.json', toJSON( obj ) )
). The optionaloptions
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 ] )
— Formatstime
to a human-readable form controlled byformat_string
. Iftime
is not given or null, the current time is used. The format string implements most of the specifiers and options of the Unixdate
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 Linuxdate
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 [, ...]]] )
- Formatsformat_string
according to its substitutions, insertsargn
values where indicated. The general form for substitution is{n}
where n is the (zero-based) argument number (e.g.{0}
would insertarg1
'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
andg
) 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 betweenf
ande
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)