Set last tripped/alarm date, or last action date to an item - best practice?

Hi together,

maybe this is a ridiculous question, but I have some contacts and motion sensors installed and I’m thinking about the best practice for setting the last “alarm” date to an Item.

I found this topic, what is describing how to update the last value to an Item, but does not provide the rule trigger or something similar: How to assign lastUpdate to an item?

What do you think is the best practice to set the last update?

  • rule with time cron every xx minute?
  • rule for each sensor/contact Item with received command?
  • ???

In my opinion the coolest way would be a Item definition with something like this:
DateTime MyLastUpdateItem1 "Last alarm [%1$tA, %1$td.%1$tm.%1$tY %1$tH:%1$tM:%1$tS]" <calendar> { MyAlarmItem1.lastUpdate }

Thank you very much!

Your “coolest way” I’m pretty sure is not possible. I just have a DateTime Item associated with the Item I want to know when the last time it was triggered (in my case a Door contact that was opened) and a rule to update the DateTime with now when the Item changes.

#Items:

// Entry Sensors
Group:Contact:OR(OPEN, CLOSED) gDoorSensors "Entry Status [MAP(en.map):%s]" <frontdoor>
Group gRemindDoorSensors
Group gDoorSensorsTime

Contact N_D_Front                        "Front Door [MAP(en.map):%s]"             <frontdoor>  (gDoorSensors, gRemindDoorSensors, gAlarmSensors, gHydraSens
ors)    { mqtt="<[mosquitto:entry_sensors/main/front_door:state:default]" }
DateTime N_D_Front_Last_Opened       "Front Door [%1$tm/%1$td %1$tH:%1$tM]"    <frontdoor>  (gDoorSensorsTime)

Contact N_D_Back                         "Back Door [MAP(en.map):%s]"              <frontdoor>  (gDoorSensors, gRemindDoorSensors, gAlarmSensors, gHydraSensors)    { mqtt="<[mosquitto:entry_sensors/main/back_door:state:default]" }
DateTime N_D_Back_Last_Opened        "Back Door [%1$tm/%1$td %1$tH:%1$tM]"     <frontdoor>  (gDoorSensorsTime)

Contact N_D_Garage                       "Garage Door [MAP(en.map):%s]"            <door>               (gDoorSensors, gRemindDoorSensors, gAlarmSensors, gHydraSensors)    { mqtt="<[mosquitto:entry_sensors/main/garage_door:state:default]" }
DateTime N_D_Garage_Last_Opened      "Garage Door [%1$tm/%1$td %1$tH:%1$tM]"   <door>       (gDoorSensorsTime)

Contact N_D_GarageDoor1              "Garage Door 1 [MAP(en.map):%s]"          <garagedoor> (gDoorSensors, gRemindDoorSensors, gGarageSensors) { mqtt="<[mosquitto:entry_sensors/main/garage/door1:state:default]" }
DateTime N_D_GarageDoor1_Last_Opened "Garage Door 1 [%1$tm/%1$td %1$tH:%1$tM]" <garagedoor> (gDoorSensorsTime)

Contact N_D_GarageDoor2              "Garage Door 2 [MAP(en.map):%s]"          <garagedoor> (gDoorSensors, gRemindDoorSensors, gGarageSensors) { mqtt="<[mosquitto:entry_sensors/main/garage/door2:state:default]" }
DateTime N_D_GarageDoor2_Last_Opened "Garage Door 2 [%1$tm/%1$td %1$tH:%1$tM]" <garagedoor> (gDoorSensorsTime)

Things to note:

  • Each Contact has a DateTime with the same name but with “_Last_Opened” appended
  • All of the DateTimes belong to the same group
  • All of the contacts belong to the gDoorSensors group which lets me create one rule to handle all the Contacts and figure out which door was opened to trigger the rule

#Rule:

rule "A Door's State Changed"
when
        Item N_D_Front changed or
        Item N_D_Back changed or
        Item N_D_Garage changed or
        Item N_D_GarageDoor1 changed or
        Item N_D_GarageDoor2 changed
then
    // Get the door that opened
    gDoorSensors?.members.filter(door|door.changedSince(now.minusSeconds(1))).forEach[ door |
        // Do stuff

        // Save the date/time for openings
        if(door.state == OPEN) {
            gDoorSensorsTime?.members.filter(dt|dt.name == door.name+"_Last_Opened").head.postUpdate(new DateTimeType)
        }
    ]
end

Things to note:

  • Because of the way Groups are processed, we have to list each door Contact separately as a rule trigger or else the rule gets triggered more than once per event
  • The line to get the most recently opened door requires persistence for changedSince to work
  • The line that gets the DateTime associated with the Contact is found by name so naming consistency is key

#Sitemap:

                        Text item=N_D_Front_Last_Opened       icon="frontdoor-open"    visibility=[N_D_Front=="OPEN"]
                        Text item=N_D_Front_Last_Opened       icon="frontdoor-closed"  visibility=[N_D_Front!="OPEN"]
                        Text item=N_D_Back_Last_Opened        icon="frontdoor-open"    visibility=[N_D_Back=="OPEN"]
                        Text item=N_D_Back_Last_Opened        icon="frontdoor-closed"  visibility=[N_D_Back!="OPEN"]
                        Text item=N_D_Garage_Last_Opened      icon="door-open"         visibility=[N_D_Garage=="OPEN"]
                        Text item=N_D_Garage_Last_Opened      icon="door-closed"       visibility=[N_D_Garage!="OPEN"]
                        Text item=N_D_GarageDoor1_Last_Opened icon="garagedoor-open"   visibility=[N_D_GarageDoor1=="OPEN"]
                        Text item=N_D_GarageDoor1_Last_Opened icon="garagedoor-closed" visibility=[N_D_GarageDoor1!="OPEN"]
                        Text item=N_D_GarageDoor2_Last_Opened icon="garagedoor-open"   visibility=[N_D_GarageDoor2=="OPEN"]
                        Text item=N_D_GarageDoor2_Last_Opened icon="garagedoor-closed" visibility=[N_D_GarageDoor2!="OPEN"]

Each DateTime is listed twice because I want to use the correct opened or closed icon to reflect the door’s current state which I achieve through the visibility attribute.

5 Likes

thanks for your answer @rlkoshak!

@rlkoshak does gDoorSensors contain all 5 items from your when condition? If so, curious why you didn’t have your condition like so:

when
   Item gDoorSensors changed
then

Maybe I’ve missed something but that would seem to condense it.

Triggers using groups do not work as you expect. A group’s state is an aggregation of its member’s states. Lets say all doors are closed, the group will be closed. One door opens and now the group’s state changes to OPEN. But when tge second door opens the group doesn’t change because it is already opened. Similarly it won’t receive a command so your trigger will only trigger when going from all closed to one being opened.

You can use “received update” but because of the way tge group’s state is calculated there are multiple updates per member change. Most of the time i can write the logic of the rule where having it run multiple times perchange to an Item isn’t a problem but in this case in tge part of the rule i don’t show i send alerts under certain circumstances and i do not want to receive them multiple times for a single event.

Maybe it works a bit differently in OH2 but I’ve been using the following:

rule "Update last updated"
when 
    Item MultiSensor_Sensors received update
then
    sensor_1_last_update.postUpdate( new DateTimeType() )
end

where MultiSensor_Sensors is the group of sensor items. I don’t tend to get multiple updates per member change.

That may be true. My only experience is with OH 1.

Trying to get the last update date/time for my my motion detectors and phones (network Health) to work. All my motion detectors and phones are setup as switches in OpenHAB. I put them all in a different group then the ones I use for my presence detection. Wasn’t sure if there would be a conflict. It is working on my phones but not on my motion detectors or TV. Not sure why. If anyone see’s any errors in my rule please let me know. Thanks.

Switch mike "Mike" (GPresenceDetectors,GSensors) {nh="192.168.x.xx"}
Switch kim "Kim" (GPresenceDetectors,GSensors) {nh="192.168.x.xx"}
Switch sean "Sean" (GPresenceDetectors,GSensors) {nh="192.168.x.xx"}
Switch brandon "Brandon" (GPresenceDetectors,GSensors) {nh="192.168.x.xx"}
Switch tv "TV" (GPresenceDetectors,GSensors) {nh="192.168.x.xx"}
Switch   LivingRoom "Living Room Motion"  (GPresenceDetectors,GSensors)
Switch   UpStairs "Up Stairs Motion"  (GPresenceDetectors,GSensors)
Switch   Foyer "Foyer Motion" (GPresenceDetectors,GSensors)
Switch   Basement "Basement Motion" (GPresenceDetectors,GSensors)

DateTime mike_Last_Triggered       "Mike [%1$tm/%1$td %1$tH:%1$tM]"  (GSensorsTime)
DateTime kim_Last_Triggered        "Kim [%1$tm/%1$td %1$tH:%1$tM]"  (GSensorsTime)
DateTime sean_Last_Triggered       "Sean [%1$tm/%1$td %1$tH:%1$tM]"  (GSensorsTime)
DateTime brandon_Last_Triggered    "Brandon [%1$tm/%1$td %1$tH:%1$tM]"  (GSensorsTime)
DateTime tv_Last_Triggered         "TV [%1$tm/%1$td %1$tH:%1$tM]"  (GSensorsTime)
DateTime LivingRoom_Last_Triggered "Living Room Motion [%1$tm/%1$td %1$tH:%1$tM]" (GSensorsTime)
DateTime UpStairs_Last_Triggered   "Up Stairs Motion [%1$tm/%1$td %1$tH:%1$tM]"  (GSensorsTime)
DateTime Foyer_Last_Triggered      "Foyer Motion [%1$tm/%1$td %1$tH:%1$tM]"  (GSensorsTime)
DateTime Basement_Last_Triggered   "Basement Motion [%1$tm/%1$td %1$tH:%1$tM]"  (GSensorsTime)
    
rule "Date & Time of last triggered occupancy"
when
        Item mike changed or
        Item kim changed or
        Item sean changed or
        Item brandon changed or
        Item tv changed or
        Item Basement changed or
        Item Foyer changed or
        Item LivingRoom changed or
        Item UpStairs changed
then
    // Get the sensor that was triggered
        GSensors?.members.filter(sensor|sensor.changedSince(now.minusSeconds(1))).forEach[ sensor |
                    if(sensor.state == ON) {
            GSensorsTime?.members.filter(dt|dt.name == sensor.name+"_Last_Triggered").head.postUpdate(new DateTimeType)
        }
    ]
end

Does your sensors work properly as switch? I mean did they show the correct state in a sitemap? How does the sensors get there state? There is no binding defined? In my opinion they should be defined as a contact item with the correct binding and then you can check the OPEN state (not ON). Or do you set the item state with a rule?

As a future proofing tip I’d configure motion sensors as Switches as that’s the recommended item type in ESH.

I Have a rule setup for the actual motion detectors that turns on the switches when motion is detected and they stay on for 3 minutes. I use them along with my presence detection rules. I have them in my site map, they turn on and off. That way I don’t need a separate line or rule for contacts(Open & Closed) and switches(On and Off). My LivingRoom motion switch has started showing the last update date/time. Not sure why. The other 3 are still not working. Maybe it takes a while for the persistence to work?

Actually it does take a little bit for persistence to catch up. You can add a Thread::sleep(250) at the start of your rule and see if that makes a difference.

Hi, i added this to my light items. Now i can see, when a light was triggered the last time.

Now i want to make a rule, when some lights are on for a very long time, so they will be switched off.

How can i get the time how long the light is on?
Do i need to make a new number-item for each light e.g. “light_on_duration” where i can store the time or is there a better way to do this?

And second:
If i want to get the date/time of the last switching of the light (ON and OFF), how do i have to change the rule?

With this rule shown above, i only get the date/time of the last switching to ON-state.

Can i simply add a second “if” with

 if(sensor.state == OFF) {...

???

The usual way would be to start a timer when the lights are switched ON, that when it expires turns the lights OFF. Not really related to storing last timestamp.
See


and many similar.

I recommend using the Expire binding (which didn’t exist when the above posts were written).

It would look something like:

Switch Light 1 { blah blah blah, expire="1h,command=OFF" }
Contact Sensor1 { blah blah blah }
rule "Motion sensor triggered"
when
    Item Sensor1 changed
then
    Light1.sendCommand(ON)
end

The expire binding sets a timer when the Light1 receives a command ON and reschedules it upon each subsequent ON. After the indicated time passes without an ON command it turns the light OFF.

Thank you! With expire binding it works without any additional rule.


But now i want to check something else:

I want to check the time of my window contacts. If a window is opened too long and outside temperature is very low i want to get a notification on my smartphone.

Can i do that with your example also?

How can i measure the time, a windows is opened? Do i have to use timers for this?

You can use the Expire binding for this as well, but will want a separate Timer Item.

Here is an example from my rules (slightly simplified).

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

Switch vFrontDoor_Timer
  { expire="1h,command=OFF" }
rule "Set Door Open Timer"
when
  Item vFrontDoor changed 
then
  if(vFrontDoor.state == OPEN) vFrontDoor_Timer.sendCommand(ON)
  else vFrontDoor_Timer.postUpdate(OFF)
end

rule "Timer expired for a door"
when
  Item vFrontDoor_Timer received command OFF
then
  aAlert.sendCommand("The front door has been open for over an hour")
  
  if(vTimeOfDay.state.toString == "NIGHT" || vTimeOfDay.state.toString == "BED") {
  	timer.sendCommand(ON) // reschedule the timer
  }
end

If you have a lot of doors and windows you want to track, you might find my full entry rules to be informative. Note that the rules below require persistence to be set up with everyChange to work.

You can find my Time of Day rules and Items here and my Alerting Items and Rules here though the names of my Items have changed since I wrote that last one.

For an explanation of how I access the Timer and LastUpdate Items from the Door Item’s name see this.

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

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

Group:Switch:OR(ON, OFF) gDoorsTimers

Switch aGarageOpener1 "Garage Door Opener 1"
  
Contact vGarageOpener1 "Garage Door Opener 1 is [MAP(en.map):%s]"
  (gDoorSensors)
  { mqtt="<[chimera:entry_sensors/main/garage/door1:state:default]" }
  
Switch vGarageOpener1_Timer
  (gDoorsTimers)
  { expire="1h,command=OFF" }
  
DateTime vGarageOpener1_LastUpdate "Garage Door Opener 1 [%1$tm/%1$td %1$tH:%1$tM]"
  (gDoorsLast)
  
Switch aGarageOpener2 "Garage Door Opener 2"
  
Contact vGarageOpener2 "Garage Door Opener 2 is [MAP(en.map):%s]"
  (gDoorSensors)
  { mqtt="<[chimera:entry_sensors/main/garage/door2:state:default]" }

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

DateTime vGarageOpener2_LastUpdate "Garage Door Opener 2 [%1$tm/%1$td %1$tH:%1$tM]"
  (gDoorsLast)
  
Contact vFrontDoor "Front Door is [MAP(en.map):%s]"
  (gDoorSensors)
  { mqtt="<[chimera:entry_sensors/main/front_door:state:default]" }

Switch vFrontDoor_Timer
  (gDoorsTimers)
  { expire="1h,command=OFF" }
  
DateTime vFrontDoor_LastUpdate "Front Door [%1$tm/%1$td %1$tH:%1$tM]"
  (gDoorsLast)
  
Contact vBackDoor "Back Door is [MAP(en.map):%s]"
  (gDoorSensors)
  { mqtt="<[chimera:entry_sensors/main/back_door:state:default]" }

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

DateTime vBackDoor_LastUpdate "Back Door [%1$tm/%1$td %1$tH:%1$tM]"
  (gDoorsLast)
  
Contact vGarageDoor "Garage Door is [MAP(en.map):%s]"
  (gDoorSensors)
  { mqtt="<[chimera:entry_sensors/main/garage_door:state:default]" }

Switch vGarageDoor_Timer
  (gDoorsTimers)
  { expire="1h,command=OFF" }
  
DateTime vGarageDoor_LastUpdate "Garage Door [%1$tm/%1$td %1$tH:%1$tM]"
  (gDoorsLast)
import org.eclipse.xtext.xbase.lib.Functions

val logName = "entry"

val Functions$Function1<SwitchItem, Boolean> openGarage = [ opener |
	var topic = "NA"
	switch opener.name{
		case "aGarageOpener1": topic = "actuators/garage1"
		case "aGarageOpener2": topic = "actuators/garage2"
	}
	
	logInfo("entry", "Publishing ON to " + topic)
	publish("chimera", topic, "ON")
	
	// TODO send an alert if cerberos is down
	true	
]
 
rule "Garage Opener 1 Triggered"
when
  Item aGarageOpener1 received command
then
  openGarage.apply(aGarageOpener1)
end

rule "Garage Opener 2 Triggered"
when
  Item aGarageOpener2 received command
then
  openGarage.apply(aGarageOpener2)
end

rule "Keep track of the last time a door was opened or closed"
when
  Item vGarageOpener1 changed or
  Item vGarageOpener2 changed or
  Item vFrontDoor changed or
  Item vBackDoor changed or
  Item vGarageDoor changed
then
  Thread::sleep(100)
  val door = gDoorSensors.members.filter[s|s.lastUpdate("mapdb") != null].sortBy[lastUpdate("mapdb")].last 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  
  val timer = gDoorsTimers.members.filter[t | t.name == door.name+"_Timer"].head as SwitchItem
  if(door.state == OPEN) timer.sendCommand(ON)
  else timer.postUpdate(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)
  }
  
  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
  Thread::sleep(100)
  val timer = gDoorsTimers.members.sortBy[lastUpdate("mapdb")].last as SwitchItem
  val doorName = transform("MAP", "en.map", timer.name)  

  aAlert.sendCommand(doorName + " has been open for over an hour")
  
  if(vTimeOfDay.state.toString == "NIGHT" || vTimeOfDay.state.toString == "BED") {
  	timer.sendCommand(ON) // reschedule the timer
  }
end

Oh… This is a little bit too much for me…

//
import org.openhab.core.library.types.*   
import org.openhab.core.types.Command
import org.openhab.core.persistence.*
import org.openhab.model.script.actions.*
import java.lang.Math
import java.util.Calendar
import java.util.Date
import java.util.TimeZone
import java.text.SimpleDateFormat
import java.util.Map
//

// DateTime speichern bei letzter Änderung
rule "Date + Time Fensterkontakte zuletzt geöffnet"
when
        Item Kontakt_EG_WC changed
then
    // Get the Contact that was triggered
        gFensterkontakte?.members.filter(contact|contact.changedSince(now.minusSeconds(1))).forEach[ contact |
                 if(contact.state == OPEN) {
						gKontakteZeit?.members.filter(dt|dt.name == contact.name+"_last_opened").head.postUpdate(new DateTimeType)
				}
		]
end

I used the same rule like i did with my lighting, only thing is i changed the Group-names and changed light.state to contact.state, contact.name and so on.

Naming of the items is ok and also persistence for both groups is active.

Here are some of my items:

Group:Contact:OR(OPEN, CLOSED)gFensterkontakte   "Fensterkontakte [%d]" <contact>
Group gKontakteZeit

Contact  Kontakt_EG_WC          	"EG WC [MAP(de.map):%s]"				(gFensterkontakte)				{ knx = "5/1/4" }


DateTime Kontakt_EG_WC_last_opened		"EG WC [%1$td.%1$tm., %1$tH:%1$tM]"			(gKontakteZeit)

I don´t get an error mesage, rule is not triggered. Nothing happens if i open the windows. I tested with expire, this works. It´s very much to write down if i want to do the expire check for every windows…

I need this rule with last opened for another thing too.

I tested another example from rlkoshak for my problem: (i´m on oh 1.8.3)

Here my rule:


rule "Fensterkontakte State Changed"
when
		Item gFensterkontakte received update // NOTE: the rule will trigger multiple times per event
then
		gFensterkontakte.members.forEach[ sensor |
				// Get the associated DateTime Item
				val dtStr = sensor.name + "_last_opened"
				val assocDT = gKontakteZeit.members.filter[dt|dt.name == dtStr].head as DateTimeItem

				// Update assocDT with the window's lastUpdate
				assocDT.postUpdate(new DateTimeType(sensor.lastUpdate))
		]
end

And here the error message:

2017-01-12 13:06:23.625 [ERROR] [o.o.c.s.ScriptExecutionThread ] - Error during the execution of rule 'Fensterkontakte State Changed': Could not invoke constructor: org.openhab.core.library.types.DateTimeType.DateTimeType(java.util.Calendar)

For the error, there is a millis or getMillis method on the Calendar object. That should solve the error:

assocDT.postUpdate(new DateTimeType(sensor.lastUpdate.millis))

For the rule:

  • I strongly recommend against using the ?. If there is a problem because something is null the ? tells the Rules DSL to ignore the error and fail silently.

  • I’ve encountered problems with filter on persistence values (lastUpdate, changedSince, etc.) when the Item is not persisted in the DB yet. You can work around this by filtering out those that are null (see below).

  • If you are unlikely to ever have contacts change within a second of each other you can use lastUpdate to get the Contact that triggered

  • It can take a few hundred milliseconds for persistence to catch up with the change so it is possible the new state isn’t saved to the DB yet when this rule runs.

  • Are you certain the rule is not triggering? Did you add a logInfo to confirm? I suspect the rule is triggering but just not doing anything due to one or more of the issues above.

  • Break it down step by step and make log statements to verify that each part of that long string of method calls on members is working.

Thread::sleep(300) // give persistence time to catch up
val haveHistory = gFensterkontakte.members.filter[c|c.lastUpdate != null]
logInfo("Test", "There are " + haveHistory.size + " contacts that have a last update")

val mostRecent = haveHistory.sortBy[lastUpdate].last
if(mostRecent == null) logError("Test", "Failed to find most recent")
else if(mostRecent.state == OPEN){
    val dt = gKontakteZeit.members.filter[dt|dt.name == mostRecent.name+"_last_opened").head
    if(dt == null) logError("Test", "Failed to find " + mostRecent.name+"_last_opened")
    else dt.postUpdate(new DateTimeType)
}

If you have errors the above will reveal them.