Single and Recursive Timers NGRE

Tags: #<Tag:0x00007f61703699e8> #<Tag:0x00007f6170369808> #<Tag:0x00007f61703694e8>

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.cancel()
    light_timer = Timer(......

Solution

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():
    try:
        timer_var_single.cancel()  
        timer_var_recursive.cancel()
    except:
        pass

# 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.

automation/lib/personal/timers.py
CHANGE:

  • 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
        try:
            with SingleTimer.__lock:
                if name != None and to != None and fnc != None:
                    for thread in threading.enumerate():
                        if isinstance(thread, _Timer) and str(thread.name) == self.__name:
#                            log.info("*** STOPPING THREAD : {}".format(thread.name))
                            thread.cancel() 
                    self.__running = True
                    tmr = Timer(to, lambda: self.__job())
                    tmr.setName(name)
                    tmr.start()
        except Exception, e: 
            log.info("SingleTimer __init___() Exception: {}".format(e))
    
    def __job(self):
        try:
            self.__fnc()
            self.__running = False
        except Exception, e:
            log.info("SingleTimer __job() Exception: {}".format(e))

    def isRunning(self):
        return self.__running

    def cancel(self):
        try:
            with SingleTimer.__lock:
                for thread in threading.enumerate():
                    if isinstance(thread, _Timer) and str(thread.name) == self.__name:
                        thread.cancel() 
            self.__running = False
        except Exception, e:
            log.info(" 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
            self.__job(init_run)

    def cancel(self):
        self.__single_tmr.cancel()

    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):
        try:
            
            if init_run or not self.__first_run:
                self.__cycles = self.__cycles + 1
                self.__cycles_left = self.__cycles_left - 1
                self.__fnc()
                
            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: 
            log.info(" 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
imp.reload(personal.timers)
from personal.timers import RecursiveTimer, SingleTimer

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

def scriptUnloaded():
    try:
        log.info("************* Unload Hello World ********* ")              
        single_timer.cancel()  
        recuring_timer.cancel()
    except:
        pass
  
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):
        log.info('*** Startup Trigger Fired ***')

    def singleTimerfunction(self):
        log.info('*** Single Timer after 10s Fired ***')
        
    def recuringTimerFunction(self):
        log.info('*** 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

2 Likes

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 timers.py
see comment.

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