Design Pattern: Working with Groups in Rules

Can we use postUpdate in both cases (that is when door opens) ?

Why do you ask?

You could but you shouldn’t. When you are creating an event that will result in something happening (in this case starting the Expire Binding) you should use sendCommand.

1 Like

Okay. Thanks again !
I asked this just to get more clear about usage difference of postUpdate and sendCommand.

https://docs.openhab.org/configuration/rules-dsl.html#manipulating-item-states

1 Like

Just updated to 2.3 release, and I’m using the new “Member of” syntax as a trigger.

this seems to work well for groups which are composed solely of Items, however I have found that if a group consists of other groups, then the triggeringItem contains the group that triggered the action, not the item that triggered the action, and only groups that have types associated with them.

This makes the trigger less useful, as you can only trigger on groups of items, and not groups of groups.

for example, if you have groups:

Group All
Group gGF 	        "Ground Floor"  <groundfloor>	(All)
Group GF_Family 	"Family Room" 	<television> 	(gGF)
Group GF_Kitchen  	"Kitchen" 		<kitchen> 	    (gGF)
Group GF_Washroom 	"Washroom" 		<toilet> 	    (gGF)
Group:Number:MAX    Max_Temperature "Max Temperature [%.2f °C]" <temperature> (All)

You could trigger a rule on “GF_Family” (if GF_Family consists of Items), but not on “All” like this:

rule "Test Item Changed"
when
    Member of All changed
then
    logDebug("Test", "Test Item Changed: " + triggeringItem.name)
end

As this gives you the name of the sub group that changed, not the item that triggered the change, and in this case only the group “Max_Temperature” gets reported - presumably as it’s the only group that has a type.

So the output of this rule is always:

Test Item Changed: Max_Temperature

This seems inconsistent, as you can trigger on a group of items that does not have a type, but not on a group that has subgroups that do not have a type. In the above example, you could trigger on “GF_Family”, but not on “gGF”. By requiring subgroups to have a type, this means that all the items in that group would have to be the same type, which in many cases is not realistic.

Just wondering is this intentional for this new trigger? or just an unintended consequence of the way triggers are designed?

Is there documentation somewhere of the behavior of the new “Member of” trigger?

Thanks.

That is the behavior I would expect. In the case you describe the Item isn’t directly a member of the parent Group, its Group is the member. You would need to flatten the Group hierarchy in this case or create a special purpose Group that includes all the Items you care about.

This is also known and expected. If the subGroup doesn’t have a type it doesn’t get updated when its members update so there would be nothing to trigger the parent Group as the subGroup never receives updates.

For you particular design perhaps it is less useful. But other designs may depend on exactly the current behavior where one wants the subGroup to be the triggeringItem. The individual who coded this chose to support the latter design rather than the former. Personally, I agree with that decision as the latter approach handles more use cases, is IMHO more consistent with other Group behaviors (e.g. if you iterate through MyGroup.members you get the subGroup Items, not the members of the subGroups), and there are workarounds to support your approach (e.g. create a special purpose Group).

Not exactly. Groups are Items too. The subGroup is the member of the Group that caused the rule to trigger. So the subGroup IS the Item that caused the rule to trigger. But, as was the case prior to the Member of trigger, Groups do not change or receive updates unless they have a type.

That is debatable and I could be convinced of this. But the solution would be requiring a Type for any Group used as a Rule trigger even when using Member of. The solution would not be going back to the case where a Group gets updated and changed even when it has no type (this caused all sorts of problems which is why it was changed) and the solution would not be doing recursive Member of triggers as that would be a breaking change and IMHO be less flexible over all. It would also be inconsistent with other Group behaviors.

Probably a little of both. The Member of trigger was always designed to only support a single level. From the posting that introduced the new trigger (see link below):

“Member of” only works on items that are direct members of a group. It does not traverse nested groups. In the rule below the “triggeringItem” implicit variable will be one of the groups that are nested within AllLights (SwitchLights or DimmerLights) and not the base items (SwitchLight01, SwitchLight02, DimmerLight01, or DimmerLight02).

The unintended consequence part is a result of the fact that Groups do not receive updates when they don’t have a type. This was actually a breaking change in OH 2.2 from before there was a Member of trigger and the change was made to fix some pretty significant problems. But as a result, subGroups will never update when their members update so those updates will never trickle up to the parent Groups.

No it doesn’t. It’s perfectly legal to do something like:

Group:Number AllStrings

And have all the members of AllStrings be String Items. AllStrings will still get updates. It just will never have a useful state itself. I’ve never tried it but I think AllStrings will remain NULL.

In a more useful case you can do

Group:Number:SUM AllSwitches

which will set the state of AllSwitches to the number of its members that are ON.

There are all sorts of combinations of Group types and member types that are possible and useful and even in those cases where the combination is not useful, the Group will still receive updates.

The posting Features for Rules that work with Groups is the most comprehensive short of looking at the source code for now.

Thanks @rlkoshak very useful as always.

Now I understand how this works, I have figured out some great new possibilities. For instance I can create a group (with no members) and then dynamically add members to this group, this can then be used as a trigger for a rule.

Using this great new feature, i have reduced my homekit code to 125 lines (including comments and extraneous “stuff”) down from 250 or so, eliminated most global variables/Maps, removed all my locks, and made the code much more readable.

Thanks for the explanation.

Hi Rich.
Perhaps you can help me.
I have 5 number items in a group, which will be updated every hour. All 5 items get an update with a brake of a few milliseconds. The group should give me the lowest, (here the cheapest value). And then after comparing the 5 items, the rule should start a command.
The problem is that the rule starts 5 times, by every update of every item in this milliseconds area. So the command will start in worst case 5 times. Is there a possibility to wait or get a delay time for a second, so that all 5 items are updated after each other an the group item gives me the cheapest value and start then the command?
Thanks and greetings,
Markus

Thread::sleep(1000) will block the rule for 1 second.

A better approach would be to use a timer.

var Timer timer = null

rule "my rule"
when
    Member of MyGroup changes
then
    if(timer !== null) {
        timer = createTimer(now.plusSeconds(1), [
            //Find the lowest and do what ever you do
            timer = null
        ])
    }
end
2 Likes

Thanks, i will test it.
Why is the timer better than the sleep method?
Greetings,
Markus

The Timer won’t block a thread. The timer is scheduled, the rule ends.
The next call of the rule will find an already scheduled timer and will end without doing anything.
A Second later, the Scheduler will execute the code in a new thread, and it will be executed only once.

If using a Thread::sleep(1000), the rule will be started 5 times and every rule start will block a thread of the thread pool (default is 5 threads, so a sixth rule won’t start at all). Moreover, the Thread::sleep(1000) would not suffice, you would have to lock the rule or take care, that the code is executed only once.
When it’s guaranteed that all 5 items will be updated in a very short time slot, the timer is for sure very elegant to solve this.

2 Likes

Hi Rich.
It works perfect and with the help of Harry and Udo, i understand the problem and the function now.
I have seen the same problem in a post of you:
https://community.openhab.org/t/solved-error-rule-rulename-null/37359
It’s the same problem here by updating items every 0,002 seconds.
After using the timer the error rule:null is gone.
Thank you very much.
Greetings,
Markus

Hi @rlkoshak I have some LIFX bulbs that when turned on at the wall switch, they return their status to openhab and the corresponding switch gets set to ON. When somone turns off the wall switch, the status isnt reported back meaning the UI switch state is still ON

I am managing this with the following:

rule "lifx online"
when
  Thing "lifx:colorlight:D073DXXX0EF4" changed 
then
 var thingStatusInfo = getThingStatusInfo("lifx:colorlight:D073DXXX0EF4")
  if ((thingStatusInfo !== null) && (thingStatusInfo.getStatus().toString() == "OFFLINE")) {
    Switch_Lifx_Toilet.sendCommand(OFF)
  } 
end 

I have a dozen LIFX bulbs - can you see any way to do the above with a group concept?

1 Like

Unfortunately Things do not support Groups or triggeringItem. I know of no way to make this generic in the Rules DSL.

I think it might be possible to make this more generic in JSR223 or the Next Gen Rules Engine, but I’m still coming up to speed on those and can’t say for sure how yet.

With JSR223, you could iterate through the ItemChannelLinkRegistry, but I decided it was more efficient (and easier) to have some sort of mapping from ThingUID to Item name. Here are two examples using JSR223-Jython and openhab2-Jython. The first uses a dictionary for the mapping, the second uses another Item.

@rule("Alert: Kodi system status update")
@when("Thing kodi:kodi:familyroom changed")
@when("Thing kodi:kodi:bedroomupstairs changed")
@when("Thing kodi:kodi:bedroomdownstairs changed")
def kodiSystemStatusUpdate(event):
    kodiMap = {"kodi:kodi:familyroom"        : "DS_FamilyRoom_Kodi_System",
               "kodi:kodi:bedroomupstairs"   : "US_MasterBedroom_Kodi_System",
               "kodi:kodi:bedroomdownstairs" : "DS_MasterBedroom_Kodi_System"}
    kodiName = kodiMap[str(event.thingUID)]
    log.debug("JSR223: Kodi system update: [{}]: [{}]".format(kodiName, event.statusInfo))
    if str(event.statusInfo) == "ONLINE":
        events.sendCommand(kodiName,"ON")
        log.info("JSR223: Kodi system update: [{}]: ON".format(kodiName))
    elif str(event.statusInfo) == "OFFLINE (COMMUNICATION_ERROR): No connection established":
        events.sendCommand(kodiName,"OFF")
        log.info("JSR223: Kodi system update: [{}]: OFF".format(kodiName))

@rule("Alert: Speaker system status update")
@when("Thing upnpcontrol:upnprenderer:5f9ec1b3-ed59-1900-4530-0007f521099e changed")
@when("Thing upnpcontrol:upnprenderer:5f9ec1b3-ed59-1900-4530-0007f521366d changed")
@when("Thing upnpcontrol:upnprenderer:5f9ec1b3-ed59-1900-4530-0007f522dcaf changed")
@when("Thing upnpcontrol:upnprenderer:5f9ec1b3-ed59-1900-4530-0007f5207868 changed")
@when("Thing upnpcontrol:upnprenderer:5f9ec1b3-ed59-1900-4530-0007f520f682 changed")
@when("Thing upnpcontrol:upnprenderer:5f9ec1b3-ed59-1900-4530-0007f5210909 changed")
@when("Thing upnpcontrol:upnprenderer:5f9ec1b3-ed59-1900-4530-0007f52220b3 changed")
@when("Thing upnpcontrol:upnprenderer:5f9ec1b3-ed59-1900-4530-0007f52109de changed")
def speakerSystemStatusUpdate(event):
    speakerName = filter(lambda item: str(item.state).replace("uuid:", "") == str(event.thingUID).split(":")[2], ir.getItem("gSpeakerUUID").members)[0].name.replace("UUID", "System")
    log.debug("JSR223: Speaker system update [{}]: [{}]".format(speakerName, event.statusInfo))
    if str(event.statusInfo) == "ONLINE":
        events.sendCommand(speakerName,"ON")
        log.info("JSR223: Speaker system update [{}]: ON".format(speakerName))
    elif str(event.statusInfo) == "OFFLINE (COMMUNICATION_ERROR): No connection established":
        events.sendCommand(speakerName,"OFF")
        log.info("JSR223: Speaker system update [{}]: OFF".format(speakerName))

What about a function?

Hi @rlkoshak

I’ve been using your DP for the Doors but my TTS is obviously just reading aloud what i’ve written rather than the state, like your broadcast alerts do.

What would be the best method for TTS to tell if its open or closed verbally?

My current rule snippet:


  // Alert if necessary
  if(alert){
    sendBroadcastNotification(msg.toString)
     Echo_Living_Room_TTS.sendCommand('The Garage Door is Open')
  }

Would this work? I want Alexa to basically verbally if the door is open or closed using TTS


  // Alert if necessary
  if(alert){
    sendBroadcastNotification(msg.toString)
     Echo_Living_Room_TTS.sendCommand(msg.ToString)
  }

Or, this?

  // Alert if necessary & Door is Open
  if(alert && state == OPEN) {
    sendBroadcastNotification(msg.toString)
     Echo_Living_Room_TTS.sendCommand('The Garage Door is Open')
  }
  // Alert if necessary & Door is Closed
  if(alert && state == CLOSED) {
    sendBroadcastNotification(msg.toString)
     Echo_Living_Room_TTS.sendCommand('The Garage Door is Closed')
  }

Thanks!

I don’t actually use TTS. I just send a myopenhab.org message.

I find TTS announcements to be too intrusive. It’s easy to ignore or delay looking at an OH alert when it beeps on my phone or watch but all conversation will stop when speaker starts talking at you.

From what I know I don’t see anything wrong with any of those. I’d work to make it more generic.

    // If alerted == OFF and triggeringItem == OFF then sensor went offline and we have not yet alerted
    // If alerted == ON and triggeringItem == ON then the sensor came back online after we alerted that it was offline
    if(alerted.state == triggeringItem.state) {
        val currState = triggeringItem.state
        // wait one minute before alerting to make sure it isn't flapping
        timers.put(triggeringItem.name, createTimer(now.plusMinutes(1), [ |
            // If the current state of the Item matches the saved state after 5 minutes send the alert
            if(triggeringItem.state == currState) {
                aInfo.sendCommand(name + " is now " + transform("MAP", "admin.map", triggeringItem.state.toString) + "!")
                alerted.postUpdate(if(currState == ON) OFF else ON)
            }
            timers.put(triggeringItem.name, null)
        ]))
    }

Thanks ill look into your suggestion. How have people previously delt with notifications that have different expire timers? One of my doors I want an alert after 1 minute, but the other is an hour.

The notification is generic in that it will say the gate (with 1m expire) has been open for one hour, despite the timer being 1 minute

It would be great if we could somehow have notifications/alerts that are specific to the group members

You’d need to use JSR223, but you can add this info (timer timeouts, notification text, etc.) into Item metadata and use it in your rule. I’ll get an example post out one of these days…