Design Pattern: Working with Groups in Rules

Please see Design Pattern: What is a Design Pattern and How Do I Use Them for how to read and use DPs.

Problem Statement

Often one finds a number of Rules that are very similar and that all work on similar Items resulting in a lot of duplicated code. One way to solve this problem is through the use of Groups.

Concept

image
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 related DPs that utilize some of the techniques discussed here in a more concrete way.

This DP presents a step-by-step tutorial for how to use Groups to consolidate and simplify an example set of several similar rules into one single rule.

The example used will be based on Contact Items that represent door and window sensors. Whenever any of these Contacts change to OPEN:

  • a timer is set to alert when the Contact has been open for over an hour
  • a timestamp of the change is recorded in a DateTime Item
  • an alert is generated when the Contact is opened and no one is home
  • an alert is generated when the Contact is opened and it is after dark

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.

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. using name will sort the Items alphabetically by Item name, lastUpdate by the last time they received an update which requires persistence)

  • MyGroup.members.take(i): where i 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 example MyGroup.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 example MyGroup.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.

Starting Point

This example has five Contacts, one for each external door. Binding configs for the sensors are left off. Each Contact has an associated DateTime and Timer. See Design Pattern: Associated Items for more explanation on the naming convention. See Design Pattern: Expire Binding Based Timers for more on the Timers. See Design Pattern: Human Readable Names in Messages for more on the use of MAP to get more human readable messages.

Items

Contact vGarageOpener1 "Garage Door Opener 1 is [%s]"
DateTime vGarageOpener1_LastUpdate "Garage Door Opener 1 [%1$tm/%1$td %1$tH:%1$tM]"
Switch vGarageOpener1_Timer { expire="1h,command=OFF" }

Contact vGarageOpener2 "Garage Door Opener 2 is [%s]"
DateTime vGarageOpener2_LastUpdate "Garage Door Opener 2 [%1$tm/%1$td %1$tH:%1$tM]"
Switch vGarageOpener2_Timer { expire="1h,command=OFF" }

Contact vFrontDoor "Front Door is [%s]"
DateTime vFrontDoor_LastUpdate "Front Door [%1$tm/%1$td %1$tH:%1$tM]"
Switch vFrontDoor_Timer { expire="1h,command=OFF" }

Contact vBackDoor "Back Door is [%s]"
DateTime vBackDoor_LastUpdate "Back Door [%1$tm/%1$td %1$tH:%1$tM]"
Switch vBackDoor_Timer { expire="1h,command=OFF" }

Contact vGarageDoor "Garage Door is [%s]"
DateTime vGarageDoor_LastUpdate "Garage Door [%1$tm/%1$td %1$tH:%1$tM]"
Switch vGarageDoor_Timer { expire="1h,command=OFF" }

Rules DSL Implementation not Using Groups

The following is a sampling of the rules. They are essentially almost identical for each contact:

rule "Garage Opener 1 changed"
when
    Item vGarageOpener1 changed
then

    // set timer
    if(vGarageOpener1.state == OPEN) vGarageOpener1_Timer.sendCommand(ON) 
    else vGarageOpener1_Timer.postUpdate(OFF) // postUpdate(OFF) cancels the timer without triggering the rule below

    // record event time
    vGarageOpener1_LastUpdate.postUpdate(new DateTimeType)

    // Alert
    val StringBuilder msg = new StringBuilder
    msg.append(transform("MAP", "entry.map", vGarageOpener1.name))

    if(vGarageOpener1.state == OPEN) msg.append(" was opened") else msg.append(" was closed")

    var alert = false
    if(vTimeOfDay.state == "NIGHT" || vTimeOfDay.state == "BED"){
        alert = true
        msg.append(" and it is night")
    }

    if(vPresent.state == OFF){
        alert = true
        msg.append(" and no one is home")
    }

    if(alert){
        msg.append("!")
        aAlerts.sendCommand(msg.toString)
    }
    logInfo("Entry", msg.toString)
end

// repeat for the other four contacts

// The Expire binding will sendCommand OFF when the Timer expires
rule "Timer expired for vGarageOpener1"
when
    Item vGarageOpener1_Timer received command OFF
then
    aAlert(vGarageOpener1 + " has been open for over an hour!")

    // If it is night time reschedule the timer to go off again
    if(vTimeOfDay.state == "NIGHT" || vTimeOfDay.state == "BED") {
        vGarageOpener1_Timer.sendCommand(ON)
    }
end

// Repeat for the other four timers

Ugh, that is pretty ugly, pages and pages of repeated code. The above runs to over 250 lines of code! Let’s make it better with Groups.

Approach

  • Put all the contacts into a Group
  • Put all the LastUpdates into a Group (see Associated Items)
  • Put all the Timers into a Group (see Associated Items)
  • Create one Rule triggered by the Group of contacts using Member of
  • Use triggeringItem/event.itemName and get a reference to the Associated Items, and generate the message.

Items

Group:Contact:OR(OPEN, CLOSED) gDoorsSensors "The doors are [%s]"
Group:DateTime:MAX gDoorsLast "The last door event was [%1$tm/%1$td %1$tH:%1$tM]" 
Group:Switch:OR(ON, OFF) gDoorsTimers

Contact vGarageOpener1 "Garage Door Opener 1 is [%s]" (gDoorsSensors)
DateTime vGarageOpener1_LastUpdate "Garage Door Opener 1 [%1$tm/%1$td %1$tH:%1$tM]" (gDoorsLast)
Switch vGarageOpener1_Timer { expire="1h,command=OFF" } (gDoorsTimers)

Contact vGarageOpener2 "Garage Door Opener 2 is [%s]" (gDoorsSensors)
DateTime vGarageOpener2_LastUpdate "Garage Door Opener 2 [%1$tm/%1$td %1$tH:%1$tM]" (gDoorsLast)
Switch vGarageOpener2_Timer { expire="1h,command=OFF" } (gDoorsTimers)

Contact vFrontDoor "Front Door is [%s]" (gDoorsSensors)
DateTime vFrontDoor_LastUpdate "Front Door [%1$tm/%1$td %1$tH:%1$tM]" (gDoorsLast)
Switch vFrontDoor_Timer { expire="1h,command=OFF" } (gDoorsTimers)

Contact vBackDoor "Back Door is [%s]" (gDoorsSensors)
DateTime vBackDoor_LastUpdate "Back Door [%1$tm/%1$td %1$tH:%1$tM]" (gDoorsLast)
Switch vBackDoor_Timer { expire="1h,command=OFF" } (gDoorsTimers)

Contact vGarageDoor "Garage Door is [%s]" (gDoorsSensors)
DateTime vGarageDoor_LastUpdate "Garage Door [%1$tm/%1$td %1$tH:%1$tM]" (gDoorsLast)
Switch vGarageDoor_Timer { expire="1h,command=OFF" } (gDoorsTimers)

JSR223 Python

These Rules use the Helper Libraries.

from core.rules import rule
from core.triggers import when
from org.joda.time import DateTime
from core.actions import Transformation

@rule("A Door Sensor Changed")
@when("Member of gDoorsSensors changed")
def door(event):
    door = event.itemName

    # set timer
    # Acquire the Timer (see Associated Items):
    timer = [timer in ir.getItem("gDoorsTimers").members if timer.name == "{}_Timer".format(door)][0]
    if items[door] == OPEN:
        events.sendCommand(timer, ON)
    else:
        events.postUpdate(OFF) # postUpdate will cancel the Timer without triggering the rule below
    # NOTE: the above is somewhat contrived to demonstrate the Group operations. A better
    # implementation would be:
    #
    #    timer = "{}_Timer".format(event.itemName)
    #    if event.itemState == OPEN:
    #        events.sendCommand(timer, "ON")
    #    else:
    #        events.postUpdate(timer, "OFF")

    # Record the event time
    # Acquire the LastUpdate (see Associated Items)
    # - same steps as above
    lastUpdate = [lu in ir.getItem("gDoorsLast").members if lu.name == "{}_LastUpdate".format(door)][0]
    events.postUpdate(lastUpdate, str(DateTime.now()))
    # Again, this example is contrived and a better implementation would be:
    #
    #     events.postUpdate("{}_LastUpdate".format(door), str(DateTime.now()))

    # Alert
    # Create the message to log and maybe alert
    alert = False
    time = ""
    present = ""
    # In JSR223, a better approach is to use Item metadata instead of a Transformation for 
    # mapping the Item name to a human readable name.
    name = Transformation.transform("MAP", "entry.map", door)
    if is_night():
        time = " and it is night"
        alert = True
    if items["vPresent"] == OFF:
        present = " and no one is home"
        alert = True
    msg = "The {} was {}{}{}!".format(name, change, time, present)

    if alert: send_alert(msg, log)
    else: log.info(msg)

@rule("Timer expired for door")
@when("Member of gDoorsTimers received command OFF")
def door_timer(event):
    name = Transformation.transform("MAP", "entry.map", event.itemName.split("_")[0]
    send_alert("{} has been open for over an hour!".format(name))

    if items["vTimeOfDay"] == "NIGHT" or items["vTimeOfDay"] == "BED":
        events.sendCommand(event.itemName, "ON")

Although somewhat contrived, the above shows how to use the list operations with the Associated Items DP to create a generic set of Rules to work on all doors.

What if we want a message with ALL the open doors send when any Timer goes off?

@rule("Timer expired for door")
@when("Member of gDoorsTimers received command OFF")
def door_timer(event):
    name = Transformation.transform("MAP", "entry.map", event.itemName.split("_")[0]
    lst = reduce(lambda lst, door: "{}, {}".format(lst, door),
                 [door.name in ir.getItem("gDoorsSensors").members if door.state == OPEN])
    msg = "{} has been open for over an hour and {} are open too!".format(name, list)
    send_alert(msg)

The above uses a filter, map and reduce to build the list of Items that are open to include in the message.

NOTE: see Design Pattern: Separation of Behaviors for details on send_alert.

Rules DSL

rule "A Door Sensor Changed"
when
    Member of gDoorsSensors changed
then

    val door = triggeringItem

    // set timer
    // Acquire the Timer (see Associated Items):
    // - use findFirst to get the Item whose name is the door.name with "_Timer" appended
    // - use head to get the first (and only) Item in the Set
    val timer= gDoorsTimers.members.findFirst[ t | t.name == door.name+"_Timer" ] as SwitchItem
    if(door.state == OPEN) timer.sendCommand(ON) 
    else timer.postUpdate(OFF) // postUpdate(OFF) cancels the Timer without triggering the rule below
    // This example is somewhat deprecated but illustrates find first. A better implementation would be
    //
    //    if(door.state == OPEN) sendCommand(door.name+"_Timer", "ON")
    //    else postUpdate(door.name+"_Timer", "OFF")
    //
    // This is one case where the Actions are better.

    // record event time
    // Acquire the LastUpdate (see Associated Items)
    // - same steps as above
    val lastUpdate = gDoorsLast.members.findFirst[ dt | dt.name == door.name + "_LastUpdate" ] as DateTimeItem
    lastUpdate.postUpdate(new DateTimeType)
    // As with above, a better implementation would be:
    //
    //    postUpdate(door.name+"_LastUpdate", now.toString)

    // Alert
    val StringBuilder msg = new StringBuilder
    msg.append(transform("MAP", "entry.map", door.name))  // note a map and transform can convert from Item name to human friendly words

    if(door.state == OPEN) msg.append(" was opened") else msg.append(" was closed")

    var alert = false
    if(door.state == "NIGHT" || vTimeOfDay.state == "BED"){
        alert = true
        msg.append(" and it is night")
    }

    if(vPresent.state == OFF){
        alert = true
        msg.append(" and no one is home")
    }

    if(alert){
        msg.append("!")
        aAlerts.sendCommand(msg.toString)
    }
    logInfo("Entry", msg.toString)
end

// The Expire binding will sendCommand OFF when the Timer expires
rule "Timer expired for a door"
when
    Member of gDoorsTimers received command OFF
then
    val timer = triggeringItem

    aAlert(transform("MAP", "entry.map", timer.name.split("_").get(0)) + " has been open for over an hour!")

    // If it is night time reschedule the timer to go off again
    if(vTimeOfDay.state == "NIGHT" || vTimeOfDay.state == "BED") {
        timer.sendCommand(ON)
    }
end

Thus, by using Groups and Associated Items we reduce the lines of code from over 250 to around 75 (including comments), a reduction of over 2/3rds. Furthermore, since there is only one copy of the rule there is only one place to make updates, reducing the amount of work to maintain it and copy and paste errors.

Finally, what if we wanted to have a message with ALL the open doors sent when any Timer goes off?

// The Expire binding will sendCommand OFF when the Timer expires
rule "Timer expired for a door"
when
    Member of gDoorsTimers received command OFF
then
    val timer = triggeringItem

    // I'll use a String so I can show off map/reduce, normally I'd probably use a StringBuilder and forEach as it is more effecient, though efficiency doesn't really matter here
    var msg = transform("MAP", "entry.map", timer.name.split("_").get(0)) + " has been open for over an hour and "
    msg = msg + gDoorsSensors.members.filter[ s|s.state == OPEN ].map[ name ].reduce[ result, name | result = result+", " + name ]
    aAlert(msg)

    // If it is night time reschedule the timer to go off again
    if(vTimeOfDay.state == "NIGHT" || vTimeOfDay.state == "BED") {
        timer.sendCommand(ON)
    }
end

Related Design Patterns

Design Pattern How It’s Used
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
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.

76 Likes
Design Pattern: Human Readable Names in Messages
Design Pattern: Cascading Timers
Openhab Rules - Grouping of functions/procedures in one rule
Getting item which triggered the rule
[SOLVED] 2 items in one line
Multiple (MQTT) Relays turn on with Single Switch with Rules
How to store additional info in ITEM?
[SOLVED] Determine which Item of a Group is switched ON
Grouping items in rules - customer data structures like pojo
Telegram Message group.members.filter - SHOW LABEL
Help with rules in OH3
How to concatenate parameters in command that I should send out
Strange Group behaviour
Comparing Dates for a group of items (Zigbee devices)
How to cancel/reset a createTimer in a generic rule
Is it possible to access the thing and location for an item in a rule?
New Binding for Caddx alarm panels [NX584, NX8e, ...]
Filtering a group and storing it in a variable
Global variables in OpenHAB and non-repetitive rules
.members.filter DSL rule help request
Rule for several items instead of one rule for each
Script to Email Item State Report
Motion Sensor Advice
Working with design pattern in rule, refer to specific item
Send status notification of a item help
Read values from arraylist
How to find out which group item caused a "group rule" to trigger
How to make a method / procedure from a rule
Group:DateTime:MIN not displayed in sitemap
Setup for home heating demand
Rules Trigger every item in group but 1
Scaling group voltages
[SOLVED] PowerOFF only powered ON lights in a group
Smart Virtual Thermostat (beta version)
Low Battery Alert (Example Rule Not Working)
Rules for latching relays group
Possibility to eliminate simple rules?
Arrays and For Loop
[SOLVED] Working with groups and Stringbuilder
Need help with rule creation - An error occurred during the script execution: index=0, size=0
Send same command to one group except one
DP - Heating Boilerplate
[SOLVED] Merge item.name by different strings
Same rule for different rooms
Sending command to indirectly referred item possible?
Make use of Group based rules
Reuseable sitemap configuration
Finding auto generated "Flight to x" events from google calendar to schedule tasks in openhab
JSR223 Python: Working with QuantityTypes, how do I get an int/float?
Create reusable 'function' for roller-shutters
Object Model / API Description
Automation #3: Smart Radiator
[SOLVED] Group for String item as a switch and Switch
Struggling with the concept of "Groups" - limitations or misunderstanding?
[SOLVED] Making this rule generic
How to use Lambda recursively
Air Quality Sensor (PMS5003)
[SOLVED] Rule to send periodically the actual state of an item to the knx bus
Sonoff trigger with Webhooks
Help with forEach command to grab one item and loop to another Item
Sending multiple pictures with Telegram Action
KNX - Different DPT for rollershutter position set and read GA
Magical light scenes
Introduction into Rules DSL
Setting a master variable / switch
Item Name as Variable in Rules
Sonoff dual to control shutters
[SOLVED] MAP function or oder to connect Max! Thermostat "Mode" to Dummy item for Google Home interaction
Calculate average, min, max from the same group
[SOLVED] VSC errors in rule
[SOLVED] How to check if two items change in 5 seconds? (Presence detection + alexa)
Naming convention
Phillips HUE Fade in at SunSet Rule
PostUpdate on Group
Design Pattern: DRY, How Not to Repeat Yourself in Rules DSL
Xtend api documentation
Release Candidate and Support: Amazon Echo Control Binding
[SOLVED] Build a dynamic group with GroupItem Methods and send a command to this dynamic group
[SOLVED] Using A Variable To Obtain An Item State
[SOLVED] Design Pattern recommendation
Adding custom Java code
Design Pattern: Encoding and Accessing Values in Rules
Hue lights misbehaving
[SOLVED] Help with String+""
Hue lights misbehaving
[SOLVED] Cast error during iteration
triggeringItem in rule
Class simulation in rules
My diy HVAC zoning setup
[SOLVED] Rollershutter group & rule
[SOLVED] Lambda calling other lambda? (JSR223/JYTHON)
Multiple battery sensor alert email - is there a better way?
Rule for inspecting many iteams by group
Finally not called -> deadlock
Retrieve the state presentation in Xtend rules
Hot Tank monitoring, with energy calculations
Simplified Jython rule definition (similar to Rules DSL) using a universal decorator
What are the available functions and methods for items and groups?
iCloud binding with multiple phone rules optimization help!
Publish group update (state change) to MQTT
Lambda functions fail (not thread safe?)
Scripting Errors
Help with group avg
Timers in functions not possible?
Dates in rules - have I got this right
"Member of" does not work in OH 2.1
[SOLVED] MQTT populating temperature values - no it is not!
File Include
[SOLVED] Graphing Power from Neurio Power Monitoring Device - Need to Subtract Metrics
Xtend Scripts vs JSR223?
Xtend Scripts vs JSR223?
zWave Monitor lastSent & lastReceive Date/Time on Nodes
Creating battery reports
[SOLVED] Help me condense this rule
Dynamic Items through Rules
Rules triggering at Group-Change of an Item
Rules triggering at Group-Change of an Item
Issue with final parameters when using lambda to create "function"
Detect all Item-Changes within a Group
[SOLVED] Proper Syntax/Use of Item.lastUpdate?
What are your top 3 automations
Opinion on rule set for programmable watering system
[SOLVED] Close Rollershutters when sun shines using groups
Using groups in rules
A More Clever System for "Locking" Lights?
How to make rules easy?
Nested forEach Loops Scope Issue
Use an string item state as a name and state for another item
Use an string item state as a name and state for another item
Can I return state of item defined in ArrayList?
Alarm clock
Use variable for item
Dynamic variable name in rules
Get Item Value by variable name
Imitating the Power Consumption of Lights (and others)
Trigger alarm if one door contact of a group opens
Rule return wrong item
Design Pattern: Proxy Item
Help simplifying rule
Rule optimization: Window OPEN reminder
Rule optimization: Window OPEN reminder
Rollershutter and window contact
Monoprice 6-zone Audio amp items, sitemap & rules
Simplifying Rule
Array of type switch
[SOLVED] Help for Http binding on command
Group rules trigger
Nice graphic alarm (Pir sensors) visualization in HABPanel
"Pass by reference" calling object to rule?
Persistence strategy respecting a threshold
Iterating over a group, want to check an alternate item, sometimes
Setting light themes in rules
Set theory operations on groups
Mail action
Roller Shutter with http-binding or exec-binding
Commands cancel each other out?
Taking A Rule To The Next Level
Create a custom Item
Triggering a rule when user navigates (back to parent level) in Basic/Classic UI
[SOLVED] Identify wich items fired the rule
Automatic sending of events
Set up EnOcean Hoppe SecuSignal FHFS-vw window handles (EEP F61000)
One Item with two channels
All Switch to Off on Startup
Wildcard for Group.members.filter - Use String Patterns and ConfigItems
Fire Rule when groupmember receives command
Best practice for .rules
Rules-Definition "Optimization" Question
Design Pattern: Cancel Activity
How to create a complex light status check rule?
Basic switch questions, clarification about returned value
Choosing the correct transformation
Design Pattern: Manual Trigger Detection
Notifications in group design pattern
How to model location of an item?
Item or Variable Name with Variable as part of the name
DateTimeType error
Rule for batch processing of input/outputs
Rule not working anymore
Switch with 2 SendCommand actions
MQTT long press design pattern (dimmer)
Rule Help Reading item Label
Unable to get DateTime to work
Make rule with 196 if-statements more generic
Rules triggers all the time without end
Groups vs virtual items?
Assign an item value to another item
Check if all items are equal
Multiple items in rules
Rule Syntax Documentation
Current and Correct Documentation of Syntax for Things and Items Needed
Goups to control visibility
How to interpret KNX scenes as ON/OFF Switch?
Best way to control timer from more than one .rules file?
Automation/Orchestration Design Patterns
How do I "correctly" create a list of switches in the *.rules file?
Individual Alarm clock for each Day of week, with adjustable duration
Get the triggering item (name)
String operations
sendCommand and itemName from variable
Rules - Heating
Whole house lighting
Arrays in Openhab 1.8.3
Help, I'm ready to give up
Retrospective power report (or, "How I learned to stop worrying and love the bomb.")
Variable Maths
Refer to Items by value or variable in Rules? (Code portability.)
Foreach loops
Members.filter for DateTime items
OH3: Jython - Notification of every item in a group that fulfills a certain condition
Reading the group item that changed group's state
Reading the group item that changed group's state
Union of two groups? Jython Rules for groups
Translate from javascript to rules DSL
Iteration over group (not working)
Hot Tank monitoring, with energy calculations
Homekit and adjustable Blinds - Problem
OH3 script for time since last update
OH3 Color/White Bulb Widget
[SOLVED] Setting a Location directly
OH3: Turn all lights off based on Semantic Model?
Searching for cause of error from logfile
String type - cut a part of
Or trigger in rules
JSR233 Jython, on Group change [resolved]
Xiaomi Door Sensor, channel, item group --> Window open alarm

Thank you @rlkoshak so very much. As you almost always do in your posts you have managed to address several of my questions at once and answered a few more that I wasn’t even ready to formulate yet.

4 Likes

Another very useful and well written post @rlkoshak - well done!

I recently just added a similar set of rules for my external doors/windows but went down the lambda route.

Would be happy to share here if anyone is interested, but don’t want to derail this thread otherwise.

Keep up these awesome posts, so much benefit to all!

4 Likes

I do not think this would be a bad place to show it. It is good to have alternative examples. If you do post it in another thread, at least link it back here so future readers can find it easily.

I used to use lambdas for this sort of thing everywhere, and still do in a couple of places. But a few months back I had a revelation as I was editing a file. In a lot of cases the only reason I needed the lambda was to pass the calling Item and one or two “associated items” to the lambda. If I could collapse everything down to one rule I don’t need the lambda any longer nor do I need all the one liner rules to call the lambda.

It can’t always be done cleanly but when it can I think it results in clearer code. Some cases where it can’t is if the events can occur closer together than the initial Thread::sleep or if you have more interesting things to do for each Item than just call the same lambda with different arguments. For example, if I wanted to do something different for the Garage Door Openers than for the rest of the doors I would probably go down the lambda route.

Ok - here is my lambda;

val Map<String, Timer> externalDoorTimers = newHashMap

val org.eclipse.xtext.xbase.lib.Functions$Function3 checkExternalDoor = [
    SwitchItem doorItem,
    Number timerMins,
    Map<String, Timer> timers |
        var itemName = doorItem.name
        var displayName = ""
        var index = 0
        for (String part : itemName.lowerCase.split("_")) {
            if (index > 0)
                displayName = displayName + part + " "
            index = index + 1
        }        
            
        // if there is already a timer, cancel it
        timers.get(itemName)?.cancel

        if (doorItem.state == OPEN) {
            // notify the event (and play a barking dog if no one home)
            if (GP_Presence_Secure.state == OFF) {
                VT_Notify_Alert.postUpdate("The " + displayName + "opened and no one is home!")
                callScript("play_dog_barking")
            } else {
                VT_Notify_Trace.postUpdate("The " + displayName + "opened")
            }

            // create a timer to announce an alert if we are still OPEN after 'n' minutes
            timers.put(itemName, createTimer(now.plusMinutes(timerMins)) [|
                VT_Announce_Alert.postUpdate("The " + displayName + "is still open!")
                timers.remove(itemName)
            ])
        } else {
            VT_Notify_Trace.postUpdate("The " + displayName + "closed")
        }
]

This allows each door/window to decide how long to wait before notifying and even allows this time to vary based on other state (see the back door rule below).

Then for each door/window I have a simple rule;

rule "Front door"
when
    Item GF_Front_Door changed
then
    checkExternalDoor.apply(GF_Front_Door, 10, externalDoorTimers)
end

rule "Back door"
when
    Item GF_Back_Door changed
then
    // long *open* timer if during the day (since we often leave the back door open)
    if (VT_Day_Outdoor.state == OFF) {
        checkExternalDoor.apply(GF_Back_Door, 10, externalDoorTimers)
    } else {
        checkExternalDoor.apply(GF_Back_Door, 360, externalDoorTimers)
    }
end

This relies on a sensible and consistent naming scheme but I like this method since I only need the door item, i.e. no timer or datetime items (I don’t time these events but I do this in a similar lambda for appliances which stores the turn-on dttm in another internal dictionary).

Definitely a place for both approaches however.

4 Likes

A couple of ideas, many of which I just had in the past couple of week of rebuilding my environment:

  • You can eliminate externalDoorTimers and all the associated Timer management logic if you use the Expire binding and Associated Items, though you can’t vary the timer period at runtime like you are now which is really nice

  • You can eliminate the String parsing to get the displayName by creating a .map file that maps Item names to display names and call transform("MAP", "doors.map", doorItem.name)

The dog barking is a nice touch. :slight_smile: It might make it worth setting up audio in my HA.

That is an excellent use of ? as well. I never thought of using it like that (sometimes the obvious escapes me). Now that I see it, that is clearly why ? exists. If I had any Timers left I’d be running to add ? all over the place. I’ve pretty much moved everything over to Expire for now though. I find the resultant code to be a little cleaner and I hate bookkeeping.

I’m really glad you shared. Its given me some ideas.

Yes I have been keeping an eye on the expire binding - and I can see a multitude of places I could use it throughtout my code, I just haven’t gotten around to implementing it yet.

And I really like that idea of using a map for item names - that is very handy. I went through a massive task of renaming my entire config last year to make generating these sorts of rules easier. But there are always exceptions and edge cases, so having something like a lookup map solves all those issues.

Very useful thread indeed!

Hello @rlkoshak!

Great tutorial! I see you are using Group:DateTime:MAX group item to get date and time of the last door event. I have tried it to get date and time of the last air quality index update, but it doesn’t work. I have created following items:

Group:DateTime:MAX Weather_AirQuality_Average_Index_DateTime "Date and time [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <clock>

DateTime Weather_AirQuality_Bjelave_Index_DateTime "Date and time [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <clock> (Weather_AirQuality_Average_Index_DateTime) {channel="airquality:aqi:bjelave:observationTime"}
DateTime Weather_AirQuality_Ilidza_Index_DateTime "Date and time [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <clock> (Weather_AirQuality_Average_Index_DateTime) {channel="airquality:aqi:ilidza:observationTime"}
DateTime Weather_AirQuality_Otoka_Index_DateTime "Date and time [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <clock> (Weather_AirQuality_Average_Index_DateTime) {channel="airquality:aqi:otoka:observationTime"}

All separate items have date and time of the last update, but the group item just shows -.-.- -:- . I’ve waited for some of the DateTime items to change, but it still doesn’t work. What am I doing wrong here?

Best regards,
Davor

Hmmmm. I’m not sure. Now that I’ve gone back to my sitemap to check I’m finding that Item to be undefined as well (that is what the -'s mean).

What version of OH are you using?

Hello!

I’m using OH2 nightly. I’ve solved it using a rule. Not the best solution, but it works. Thank you for your help.

Best regards,
Davor

Hi Rich,
I’m looking at your example code of groups and trying to incorporate something similar but i’m not sure its possible (although it looks likeit could be).

Basically instead of storing a value in a Item, is it possible to create a variable and store it there instead when it loops though a group of items.

For example here’s what i’m trying to accomplish:

Item:
vGarageLight (gSoffitLights)
vHouseLight (gSoffitLights)

  1. Door Opens, check gSoffitLights (gSoffitLights.members.filter[i|i.state != 0].forEach [ i | ???]) group for all lights currently ON and the dimmer value.

  2. Create a variable called “item.name + __restore” and store the dimmer value of the light in that. Eg. vGrageLight_restore, vHouseLight_restore

How would code it to store the variable in a variable, is that possible?

First I’ll say I usually recommend storing such things in Items because you then can take advantage of restoreOnStartup which will allow your Rules to behave more consistently during OH startup or .rules files reloading. In those cases all your vars get wiped out and reinitialized.

To answer your specific question:

Whenever you see [ ] in the code above you are seeing a lambda. These lambdas have a limitation that they can only reference and use vals (i.e. constants). So, you have a problem because once you set a val you cannot change it later which I think would eliminate the possibility to achieve what you are trying to achieve.

So you have three approaches you can use to solve this problem (in my preferred order):

  1. Set up persistence on the Lights (not MapDB because we need the previous value) and get the last Dimmer values from persistence using item.previousState(true).state as PercentType in your 2. restore loop.

  2. Add new Items with findable names (like above) and store the current Dimmer values in those Items. The references to Items are constant so you can access and postUpdate to them from inside the lambdas.

  3. Create a global val Map<String, PercentType> prevDimmerVals = newHashMap and in your 1. forEach put the current Dimmer value into prevDimmerVals using the i.name as the key. In 2. you can get the old Dimmer value back out to apply to the lights. This works because the Map is a val but provides methods that lets us change it from inside the lambda.

1 Like

As I understand it, there is no guarantee that this gives the item that triggered the rule, because
a) 100ms may be to short to wait for the update during high load
b) 100ms may be to long to wait for the update in case several items are updated at the same time.

So, is there any better way to do it?

Anyhow, please don’t call such a dirty workaround a design pattern… it’s more an anti-pattern…

2 Likes

Of course, simply have individual rules triggered on individual Items, so that you will have access to exactly what the trigger was.
See Ben Jones lambda example above for one way to re-use common code across many rules.

  1. One must work within the limitations the Rules DSL imposes if one is to work within the Rules DSL. If you trigger a rule by a group or by multiple items, this is the ONLY way to get the triggering item.

  2. If you want guaranteed don’t use this approach. If you want something that works 99% of the time in the home automation context this is an excellent way to simplify your errors and reduce lines of code.

  3. That is one example line among eight demonstrated ways to use groups in rules. Would you there the baby it with the bath water?

So, if the only way to achieve something that works 99% of the time in the given problem domain and which makes up a small fraction of the article is an antipattern, that is your option and one I wholly disagree with.

Hello Rich,

Thanks for the very detailed rule guidance.

I have a problem with the “lastUpdate” property.

I have persistence (mapdb, and influxdb) both are set to “everyChange”

When i try to use the mapdb within the rule for a group of two items, only one is getting updating correctly with the lastUpdate property, while the other is somehow shows the last update is from 2 weeks ago which is wrong.

When i try with influx, it shows both are updated exactly at the same time (which is also wrong).

Any idea what can cause this behaviour?

The MapDB problem looks like for some reason the Item is not being persisted to MapDB. Double check your .persist file and make sure the Item is configured to be saved.

Add a sleep before calling lastUpdate to make sure persitence has a chance to catch up. Sometimes a rule can run too fast and execute before the value has been saved.

Add a chart for InfluxDB to see what is actually in the database. Or do some manual queries.

Compare the values in the DB with what you are seeing in events.log. The binding or one of your rules might be updating these Items behind the scenes.

Keep in mind that when using everyChange, lastUpdate will only change the timestamp if the new value posted to the Item is a change. If it is just an update with the same value the timestamp will not be updated. If this is the case I recommend using everyUpdate instead of everyChange.

Hi @rlkoshak - great pattern, thank you.
Can i use:

rule "A Door Sensor Changed"
when
    Item gDoorSensors changed 
then

to shorten my code, instead of list all items in group?

You can use any trigger on a Group that you can on an Item. However, it may not work like you need it to.

In this case, it would not work as you want because gDoorSensors will only change when all the doors are closed and one opens or all the doors become closed.

Imagine this scenario.

Step Doors State gSensors.state Rule Triggers?
1 All closed CLOSED No
2 One door opens OPEN Yes
3 Second door opens OPEN No
4 Third door opens OPEN No
5 Third door closes OPEN No
6 Second door closes OPEN No
7 First door closes CLOSED Yes

As you can see, the Rule would trigger when the first door opens but will not trigger again until ALL the doors are closed again. You would miss all the openings and closings of all the other doors after the first one opens. That is almost certainly not the behavior you want.

The only way to trigger a Rule for every change to a Group’s members is to use received update, though then your Rule has to deal with the fact that the Rule will be triggered multiple times for one change to one of its members. Often this doesn’t matter but in cases like the rule above, it does matter and it was far easier to write the rule with one trigger per door and know the Rule will only trigger once per change than it was to manage filtering, reentrant locks, and all the other code necessary to deal with the fact that the Rule would trigger multiple times per change to a door, and that those multiple Rules will be running at the same time.

1 Like

Is any right way to use something like:

Group:Number:SUM  gDoorSensors "[%d]"

to manage 10+ items?