Determining the triggering item in the body of a rule


(hildner) #1

Hi all, I am searching for a way to determine what item has triggered a rule.
The "when" part of the rule could be an "or" of conditions, or it could be a group of items.

For example this piece of code that is supposed to output which motion sensor triggered a burglary alarm:

rule "burglary alarm"
when
	Item Alarm_Burglary_State_Toggle changed from OFF to ON or /* Panic */
	Item Fibaro_1_Motion changed from OFF to ON or
	Item Fibaro_2_Motion changed from OFF to ON or
	Item Aeotec_1_Motion changed from OFF to ON
then
	// if armed, trigger alarm
	// needs to know which item has triggered it for logging, updating a descriptive String item, etc.)
end

There are the implicit object receivedCommand and previousState (ON/OFF), but I cannot figure out how to navigate to an object reference.

An answer probably lies within https://github.com/openhab/openhab/wiki/Taking-Rules-to-New-Heights ("The Mother-of-All Light-Off Rules"), but I don't get it.


(Rich Koshak) #2

NOTE: This posting is incorrectly tagged. It should be tagged with Rules. It has nothing to do with my.openhab.

The short answer is you can't. This is a much requested feature and will probably be implemented in the OH 2 Rules Engine rewrite but in OH 1 you have to go about it indirectly.

If your sensors are members of the same group you can the most recent through lastUpdate.

Thread::sleep(250) // give persistence time to populate lastUpdate
val triggerItem = groupName.members.sortBy[lastUpdate].last as SwitchItem

I know of no other approach that works. You can experiment with the sleep amount for your particular deployment. If you are on a powerful machine a shorter sleep might be fine. On a slower machine (e.g. Raspberry Pi) a longer wait may be required.

However, it might be better to not care about which item triggered the rule in this case and just report (log, updating descriptive string, etc) based on the state of all your sensors so you get a fuller picture of what is going on. Just a thought.

Rich


(bob_dickenson) #3

A more brute force approach would be to have multiple rules with simpler When clauses


rule "burglary alarm 1"
when
Item Alarm_Burglary_State_Toggle changed from OFF to ON /* Panic */

then
// if armed, trigger alarm
...
end

rule "burglary alarm 2"
when
Item Fibaro_1_Motion changed from OFF to ON
then
// if armed, trigger alarm
...
end
rule "burglary alarm 3"
when
Item Fibaro_2_Motion changed from OFF to ON

then
// if armed, trigger alarm
...
end

rule "burglary alarm 4"
when
Item Aeotec_1_Motion changed from OFF to ON
then
// if armed, trigger alarm
...
end

This way you would KNOW which rule fired without having to worry about the innards of things.

My two cents.

Bob


(Neutral) #4

`

For anyone searching for a solution this may help, as it took me a while to work it out.

rule "Battery updated"

    when 
    	// Group to monitor
        Item GF_Battery received update 
    
    then
    	
    	//Get item from group
		val lastItem = GF_Battery.members.sortBy[lastUpdate].last
		
		// Grab name of item
		val nameOfItem = lastItem.name
		
		// Grab value of item as appropriate type
		val valueOfItem = lastItem.state as DecimalType
		
		logInfo("Battery Updated", nameOfItem + " received: " + valueOfItem)
end

Hope this helps someone


Identify wich items fired the rule
Determining the triggering item in the body of a rule - Dummy Persistence?
(Enc-X) #5

I made similar solution:
items:

Group gEntranceContact
Group PersistentCurrent
Contact Input1_violation "Entrance 1"  <frontdoor> (gEntranceContact,PersistentCurrent)  { // binding config }
Contact Input2_violation "Entrance 2"  <frontdoor> (gEntranceContact,PersistentCurrent)  { // binding config }
Contact Input3_violation "Entrance 3"  <frontdoor> (gEntranceContact,PersistentCurrent)  { // binding config }

I use mapdb persistance persistence/mapdb.persist

Items {
  PersistentCurrent* : strategy = everyChange, restoreOnStartup
}

rules

import java.util.HashMap
var HashMap<String, Boolean> entranceChanged = newHashMap

rule initHashMap
when
  System started
then
  gEntranceContact.members.forEach[ currItem |
    val itemName = currItem.name
    if ((entranceChanged.get(itemName) == null))
    {
      logInfo("hashInit", "item: " + itemName + ", value: true" )
      entranceChanged.put(itemName, true)
    }
  ]
end

rule entraceOpen
when
    Item gEntranceContact received update
then
    Thread::sleep(250)
    try {
      gEntranceContact.members.forEach[ currItem |
        val itemName = currItem.name
        if (entranceChanged.get(itemName) == null)
        {
          entranceChanged.put(itemName, true)
        }
        else if ( entranceChanged.get(itemName) && currItem.state == OPEN )
        {
          entranceChanged.put(itemName, false)
          logInfo("entranceOpen", "          ---> ALERT for "+ itemName)
          // Here add your way of processing changed item.
        } 
        else if ( (! entranceChanged.get(itemName))  && currItem.state == CLOSED )
        {
        }
      ]
    } catch (Exception e) {
      logInfo("entranceOpen", "exception: " + e)
    }
end

I had to use such approach bacause my security system (Satel) updates all inputs at one time (even input didn't change its state) so all have the same (or even random) update time and I can't trust sortBy methodology. I also didn't want to make huge persistance (so I'm using mapdb only to keep las val) and I'm detecting change of state between each update with HashMap.
Only problem is lack of item label. I can't find method to read item label in rule, so naming schema of items should be more understandable for the people than for machines :slight_smile:


Get var to use with executeCommandLine
(Rich Koshak) #6

That is because there isn't a method. You can't get the label from a rule.

This is good advice all the time, not just in this situation.


(Enc-X) #7

This discussion can become very philosophical :slight_smile: because in my opinion access to labels from rules may be very usefull. For example when I send alert/email from rule, label is better (may contain spaces and more informations)
Finally I resigned from modification of names and I added additional hasmap in rule with tranlation from name to label - it works, but it is not nice, I have to define the same relation (name->label) twice.


(Rich Koshak) #8

I'm not arguing for or against this capability. It doesn't exist today though and I'm sure there is either a philosophical or technical reason why. I wonder if part of the problem is that the processing of the "[%s]" and the like takes place in the sitemap code and not in the Item code, which would make providing access to the label in Rules a challenge.

There are lots of different ways to handle this situation though. I use a consistent naming pattern and some simple String processing to convert an Item name to a meaningful and human readable alert message or log statement.

I actually haven't encountered a situation where I've wanted to reproduce an Item's label in an alert or other type of message. I usually want to say something quite different than what I put on my sitemap. For example, I have two garage door openers with several Items that can trigger opening it (sitemap, minimote buttons, MQTT topic) and I can use the name of the triggering Item to determine which of the two openers is being opened and format my logging accordingly:

                if(trigger.name.contains("Garage1") || trigger.name.contains("Button1")) {
                        logInfo(logNameGarage, "Garage Door 1 triggered by " + trigger.name)
                        num = "1"
                        T_D_Garage1_MQTT.sendCommand(ON)
                }

I usually try to avoid hashMaps as much as possible. They are hard to maintain and tend to make the code bloated and difficult to read. There is almost always a better way.


(Enc-X) #9

I totally agree that processing of labels in rules may be issues in case of item value.
I think that everything depends on way how you write rules - there are multiple ways.

In my example I have multiple entrances to house with violation sensors, all entrance items are in one group gEntranceContact. I don't want to write multiple rules - one for each entrance. My idea is to have one rule which starts based on whole group monitoring but in rules' code I want to send alert with informations about specific item, which triggered the rule. So label in my opinion is most natural place for such information.
Like I wrote - it's very philosophical subject for discussion.

I have to think through my items naming schema :slight_smile:


(Rich Koshak) #10

Here is how I do it.

NOTE 1: My entry sensors start with "N_D_" and end with the name of the door. If the name of the door is more than one word I use _ to separate the words. For example "N_D_Front_Door" and "N_D_Garage_Door_1". It is a pretty lame naming scheme that I'm too lazy to go back and fix, but the N stands for Sensor and the D for Door.

NOTE 2: the lambda is from memory and may contain typos.

NOTE 3: I will likely move the names to a String Item and do away with the String processing. I find Rules to be much simpler to write and maintain if state is stored in Items rather than internal data structures like hashMaps.

NOTE 4: I can't use gDoorSensors to trigger the rule because of the way a Group's state is processed which will either not cause the rule to be triggered or cause it to be triggered more than once per door opening event.

val Functions$Function1 getName = [GenericItem i |
    var split = i.name.split("_")
    val StringBuilder name = new StringBuilder()
        
    split.forEach[ part | 
        if(part != "N" && part != "D") {
            name.append(part)
            name.append(" ")
        } // note adds a space to the end of the name
    ]
    return name.toString.trim
]

rule "Reminder for doors that are open for over hour that we want to know any time"
when
        Time cron "0 0/15 * * * ?"
then
        gRemindDoorSensors.members.filter[s|s.state == OPEN && !s.changedSince(now.minusHours(1))]?.forEach[ door |
                if(!openNotif.get(door.name)){
                        val String msg = getName.apply(door) + " is still open after an hour!"
                        Notification_Proxy_Alarm.postUpdate(msg)
                        openNotif.put(door.name, true)
                }
        ]
end

rule "A Door's State Changed"
when
        Item N_D_Front_Door changed or
        Item N_D_Back_Door changed or
        Item N_D_Garage_Door changed or
        Item N_D_Garage_Door_1 changed or
        Item N_D_Garage_Door_2 changed
then
        gDoorSensors.members.filter(door|door.changedSince(now.minusSeconds(1))).forEach[ door |

                val StringBuilder msg = new StringBuilder
                msg.append("The ")
                msg.append(getName.apply(door))
                msg.append(if(door.state == OPEN) " was opened" else " was closed")

        var alarm = false

        // Generate alarm message if needed
        if(Present.state != ON) {
                msg.append(" and no one is home")
                alarm = true
        }

                if(TimeOfDay.state.toString == "Night") {
                msg.append(" and its night")
                alarm = true
        }


        msg.append("!")

        if(door.name.contains("GarageDoor") && S_C_Cerberos.changedSince(now.minusMinutes(6))){
                logInfo(logNameEntry, "Garage controller was reset, ignoring this update")
        }
        else {
                // Save the date/time for openings
            if(door.state == OPEN) {
                        gDoorSensorsTime.members.filter(dt|dt.name == door.name+"_Last_Opened").head.postUpdate(new DateTimeType().toString)
                }

                // Note the opening and send alert
            logInfo(logNameEntry, msg.toString)
                if(alarm) {
                        // Wait 30 seconds and check again before sending an alert if we are not home
                        if(msg.toString.contains("no one") && !msg.toString.contains("night")) {
                        try {Thread::sleep(30000)} catch(InterruptedException e){}
                                if(Present.state != ON) Notification_Proxy_Alarm.sendCommand(msg.toString)
                        }
                        // At night send the alarm immediately
                else {
                                Notification_Proxy_Alarm.sendCommand(msg.toString)
                        }
                }

                // Reset the notification flag
                openNotif.put(door.name, false)
        }
        ]
end

(Enc-X) #11

Thanks, I think I got your point


(Ian Hubbertz (Euphi)) #12

Using lastUpdate results in an error for me:

13:53:09.416 [ERROR] [.script.engine.ScriptExecutionThread] - Rule 'All Updates for Group Heizung': null

I use the following rule:

rule "All Updates for Group Heizung"

when Item Heizung received update

then
   logInfo("BOT", "Group 'Heizung' received update" + Heizung)
   Thread::sleep(1500)
   val triggerList = Heizung.members.sortBy[lastUpdate]
   logInfo("BOT", "List: " + triggerList)
   val triggerItem = triggerList.last as SwitchItem
   logInfo("BOT", "Triggered by " + triggerItem)

end

Note: I seperated the triggerList varibale to confirm that .sortby[lastUpdate] causes the problem. If i remove the .sortby, the triggerList is filled correct (but obviously not sorted by last update).

I'm using MapDB persistance:

Strategies {
	// If no strategy is specified for an item entry below, the default list will be used.
	default = restoreOnStartup 
}

Items {
	* : strategy = everyChange, restoreOnStartup
}

Any ideas what is the problem?


(Rich Koshak) #13

Is MapDB your default persistence engine? If not you need to use lastUpdate("mapdb").

Also make sure all of the items have been updated at jar once since you enabled MapDB.


(Ian Hubbertz (Euphi)) #14

Thanks! Not all items have been updated yet.

I was shure they were, but autoupdate was off and due to a bug in my MQTT device it did not responded for some of the configured items.