Design Pattern: Working with Groups in Rules

You either have to deal with the fact that the Rule gets triggered 9+ times for every update to a member (e.g. make it so what the rule does can be done multiple times in a row and get the same result, add locks to avoid multiple instances of the rule from running that the same time, etc) or you have to list all 10+ Items as rule triggers individually.

Or you can consider the JSR223 Rules which might have more feature to let you handle that a little more reasonably.

With JS223 you can add triggers by script, so you can add a rule-trigger for each member of a group.

The openhab2-jython scripts (https://github.com/steve-bate/openhab2-jython) from @steve1 add a decorator to do this, so writing a rule that is triggered by each member of a group is as simple as:

@item_group_triggered("TestGroup", result_item_name="TestString1")
def example(event):
    return event.itemName + " triggered me!"

As you can see, there is also an easy way to get the triggering item.

2 Likes

Hi

Group of strings does not trigger received update in rule.
Is this by meaning or bug?

/Mike

Hmmmm. This may be a side effect of the changes made to the way a Group’s state is calculated. Post your Group definition so I know exactly what we are dealing with.

Group gLC "Lowercase switches" (All)
String VolvoHeater_LC	"Car Heather LC"	 				(gLC)
String Light_GF_Living_CeilingTable_LC 	"Dinner Table LC"	(gLC)
String Light_GF_Living_Read_LC	"Reading light"  			(gLC)		
String Light_GF_Living_Ceiling_LC	"Livingroom Ceiling LC"	(gLC)
String gsKitchen_Lights_LC	"Kitchen Lights LC"				(gLC)
String Light_GF_Corridor_Ceiling_LC "Hallway LC"			(gLC)
String Light_GF_Corridor_Wardrobe_LC "Closet LC"			(gLC)
rule "IFTTT lowercase string"
when
	Item gLC received update 
then
 
    val changedswitch = gLC.members.sortBy[lastUpdate].last
    val switchname = changedswitch.name.substring(0, changedswitch.name.lastIndexOf('_')) 
	if (changedswitch.previousState !== null){ 
	sendCommand(switchname, changedswitch.state.toString.toUpperCase) 
	logInfo("IFTTT", switchname + " " + changedswitch.state.toString.toUpperCase )
	}
end

Yes, the problem is your Group doesn’t have a Type. There was a change early on in the 2.2 snapshot I think, maybe earlier. All Groups must have a Type to receive an update.

So just change

Group:String gLC "Lowercase switches" (All)

and it should work.

1 Like

Thanks

Now it is working.

/Mike

Hi Guys,
i want to use this for my online/offline detection of some network components in my house and adjusted the items, groups and rules a bit. But i get this error and have not found anything similar so far:
2018-01-11 12:33:26.278 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule ‘A Door Sensor Changed’: ‘name’ is not a member of ‘java.util.ArrayList’; line 30, column 16, length 13

Any idea? Do i need to load any library in the rule?

BR
Andreas

What is line 30 in your .rules file? There is a typo on that line.

Hi Rich,
i simply copied over your code and modified it, but i guess one of the " " may have been a problem.
I wrote some lines, also line 30, complete fresh, so replaced the copy and now it seems to work. At least i get no error message anymore.

BTW:
In your code you have this line:
val door = gDoorSensors.members.filter[s|s.lastUpdate(“mapdb”) != null].sortBy[lastUpdate(“mapdb”)].last as ContactItem

Shouldn’t this be !== instaed of != ?

BR
Andreas

The code for this DP was written long before that warning started being reported in the logs and I haven’t come back to all the DPs yet to update them for the latest 2.2 release. This is one of the errors you will find throughout the DPs. The other big one is the deprecation warnings when trying to use calendar.

Thanks for pointing it out though. It makes my work easier.

If you can tell me what line is line 30 in your .rules file I can fix the quotes problem as well. It’s really hard to find lines just by line number in a forum posting and I don’t know if you have extra lines or white space in your file which would push the count off.

Hi Rich,
glad i could help a little bit. Your tutorial helps me a lot more than i could give back :slight_smile:
Ähm, line 30… i modified the code a lot but i tried to re-think and i am very sure it was this line:

val lastUpdate = gDoorsLast.members.filter[dt | dt.name == door.name + “_LastUpdate”].head as DateTimeItem

I modified your code to report me a few things. I did a version for

  1. whenever a device comes online or goes offline, like my NAS, TV, Homematic CCU etc.
  2. monitoring all my battery driven homematic devices

My next project would be a window open timer check. Means when a window got open run for 10 min, check again if still open and notify. If the Window got closed during the 10 Min do nothing. I hope i can get this to work, maybe i will post here a question for you if i do not get this working on my own :slight_smile:

Cheers
Andi

At the risk of veering off topic, here is my working code that does the window open alerts (doors in my case).

Group:Contact:OR(OPEN,CLOSED) gDoorSensors "The doors are [MAP(en.map):%s]"
        <door>

Group:Number:SUM gDoorCounts "Open Doors [%d]" // this Item is not needed, if one uses %d on the lable for gDoorSenors it will print the count
  <door>

Group:DateTime:MAX gDoorsLast "The last door event was [%1$tm/%1$td %1$tH:%1$tM]"
        <time>

Group:Switch:OR(ON, OFF) gDoorsTimers

Contact vGarageOpener1 "Garage Door Opener 1 is [MAP(en.map):%s]"
  <garagedoor> (gDoorSensors,gDoorCounts)
  { mqtt="<[mosquitto:entry_sensors/main/garage/door1:state:default]" }

Switch vGarageOpener1_Timer
  (gDoorsTimers, gResetExpire)
  { expire="1h,command=OFF" }

DateTime vGarageOpener1_LastUpdate "Garage Door Opener 1 [%1$tm/%1$td %1$tH:%1$tM]"
  <time> (gDoorsLast)

String GarageOpener1_Cmd
  { mqtt=">[mosquitto:entry_sensors/main/garage/door1/cmd:state:*:default]" }

Contact vGarageOpener2 "Garage Door Opener 2 is [MAP(en.map):%s]"
  <garagedoor> (gDoorSensors, gDoorCounts)
  { mqtt="<[mosquitto:entry_sensors/main/garage/door2:state:default]" }

Switch vGarageOpener2_Timer
  (gDoorsTimers, gResetExpire)
  { expire="1h,command=OFF" }

DateTime vGarageOpener2_LastUpdate "Garage Door Opener 2 [%1$tm/%1$td %1$tH:%1$tM]"
  <time> (gDoorsLast)

String GarageOpener2_Cmd
  { mqtt=">[mosquitto:entry_sensors/main/garage/door2/cmd:state:*:default]" }

Contact vFrontDoor "Front Door is [MAP(en.map):%s]"
  <door> (gDoorSensors,gDoorCounts)
  { mqtt="<[mosquitto:entry_sensors/main/front_door:state:default]" }

Switch vFrontDoor_Timer
  (gDoorsTimers, gResetExpire)
  { expire="1h,command=OFF" }

DateTime vFrontDoor_LastUpdate "Front Door [%1$tm/%1$td %1$tH:%1$tM]"
  <time> (gDoorsLast)

Contact vBackDoor "Back Door is [MAP(en.map):%s]"
  <door> (gDoorSensors, gDoorCounts)
  { mqtt="<[mosquitto:entry_sensors/main/back_door:state:default]" }

Switch vBackDoor_Timer
  (gDoorsTimers, gResetExpire)
  { expire="1h,command=OFF" }

DateTime vBackDoor_LastUpdate "Back Door [%1$tm/%1$td %1$tH:%1$tM]"
  <time> (gDoorsLast)

Contact vGarageDoor "Garage Door is [MAP(en.map):%s]"
  <door> (gDoorSensors, gDoorCounts)
  { mqtt="<[mosquitto:entry_sensors/main/garage_door:state:default]" }

Switch vGarageDoor_Timer
  (gDoorsTimers, gResetExpire)
  { expire="1h,command=OFF" }

DateTime vGarageDoor_LastUpdate "Garage Door [%1$tm/%1$td %1$tH:%1$tM]"
  <time> (gDoorsLast)
val logName = "entry"

rule "Keep track of the last time a door was opened or closed"
when
  Item vGarageOpener1 changed from CLOSED to OPEN or
  Item vGarageOpener1 changed from OPEN to CLOSED or
  Item vGarageOpener2 changed from CLOSED to OPEN or
  Item vGarageOpener2 changed from OPEN to CLOSED or
  Item vFrontDoor changed from CLOSED to OPEN or
  Item vFrontDoor changed from OPEN to CLOSED or
  Item vBackDoor changed from CLOSED to OPEN or
  Item vBackDoor changed from OPEN to CLOSED or
  Item vGarageDoor changed from CLOSED to OPEN or
  Item vGarageDoor changed from OPEN to CLOSED
then
  val door = triggeringItem as ContactItem

  // Update LastUpdate
  val last = gDoorsLast.members.filter[dt | dt.name == door.name+"_LastUpdate"].head as DateTimeItem
  last.postUpdate(new DateTimeType)

  // Set/cancel the Timer
  if(door.state == OPEN) sendCommand(door.name+"_Timer", "ON")
  else postUpdate(door.name+"_Timer", "OFF")

  // Log and alert
  val StringBuilder msg = new StringBuilder
  val doorName = transform("MAP", "en.map", door.name)

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

  var alert = false
  if(vTimeOfDay.state.toString == "NIGHT" || vTimeOfDay.state.toString == "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("!")
        aAlert.sendCommand(msg.toString)
  }
  else {
    logInfo(logName, msg.toString)
  }
end

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
  val doorName = transform("MAP", "en.map", triggeringItem.name)

  // send the alert
  aAlert.sendCommand(doorName + " has been open for over an hour")

  // repeate the alerts if it is night time or after bed time
  if(vTimeOfDay.state.toString == "NIGHT" || vTimeOfDay.state.toString == "BED") {
        triggeringItem.sendCommand(ON) // reschedule the timer
  }
end
3 Likes

Hi there,

not sure were to put this and whether this may be an already known bug (haven’t found anything about this), but it seems to me that Group changes in Rules do not get triggered if the Group name conatins an underscore… It took me nearly one day to find out why my rule is not triggered :frowning: so I thought this might be worth sharing.


Working:

.items

Group:Switch gTimer01OnDays	"alle Timer 01 On_<day>"	

.rules

rule "gTimer01OnDays_RCV_UPD"
when 
	Item gTimer01OnDays received update
then
	logWarn( "(timer)", "(timer) TestGroup received update!! " + getStrOnDay.apply( gTimer01OnDays ) )
end	

NOT working:

.items

Group:Switch gTimer_01_On_Days	"alle Timer 01 On_<day>"	

.rules

rule "gTimer_01_On_Days_RCV_UPD"
when 
	Item gTimer_01_On_Days received update
then
	logWarn( "(timer)", "(timer) TestGroup received update!! " + getStrOnDay.apply( gTimer_01_On_Days ) )
end
1 Like

Please post an issue to https://github.com/eclipse/smarthome

Many thanks, this makes it much easier for me.

Best Regards
Andi

Wow! Thank you! Not only does it not trigger rules, but it doesn’t report a group state (null), it doesn’t persist the group. I think this is not true for all group types though. Group:Numbers works, but Switch and Contact type do not.

Thanks for confirming! I thought I might be doing something wrong :wink:

btw: I tried to submit an issue on github, but after registration I haven’t received any confirmation email for my account. It’s also not in any spam-folder, so I fear somebody else needs to raise it until I get this sorted out.

Triggers should work if the group is defined to a type, e.g. Group:Number gMyNumber or Group:Switch gMySwitch should trigger updates, changes and even commands - but be aware that changes on groups will be dependent on all group members and in which way the group state is calculated (OR, AND, SUM, AVG, MIN, MAX)
Groups should be persisted, if the group itself is added to the *.persist file, but with one restriction: the persistence service has to provide a method to persist the data - e.g. switch, contact and string can’t be persisted by rrd4j.

Hey @rlkoshak I have a question about the usage of sortBy.

I have this code snippet in my rule.

    val StringBuilder body = new StringBuilder("Battery health report:\n\n")

    gBattery.members.sortBy[state].forEach [ NumberItem myItem | {
        body.append(String::format("%-30s%8.4s\n", myItem.name, myItem.state.toString))
    }]

gBattery is a group of Number items, each of which whose state represents the battery level of my battery-powered zwave devices. I use this in a rule that sends me a daily email of the items’ battery level sorted lowest to highest. This rule works, but vscode complains about it.

Bounds mismatch: The type arguments <Item, State> are not a valid substitute for the bounded type parameters <T, C extends Comparable<? super C>> of the method sortBy(Iterable, Function1<? super T, C>)

While not a very helpful error message, I’ve determined that if I change the statement to this, vscode is happy.

gBattery.members.sortBy[(state as Number).intValue].forEach [ NumberItem myItem | {

Is this because state doesn’t extend Comparable? And, when I change the statement to use a primitive int value, vscode stops complaining? If so, then why does it work when I just use sortBy[state]?

2 Likes