Best practice for timers in Python scripting

I’ve been experimenting with re-settable / re-triggerable timers in Python, and I’ve found two ways to do so. Firstly is the closest comparison to how it is implemented in JS scripting, by referencing the threading.Timer object in the private cache:

from scope import cache
import threading

private_cache = cache.privateCache
timer_key = 'private_timer'
TIMEOUT = 5

def onTimerFinish():
  print('Finished timer')

print('******************************')
timer = private_cache.get(timer_key)
if isinstance(timer, threading.Timer):
  if timer.is_alive():
    print('Cancelling existing timer')
    timer.cancel()
else:
  print('Timer does not exist')

print('Creating new timer')
private_cache.put(timer_key, threading.Timer(TIMEOUT, onTimerFinish))
private_cache.get(timer_key).start()

The second way is more Python-native, although it tests my understanding about the way that the script is executed:


import threading

TIMEOUT = 5

def onTimerFinish():
  print('Finished timer')

print('**************************************')  

if 'timer' in globals():
  print('timer is in globals')
  if isinstance(timer, threading.Timer):
    print('timer is an instance of threading.Timer')
    if timer.is_alive():
      print('cancelling timer')
      timer.cancel()
    else:
      print('timer is NOT alive')
else:
  print('timer is NOT in globals')
    
timer = threading.Timer(TIMEOUT, onTimerFinish)
timer.start()
print('Timer created')

I assume that in this case, after the script runs, all the memory is retained from the previous run hence why timer exists in the global space. All the variables declared in the main thread are overwritten giving the impression of a “clean slate”. Saving the script obviously does a reset of the memory.

Are there notable pros / cons / best practice approaches when it comes to either way?

JS Scripting does provide access to the JS native setTimeout and setInterval. And you can certainly use that in JS Scripting. Both just return an integer which you can put in the cache to cancel the timer later. But that’s about all you can do with the native timers.

I don’t know what threading.Timer is and assume it’s Python’s native multithreading library. But if that’s the case both of your approaches are Python native.

Regardless, the built in way for OH to use timers is to use the ScriptExecution.createTimer() action. I don’t know how you access OH actions from Python these days. In JS Scripting it’s accessed using actions.ScriptExecution.createTimer().

This action creates a Java openHAB provided timer which usually has far more features than the native timers provided by the various rules languages. See Actions | openHAB for all that OH native timers can do. These can be put in the cache.

I can’t speak for what’s best practice in Python, but I’d say it’s best practice in openHAB to use the openHAB provided timers. These can be added to the cache and, unlike the “native” timers these will get cancelled when the rule is unloaded, avoiding orphaned timers from running later and throwing up errors.

//turn the radio off after X minutes
let zmtimer=60
let GETITEM="Zigbee_radio_switch"
let ITEM = items.getItem(event.itemName).label;

//Below is version 5 timer. 
items.getItem(GETITEM).sendCommand('ON', time.Duration.ofMinutes(zmtimer), 'OFF')
 

Above is what I use for items in version 5 openahb. Don’t know if that is what you are after or not.

@Johnno

just to clarify. The scope/context of an python script is created when the rule is loaded and compiled. It exists, until the script is deleted or reloaded. This means that all variables exists during this lifetime. Keep in mind, file based script file can also contain more then one rule.

e.g. if you trigger the following script as a rule, created via webui and triggered by a cron expression, I will increase the counter ‘i’ for ever. Until it is reloaded

if 'i' in globals():
  i += 1
else:
  i = 0

print(i)

to your other question, I don’t fully understand what exactly you want. I would recommend to have a clean python script which will be triggered by a openhab cron trigger.

the cache object, specially the shared cache, is used to share data between rules or script files. In case of a python rule, you don’t need the private cache.

I’m just trying to recreate the timer function as would be used in JS scripting. The threading.Timer object in python matches pretty much 1:1 (it runs a callback function after a delay in a separate thread), and it would seem that Python Scripting handles any risk of unterminated threads when the script unloads.

I’m guessing the java object that Rich is mentioning would be accessed via:

import openhab.actions

timer = openhab.actions.ScriptExecution.createTimer()

Though I’m not sure on the syntax of the accepted time delta and callback function.

all python timers and threads are closed/cleaned on script unload. Additionally, there is a unload function, where you can add your own cleanup logic.

but it still feels a bit more complicated then using a openhab cron timer for that.

I use threads/ timers in scenarios like below.

  1. I expect 2 different State Change Events of 2 different Items in a rows during 5 seconds.
  2. I want to calculate something with these 2 values.
  3. If I got now the first state change event, I start a timer.
  4. If I don’t get a second StateChangeEvent during the next 6 seconds, I continue with my calculation.
  5. If I got the second StateChangeEvent during this time, I continue too.

another example is, if something happens and I want to shutdown/clean after some time. e.g. On motion events enable the outdoor light. If no new motion events are triggered, I disable the outdoor light again.

from openhab import rule
from openhab.triggers import ItemStateChangeTrigger

import threading
import scope

@rule(
    triggers = [
        ItemStateChangeTrigger("MotiondetectorItem", scope.OPEN )
    ]
)
class Test:
    def __init__(self):
        self.timer = None

    def callback(self):
         Registry.getItem("LightItem").sendCommandIfDifferent(scope.OFF)

    def execute(self, module, input):
        Registry.getItem("LightItem").sendCommandIfDifferent(scope.ON)

        if self.timer is not None:
            self.timer.cancel()
        self.timer = threading.Timer(60, self.callback)
        #self.timer = threading.Timer(60, lambda: Registry.getItem("LightItem").sendCommandIfDifferent(scope.OFF))
        self.timer.start()

But all my timers which are based on a cron schedule, are real cron based timers.

e.g. for a file based script

from openhab import rule
from openhab.triggers import GenericCronTrigger

@rule(
    triggers = [
        GenericCronTrigger("0 */5 * * * ?")
    ]
)
class Test:
    def execute(self, module, input):
        print("This is executed every 5 Minutes")

For this example it would be for a motion senor timeout script triggered by multiple sensors on the falling edge. Not really a job for a cron timer. I think I will utilise the threading approach. Combined with memory persistence between script runs, it eliminated the need to read/write the cache.

Others might get some ideas from it. You’d put your item commands in place of the demo print statements.

import threading

inputs_int = [
  Registry.getItemState('input_1').intValue(),
  Registry.getItemState('input_2').intValue(),
]

motion_timeout = 10 #10 Seconds for demo

def on_timer_finish():
  print('Finished timer')

def check_timer(timer_name: str):
  if timer_name in globals() and isinstance(globals()[timer_name], threading.Timer):
    timer_exists = True
    timer_is_alive = timer.is_alive()
  else:
    timer_exists = False
    timer_is_alive = False
  return (timer_name, timer_exists, timer_is_alive)

if not 'previous_state' in globals():
  previous_state = 0

timer_check = check_timer('timer')

new_state = 0
for item in inputs_int:
  new_state |= item

state_change = bool(new_state ^ previous_state)
state_bool = bool(new_state)
if state_change:
  if not state_bool:
    print('Falling Edge: Creating new timer')
    timer = threading.Timer(motion_timeout, on_timer_finish)
    timer.start()
  elif state_bool and timer_check[2]:
    print('Rising Edge: Cancelling timer')
    timer.cancel()
  else:
    print('Rising Edge: No timer to cancel')

previous_state = new_state

BTW for this example I also tried substituting and simplifying check_timer() and the previous_state global lookup with try/except blocks since they should only be required on the first script run like so:

try:
  dummy = previous_state
  timer_check = timer.is_alive()
except:
  previous_state = 0
  timer = threading.Timer(motion_timeout, on_timer_finish)
  timer_check = False

timer_check is reduced to a boolean rather than a tuple, though sometimes you might want to know if the timer exists and is / is not running separately.

In either case the script settles into running in under 1ms, so there’s no drawback with the initial implementation.