Only report when state remains unchained for some time

Hello!

I’ve been trying to write a rule that essentially debounces a contact. I want to receive an email if and only if the state of said contact was changed and has remained changed for a given time. This is what I have tried so far:

import org.openhab.core.library.types.*
import org.openhab.core.persistence.*
import org.openhab.model.script.actions.*

import org.joda.time.*

var Timer timerNeato
var OpenClosed stateNeato

rule "Neato Basisstation"
when
	Item hmK1State changed from OPEN to CLOSED or
	Item hmK1State changed from CLOSED to OPEN
then
	if(timerNeato==null || stateNeato==null)
	{
		stateNeato = (hmK1State.state as OpenClosedType)
	}

	if(timerNeato!=null)
	{
		timerNeato.cancel
		timerNeato=null
	}
	
	timerNeato = createTimer(now.plusSeconds(30)) [|	
		if (hmK1State.state==stateNeato)
		{
			var String Titel = "Neato "
			var String Meldung = "Neato is "
			
			if (hmK1State.state == OPEN)
			{
				Titel = Titel + "started"
				Meldung = Meldung + "started"
			}
			else
			{
				Titel = Titel + "back home"
				Meldung = Meldung + "back home"	}
			
			sendMail("myemail", Titel, Meldung)
			timerNeato=null
		}
	]
end

My idea was that if no timer is running, I save the current state to a variable. If the timer is running, I will restart it, but preserve the original value.

If the timer runs out and the current state of the contact is identical from the state when the first change was detected, the contact has remained changed and I will send an e-mail.

My problem is that the comparison in the timer (if(hmK1State.state==stateNeato)) gives me an awful exception.

==> openhab.log <==
2016-06-17 17:47:38.216 [ERROR] [org.quartz.core.JobRunShell   ] - Job DEFAULT.2016-06-17T17:47:37.492+02:00: Proxy for org.eclipse.xtext.xbase.lib.Procedures$Procedure0: org.eclipse.xtext.xbase.impl.XClosureImplCustom@1fc2dc3 (explicitSyntax: true) threw an unhandled Exception:
java.lang.IllegalStateException: Could not invoke method: org.openhab.model.script.lib.NumberExtensions.operator_equals(org.openhab.core.types.Type,java.lang.Number) on instance: null
        at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.invokeOperation(XbaseInterpreter.java:738) ~[na:na]
        at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._featureCallOperation(XbaseInterpreter.java:713) ~[na:na]
        at sun.reflect.GeneratedMethodAccessor80.invoke(Unknown Source) ~[na:na]
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_65]
        at java.lang.reflect.Method.invoke(Method.java:497) ~[na:1.8.0_65]
        at org.eclipse.xtext.util.PolymorphicDispatcher.invoke(PolymorphicDispatcher.java:291) ~[na:na]
        at org.openhab.model.script.interpreter.ScriptInterpreter.internalFeatureCallDispatch(ScriptInterpreter.java:69) ~[na:na]
        at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateAbstractFeatureCall(XbaseInterpreter.java:658) ~[na:na]
        at sun.reflect.GeneratedMethodAccessor77.invoke(Unknown Source) ~[na:na]
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_65]
        at java.lang.reflect.Method.invoke(Method.java:497) ~[na:1.8.0_65]
        at org.eclipse.xtext.util.PolymorphicDispatcher.invoke(PolymorphicDispatcher.java:291) ~[na:na]
        at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:218) ~[na:na]
        at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateIfExpression(XbaseInterpreter.java:327) ~[na:na]
        at sun.reflect.GeneratedMethodAccessor89.invoke(Unknown Source) ~[na:na]
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_65]
        at java.lang.reflect.Method.invoke(Method.java:497) ~[na:1.8.0_65]
        at org.eclipse.xtext.util.PolymorphicDispatcher.invoke(PolymorphicDispatcher.java:291) ~[na:na]
        at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:218) ~[na:na]
        at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateBlockExpression(XbaseInterpreter.java:321) ~[na:na]
        at sun.reflect.GeneratedMethodAccessor85.invoke(Unknown Source) ~[na:na]
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_65]
        at java.lang.reflect.Method.invoke(Method.java:497) ~[na:1.8.0_65]
        at org.eclipse.xtext.util.PolymorphicDispatcher.invoke(PolymorphicDispatcher.java:291) ~[na:na]
        at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:218) ~[na:na]
        at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.evaluate(XbaseInterpreter.java:204) ~[na:na]
        at org.eclipse.xtext.xbase.interpreter.impl.ClosureInvocationHandler.doInvoke(ClosureInvocationHandler.java:46) ~[na:na]
        at org.eclipse.xtext.xbase.interpreter.impl.AbstractClosureInvocationHandler.invoke(AbstractClosureInvocationHandler.java:28) ~[na:na]
        at com.sun.proxy.$Proxy81.apply(Unknown Source) ~[na:na]
        at org.openhab.model.script.internal.actions.TimerExecutionJob.execute(TimerExecutionJob.java:44) ~[na:na]
        at org.quartz.core.JobRunShell.run(JobRunShell.java:213) ~[quartz-all-2.1.7.jar:na]
        at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:557) [quartz-all-2.1.7.jar:na]
Caused by: java.lang.IllegalArgumentException: argument type mismatch
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_65]
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_65]
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_65]
        at java.lang.reflect.Method.invoke(Method.java:497) ~[na:1.8.0_65]
        at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.invokeOperation(XbaseInterpreter.java:729) ~[na:na]
        ... 31 common frames omitted
2016-06-17 17:47:38.456 [ERROR] [org.quartz.core.ErrorLogger   ] - Job (DEFAULT.2016-06-17T17:47:37.492+02:00: Proxy for org.eclipse.xtext.xbase.lib.Procedures$Procedure0: org.eclipse.xtext.xbase.impl.XClosureImplCustom@1fc2dc3 (explicitSyntax: true) threw an exception.
org.quartz.SchedulerException: Job threw an unhandled exception.
        at org.quartz.core.JobRunShell.run(JobRunShell.java:224) ~[quartz-all-2.1.7.jar:na]
        at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:557) [quartz-all-2.1.7.jar:na]
Caused by: java.lang.IllegalStateException: Could not invoke method: org.openhab.model.script.lib.NumberExtensions.operator_equals(org.openhab.core.types.Type,java.lang.Number) on instance: null
        at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.invokeOperation(XbaseInterpreter.java:738) ~[na:na]
        at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._featureCallOperation(XbaseInterpreter.java:713) ~[na:na]
        at sun.reflect.GeneratedMethodAccessor80.invoke(Unknown Source) ~[na:na]
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_65]
        at java.lang.reflect.Method.invoke(Method.java:497) ~[na:1.8.0_65]
        at org.eclipse.xtext.util.PolymorphicDispatcher.invoke(PolymorphicDispatcher.java:291) ~[na:na]
        at org.openhab.model.script.interpreter.ScriptInterpreter.internalFeatureCallDispatch(ScriptInterpreter.java:69) ~[na:na]
        at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateAbstractFeatureCall(XbaseInterpreter.java:658) ~[na:na]
        at sun.reflect.GeneratedMethodAccessor77.invoke(Unknown Source) ~[na:na]
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_65]
        at java.lang.reflect.Method.invoke(Method.java:497) ~[na:1.8.0_65]
        at org.eclipse.xtext.util.PolymorphicDispatcher.invoke(PolymorphicDispatcher.java:291) ~[na:na]
        at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:218) ~[na:na]
        at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateIfExpression(XbaseInterpreter.java:327) ~[na:na]
        at sun.reflect.GeneratedMethodAccessor89.invoke(Unknown Source) ~[na:na]
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_65]
        at java.lang.reflect.Method.invoke(Method.java:497) ~[na:1.8.0_65]
        at org.eclipse.xtext.util.PolymorphicDispatcher.invoke(PolymorphicDispatcher.java:291) ~[na:na]
        at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:218) ~[na:na]
        at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._evaluateBlockExpression(XbaseInterpreter.java:321) ~[na:na]
        at sun.reflect.GeneratedMethodAccessor85.invoke(Unknown Source) ~[na:na]
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_65]
        at java.lang.reflect.Method.invoke(Method.java:497) ~[na:1.8.0_65]
        at org.eclipse.xtext.util.PolymorphicDispatcher.invoke(PolymorphicDispatcher.java:291) ~[na:na]
        at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:218) ~[na:na]
        at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.evaluate(XbaseInterpreter.java:204) ~[na:na]
        at org.eclipse.xtext.xbase.interpreter.impl.ClosureInvocationHandler.doInvoke(ClosureInvocationHandler.java:46) ~[na:na]
        at org.eclipse.xtext.xbase.interpreter.impl.AbstractClosureInvocationHandler.invoke(AbstractClosureInvocationHandler.java:28) ~[na:na]
        at com.sun.proxy.$Proxy81.apply(Unknown Source) ~[na:na]
        at org.openhab.model.script.internal.actions.TimerExecutionJob.execute(TimerExecutionJob.java:44) ~[na:na]
        at org.quartz.core.JobRunShell.run(JobRunShell.java:213) ~[quartz-all-2.1.7.jar:na]
        ... 1 common frames omitted
Caused by: java.lang.IllegalArgumentException: argument type mismatch
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_65]
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_65]
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_65]
        at java.lang.reflect.Method.invoke(Method.java:497) ~[na:1.8.0_65]
        at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.invokeOperation(XbaseInterpreter.java:729) ~[na:na]
        ... 31 common frames omitted

I am at a loss at what’s going on here. Any idea what is happening there?

So I think the root problem is that the global var is not preserved inside the Timer’s context. I would have expected it to so I’m not really sure what is going on. Do you get the same error if instead of cancelling and nulling out the Timer you just reschedule it?

I think the crux of the problem is that the Timer gets a copy of the rule’s context at the time the Timer was created. However, this copying of context may not include global vars (it does get a copy of global vals). However, if you move stateNeato to be a rule local var (actually you can make it a val if you do that) and just reschedule the Timer instead of recreating it you should be able to bypass this because the Timer will get a copy of it when it is created and the value will be preserved within the Timer.

import org.openhab.core.library.types.*
import org.openhab.core.persistence.*
import org.openhab.model.script.actions.*

import org.joda.time.*

var Timer timerNeato

rule "Neato Basisstation"
when
	Item hmK1State changed from OPEN to CLOSED or
	Item hmK1State changed from CLOSED to OPEN
then
	if(timerNeato==null)
	{
		val State stateNeato = hmK1State.state
                timerNeato = createTimer(now.plusSeconds(30), [|
                    if(hmK1State.state == stateNeato) {
                        sendMail("myemail", "Neato started")
                    }
                    else {
                        sendMail("myemail", "Neato back home")
                    }
                    timerNeato = null
                ])
	}
        else {
            timerNeato.reschedule(now.plusSeconds(30))
        }
end

NOTE: I collapsed the construction of the message because frankly with just these two messages it adds a lot more code and inhibits code readability without really saving you anything in terms of DRY or flexibility.

Maybe you can ease your approach:

import org.openhab.core.library.types.*
import org.openhab.core.persistence.*
import org.openhab.model.script.actions.*

import org.joda.time.*

var Timer timerNeato = null

rule "Neato Basisstation"
when
    Item hmK1State changed  //from whatever to whatever
then
    if(previousState!=OPEN && previousState!=CLOSED) { //don't create a message if openHAB just started
        // for OH1 uninitialized state would be Uninitialized, for OH2 it will be null
        return(false) //abort rule
    }

    if(timerNeato!=null) {
        timerNeato.cancel
        timerNeato=null
    }

    timerNeato = createTimer(now.plusSeconds(30), [|	
        if (hmK1State.state == OPEN)
            sendMail("myemail", "Neato started", "Neato is started")
        else
            sendMail("myemail", "Neato back home", "Neato is back home")
        timerNeato=null
    ] )
end

If the state of hmK1State changes through runtime of the timer, the timer will be canceled and recreated, so no need to check for same state.

So, whenever the state changes (after the first change from uninitialized to OPEN or CLOSED) you will get an email after 30 seconds except there will be another change within these 30 seconds, which will generate a new email after 30 seconds. Is that the expected behavior?

Hi and thanks for the reply.

Maybe I problem description isn’t the best. I basically only want to receive an e-mail when the state changes and does not return to its initial state within the 30 seconds.

@Udo_Hartmann,
Your code does work in preventing me being spammed by my own system if the state flip flops back and forth quickly enough.

But what if it returns to it initial state? Let’s say it starts out as CLOSED and changes its state to OPEN, causing the 30s timer will start. If it returns to CLOSED, the old timer will be stopped and a new timer will be started. This timer will then finish its 30s wait time and send me an e-mail. This e-mail is what I’d like to suppress.

@rlkoshak,

Hello rlkoshak,

Your explanation sounds plausible to me. The variable not being in the scope necessary for it to be seen in the context of the timer makes sense to me.

I have tried your changed however and can only report that the error reoccurs. Its once again the comparison against stateNeato that causes an exception.

The exception is only thrown when the timer actually executes. Cancelling/nulling the timer works just fine. And so does the timer if I remove the check against the previous state.

There doesn’t happen to be a way to pass my initial state to the timer as an argument or something?

EDIT: I have tried substituting stateNeato for a variable that definitively does not exit and this results in a different exception. So it seems as if the timer is capable of resolving the name stateNeato.

Ah, then you should add a

        return(false) 

right after the

timerNeato=null

If the initial state is OPEN and the state changes, the timer will be started. If the state changes again within 30 seconds, the state will be OPEN again and the timer is stopped. If not, the timer will send the mail and reset to null. Now CLOSED is the new initial state…

Add some logInfo to your timer and log the value of stateNeato and the value of hmK1State.state. Make sure to log them in separate lines. It may be that we are focusing on the wrong thing and it is the Item is is having a problem with, as unlikely as that may seem.

There is a way to create a Timer with arguments but I’m not sure it would help in this case. Something is going on which is making this not work and I’m not convinced passing the argument would help. Search the forum for “createTimerWithArgument” for examples.

Do you have persistence set up? There might be something we can figure out with historic values.

You could also set up an Item instead of a val/var for stateNeato to store the first state in the sequence. This would be guaranteed to be available in your Timer.

Neither of these two options actually tells us what is wrong with the way you are doing it now (it looks like it should work as is) but will get you to your end state at least.

Oh… of course!

Since its a 2-state variable it must be changing back to its initial state on the second change event. And if the rule is triggering on a change of state and the timer is already running this must mean that it changed back to its initial state and I can safely cancel the timer and not reschedule it.

Brilliant. I’m going to test that tonight and will report back.

I’ve had the code by @Udo_Hartmann running for the last week and can report no errors.

It’s working fine!