Universal timer rule

Hello all,
I have created a semi-universal timer rule for my wlan sockets and I am not sure if the assignment of the tGeneral variable is correct, it feels wrong because there are always warnings in the logfile. (after start of the timer, before expiration, exact time unknown)

The goal is to time some items with this rule independently of each other, so switch on a certain time.

Maybe you have a tip for me how I can eliminate the following warnings. Otherwise it works already.

Items:

//WLAN/FUNKSTECKDOSEN
Number Steckdose_S20_Timer "S20_Timer"
Number Steckdose_Obi_Timer "Obi_Timer"

Switch Steckdose_S20 "S20 Wlan Steckdose" <poweroutlet> {channel="mqtt:topic:tasmota:s20:PowerSwitch"}
Switch Steckdose_Obi "Obi Wlan Steckdose" <poweroutlet> {channel="mqtt:topic:tasmota:obi:PowerSwitch"}

Sitemap:

Selection item=Steckdose_Obi_Timer icon="time" mappings=[0="Aus",1="1min.",5="5min.",10="10min.",15="15min.",30="30min.",45="45min.",60="60min.",90="90min.",120="120min."] 
Selection item=Steckdose_S20_Timer icon="time" mappings=[0="Aus",1="1min.",5="5min.",10="10min.",15="15min.",30="30min.",45="45min.",60="60min.",90="90min.",120="120min."] 

Rule:

// Steckdosen  WLAN / FUNK
import org.eclipse.smarthome.model.script.ScriptServiceUtil
var Timer tS20 = null
var Timer tObi = null
var Timer tGeneral = null
rule "General Timer"
    when
        Item Steckdose_Obi_Timer received command or 
        Item Steckdose_S20_Timer received command        
    then
    
    var String TargetItemName = triggeringItem.name.toString.replace("_Timer","")    
    var String TargetItemSelectName = triggeringItem.name.toString
    val TargetItem = ScriptServiceUtil.getItemRegistry?.getItem(TargetItemName)    
    val TargetItemSelect = ScriptServiceUtil.getItemRegistry?.getItem(TargetItemSelectName)
 
    switch(TargetItemSelectName) {
            case  "Steckdose_S20_Timer" : { tGeneral = tS20
                                            logInfo("Steckdosen", "Timer = Obi") 
            }
            case  "Steckdose_Obi_Timer" : { tGeneral = tObi        
                                            logInfo("Steckdosen", "Timer = Obi")
            }            
            default : {
                tGeneral = null
                logInfo("Steckdosen", "Timer = Null")
                return;
            }
    }
 
    //mappings=[0="Aus",10="10min.",15="15min.",30="30min.",45="45min.",60="60min.",90="90min.",120="120min."] 
    logInfo("Steckdosen", ":. " +triggeringItem.name.toString +" erhielt Kommando " +receivedCommand.toString)
    logInfo("Steckdosen", "Ziel Item: " + TargetItemName)
    if (tGeneral!== null) {
        tGeneral.cancel
        tGeneral = null
    }
    if (receivedCommand == 0) {
        logInfo("Steckdosen", "=> Szene Aus")		  		 
        sendCommand(TargetItem,OFF)
    }
    else{
        logInfo("Steckdosen", "=> Setze Timer auf " + receivedCommand.toString + " Minuten")	
        TargetItem.sendCommand(ON)
        tGeneral = createTimer(now.plusMinutes((receivedCommand as DecimalType).intValue)) [|		  
          sendCommand(TargetItemSelect,0)
        ]
    }    
end 
	... 1 more
	at org.quartz.core.JobRunShell.run(JobRunShell.java:202) ~[?:?]
	at org.eclipse.smarthome.model.script.internal.actions.TimerExecutionJob.execute(TimerExecutionJob.java:48) ~[?:?]
	at com.sun.proxy.$Proxy404.apply(Unknown Source) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.AbstractClosureInvocationHandler.invoke(AbstractClosureInvocationHandler.java:29) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.ClosureInvocationHandler.doInvoke(ClosureInvocationHandler.java:46) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.evaluate(XbaseInterpreter.java:201) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:215) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:226) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:239) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:458) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:215) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:226) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:235) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:954) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:991) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.invokeFeature(ScriptInterpreter.java:151) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.invokeFeature(XbaseInterpreter.java:1081) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._invokeFeature(XbaseInterpreter.java:1135) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.evaluateArgumentExpressions(XbaseInterpreter.java:1205) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:215) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:226) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:235) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:954) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:991) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.invokeFeature(ScriptInterpreter.java:140) ~[?:?]
	at org.eclipse.smarthome.model.script.engine.ScriptError.<init>(ScriptError.java:65) ~[?:?]
Caused by: java.lang.NullPointerException
	at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573) [bundleFile:?]
	at org.quartz.core.JobRunShell.run(JobRunShell.java:213) [bundleFile:?]
org.quartz.SchedulerException: Job threw an unhandled exception.
} ] threw an exception.
  sendCommand(<XFeatureCallImplCustom>,<XNumberLiteralImpl>)
2022-07-13 12:04:33.795 [ERROR] [org.quartz.core.ErrorLogger         ] - Job (DEFAULT.Timer 419 2022-07-13T12:04:33.784+02:00: Proxy for org.eclipse.xtext.xbase.lib.Procedures$Procedure0: [ | {

Thanks

TargetItemSelect is an Item object. I think you could do
TargetItemSelect.sendCommand(0)
or
sendCommand(TargetItemSelectName,"0")

With the variety of rules systems now in use it pays to explain how you are working. OH3 xxx.rules file I guess.

Thanks for the answer.
I am still using OpenHAB 2.5.
Turn on and off also works independently, I’m just wondering about the error messages in the log.
I am not sure if it is because of the tGernal variable, which I reassign every time.

It’s basically impossible to say because you’ve omitted the most important part of the error. there should be a good deal of stuff above that “… 1 more” that has a bunch of relevant info we need. But that does look like what happens when you have a timer running from a .rules file and you reload the .rules file. The timer(s) become disconnected from their context and generally end up throwing an exception when they finally go off.

Scanning through this rule I’m not really seeing what it does that couldn’t/shouldn’t be implemented using Expire. But you could make it even more generic by putting the timers into a Map and using the Item name as the key. Then if you added more timers you wouldn’t need to modify this rule later.

import org.eclipse.smarthome.model.script.ScriptServiceUtil

val timers = newHashMap

rule ...

    var tGeneral = timers.get(TargetItemSelectName) // returns null if there is no entry

    logInfo("Steckdosen", ":. "...

The cancelling of the old timer could be shortened to

    tGeneral?.cancel
    timers.put(TargetItemSelectName, null)

expire is harcoded expiration time…

Question @rlkoshak:

As I’m not familiar with all this extra stuff :wink:

import org.eclipse.smarthome.model.script.ScriptServiceUtil

val <String,Timer> timers = newHashMap


rule "some timers"
when
    Member of gTimers changed
then
    val Integer iTime   = if(newState instanceof Number) (newState as Number).intValue else 0
    val String strTimer = triggeringItem.name
    val myItem =  ScriptServiceUtil.getItemRegistry?.getItem(strTimer.replace("_Timer",""))
    if(timers.get(strTimer) === null) {
        if(iTime > 0) {
            timers.put(strTimer,createTimerWithArgument(now.plusMinutes(iTime),strTimer,[strTimer|
                val myItem =  ScriptServiceUtil.getItemRegistry?.getItem(strTimer.replace("_Timer","")) 
                myItem.sendCommand(OFF)
                timers.delete(strTimer)
            ]))
            myItem.sendCommand(ON)
        }
    } else {
        if(iTime > 0)
            timers.get(strTimer).reschedule(now.plusMinutes(iTime))
        else {
            timers.get(strTimer).cancel
            timers.delete(strTimer)
            myItem.sendCommand(OFF)
        }
    }
end

Could this code work as intended?
Given Item pair Switch MyItem and Number MyItem_Timer, where MyItem_Timer is member of Group gTimers.
So in theory, if the *_Timer Item changed, the rule will fire. If there is no timer yet, if Time is not 0, it creates one and switches the Item to ON. Timer will switch OFF the Item.
If there is already a timer, if Time is 0, it will cancel the Timer, otherwise will reschedule.

thank you for the many answers, apparently the error occurred due to the many editing of the rule, now it runs without error.
Current status:

import org.eclipse.smarthome.model.script.ScriptServiceUtil
var Timer tS20 = null
var Timer tObi1 = null
var Timer tObi2 = null
var Timer tPumpe = null
var Timer tDect210 = null
var Timer tGeneral = null
rule "S20 Timer"
    when
        Item TasmotaS20_Timer received command or 
        Item TasmotaObi1_Timer received command or 
        Item TasmotaObi2_Timer received command or 
        Item G_Gartenpumpe_Timer received command or
        Item Dect210_2_Schalter_Timer received command
    then
    
    var String TargetItemName = triggeringItem.name.toString.replace("_Timer","")    
    var String TargetItemSelectName = triggeringItem.name.toString
    val TargetItem = ScriptServiceUtil.getItemRegistry?.getItem(TargetItemName)    
    val TargetItemSelect = ScriptServiceUtil.getItemRegistry?.getItem(TargetItemSelectName)
 
    switch(TargetItemSelectName) {
            case  "TasmotaS20_Timer" : { tGeneral = tS20
                                            logInfo("Steckdosen", "Timer = S20") 
            }
            case  "TasmotaObi1_Timer" : { tGeneral = tObi1
                                            logInfo("Steckdosen", "Timer = Obi1")
            }
            case  "TasmotaObi2_Timer" : { tGeneral = tObi2
                                            logInfo("Steckdosen", "Timer = Obi2")
            }
            case  "G_Gartenpumpe_Timer" : { tGeneral = tPumpe
                                            logInfo("Steckdosen", "Timer = tPumpe")
            }
            case  "Dect210_2_Schalter_Timer" : { tGeneral = tDect210
                                            logInfo("Steckdosen", "Timer = tDect210")
            }
            default : {
                tGeneral = null
                logInfo("Steckdosen", "Timer = Null")
                return;
            }
    }
 
    //mappings=[0="Aus",10="10min.",15="15min.",30="30min.",45="45min.",60="60min.",90="90min.",120="120min."] 
    logInfo("Steckdosen", ":. " +triggeringItem.name.toString +" erhielt Kommando " +receivedCommand.toString)
    logInfo("Steckdosen", "Ziel Item: " + TargetItemName)
    if (tGeneral!== null) {
        tGeneral.cancel
        tGeneral = null
    }
    if (receivedCommand == 0) {
        logInfo("Steckdosen", "=> Szene Aus")       
        sendCommand(TargetItem,OFF)
    }
    else{
        logInfo("Steckdosen", "=> Setze Timer auf " + receivedCommand.toString + " Minuten")	
        TargetItem.sendCommand(ON)
        tGeneral = createTimer(now.plusMinutes((receivedCommand as DecimalType).intValue)) [|		  
          sendCommand(TargetItemSelect,0)
        ]
    }    
end 

It looks like it should work but ultimately stuff like this needs to be tested.

You don’t need to pull myItem from the Item registry in the Timer’s body though. Use the sendCommand action: sendCommand(strTimer.replace('_Timer', ''), "ON")

Instead of timers.delete(strTimer) though you want to use timers.put(strTimer, null). There is no delete method on a Java Map.

I might rework the order of operations in the rule too to simplify things. Test iTime < 0 up front and cancel the timer if it exists and exit. Then you never need to test if iTime > 0 for the rest of the rule.

Also, don’t over specify the types of variables. It slows down parsing and can cause problems.

I’m not sure if in Rules DSL that you need to pass strTimer as an argument to the Timer. I think that, because the variables are not global (not to mention they are val) that they won’t change inside a timer even when the rule triggers again later. Put another way, each timer will have it’s own copy of the vals so we don’t need to pass them as arguments.

Finally, I changed the trigger to received update because otherwise this rule would only work if one changed the timer time but I imagine most of the time one wants to use the same amount of time more than once in a row. Another alternative would be to update the Timer Item to 0 at the end when the timer goes off instead of deleting the timer from the Map. Then then it can be kicked off again with the same value as before because the Item changed to 0 inbetween. The rule will be triggered again when it’s updated to 0 and that first part of the if/then/else will delete the expired timer. But then you’d lose the previous value of the timer time which may be undesireable.

received command is probably the best choice though. Then you know it’ll be a number and don’t need to test for that.

import org.eclipse.smarthome.model.script.ScriptServiceUtil

val <String,Timer> timers = newHashMap

rule "Generic timer rule"
when
    Member of gTimers received command
then
    val iTime   = receivedCommand as Number
    val strTimer = triggeringItem.name
    val myItem =  ScriptServiceUtil.getItemRegistry?.getItem(strTimer.replace("_Timer",""))
    
    // time changed to 0, cancel the timer if it exists and turn off the switch
    if(iTime <= 0) {
        timers.get(strTimer)?.cancel()
        timers.put(strTimer, null)
        myItem.sendCommand(OFF) // may want to check if it's already OFF first
    } 

    // time changed to something greater than zero, no timer exists so create the timer and turn on the switch
    else if(timers.get(strTimer) === null) {
        timers.put(strTimer, createTimer(now.plusMinutes(iTime.intValue),[
            myItem.sendCommand(OFF)
            timers.put(strTimer, null)
        ]))
        myItem.sendCommand(ON)
    } 

    // time change to something greater than zero, reschedule the existing timer
    else {
        timers.get(strTimer).reschedule(now.plusMinutes(iTime))
    }
end
1 Like

As always, it’s enlightening to read your comments, thanks a lot!