[SOLVED] Jython reschedule timer

Ok, going through converting my rule
A simple single delay timer, followed the example. Works… Yeepee

This one I am not having luck in finding any docs or help
I need to reschedule a timer:

DSL:

val int COFFEELIGHTTIMER = 120
val int COFFEELIGHTRESCHEDULETIMER = 75

var Timer kitchenCoffeeLightTimer = null

rule "Kitchen Coffee Light"
when
    Item Kitchen_CoffeeLight_PIR changed to ON
then
    //logInfo("TEST", "Kitchen Coffee Light Rules Triggered")
    if (kitchenCoffeeLightTimer === null) {
        Kitchen_CoffeeLight.sendCommand(ON)
        //logInfo("TEST", "Kitchen Coffee Light Timer Started")
        kitchenCoffeeLightTimer = createTimer(now.plusSeconds(COFFEELIGHTTIMER), [ |
            if (PrayerTime_MaghribPeriod.state == OFF) {
                Kitchen_CoffeeLight.sendCommand(OFF)
                //logInfo("TEST", "Kitchen Coffee Light Timer Cancelled")
                kitchenCoffeeLightTimer = null
            } else {
                //logInfo("TEST", "Kitchen Coffee Light Timer Rescheduled")
                kitchenCoffeeLightTimer.reschedule(now.plusSeconds(COFFEELIGHTRESCHEDULETIMER))
            }
        ])
    } else {
        //logInfo("TEST", "Kitchen Coffee Light Timer Rescheduled")
        kitchenCoffeeLightTimer.reschedule(now.plusSeconds(COFFEELIGHTRESCHEDULETIMER))
    }
end

My attempt at Jython

'''
Possible timer states:
NEW - instantiated
RUNNABLE - defined
TIMED_WAITING - timer is running (slight delay after timer is stopped)
TERMINATED - timer completed or stopped
'''
from threading import Timer
chargerTimer1 = None

COFFEELIGHTTIMER = 120
COFFEELIGHTRESCHEDULETIMER = 75

def timer1_body():
    #logging.info("Timer expired: " + msg)
    if items.PrayerTime_MaghribPeriod == OFF:
        events.sendCommand("Kitchen_CoffeeLight", "OFF")
    else:
        # HOW DO I RESCHEDULE THE TIMER FOR ANOTHER 75 SECONDS

@rule("Kitchen Coffee Light", description="Kitchen Coffee Light", tags=["Kitchen", "Light"])
@when("Kitchen_CoffeeLight_PIR changed to ON")
def kitchen_coffee_light(event):
    global chargerTimer1
    if chargerTimer1 is None or str(chargerTimer1.getState()) == "TERMINATED":
        events.sendCommand("Kitchen_CoffeeLight", "ON")
        chargerTimer1 = Timer(COFFEELIGHTTIMER, lambda: timer1_body())
        chargerTimer1.start()
    elif chargerTimer1 is not None and str(chargerTimer1.getState()) == "TIMED_WAITING":
        # HOW DO I RESCHEDULE THE TIMER FOR ANOTHER 75 SECONDS

Thanks

Perhaps some hints in the code here? I believe they reschedule timers. (At least, I hope so.)

The problem is that you are using a Jython timer, when you promote it to a global it gets typed differently (you get the underlying Java thread if I recall from our experiments with it). If you use the timer provided by openHAB then it behaves as expected in all contexts and all DSL documentation remains applicable. With the import and assignment below, createTimer can be used as it is in DSL rules, with Python syntax of course.

from core.actions import ScriptExecution

# Shortcut for similarity to DSL rules
createTimer = ScriptExecution.createTimer

Side note, if your function has no arguments you don’t need to turn it into a lambda. Lambdas are only needed in cases where you must provide only a function name but need to provide arguments to that function.

# these work the same as arguments to the timer
lambda: timer1_body()
timer1_body
3 Likes

And so is the code I linked to. IT DOES appear to work.

The only timer I am aware of in OPenHAB is in the v1 Expire binding

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…