Design Pattern: Working with Groups in Rules

groups
designpattern
Tags: #<Tag:0x00007fae076001f0> #<Tag:0x00007fae07600088>

(Rich Koshak) #1

NOTE: On recent (March 30, 2018) 2.3 snapshots there is a minor change to the Rules DSL that requires a space after the [ and before the ] when defining lambdas. I’ve updated the examples below to include the spaces.

Problem Statement

Often one finds a number of rules that are very similar and that all work on similar Items. One way to solve this problem is through the use of lambdas but an even more flexible approach is to use Groups.

This Design Pattern presents a step-by-step tutorial for how to use Groups to consolidate and simplify the these 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

Related Design Patterns

Prerequisites

This example has several prerequisites:

  • Persistence is configured on the Contact Items, MapDB is usable for this
  • The Expire binding is installed. This example uses Expire instead of rules based Timers

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. The Rules DSL provides a number of operations that will be used in the example below.

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.

  • 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, GroupItems that are also members are not part of the set

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

  • 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 ]

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 and still 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.

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:
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(vGarageOpener1.name)  // note a map and transform can convert from Item name to human friendly words

    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 any one of the contacts (note: Rules triggered by updates to a Group Item will generate multiple triggers for each single change to an Item. Sometimes one can write a rule such that this doesn’t matter. In this case we cannot so the Rule will trigger off of the Group)
  • Use the Group methods above to figure out which Contact triggered the rule, get a reference to the Associated Items, and generate the message.

Items:

Group:Contact:OR(OPEN, CLOSED) gDoorSensors "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)

Rules:

rule "A Door Sensor Changed"
when
    Item vGarageOpener1 changed or
    Item vGarageOpener2 changed or
    Item vFrontDoor changed or
    Item vBackDoor changed or
    Item vGarageDoor changed
then
    Thread::sleep(100) // give persistence time to catch up

    // Acquire the door that triggered the rule by:
    // - use filter remove the Contacts that are not saved to persitence yet (i.e. lastUpdate returns null)
    // - use sortBy to order the members by lastUpdate, the most recent will be last
    // - get the last updated Item, Contacts are unlikely to change within 100 msec of each other so this approach works
    //   If they are likely to change that close together this approach will fail
    val door = gDoorSensors.members.filter[ s|s.lastUpdate("mapdb") != null ].sortBy[ lastUpdate("mapdb") ].last as ContactItem

    // set timer
    // Acquire the Timer (see Associated Items):
    // - use filter to get a Set containing only 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= gDoorTimers.members.filter[ t | t.name == door.name+"_Timer" ].head as SwitchItem
    if(door.state == OPEN) timer.sendCommand(ON) 
    else timer.postUpdate(OFF) // postUpdate(OFF) cancels the Timer without triggering the rule below

    // record event time
    // Acquire the LastUpdate (see Associated Items)
    // - same steps as above
    val lastUpdate = gDoorsLast.members.filter[ dt | dt.name == door.name + "_LastUpdate" ].head as DateTimeItem
    lastUpdate.postUpdate(new DateTimeType)

    // Alert
    val StringBuilder msg = new StringBuilder
    msg.append(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
    Item vGarageOpener1_Timer received command OFF or
    Item vGarageOpener2_Timer received command OFF or
    Item vFrontDoor_Timer received command OFF or
    Item vBackDoor_Timer received command OFF or
    Item vGarageDoor_Timer received command OFF
then
    Thread::sleep(100) // give persistence a chance to catch up
    // Get the Timer that triggered the rule (see above for explanation)
    val timer = gDoortimers.members.filter[ t|t.lastUpdate("mapdb") != null ].sortBy[ lastUpdate("mapdb") ].last as SwitchItem

    aAlert(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
    Item vGarageOpener1_Timer received command OFF or
    Item vGarageOpener2_Timer received command OFF or
    Item vFrontDoor_Timer received command OFF or
    Item vBackDoor_Timer received command OFF or
    Item vGarageDoor_Timer received command OFF
then
    Thread::sleep(100) // give persistence a chance to catch up
    // Get the Timer that triggered the rule (see above for explanation)
    val timer = gDoortimers.members.filter[ t|t.lastUpdate("mapdb") != null ].sortBy[ lastUpdate("mapdb") ].last as SwitchItem

    // 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 = timer.name.split("_").get(0) + " has been open for over an hour and "
    msg = msg + gDoorSensors.members.filter[ s|s.state == OPEN ].map[ name ].reduce[ result, name | 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

I hope this is useful. Please post comments or questions below.


Openhab Rules - Grouping of functions/procedures in one rule
Getting item which triggered the rule
2 items in one line
Multiple (MQTT) Relays turn on with Single Switch with Rules
How to store additional info in ITEM?
Design Pattern: Encoding and Accessing Values in Rules
Design Pattern: Gate Keeper
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
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
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
Design Pattern: Human Readable Names in Messages
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
Design Pattern: Cascading Timers
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.)
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
Determine which Item of a Group is switched ON
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
(lipp_markus) #2

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.


(Ben Jones) #3

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!


(Rich Koshak) #4

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.


(Ben Jones) #5

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.


Refer to Items by value or variable in Rules? (Code portability.)
(Rich Koshak) #6

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.


Persitance of items in group
(Ben Jones) #7

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!


(davorf) #8

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


(Rich Koshak) #9

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?


(davorf) #10

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


(Chris) #11

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?


(Rich Koshak) #12

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.


(Ian Hubbertz (Euphi)) #13

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…


(Rossko57) #14

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.


Turn on items based on lowest runtime - where to start?
(Rich Koshak) #15
  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.


(Negm) #16

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?


(Rich Koshak) #17

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.


(martiniman) #18

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?


(Rich Koshak) #19

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.


(martiniman) #20

Is any right way to use something like:

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

to manage 10+ items?