Design Pattern - Timer Management

Please see Design Pattern: What is a Design Pattern and How Do I Use Them for a description of DPs.

Problem Statement

Frequently, when attempting to write generic Rules, especially when applying Design Pattern: Associated Items, one may need to set a separate Timer for each Item.

Concept

Store the Timers into a Map or dict using the Item name as the key.

Example

OH 3.x JavaScript

I’ve written a timerMgr class located at GitHub - rkoshak/openhab-rules-tools: Library functions, classes, and examples to reuse in the development of new Rules. which can be downloaded and used in your rules.

Usage:

// Send an alert message when a door is left open for an hour

// Load TimerMgr
var OPENHAB_CONF = java.lang.System.getenv("OPENHAB_CONF");
load(OPENHAB_CONF+'/automation/lib/javascript/community/timerMgr.js');

this.tm = (this.tm === undefined) ? new TimerMgr() : this.tm;

if(event.itemState == OPEN){
  tm.check(event.itemName,
           "1h",
           function(){ events.sendCommand("Alert", event.itemName + " is still open after an hour!");
}
else if(event.itemState == CLOSED){
  tm.cancel(event.itemName);
}

tm.check will check to see if a timer already exists for event.itemName. If not it creates one to go off in one hour and will call the function to sendCommand to the Alert Item with the door is open message.

Be sure to read the docs for the library as there are arguments which let you control what happens when the timer already exists:

  • reschedule the timer
  • cancel the timer
  • call a different function when the timer already exists

OH 2.5 Python

For Python I’ve written a timer_mgr library you can obtain from GitHub - rkoshak/openhab-rules-tools: Library functions, classes, and examples to reuse in the development of new Rules.. This library implements all the management of timers like this for you. All you need do is instantiate the class and call the methods. See the README at that location for usage and other details.

For example, to use the library to set a Timer to send an alert when a door has been left open for 60 minutes:

from core.rules import rule
from core.triggers import when
from community.timer_mgr import TimerMgr

tm = TimerMgr()

@rule("Send an alert if a door has been left open for 60 minutes")
@when("Member of Doors changed")
def door_reminder(event):
    if event.itemState == OPEN:
        tm.check(event.itemName, 
                 (60*60*1000), 
                 function=lambda: events.sendCommand("Alert", event.itemName + " is still open!"))
    elif event.itemState == CLOSED:
        tm.cancel(event.itemName)

def scriptUnloaded():
    tm.cancel_all()

The above rule will send a command to the Alert Item when the door remains open for more than 60 minutes. If the door closes before 60 minutes the Timer is cancelled. All the book keeping and checking whether the Timer exists and such is handled by the library. You can pass a reschedule=True argument to reschedule the Timer if it already exists and pass in a flapping_function that gets called if the Timer already exists when calling check.

Rules DSL

The below code should work in either OH 2.5 or OH 3. See the “Group findFirst” example at Design Pattern: Associated Items for another full example. Pay attention to the lines where the variable timers is used.

import java.util.Map

val Map<String, Timer> timers = newHashMap

rule "Send alert if a door has been open for more than 60 minutes"
when
    Member of Doors changed
then
    // Always cancel the existing Timer
    val timer = timers.get(triggeringItem.name)
    timer?.cancel
    timers.put(triggeringItem.name, null)

    // Create a Timer if the door opened
    if(triggeringItem.state == OPEN) {
        timer.put(timerItem.name, createTimer(now.plusHours(1), [ |
            Alert.sendCommand(triggeringItem.name + " is still open!")
        ])
    }
end

Advantages and Disadvantages

Managing Timers in this way makes it possible to write generic Rules that work with separate Timers for each Item. When using the Python or JavaScript library, it only takes a couple of lines of code.

Related Design Patterns

Design Pattern How It’s Used
Design Pattern: Associated Items Includes an example of this DP.

Update: Added JavaScript implementation and updated for OH 3.

5 Likes

Dang Rich, I see you been busy! :slight_smile: Two of these (that I noticed) in two days!

I tried searching, but apparently could not put together the right combination of terms to get the most up to date information on the following, so forgive me for going offtopic and by all means please point me in direction of another thread where this is being discussed even semi-recently, as I have not been able to find it…

Anyway, I noticed you seem to be changing all your rules examples over to Python. I also note you are calling it Python and not Jython. I have sort of been re-doing my setup from scratch lately, and I have been thinking this might be a good time to change over, assuming the old Rules DSL is going to be deprecated, or anyway is not “the way” going forward. Or maybe you just chose Python because it is more expressive?

Anyway, this is probably not the place, but hoping maybe you could point me somewhere, I know you know these forums like the back of your hand. :slight_smile:

I use the word “Python” because the language is Python. Jython is the specific implementation of Python that OH currently supports. So for the most part, unless told otherwise, Python means “rules or modules written in Python run by the Jython interperter” or something like that. And they can be used interchangeably when discussing Rules (unless you are talking about pure Python not running within openHAB).

As for why did I switch? Rules work much better using the new rules engine (it will just be *the rules engine" in OH 3). A lot of issues go away. OH 3 has support for .rules files in the new rules engine but it remains to be seen how many, if any, of the issues with the old rules engine go away.

It’s more expressive to use a more fully capable language like Python or JavaScript which supports stuff like libraries, functions, classes, etc. You can’t just import a library and use it in Rules DSL.

It’s much easier to share working and generic code (like my recent posts and other code you will find in the repo above). With Rules DSL copy/paste/edit is about the only option.

It’s faster, though that hardly matters in home automation.

Why did I choose Python/Jython over JavaScript or Groovy?

The Helper Libraries are far more mature for Python than the other languages. That’s pretty much it. Given my druthers, I’d use JavaScript as it doesn’t require installing anything extra into openHAB to use.

But even these Rules I’ve converted to Python are mostly just a bridge until the new UI for building rules becomes available in OH 3. At that point I’ll likely move all my rules again to be stored in JSONDB and distribute rule templates to import and use rather than .py files like that above. At that point I might choose JavaScript instead assuming that OH 3 doesn’t require the installation of anything else to use them.

In the mean time, after I converted my .rules to Python I came up with a bunch of libraries and reusable Rules that should make it easier for others to create Python rules. I submitted them to the Helper Libraries but the PRs still sit unreviewed and I want a way to share them more easily and continue development on them until such time that reviewing such PRs becomes a higher priority. It’s also possible that my “build tools to help users solve common problems in rules” is not what was intended or expected for the Community libraries section of the Helper Libraries and instead they want full capabilities. So I’m moving them to their own repo so I can continue working on them and share them more widely and easily.

1 Like

Thanks, as ever, for the detailed and thoughtful reply.

I had assumed such discussion on rules engines and languages to exist already, but since you answered in here maybe such thread does not exist? If so, maybe we should split these posts off into such a new thread, with more appropriate title? I leave it up to you as you are Mod anyway…

Thanks for clearing this up. Just reading the word “Jython” always induced anxiety for me as I wondered what sort of subtle issues might be introduced simply by virtue of such an abomination. :smile:

So, just to clarify, we might as well just think of it as regular Python? If that’s the case, I do like your choice of using “Python” as it immediately conveys to me the notion that it is just the same old familiar Python. In fact this usage of the term is one thing that immediately made me more comfortable in thinking about moving towards it. Perhaps this is all intentional on your part.

I won’t go quote by quote, but suffice it to say that I am aware of the benefits of using a “real” language as you so eloquently detail. But it will be good for others reading later, as I know is your intention.

It is also always good to hear your take on “the direction” of things in OpenHAB as well, as I know you seem to keep very on top of things around here, and with a much broader focus than whatever few things I come here to look up sporadically. So, cheers. :beers:

Good for you man. Good luck with your little project! I am sure it will help a lot of people (myself included).

EDIT:
One final question(s). Are there plans to eventually deprecate the Rules DSL? Or is all this other stuff supposed to exist in parallell to it? Forever? Or at least the foreseeable future?

Sometimes it’s easier to just answer than find a post. I think I discussed my reasoning somewhere in the Journey to JSR223 Python 1 of 9 or maybe in the Experimental Next-Gen Rules Engine Documentation 1 of : Introduction but didn’t want to scan through them to find. Some of those posts are old too and a lot has changed since posting.

As with anything, there might be quirks or odd differences. I’ve not encountered any but that doesn’t mean they are there. Zulu and OpenJDK are two implementation of Java and there are little differences in behavior and such between those too.

Think of it like differences between compiling with gcc verses xlc. They both compile C++ but there are differences.

All I can say for certain is that Rules DSL has been ported to and is available to run on the OH 3 Rules Engine (called the Next Gen Rules Engine, NGRE, Experimental Rules Engine, etc. in OH 2.). The current Rules DSL Rules Engine is gone but the language remains supported. For how long remains to be seen but I would expect the language to be supported for a good long time.

Rules DSL still has support in OH 3. But there will be subtle differences in how they run. I don’t know the extent of the differences yet.

1 Like

Hi,
I am having some problems using @rlkoshak timer_mgr, if I define more than one timer at the same time. I have definded a group of DateTime items called “Routinen”:

Group:DateTime                  Routinen                                "Routinen"                                                  <calendar>  
DateTime                        Rolladen_Wohnzimmer_Morgens             "Zeitsteuerung"                                             <time>          (Routinen)                              {target="Rolladen_Wohnzimmer", task="UP"}
DateTime                        Rolladen_Wohnzimmer_Abends              "Zeitsteuerung"                                             <time>          (Routinen)                              {target="Rolladen_Wohnzimmer", task="UP"}
DateTime                        Rolladen_Wohnzimmer_Mittags             "Zeitsteuerung"                                             <time>          (Routinen)                              {target="Rolladen_Wohnzimmer", task="DOWN"}

Now I have defined a rule, which triggers, when a Member of Routinen is changed

I am using this rule:

this.OPENHAB_CONF = (this.OPENHAB_CONF === undefined) ? java.lang.System.getenv("OPENHAB_CONF") : this.OPENHAB_CONF;
load(OPENHAB_CONF+'/automation/lib/javascript/community/timerMgr.js');
load(OPENHAB_CONF+'/automation/lib/javascript/community/timeUtils.js');
this.ZonedDateTime = (this.ZonedDateTime === undefined) ? Java.type("java.time.ZonedDateTime") : this.ZonedDateTime;

var log = Java.type("org.openhab.core.model.script.actions.Log");

// Only create a new manager if one doesn't already exist or else it will be wiped out each time the rule runs
this.tm = (this.tm === undefined) ? new TimerMgr() : this.tm;


log.logWarn("Routinen", "Routine "+ event.itemName +" activated.  - Time: " + event.itemState );

this.tm.check(event.itemName,
              event.itemState,
              function() {
                           log.logWarn("Test", "Timer " + event.itemName + " ended"); 
                         }
             );

(The complete rule is more complex because of using metadata)

Thats working fine for one Timer. But if I change more than one item of the Group “Routinen”, it seems to override some variables.

Example:

If I use this scipt as a trigger:

var Rolladen_Wohnzimmer_Morgens = "2021-04-05T13:03:31.000+0200";
var Rolladen_Wohnzimmer_Abends = "2021-04-05T13:03:41.000+0200";
var Rolladen_Wohnzimmer_Mittags = "2021-04-05T13:03:51.000+0200";

events.postUpdate('Rolladen_Wohnzimmer_Morgens', Rolladen_Wohnzimmer_Morgens);
events.postUpdate('Rolladen_Wohnzimmer_Abends', Rolladen_Wohnzimmer_Abends);
events.postUpdate('Rolladen_Wohnzimmer_Mittags', Rolladen_Wohnzimmer_Mittags);

3 Timers are defined, thats the log:

2021-04-05 13:03:22.486 [WARN ] [g.openhab.core.model.script.Routinen] - Routine Rolladen_Wohnzimmer_Morgens activated.  - Time: 2021-04-05T13:03:31.000+0200

2021-04-05 13:03:22.675 [WARN ] [g.openhab.core.model.script.Routinen] - Routine Rolladen_Wohnzimmer_Abends activated.  - Time: 2021-04-05T13:03:41.000+0200

2021-04-05 13:03:22.700 [WARN ] [g.openhab.core.model.script.Routinen] - Routine Rolladen_Wohnzimmer_Mittags activated.  - Time: 2021-04-05T13:03:51.000+0200

Looks fine. The Timers are set and ending at :31, :41 and :51. But the logs shows:

2021-04-05 13:03:31.065 [WARN ] [org.openhab.core.model.script.Test  ] - Timer Rolladen_Wohnzimmer_Mittags ended
2021-04-05 13:03:41.003 [WARN ] [org.openhab.core.model.script.Test  ] - Timer Rolladen_Wohnzimmer_Mittags ended
2021-04-05 13:03:51.001 [WARN ] [org.openhab.core.model.script.Test  ] - Timer Rolladen_Wohnzimmer_Mittags ended

The time is correct, but event.itemName is not correct any more. “Rolladen_Wohnzimmer_Mittags” is the last one, which seems to override the other values . Does anyone know, how to solve that? I would like to use a simple Rule for define these timers.

This is a problem in the language. There is a similar problem in Python as well. Without going into a whole lot of detail, unlike in Rules DSL where each lambda function that gets passed to the timer gets a copy of the current set of variables, in JavaScript the lambda gets a pointer to the actual variable.

And because the context gets reused for every call to the script action, the event variable gets reused.

Combine those two and what you end up with is event.itemName in your Timer will show you what ever item most recently triggered the rule, not the name of the item that triggered the rule when the Timer was created.

To avoid this you need to “fix” the state of the variables before you create the function that gets passed to the timer so that it gets a copy instead of the pointing to the “real” events varaible.

Based on my research the “JavaScript” way to do this is through function generators.

this.OPENHAB_CONF = (this.OPENHAB_CONF === undefined) ? java.lang.System.getenv("OPENHAB_CONF") : this.OPENHAB_CONF;
load(OPENHAB_CONF+'/automation/lib/javascript/community/timerMgr.js');
load(OPENHAB_CONF+'/automation/lib/javascript/community/timeUtils.js'); // you don't need this import
this.ZonedDateTime = (this.ZonedDateTime === undefined) ? Java.type("java.time.ZonedDateTime") : this.ZonedDateTime;

var log = Java.type("org.openhab.core.model.script.actions.Log");

// Only create a new manager if one doesn't already exist or else it will be wiped out each time the rule runs
this.tm = (this.tm === undefined) ? new TimerMgr() : this.tm;


log.logWarn("Routinen", "Routine "+ event.itemName +" activated.  - Time: " + event.itemState );

var timerFuncGenerator = function(itemName) {
    return function() {
        log.logWarn("Test", "Timer " + event.itemName + " ended"); 
    }
}

this.tm.check(event.itemName,
              event.itemState,
              timerFuncGenerator(event.itemName)
             );

Another approach, which would require changes to timerMgr itself, might be to use createTimerWithArguments instead of createTimer but that is on my long and growing list of things to do.

Thanks, was afraid that could be the problem, I will test your suggested fix later.

But I don’t understand why thats only the problem with item event.itemName. The Timer expires at the correct time, so event.itemState should be correct, right?

I don’t think you understood my explanation.

event gets reused. All three timers are pointing at the same event variable. The sequence of events is as follows:

Event event.itemName value Action
rule is triggered Rolladen_Wohnzimmer_Morgens timer is created
rule is triggered Rolladen_Wohnzimmer_Abends another timer is created
rule is triggered Rolladen_Wohnzimmer_Mittags a third timer is created
first timer runs Rolladen_Wohnzimmer_Mittags the current value of event.itemName is Rolladen_Wohnzimmer_Mittags and that’s what is logged.

The timer sees the current and most recent value of event.itemName, not what it happened to be when the Timer was create.

It’s not only a problem with event.itemName. Any variable used by the timer’s function will have the same issue. Unless you force a copy to be made by using a function generator, all the timers will be using the exact same variable meaning that what ever value the variable had when the timer was created will be replaced by subsequent runs of the rule.

1 Like

Thanks for your patience, now I understood!
I changed the code and added the function generator, but the problems stays the same:

2021-04-05 18:31:31.036 [WARN ] [org.openhab.core.model.script.Test  ] - Timer Rolladen_Wohnzimmer_Mittags ended
2021-04-05 18:31:41.002 [WARN ] [org.openhab.core.model.script.Test  ] - Timer Rolladen_Wohnzimmer_Mittags ended
2021-04-05 18:31:51.001 [WARN ] [org.openhab.core.model.script.Test  ] - Timer Rolladen_Wohnzimmer_Mittags ended

Show the code.

I just copied your code from Design Pattern - Timer Management - #7 by rlkoshak.

Ok, there is a copy and paste error there. Log itemName, the name of the variable passed to timerFuncGenerator as an argument, not event.itemName.

1 Like

Great, thats working as expected! Now I will add the other variables and metadata the same way.

Dear Rich,
i tried to use your DP time_mgr and time_utils to create rules ising timers.
Unfortunately i am not able to get it run with your *tests.py files. I got errors that org.joda.time is not available (somewhere i read that OH3 does not support Joda any longer). I switched the output of time_utils.py to python and Java without succes.
Do you have a hint how to get your timers run in OH3?
Best regards
Pudermueller

As I’ve said a few posts up in this thread (don’t worry I don’t expect you to have read that), only the JavaScript version is written for and working in OH 3. I have no plans to update the Rules DSL version (and will probably remove it from the original post at some point when I have time) and the Python version only works in OH 2. I’ve not had time and likely will not have time to update any of the openhab-rules-tools Python libraries to OH 3. The challenging part is making them backwards compatible with Oh 2.5 so as not to cut off those who still use them there.

But PRs are always welcome for anyone willing to take a shot at it.

You have to migrate all the uses of Joda DateTime to java.time.ZonedDateTime.

1 Like

java.time.ZonedDateTime seems to work, at least when I print debug messages to the log I get valid time stamps for ZonedDateTime.now().plusMinutes(5).
But there is still something wrong with my timer - instead of delaying for the time configured, the timer function is called hundreds of times without delay until I get a RuntimeError: maximum recursion depth exceeded (Java StackOverflowError).

I think I follow the example in the helper-libraries, I use the ScriptExecution.createTimer version since @rlkoshak mentions in another post that Pythons threading.Time is unreliable for rescheduling timers, and I replaced Joda DateTime with java.time.ZonedDateTime.
So what’s wrong? Complete rule:

from core.rules import rule
from core.triggers import when
from core.log import logging, LOG_PREFIX
from core.actions import ScriptExecution
from java.time import ZonedDateTime

log = logging.getLogger("{}.colorscenes".format(LOG_PREFIX))
rotateTimer = None
timeInBetweenRotations = 10 # in minutes


@rule("Random Colors - Syncron", description="random changing colors synchronized over all color lights", tags=["color", "scene"])
@when("Item SceneColor received command 400")
def syncedColors(event):
    # 1. Check to see if the Rule has to run at all, if not exit
    if event.itemCommand != DecimalType(400): return
    log.info("Rule \"Random Colors - Syncron\" triggered")

    # 2. Calculate what needs to be done
    log.debug("if timer != null: cancel timer")
    global rotateTimer
    if rotateTimer is not None and not rotateTimer.hasTerminated():
        rotateTimer.cancel()
        log.debug("stopped timer")
    log.debug("if all color lights are off: do nothing")
    if ir.getItem("gColorLights").state == OFF:
        log.debug("all color lights are OFF -> do nothing")
        return
    
    # 3. Do it. Only do actions in like sendCommand in one place at the end of the Rule
    def timerFunc():
        global timeInBetweenRotations
        global rotateTimer
        log.debug("create random number between 1-360")
        color = 12 #TODO (Math::random * 360.0).intValue + 1
        log.debug("send commands to rooms")
        events.sendCommand("SceneColorBedroom", str(color))
        events.sendCommand("SceneColorLivingroom", str(color))
        if rotateTimer is None or rotateTimer.hasTerminated():
            log.debug("reschedule timer")
            rotateTimer = ScriptExecution.createTimer(ZonedDateTime.now().plusMinutes(timeInBetweenRotations), timerFunc())
    log.debug("call timer function")
    timerFunc()
    log.debug("Rule \"Random Colors - Syncron\" finished")

Okay - I found my mistake. Yes, I’m just getting started with Python syntax, so I was not aware about the difference of using timerFunc()or timerFunc in the createTimer call.

  • versions 1 (timerFunc()) calls the function - immediately
  • version 2 (timerFunc) threads the function as an argument, so the function will only be called when the timer is finished (Thats what we want here!)

That’s an important thing to understand in JavaScript too. In Rules DSL functions that can be passed around like any other variable are special and we call them “lambdas”. Any time you see [ | in Rules DSL you are seeing the creation of a lambda. And, as you should see by now, createTimer requires a lambda which is the function that gets called when the timer expires.

In JavaScript, regular old functions can already be passed around like any other variable. So you don’t have any special syntax to define such a function. It’s just a function. Therefore, in order to distinguish between when you want the function and when you want to call the function, the parens are used. As you accurately describe, passing the function itself is timerFunc without the parens. Calling the function though would use timerFunc() with the parens.

In Python there is also the concept of a lambda. But in this case, the lambda can only contain one line of code. Usually you want to do more than that so you would call some other function you’ve created from the lambda.

There is a further complication that comes in with JavaScript and in rare cases in Python too. Sometimes you need to create a function and explicitly pass the data it needs to it as an argument and then that function returns the function that gets called. I’ve seen this called a “functionGenerator”. the reason you might want to do this is if you just rely on the implicit existence of a variable to use it inside the function (e.g. if you use event.itemName in timerFunc), when the timer finally runs, event.itemName will be what ever it was the most recent time the rule was triggered, not the value it was when the timer was created.

In JavaScript you would do something like:

var timerFunGenerator = function(itemName) {
    return function() {
        // code that runs when the timer expires goes here
    };
};

ScriptExecution.createTimer( sometime, timerFunGenerator(event.itemName));

In Python it would look something like:

def timerFunc(itemName):
    # code that runs when the timer expires goes here

ScriptExecution.createTimer(sometime, lambda itemName=event.itemName: timerFunc(itemName))

Most of the time in Python this isn’t necessary. However, when iterating over a collection of something it’s required or else all the timerFuncs get called with what ever was the last item in the list.

3 Likes

@rlkoshak do you already have migrated your amazing library to oh3 conditions? Like the joda.time issue?
I want to use the timer management, but only find the oh2 version in your repo