Checking Group of Dates for any Older than 2 Days

This rule isn’t working to email any devices that are 2 days old from being updated. I can’t figure out why.

Group:DateTime:EARLIEST gSensorUpdates				<time>			// used to track sensor updates 
DateTime			FurnaceSensor_Update		"Update [%1$tF %1$tR]"  				 <clock>	(gSensorDateTag, gSensorUpdates)

if (gSensorUpdates.allMembers.filter[ GenericItem s | s.state == NULL || s.state === null || s.state == UNDEF || s.changedSince(now.minusDays(2)) ]) {

var String subjectemaildate = "zWave Update Date is OLD!"
var batterymessage = "zWave Update Date Field is 2 Days OLD device is " + s.name
mailActions.sendMail(JaygMail, subjectemaildate, batterymessage)
}

Any help would be appreciated.

Best, Jay

Those four words contain an infinity of possibilities. Not working how? Errors in the logs? Doesn’t do anything at all? Does the wrong thing?

s.state === null will always be false. An Item will never have a null state. It’s initialized to NULL and cannot be changed to null so you’ll never see s.state that’s null. But that’s not causing problems here, it’s just redundant.

.filter returns a List of all the members that meet the condition, not a boolean. So the if statement is meaningless. It’s the same as if(gSensorUpdates.allMembers) {. Obviously that doesn’t make sense.

There is no such thing as s outside of the [ ] of your filter. Yet you are trying to use s.name inside the if statement. Do you mean to loop over all the members that meet the filter and send an email for each one?

Since the members of gSensorUpdates are already DateTime Items, don’t you just want to test the state of the Item to see if it’s three days ago? Why query persistence? That’s a pretty heavy operation.

val twoDaysAgo = now.minusDays(2)
val staleItems = gSensorUpdates.allMembers.filter[ s | s.state == NULL 
                                                       || s.state == UNDEF 
                                                       || (s.state as DateTimeType).toZonedDateTime.isBefore(twoDaysAgo) ]

staleItems.forEach[s |
    var subjectemaildate = "zWave Update Date is OLD!" // never force the type unless you have to in Rules DSL
    var batterymessage = "zWave Update Date Field is 2 Days OLD device is " + s.name
    mailActions.sendMail(JaygMail, subjectemaildate, batterymessage)
]

Or if you only want the one message:

val twoDaysAgo = now.minusDays(2)
val staleItems = gSensorUpdates.allMembers.filter[ s | s.state == NULL 
                                                       || s.state == UNDEF 
                                                       || (s.state as DateTimeType).toZonedDateTime.isBefore(twoDaysAgo) ]

val staleNames = staleItems.map[ s | s.name ].reduce[ names, curr | names names + ", " + curr ]
val subjectemaildate = "zWave Update Date is OLD!"
val batterymessage = "zWave Update Date Field is 2 Days OLD device is " + staleNames
mailActions.sendMail(JaygMail, subjectemaildate, batterymessage)

In JS it would be:

var staleItems = items.gSensorUpdates.descendents.filter( s => s.isUninitialized || time.toZDT(s).isBefore(time.toZDT("P-2D"))
                                                 .map(s => s.name)
                                                 .join(', ');

if(staleItems.length > 0) {
  actions.get('mail', 'thingID').sendMail(JayMail, 
                                          "zWave Update Date is OLD!",
                                          "zWave Update Date Field is 2 Days OLD device is " +  staleItems);
}

Of course there are other ways to do this.

  • https://community.openhab.org/t/threshold-alert-and-open-reminder-4-0-0-0-4-9-9-9/144863: why code anything if you don’t have to?
  • in OH 5 lastUpdate will be a member of the Item so you won’t need to make a persistence call nor need a separate Item to store the last time an Item was updated
  • Expire can be useful here to expire the sensor readings when they are not updated and you can expire them to UNDEF which can trigger a rule to immediately report that the sensor went offline.

I actually use a combo of the first and last for my sensors. Expire on the Item will set the Item to UNDEF if it doesn’t update for long enough and the rule template will call a rule if it remains UNDEF for few minutes (just in case the sensor comes back) and again when the sensor does come back so I can cancel the notification.

Thank you Rich! Your explanation educated me to look at my other filter rules and tweak them.

I can always count on you!

Best, Jay

Hey @rlkoshak

The rule ran but threw this error below.

2025-03-17 18:35:01.936 [ERROR] [cript.internal.handler.AbstractScriptModuleHandler] - Script execution of rule with UID ‘default-11’ failed: ‘toZonedDateTime’ is not a member of ‘org.openhab.core.library.types.DateTimeType’; line 5136, column 110, length 41 in default

Best, Jay

I haven’t use Rules DSL for many years now. Maybe it’s getZonedDateTime or just zonedDateTime. there might be a deprecation warning. There is a bunch of stuff going on with DateTimeType and moving to an Instant instead of a ZonedDateTime. I’m not sure what change happened when in relation to the version of OH you are running.

Perhaps .getZonedDateTime(ZoneId.systemDefault()) will work for this? I have some DSL rules that use this for date processing.

EDIT:

@laursen, can you recommend the correct syntax for this below to remove the deprecated message? I see your fixed the core around this but I’m unclear the syntax. I’m on OH 4.3.0.

(s.state as DateTimeType).getZonedDateTime.isBefore(twoDaysAgo)


Here’s what is working now except the compiler error now.

val twoDaysAgo = now.minusDays(2)
val staleItems2 = gSensorUpdates.allMembers.filter[ GenericItem s | s.state == NULL || s.state == UNDEF || (s.state as DateTimeType).getZonedDateTime.isBefore(twoDaysAgo) ]

Compiler error for OH 4.3.0

The method getZonedDateTime() from the type DateTimeType is deprecated

Found this blurb on another post about getZonedDateTime

The reason is that DateTimeType no longer contains time-zone, so this will have to be applied.
The deprecated method getZonedDateTime() uses ZoneId.systemDefault() internally. However, this can be misleading, since it is no longer the time-zone used when creating the DateTimeType object, because that time-zone is now discarded.

Best, Jay

The minimal solution is to replace getZonedDateTime by getZonedDateTime(ZoneId.systemDefault()) .

1 Like

Final working rule below:

			val twoDaysAgo = now.minusDays(2)
			val staleItems2 = gSensorUpdates.allMembers.filter[ GenericItem s | s.state == NULL || s.state == UNDEF || (s.state as DateTimeType).getZonedDateTime(ZoneId.systemDefault()).isBefore(twoDaysAgo) ]

			staleItems2.forEach[ GenericItem s |
			
				subjectemailbattery = s.name + " Date is OLD!" 
				batterymessage = "Update Date Field is 2 Days OLD. The device is " + s.name + " and the date is " + s.state
				mailActions.sendMail(JaygMail, subjectemailbattery, batterymessage)	
			]