Passing a timer handle between rules and functions

I’m getting my head around timers using Jython. Some time back @5iver recommended I use the ScriptExecution.createTimer method, rather than the native Jython threading.Timer. Naturally, therefore, I’ve started my experiments by using threading.Timer… :slight_smile:

Goal

  1. Dim lights over a long period of time…
  2. Using a re-useable python function that can be called from within a ‘normal’ rule…
  3. With a timer…
  4. And be able to cancel the dimming at any time.

I can do 1, 2 and 3. I’m kind of stuck on 4.

Method

Here’s what I currently have (abridged) that addresses goals 1, 2, 3 and 4 (sort of):

From within a rule, I call the function toggle_dimming. Into the function I pass the name of the Item that I want to dim, and the length of time over which dimming should occur.

import personal.personal_functions
reload(personal.personal_functions)
from personal.personal_functions import toggle_dimming

...

toggle_dimming(dimmer_item_name, t)

My file personal_functions includes the following:

from core.jsr223.scope import ir
from core.jsr223.scope import events
from threading import Timer

...

#Initialise a timer
dimming_timer = None

...

def toggle_dimming(item_name, interval):
    global dimming_timer

    if dimming_timer is None:
    #if timer is None:
        start_dimming(item_name, interval)
    else:
        stop_dimming()

...

def start_dimming(item_name, interval):

    #Grab current dimming level
    current_dimmer_level = int(str(ir.getItem(item_name).state))
    
    global dimming_timer

    if current_dimmer_level is not 0:
        #Adjust dimmer
        events.sendCommand(ir.getItem(item_name), str(current_dimmer_level-1))

        #Start timer
        dimming_timer = Timer(interval, start_dimming, [item_name, interval])
        dimming_timer.start()

...

def stop_dimming():

    global dimming_timer

    if dimming_timer is not None and dimming_timer.isAlive():
            dimming_timer.cancel()
            dimming_timer = None

This is all great. It’s not particularly robust, but I can start and stop the dimming process.

However, I have to lights that I want to dim, independently, using this same python function. Currently, the timer is attached to the global variable dimming_timer, which means I can’t start a second dimming process without interfering with the first (I think).

What I’d like to do is create a timer handle back in the rules file, which I then pass through to toggle_dimming as an argument. The toggle_dimming function would then use this argument to check whether the timer exists or not.

  • If it doesn’t, continue on to start_dimming, passing the handle through once more as an argument. start_dimming would then use that handle for the timer itself.
  • If it does, continue on to stop_dimming, passing the handle through once more as an argument to enable stopping of the timer.

I’ve tried creating a handle in the rules file. I’ve tried importing a handle from the module file (personal_functions). Neither work - I guess the handle only becomes a timer when inside of start_dimming, and because it’s no longer a global variable, nothing else knows about it, or can access it. Additionally, when creating the timer, I can’t add the handle argument into the lambda because it doesn’t yet exist as a timer object - it will only exist once the timer is created, but then it’s too late to pass an argument to the lambda (or is it?)

So I guess the question is: how can I transfer a timer handle between rules and functions, without using a global variable?

(For completeness, and to put @5iver at ease, I am also experimenting with the ScriptExecution.createTimer method. This topic is more to improve my understanding of the underlying basics, I guess…)

The problem is that you get a Python timer if it’s in the local scope, but a Java thread if it’s global. See these topics for investigations and experiments we’ve already done:

I’ve gone through those threads before, but I guess there’s a step in the logic which I’m missing in order to tie it all together.

Are you explicitly saying that it is not possible to do what I am specifically trying to do?

That is exactly what I’m saying. There may be a way to do this by intentionally spawning a Java thread and writing your own timer code, like what is found in threading.Timer but that’s a lot of work. The ScriptExecution.createTimer function is native within OH and is easier to use, why make all that work for yourself?

Because I’m not a programmer, so didn’t know that my intended direction doesn’t work until I tried it or I get told that it’s not possible.

I’ve now been told! Cheers @CrazyIvan359!

I thought, based on this statement, that you were and wanted to do your own experiments for the experience of it.

Rich, 5iver, and I all did experiments with it and discovered that the underlying Java thread ends up in a global variable. Then you can’t seem to access even the threading.Timer methods as you would expect because the object type has changed. The preferred method has always been the native OH method but we also wanted to poke the bear, as it were, and try Python threading.

That’s still true!

So, can I actually do what I’m wanting with the native OH timers? A reusable re-scheduling bespoke function based on a timer which can be run and stopped from independent triggers (run concurrently, using different handlers which will be passed through from the triggering rule)?

(I’m not expecting a solution, I’ll still experiment, though if it’s a dead end from the get go I’ll focus on other time wasting activities!!)

Absolutely! I’m actually doing something similar myself for the same purpose. My implementation is only for a single fixture in my automation (bedroom sunlamp) but you can scale that up no problem.

Take special note of the use of scriptUnloaded when dealing with timers to avoid orphaned threads randomly doing things you don’t expect. See the Design Pattern I referenced for details, but also a search on this forum will net you several other examples.

Ah, yes. I do actually have this implemented, I promise! Just didn’t show it in the OP for clarity.

Thanks again - I’ll shout when I fail! :slight_smile:

1 Like

Even though it is not recommended, I have included examples that show how to use threading.Timer (I need to push a doc update). The examples include the use of a dict to store the timers. I use this in Area Triggers and Actions, where there is a dict that potentially holds a timer for each Item.

Actually, from what you’ve posted, Area Triggers and Actions could provide the functionality you are trying to implement, plus a LOT more…

I think this might be the answer, if I understand it all correctly. I’ll try tomorrow, based off this example.

Quick question: on line 43 you have

MY_TIMERS["fan_timer"] = Timer(600, lambda: timer_function())

I thought for threading.Timer the lambda syntax isn’t required. Is there a specific reason it’s included here? i.e why not

MY_TIMERS["fan_timer"] = Timer(600, timer_function)

The lambda is not necessary. This example probably shouldn’t have it in there and was probably a remnant from an earlier example that was not calling a function but directly calling events.sendCommand in the Timer. The example could have been simplified from one that passed arguments to the function. For people unfamiliar with Python, lambdas make it a little more intuitive to pass arguments to the function used in the Timer.

BTW, use ScriptExecution.createTimer! :wink:

1 Like

This was the solution, and it makes sense. Using a key/value pair to identify and ‘hold’ a timer is clever! I now also known what a python dictionary is…!

For completeness, here is the code from the OP modified so it now works:

Rule

import personal.personal_functions
reload(personal.personal_functions)
from personal.personal_functions import toggle_dimming

...

toggle_dimming(dimmer_item_name, t)

personal_functions.js

from core.jsr223.scope import ir
from core.jsr223.scope import events
from threading import Timer

...

#Initialise a timer dict
my_timers = {}

...

def toggle_dimming(item_name, interval):
    if my_timers.get(item_name) is None or not my_timers[item_name].isAlive():
        # A timer does not exist, so create one
        start_dimming(item_name, interval)
    else:
        stop_dimming(item_name)

...

def start_dimming(item_name, interval):

    #Grab current dimming level
    current_dimmer_level = int(str(ir.getItem(item_name).state))

    if current_dimmer_level is not 0:
        #Adjust dimmer
        events.sendCommand(ir.getItem(item_name), str(current_dimmer_level-1))

        #Start timer
        my_timers[item_name] = Timer(interval, start_dimming, [item_name, interval])
        my_timers[item_name].start()
    else:
        #Dimming finished
        my_timers[item_name] = None

...

def stop_dimming(item_name):

    if my_timers.get(item_name) is not None and my_timers[item_name].isAlive():
        my_timers[item_name].cancel()

:+1:

Just a little note about this that I’ve run into before. If the key is not in the dict this call will error, use a default value of None like this so the call will not error if the key does not exist:

def toggle_dimming(item_name, interval):
    if my_timers.get(item_name, None) is not None and not my_timers[item_name].isAlive():
1 Like

Altering

if my_timers.get(item_name) is None or not my_timers[item_name].isAlive():

to

if my_timers.get(item_name, None) is not None and not my_timers[item_name].isAlive():

results in this function always moving to the else, and therefore the stop_dimming() function. Am I missing a subtlety? I tried changing the and to or:

if my_timers.get(item_name, None) is not None or not my_timers[item_name].isAlive():

but that results in the following error:

11:19:28.303 [ERROR] [jsr223.jython.IKEA switch pressed    ] - Traceback (most recent call last):
  File "/etc/openhab2/automation/lib/python/core/log.py", line 51, in wrapper
    return fn(*args, **kwargs)
  File "<script>", line 152, in action_ikea_switch_changed
  File "/etc/openhab2/automation/lib/python/personal/personal_functions.py", line 32, in toggle_dimming
    if my_timers.get(item_name, None) is not None or not my_timers[item_name].isAlive():
KeyError: 'dRGB1'

11:19:28.311 [ERROR] [re.automation.internal.RuleEngineImpl] - Failed to execute rule '44278096-7d8a-4c0a-ad8c-79e1531a14b9': Fail to execute action: 1

toggle_dimming() is called with dRGB1 as its item_name argument from within a rule.

Curiously, my original if statement never showed any errors in the logs…

You missed that I changed your or to an and to prevent this. You show the change in your post, but the snippet in the error still shows or.

If the key does not exist, dict.get will not error, but will return None…

https://docs.python.org/2.7/library/stdtypes.html?highlight=dict%20get#dict.get

1 Like

I didn’t first time round, as mentioned in my post…

Ah, OK, that makes sense!

You are correct, I must have been thinking of the behaviour of dict.pop.

Or maybe the curly braces in dict.get(“something”, {}).get(“something else”)?