Edit: Updates for OH 4, inclusion of JS Scripting and Blockly examples, removed the doors example as it’s superfluous.
Please see Design Pattern: What is a Design Pattern and How Do I Use Them for how to read and use DPs.
Problem Statement
One powerful way to consolidate code in rules is to use array/list manipulation operations on the members of a Group. For example, filter down to just those members that are ON
, get a list of the labels of those Items that are NULL
, etc.
Concept
This DP is not a standard DP in that it provides a single concrete template to follow to solve a specific problem. Instead this DP is more of a grand exploration of some of the things one can do with Groups. See the bottom for a list of some related DPs that utilize some of the techniques discussed here in a more concrete way.
This DP presents a series of examples for how to access and manipulate the members of Groups which is a common requirement in many use cases.
Important GroupItem Methods
Within Rules, a Group is represented using a GroupItem. The GroupItem has a method named members
which provides access to all the Items that are members of that Group as a Set
. In some languages, that Java Set
is converted to a native type (e.g. a JavaScript Array
). Some languages also has a method to get the members of sub Groups.
Blockly
Unfortunately Blockly does not support list streaming operations like other languages do so most operations involve looping through the lists and creating a new list. Any of the JS Scripting examples below can be uses in an inline script block where necessary. This section will show the blocks to work with the members of a Group.
-
: Get the members of a DoorStatus
-
: Loop through all members of DoorStatus and execute
<code>
on each. -
: Puts a list of all the members of MyGroup that have the state ‘ON’ into the variable
filteredList
. -
: Puts the first member of the DoorStatus whose state is ‘ON’ into the variable
found
. (Note the “in list X find first” block cannot do this as it only works with whole Object matching, not matching one member of an Object) - : Sorts the list of Items based on the Item’s numeric state (the sharp eyed among you may recognize the bubble sort algorithm). Note this is an in place sort and does not create a new list. (Note the existing sort block only works on numbers and strings, not Objects)
-
: Setsslice
to a list containing the first six members of MyGroup. -
: Sets
mapped
to a list containing a string for each Item that is the “Item’s label is state” (e.g. “MyItem is ON”) -
: Sets
mapped
to a list containing the Item’s states. -
: Sums up the states of all members of MyGroup and stores it in the variable
sum
.
Combining some of the above, let’s build a String showing all the doors that are open:
JS Scripting (aka GraalVM JavaScript)
-
items.MyGroup.members
: get a list of all the direct members of the Group, GroupItems that are also members will be included but not their members. -
items.MyGroup.descendents
: get a list of all members of the Group and all members of the Groups that are members of this Group. The GroupItems are not included. -
items.MyGroup.forEach( item => { <code> })
: Loop through all members of MyGroup and execute<code>
on each. -
items.MyGroup.members.filter( item => item.state == 'ON' )
: returns a list of all the members of MyGroup that have the state ON -
items.MyGroup.members.find( item => item,.state == 'ON' )
: Returns the first member of the Group whose state is ‘ON’. -
items.MyGroup.members.sort(a, b => { if(a.state > b.state) return 1; else if(a.state < b.state) return -1; else return 0; }
: Return the members sorted by their state as a number -
items.MyGroup.members.sort(a, b => a.numeriucState - b.numericState)
: a more concise way to sort by numericState but which works with numbers only -
items.MyGroup.members.sort(a, b => a.quantityState.minus(b).float)
: a concise way to sort byQuantity
states -
items.MyGroup.members.slice(0, 4)
: Returns an array containing the first five members of MyGroup. -
items.MyGroup.members.map(item => item.label + ' is ' + item.state)
: Returns an array containing a string for each Item that is the “Item’s label is state” (e.g. “MyItem is ON”) -
items.MyGroup.members.map(item => item.state)
: Returns an array containing the Item’s states. -
item.MyGroup.members.map.(item => item.numericState).reduce(result, currVal => result + currValue, 0)
: Sums up the states of all members of MyGroup.
Map and Reduce are great ways to create summaries on the members of the Group. For example, to build a String showing all the doors that are open:
var msg = item.MyGroup.members
.filter( door => door.state == 'OPEN' ) // Get the OPEN doors
.map( door => door.name ) // Get the door names
.reduce( result, doorName => result + ', ' + doorName, '' ); // Build a comma separated string of the door names
// Using join instead of reduce
var msg = item.MyGroup.members
.filter( door => door.state == 'OPEN' ) // Get the OPEN doors
.map( door => door.name ) // Get the door names
.join( ', ' ); // Build a comma separated string of the door names
Nashorn JavaScript
Nashorn uses the raw Java Classes. See Stream (Java SE 17 & JDK 17)
JSR223 Python
-
ir.getItem("MyGroup").members
: get a list of all the members of the Group, GroupItems that are also members are of the list. -
ir.getItem("MyGroup").allMembers
: get a list of all members of the Group and members of subgroups; the subgroup GroupItems are not included, only their members. -
for i in ir.getItem("MyGroup").members: <code>
: Loop through all members of MyGroup and execute<code>
on each. -
filter(lambda item: item.state == ON, ir.getItem("MyGroup").members)
: returns a list of all the members of MyGroup that have the state ON -
[item for item in ir.getItem("MyGroup").members if item.state == ON]
: alternative to filter using Python list comprehension -
filter(lambda item: item.state == ON, ir.getItem("MyGroup").members)[0]
: Returns the first member of the Group that meets the condition defined in the lambda. -
[ item for item in ir.getItem("MyGroup").members if item.state == ON][0]
: Returns the first member of the Group that meets the condition defined in the lambda using Python list comprehension. -
sorted(item for item in ir.getItem("MyGroup").members , key = lambda item: item.state)
: Returns a list containing the members of MyGroup sorted by their state. -
ir.getItem("MyGroup").members[0:5]
: Returns a list containing the first five members of MyGroup. -
map(lambda item: item.state, ir.getItem("MyGoup"))
: Returns a list of the states of all the Items that are a member of MyGroup. The map lambda can be more involved (e.g. format a String for building a summary message). -
[item.state for item in ir.getItem("MyGroup")]
: Returns a list of the states of the Items in MyGroup using Python list comprehension. -
reduce(lambda: result, value: result.add(x), map(lambda item: item.state, ir.getItem("MyGroup").members))
: Generates a single sum of all the states of all the Items that are members of MyGroup.
Map and Reduce are great ways to create summaries on the members of the Group. For example, to build a String showing all the doors that are open:
msg = reduce(lambda: msg, name: "{}, {}".format(msg, name), # build the string
map(lambda: door.name, # extract the Item name
filter(lambda door: door.state == OPEN, # get the OPEN Items
ir.getItem("MyGroup").members))) # get the door Items from the Group
# Using list comprehensions
msg = reduce(lambda: msg, name: "{}, {}".format(msg, name),
[door.name for door in ir.getItem("MyGroup").members if door.state == OPEN])
Rules DSL
-
MyGroup.members
: get a Set of all the members of the Group, GroupItems that are also members are part of the set -
MyGroup.allMembers
: get a Set of all the members of the Group and all members of subgroups; the subgroup GroupItems are not included, only their members -
MyGroup.members.forEach[ i | <code> ]
: Execute<code>
on each member of MyGroup, e.g.MyGroup.members.forEach[ i | i.postUpdate(CLOSED) ]
-
MyGroup.members.filter[ i | <condition> ]
: Return a Set of all the members that meet the provided<condition>
-
MyGroup.members.findFirst [ i | <confition> ]
: Returns the first member that matches the provided<condition>
-
MyGroup.members.sortBy[ <method> ]
: Return a List of the members sorted by the value returned by<method>
(e.g. usingname
will sort the Items alphabetically by Item name,lastUpdate
by the last time they received an update which requires persistence) -
MyGroup.members.take(i)
: wherei
is an integer, returns a set containing the first i Items in members -
MyGroup.members.map[ <method> ]
: return a Set containing the result of calling<method>
on each member, for exampleMyGroup.members.map[ name ]
returns a Set of Strings containing the names of all the Items -
MyGroup.members.reduce[ <result>, <value> | <result> <operation> <value> ]
: returns the reduction of all the values in the Set by the<operation>
, for exampleMyGroup.members.reduce[ sum, v | sum + v ]
will return the sum of all members of MyGroup
A note on the use of ?
: a number of examples found on the wiki and on this forum will use a ?
between the Group name and .members
as in MyGroup?.members
. This means that if MyGroup.members === null ignore the line
. Put another way, it will not generate an error if MyGroup has no members. I do not recommend using this syntax as it can hide some types of errors.
A note on ( )
verses [ ]
. The Rules DSL has a concept of a lambda. A lambda is a function that you can treat as if it were like any other var or val. Lambdas are usually defined by surrounding the function in [ ]
(note there must be a space after the [ and before the ]). However, the Rules DSL allows parens to be used instead in some circumstances. I prefer to stick to [ ]
and will do so in the examples below. Do not be confused however if you ever see parens instead as that is also allowed. For example MyGroup.filter[ i|i.state==OPEN ]
and MyUpdate.filter(i|i.state==OPEN)
are both valid.
A note on var and vals. In the lambdas passed to these functions, only vals can be accessed. Unfortunately, that means it is impossible, for example, to calculate the sum of all the NumberItems in a Group using forEach
. That is where map
and reduce
come in. To get the sum of NumberItems one can use:
MyGroup.members.map[ state as Number ].reduce[ sum, v | sum + v ]
To build a String containing all the names of Items that are OPEN there are two approaches: use map/reduce or use a val StringBuilder.
// StringBuilder
val StringBuilder sb = new StringBuilder
MyGroup.members.filter[ i | i.state == OPEN].forEach[i | sb.append(", " + i.name) ]
// Map/Reduce
val str = MyGroup.members.filter[ i | i.state == OPEN ].map[ name ].reduce[ s, name | s + ", " + name ]
The StringBuild approach works because we can declare it as a val but still can call methods on it to change it.
Related Design Patterns
Design Pattern | How It’s Used |
---|---|
[Deprecated] Design Pattern: Time Of Day | For determining when it is NIGHT or BED time |
Design Pattern: Associated Items | For an explanation on how to pull out the Timer and DateTime Items associated with a given Contact |
Design Pattern: Group Based Persistence | For setting up persistence |
Design Pattern: Sensor Aggregation | For detecting presence |
Design Pattern: Expire Binding Based Timers | The Timers used |
Design Pattern: Human Readable Names in Messages | Converting Item names to a more friendly name for messages and alerts |
Design Pattern: Cascading Timers | Using Groups to create Timers that trigger other Timers |
Design Pattern: Group Based Persistence | Using Groups to specify how Items are persisted |
[Deprecated] Design Patterns: Generic Is Alive | Using Groups to monitor whether a sensor or device is alive or hasn’t reported in awhile |
Edit: Updated to use triggeringItem and Member of Rule trigger instead of the persistence hack.