Single and Recursive Timers NGRE

Starting Point

When migrating my code from DSL to NGRE I was happy to get rid of my lambda-heavy-loaded code and define clear class structures.

A lot of problems I still had with the usage of timers. I have a lot of single-shot timers but also recursive timers. For the single-shot timers, I had situations in which timers for the same purpose were overlapping and creating racing conditions. On the other side, I had the situation that recurring timers were not cleaned up during the rule module reload. The recursive timers were then running in parallel producing unpredictable results.

For example, such a situation happened:

light_timer = Timer(60, lambda: self.turn-light_off())
# on other place in code
# here the previous timer is not stopped
light_timer = Timer(60, lambda: self.turn-light_on())

All around the code the usage pattern was:

if light_timer != None:
    light_timer = Timer(......


I defined 2 classes: SingleTimer and RecursiveTimer

The usage pattern is:

# define a timer variable as global or class member
timer_var_single = SingleTimer()
timer_var_recursive = RecursiveTimer()

# define cleanup when a module reloaded
def scriptUnloaded():

# when and where the timer is needed, assign to the variable the timer again
# give the timer a unique name, timeout value and the function to called
timer_var_single = SingleTimer("MySingleTimer10s", 10, lambda: timer_function())
timer_var_recursive = RecursiveTimer("MyRecursiveTimer60s", 60, lambda: polling_timer_function())

The timer classes maintain a dictionary of all timers based on the timer name. If a name is reused the running timer is canceled and a new is started. There is no need to test on None prior to cancel too.


  • removed inheritance between SingleTimer and RecursiveTimer and
  • added thread locking
  • removed destructor of SingleTimer
  • added exception handling
import threading, sys
from threading import Timer, _Timer
from core.log import logging, LOG_PREFIX
log = logging.getLogger(LOG_PREFIX + ".timers.log")     

class SingleTimer(object):
    __lock = threading.Lock()
    def __init__(self, name = None, to = None, fnc = None):
        self.__name = name
        self.__fnc = fnc
        self.__running = False
            with SingleTimer.__lock:
                if name != None and to != None and fnc != None:
                    for thread in threading.enumerate():
                        if isinstance(thread, _Timer) and str( == self.__name:
#                  "*** STOPPING THREAD : {}".format(
                    self.__running = True
                    tmr = Timer(to, lambda: self.__job())
        except Exception, e: 
  "SingleTimer __init___() Exception: {}".format(e))
    def __job(self):
            self.__running = False
        except Exception, e:
  "SingleTimer __job() Exception: {}".format(e))

    def isRunning(self):
        return self.__running

    def cancel(self):
            with SingleTimer.__lock:
                for thread in threading.enumerate():
                    if isinstance(thread, _Timer) and str( == self.__name:
            self.__running = False
        except Exception, e:
  " SingleTimer cancel() Exception: {}".format(e))

class RecursiveTimer(object):
    def __init__(self, name = None, to = None, fnc = None, init_run = False, cycles_to_run = None):
        self.__single_tmr = SingleTimer()
        self.__cycles = 0
        self.__cycles_left = sys.maxint if cycles_to_run == None else cycles_to_run
        if name != None and to != None and fnc != None:
            self.__first_run = True
            self.__fnc = fnc
            self.__to = to 
            self.__name = name

    def cancel(self):

    def cyclesToRun(self, cycles_to_run):
        self.__cycles_left = cycles_to_run

    def cyclesLeft(self):
        return self.__cycles_left

    def cyclesDone(self):
        return self.__cycles
    def __job(self, init_run = False):
            if init_run or not self.__first_run:
                self.__cycles = self.__cycles + 1
                self.__cycles_left = self.__cycles_left - 1
            if self.__cycles_left > 0:
                self.__first_run = False
                self.__single_tmr = SingleTimer(self.__name, self.__to, lambda: self.__job(init_run))
        except Exception, e: 
  " RecursiveTimer __job() Exception: {}".format(e))

Hello World Example

from core.rules import rule
from core.log import logging, LOG_PREFIX
from core.triggers import StartupTrigger
import threading
from org.python.core import FunctionThread
import imp
import personal.timers
from personal.timers import RecursiveTimer, SingleTimer

log = logging.getLogger(LOG_PREFIX + ".hello.log")    

def scriptUnloaded():
    try:"************* Unload Hello World ********* ")              
single_timer = SingleTimer()
recuring_timer = RecursiveTimer()

@rule("Hello World Timer", description="TimerTest")
class ExampleExtensionRule(object):
    def __init__(self):
        self.triggers = [StartupTrigger("START").trigger]
        global single_timer, recuring_timer
        single_timer = SingleTimer("MySingleTimer10s", 10, lambda: self.singleTimerfunction())
        recuring_timer= RecursiveTimer("MyRecurringTimer5s", 5, lambda: self.recuringTimerFunction())

    def execute(self, module, inputs):'*** Startup Trigger Fired ***')

    def singleTimerfunction(self):'*** Single Timer after 10s Fired ***')
    def recuringTimerFunction(self):'*** Recurring Timer every 5s Fired***')

In the example, the usage of these classes might look like a big overhead. I can assure you once the code gets more complicated and dynamic this will save a lot of headaches.

Feedback is wormly wellcome


I find it helps understanding how timers get “orphaned” by remembering that in code like this, the light_timer variable is only a handle or pointer to a completely independent timer.
Destroying or changing the handle does not affect the independent timer … but now you’ve lost your only means to communicate with it.

Even worst if you have a recursive timer. The only solution is restarting OH.

Changed the
see comment.

This topic was automatically closed 41 days after the last reply. New replies are no longer allowed.