[SOLVED] Jython reschedule timer

Really?
What about the createTimer action?

The one from the rules DSL. See the rest of my post that mentions it and the DSL version of the rule in question in the first post.

Right, it is an Action, you are correct. This discussion is about rules so I thought it would be implied that’s the one we’re taking about.

And many of us are moving away from Rules DSL to jsr223 rules such as Jython. That is the future as evidenced my NGRE.
@rlkoshak or @5iver should be able to shed some light on the issue.

Yes, Jython timers do work. They change type, and therefore available methods change, when promoted to global though. This leads to much confusion, so it is easier to use the timer action from openhab. There is also plenty of documentation already on the forums of how to use them since (aside from a few brackets) the syntax remains the same.

Ok so now I have this:

chargerTimer1 = None

COFFEELIGHTTIMER = 10
COFFEELIGHTRESCHEDULETIMER = 10

def timer1_body():
    #logging.info("Timer expired: " + msg)
    if items.PrayerTime_MaghribPeriod == OFF:
        events.sendCommand("Kitchen_CoffeeLight", "OFF")
        chargerTimer1.cancel()
    else:
        chargerTimer1.reschedule(COFFEELIGHTRESCHEDULETIMER)

@rule("Kitchen Coffee Light", description="Kitchen Coffee Light", tags=["Kitchen", "Light"])
@when("Item Kitchen_CoffeeLight_PIR changed to ON")
def kitchen_coffee_light(event):
    #log.debug("Battery charging monitor: {}: start".format(event.itemState))
    global chargerTimer1
    if chargerTimer1 is None or chargerTimer1.hasTerminated():
        events.sendCommand("Kitchen_CoffeeLight", "ON")
        ScriptExecution.createTimer(DateTime.now().plusSeconds(COFFEELIGHTTIMER), timer1_body)
    elif chargerTimer1 is not None and not chargerTimer1.hasTerminated():
        chargerTimer1.reschedule(COFFEELIGHTRESCHEDULETIMER)

openHAB throw a wobbly when the light goes off, ie when I do chargerTimer1.cancel()

2019-11-07 12:55:23.784 [ERROR] [org.quartz.core.JobRunShell         ] - Job DEFAULT.Timer 109 2019-11-07T12:55:23.779Z: <function timer1_body at 0x3> threw an unhandled Exception: 
org.python.core.PyException: null
	at org.python.core.Py.AttributeError(Py.java:205) ~[?:?]
	at org.python.core.PyObject.noAttributeError(PyObject.java:1013) ~[?:?]
	at org.python.core.PyObject.__getattr__(PyObject.java:1008) ~[?:?]
	at org.python.pycode._pyx55.timer1_body$1(<script>:18) ~[?:?]
	at org.python.pycode._pyx55.call_function(<script>) ~[?:?]
	at org.python.core.PyTableCode.call(PyTableCode.java:167) ~[?:?]
	at org.python.core.PyBaseCode.call(PyBaseCode.java:124) ~[?:?]
	at org.python.core.PyFunction.__call__(PyFunction.java:403) ~[?:?]
	at org.python.core.PyFunction.__call__(PyFunction.java:398) ~[?:?]
	at org.python.core.PyFunction.invoke(PyFunction.java:533) ~[?:?]
	at com.sun.proxy.$Proxy14244.apply(Unknown Source) ~[?:?]
	at org.eclipse.smarthome.model.script.internal.actions.TimerExecutionJob.execute(TimerExecutionJob.java:48) ~[?:?]
	at org.quartz.core.JobRunShell.run(JobRunShell.java:202) [182:org.openhab.core.scheduler:2.5.0.M4]
	at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573) [182:org.openhab.core.scheduler:2.5.0.M4]

I need to cancel this timer at some point. I used to do it when it expired (ie: in the lambda)
This doesn’t seem to be working

Make sure the timer isn’t None before you try to cancel it.

If chargerTimer1:
    chargerTimer1.cancel()

Ok got it:
I had some errors in the code

Lesson learned:
To use timers that need to be rescheduled we need to use the DSL action timers

Final code:

from core.rules import rule
from core.triggers import when
from core.actions import ScriptExecution
from org.joda.time import DateTime


COFFEELIGHTTIMER = None

COFFEELIGHTFIRSTTIME = 120
COFFEELIGHTRESCHEDULETIME = 75

def timer1_body():
    #logging.info("Timer expired: " + msg)
    if items.PrayerTime_MaghribPeriod == OFF:
        events.sendCommand("Kitchen_CoffeeLight", "OFF")
        COFFEELIGHTTIMER.cancel()
    else:
        COFFEELIGHTTIMER.reschedule(DateTime.now().plusSeconds(COFFEELIGHTRESCHEDULETIME))

@rule("Kitchen Coffee Light", description="Kitchen Coffee Light", tags=["Kitchen", "Light"])
@when("Item Kitchen_CoffeeLight_PIR changed to ON")
def kitchen_coffee_light(event):
    global COFFEELIGHTTIMER
    if COFFEELIGHTTIMER is None or COFFEELIGHTTIMER.hasTerminated():
        events.sendCommand("Kitchen_CoffeeLight", "ON")
        COFFEELIGHTTIMER = ScriptExecution.createTimer(DateTime.now().plusSeconds(COFFEELIGHTFIRSTTIME), timer1_body)
    elif COFFEELIGHTTIMER is not None and not COFFEELIGHTTIMER.hasTerminated():
        COFFEELIGHTTIMER.reschedule(DateTime.now().plusSeconds(COFFEELIGHTRESCHEDULETIME))
1 Like

I don’t see any issue here. In Jython, as Michael described, Python threading.Timer has some quirks in how it works. But if you use the createTimer Action, which is the same thing that you are using in Rules DSL when you call createTimer, the behavior is more consistent. Therefore it was recommended to me by Scott and Michael and I’ll recommend to others to stick to using createTimer instead of trying to use the native Python Timer.

The Expire binding is a binding. It’s not related to the discussion here. And honestly, using Expire binding for timers was not an expected or intended use of the Expire binding when it was first written. And it has a significant limitation in that you cannot dynamically define the time for the timer.

One of the advantages of using Expire for Timers in Rules DSL was that it made the code a little bit simpler to write and use because you do not have the code for the Timer embedded in the Rule that created the Timer. But in Python at least, that advantage goes away and in fact it is more complicated and more complex to have to define another Rule than it is to just define a function that you pass to createTimer.

So I think the best practice here is to use the createTimer Action over the native Python Timer and if you are migrating your Rules from Rules DSL to Python, consider replacing Expire with createTimer over time.

And it’s worth mentioning that Michael is far more an expert in this stuff than I am. The only reason I’m as competent in this at all is that I have a pretty good understanding of how Rules DSL works. But Michael is the expert on this thread, not me.

No, it doesn’t. The Expire binding library submission uses the OH createTimer Action, not native Python Timers.

Technically, you don’t need to. But you should because of the weirdness in how Jython handles Timers when you global them. You get a much more consistent behavior if you stick to the createTimer Action we’ve all come to know and love.

1 Like

…BUT you said you completely moved away from Rules DSL. :face_with_raised_eyebrow::crazy_face:

openHAB Actions are not Rules DSL any more than a binding is Rules DSL. Actions are available to be called from Rules of any type (Rules DSL or NGRE) and they are a way for Rules of any type to interact with other parts of openHAB. In Python, when you call events.sendCommand, you are calling an Action too, just like when you call ScriptActions.createTimer.

1 Like

…still learning…

1 Like

A day we don’t learn something is a day wasted

1 Like

Guys, still not out of the woods
I have another rules with reschedule
I get the rescheduling but I think I have a variable scoping problem
But I declare it globally:

1 from core.rules import rule
2 from core.triggers import when
3 from core.actions import ScriptExecution
4 from core.actions import Voice
5 from org.joda.time import DateTime
6
7 FRIDGEDOORTIMER = None
8
9 fridge_door_volume = 0
10 
11 def fridgedoortimer_body():
12     if items.LivingRoom_GoogleHome_Volume != 100:
13         fridge_door_volume = int(str(items.LivingRoom_GoogleHome_Volume))
14         events.sendCommand("LivingRoom_GoogleHome_Volume", str(fridge_door_volume))
15     events.sendCommand("LivingRoom_EchoDot_TTS", "The fridge door is open!")
16     Voice.say("The fridge door is open!")
17     FRIDGEDOORTIMER.reschedule(now.plusSeconds(10))
18
19 @rule("Fridge Door Opened for more than a minute", description="Fridge Door Opened for more than a minute", tags=["Fridge"])
20 @when("Item Fridge_Door changed")
21 def fridge_door(event):
22    global FRIDGEDOORTIMER
23    if items.Fridge_Door == OPEN:
24        fridge_door_volume = int(str(items.LivingRoom_GoogleHome_Volume))
25        if FRIDGEDOORTIMER is None or FRIDGEDOORTIMER.hasTerminated():
26            FRIDGEDOORTIMER = ScriptExecution.createTimer(DateTime.now().plusMinutes(1), fridgedoortimer_body)
27    elif items.Fridge_Door == CLOSED:
28        if FRIDGEDOORTIMER is not None and not FRIDGEDOORTIMER.hasTerminated():
29            if int(str(items.LivingRoom_GoogleHome_Volume)) != fridgeDoorVolume:
30                events.sendCommand("LivingRoom_GoogleHome_Volume", str(fridgeDoorVolume))
31            FRIDGEDOORTIMER.cancel()

What it meant to do is repeat every 10 seconds that the fridge door is opened 1 minute after it was first opened. The repeating will stop when the door is closed.
The current volume of the google mini is saved and set to 100 for the repeating and then restored

I get this error:

Line 13 Redefining name 'fridge_door_volume' from outer scope (line 9)pylint(redefined-outer-name)
Line 24 Redefining name 'fridge_door_volume' from outer scope (line 9)pylint(redefined-outer-name)

Or should I not worry about these?

You should worry about them. While I don’t understand why you are saving the volume like this (it looks like it will always be the same value as the item at the end?), I do see the problem. You are correct, scope is the problem, and you are not alone with this one coming from DSL to Python.

The fridge_door_volume on line 9 is in the module’s (file’s) scope.
The fridge_door_volume on line 13 is in the fridgedoortimer_body function’s scope.
The fridge_door_volume on line 29 from the module’s scope.

See this post for an explaination of what is going on and how global works.

OK, @CrazyIvan359
Thanks
So I promoted the variable to global in both function
And changed the code a bit
It saves the volume when the door opens
It saves it again when the timer starts, just in case it changed in the mean time (unlikely but one never knows what the kids will do)
Then restores it when the door closes

I had to put it in capitals because pylint was complaining

FRIDGE_DOOR_VOLUME = 0

def fridgedoortimer_body():
    if items.LivingRoom_GoogleHome_Volume != 100:
        global FRIDGE_DOOR_VOLUME
        FRIDGE_DOOR_VOLUME = int(str(items.LivingRoom_GoogleHome_Volume))
        events.sendCommand("LivingRoom_GoogleHome_Volume", "100")
    events.sendCommand("LivingRoom_EchoDot_TTS", "The fridge door is open!")
    Voice.say("The fridge door is open!")
    FRIDGEDOORTIMER.reschedule(now.plusSeconds(10))

@rule("Fridge Door Opened for more than a minute", description="Fridge Door Opened for more than a minute", tags=["Fridge"])
@when("Item Fridge_Door changed")
def fridge_door(event):
    global FRIDGEDOORTIMER
    global FRIDGE_DOOR_VOLUME
    if items.Fridge_Door == OPEN:
        FRIDGE_DOOR_VOLUME = int(str(items.LivingRoom_GoogleHome_Volume))
        if FRIDGEDOORTIMER is None or FRIDGEDOORTIMER.hasTerminated():
            FRIDGEDOORTIMER = ScriptExecution.createTimer(DateTime.now().plusMinutes(1), fridgedoortimer_body)
    elif items.Fridge_Door == CLOSED:
        if FRIDGEDOORTIMER is not None and not FRIDGEDOORTIMER.hasTerminated():
            if int(str(items.LivingRoom_GoogleHome_Volume)) != FRIDGE_DOOR_VOLUME:
                events.sendCommand("LivingRoom_GoogleHome_Volume", str(FRIDGE_DOOR_VOLUME))
            FRIDGEDOORTIMER.cancel()
1 Like

tried this example. Once reschedule is active, I have a problem with “hasTerminated”
When CLOSED, and timer is not None, hasterminated is true and keeps being true.
when in the CLOSED part, I only test the timer is not none, it works…
I’ve read in the docs: hasterminated : returns true if the code has run and completed
so, I guess, if fridgedoortimer_body has run and completed, hasTermintated is true. BUT, a reschedule is done, so, it will be started again…

You’ll need to be more specific, since there are several in this topic. I’ll make two suggestions though, right off the bat. Use the createTimer Action and read the examples in the documentation.

Timer example from @vzorglub 'Nov 19:

if FRIDGEDOORTIMER is not None and not FRIDGEDOORTIMER.hasTerminated()
FRIDGEDOORTIMER.hasTerminated() is always true, once fridgedoortimer_body() is running…
apparantly, a reschedule doesn’t make it evaluate the FRIDGEDOORTIMER.hasTerminated() to false again… Probably, because, before fridgedoortimer_body() has ended, a reschedule is done…
FRIDGEDOORTIMER also needs to be declared global in fridgedoortimer_body() …

I’ve had a door sensor on my fridge for a couple of years (maybe longer) and I’ve never found time to write a rule to check if the door was left open. I saw this thread and thought I’d try and adapt the rule for my needs. So far, I have the following:

import core.items
from core.rules import rule
from core.triggers import when
from core.actions import ScriptExecution
from core.actions import Voice
from org.joda.time import DateTime
from core.actions import Pushover
from core.log import logging, LOG_PREFIX

FRIDGEDOORTIMER = None

    @rule("Fridge Door Opened for more than a minute", description="Fridge Door Opened for more than a minute", tags=["Fridge"])
@when("Item SensorFridgeDoor_DoorSensor changed")
@when("Item fridgeTestSwitch received update")
def fridge_door(event):
        global FRIDGEDOORTIMER
        if ( event.itemState == OPEN ) or ( event.itemState == ON ):
            log = logging.getLogger("org.eclipse.smarthome.model.script.security")
            log.info("SECURITY: Fridge door is open.")
            if FRIDGEDOORTIMER is None or FRIDGEDOORTIMER.hasTerminated():
            FRIDGEDOORTIMER = ScriptExecution.createTimer(DateTime.now().plusMinutes(1), fridgedoortimer_body)
        elif ( event.itemState == CLOSED ) or ( event.itemState == OFF ):
            if FRIDGEDOORTIMER is not None and not FRIDGEDOORTIMER.hasTerminated():
                FRIDGEDOORTIMER.cancel()

def fridgedoortimer_body():
    Pushover.sendPushoverMessage(Pushover.pushoverBuilder("Fridge door has been open for over a minute!"))
        FRIDGEDOORTIMER.reschedule(now.plusSeconds(10))

If I open the fridge door or turn on the test switch I see a message in the log that the fridge door is open but I don’t get a Pushover message. Can anyone suggest what I’m doing wrong?

(Sorry about the formatting and indentation of the code above, whenever I cut and paste from Visual Studio Code to here I end up with blank lines between every line and then when I try and remove the blank lines the indenting goes screwy.)

from core.rules import rule 
from core.triggers import when
from core.actions import ScriptExecution
from core.actions import Voice
from org.joda.time import DateTime
from core.log import logging, LOG_PREFIX
from time import sleep
 
RULE_NAME="SensorTimer"
log=logging.getLogger(LOG_PREFIX+"."+RULE_NAME)

Test_Sensor_Timer=None
woonkamerspeaker_Alarm_Volume=30
save_Woonkamerspeaker_Volume=None
alarm_Fired=False
woonkamerspeaker="chromecast:chromecast:55af8aa2114176d12ad027f3fedd199d"

def GV_Test_Sensor_Timer_body():
    global woonkamerspeaker
    global woonkamerspeaker_Alarm_Volume
    global save_Woonkamerspeaker_Volume
    global Test_Sensor_Timer
    global alarm_Fired
    
    if int(str(items.GV_Woonkamer_Speaker_Volume)) !=woonkamerspeaker_Alarm_Volume:
        log.info("save volume in body") 
        save_Woonkamerspeaker_Volume=int(str(items.GV_Woonkamer_Speaker_Volume))
        events.sendCommand("GV_Woonkamer_Speaker_Volume",str(woonkamerspeaker_Alarm_Volume))  
    alarm_Fired=True   
    log.info("say sensor timer is open. Uncomment next line ")    
    #Voice.say("test timer is open","voicerss:nlNL",woonkamerspeaker)
    if Test_Sensor_Timer is not None:
        Test_Sensor_Timer.reschedule(DateTime.now().plusSeconds(5))
    else: log.info("Test_Sensor_Timer is null!")   
 


@rule(RULE_NAME, description="When sensor is opened for more than 1 minute, do actiob", tags=["sensor","timer"])
@when("Item GV_Test_Sensor_OpenClose changed")
def sensortimer(event):
    log.info("Begin "+RULE_NAME)
    global woonkamerspeaker
    global save_Woonkamerspeaker_Volume
    global Test_Sensor_Timer
    global alarm_Fired
     
    log.info("GV_Test_Sensor_OpenClose= "+str(items.GV_Test_Sensor_OpenClose))
    if save_Woonkamerspeaker_Volume is None:
        save_Woonkamerspeaker_Volume=int(str(items.GV_Woonkamer_Speaker_Volume))    
        log.info("initialising save value"+str(items.GV_Woonkamer_Speaker_Volume))

    if items.GV_Test_Sensor_OpenClose==OPEN:
        log.info("starting volume={}".format(items.GV_Woonkamer_Speaker_Volume))
        save_Woonkamerspeaker_Volume=int(str(items.GV_Woonkamer_Speaker_Volume))
        alarm_Fired=False
        scheduleTime=DateTime.now().plusSeconds(10)
        if Test_Sensor_Timer is None:
            log.info("Creating timer") 
            Test_Sensor_Timer=ScriptExecution.createTimer(scheduleTime,GV_Test_Sensor_Timer_body)
        else:
            log.info("rescheduling timer")
            Test_Sensor_Timer.reschedule(scheduleTime)
    elif items.GV_Test_Sensor_OpenClose==CLOSED:
        if Test_Sensor_Timer is not None:
            if alarm_Fired:
                 log.info("say sensor timer is closed now. Uncomment next line ot hear it ") 
                #Voice.say("De test timer is nu gesloten","voicerss:nlNL",woonkamerspeaker)
            log.info("Cancelling Test_Sensor_Timer")
            Test_Sensor_Timer.cancel()
        log.info("current volume={}".format(items.GV_Woonkamer_Speaker_Volume))
        log.info("save volume={}".format(save_Woonkamerspeaker_Volume)) 
        if int(str(items.GV_Woonkamer_Speaker_Volume)) != save_Woonkamerspeaker_Volume:
                events.sendCommand("GV_Woonkamer_Speaker_Volume",str(save_Woonkamerspeaker_Volume))
        sleep(10) #waiting a little, so GV_Woonkamer_Speaker_Volume has time te updated. 
        log.info("volume at close (should be starting volume)={}".format(items.GV_Woonkamer_Speaker_Volume))
             
             
    log.info("Einde "+RULE_NAME)






def scriptLoaded(*args):
    log.info(RULE_NAME+" loaded.") 

def scriptUnloaded(*args):
    global Test_Sensor_Timer
    if Test_Sensor_Timer is not None:
        Test_Sensor_Timer.cancel() 
        Test_Sensor_Timer=None
    log.info(RULE_NAME+" unloaded.")

This works. for now, I can’t let it crash during testing :wink:
When working with timers, and testing them, be sure to cancel them… Otherwise, they stay in memory.
During my testing and trying, I had to restart OH several times. Therefore, the scriptUnloaded function is interesting, as in that function, you can cleanup the timer…

3 Likes