Design Pattern: Looping Timers

I have an oscillating light rule in my rules. It reacts to a dummy switch and then changes light intensity and color.

This has not really been edited for publication, so code hygiene and readability may not be up to rich’ standards, but you can hopefully see how I did it. My frequency is much much lower, if I do it faster my zigbee network clogs up. The randomness I have coded in will result in each lamp being changed every 4 seconds on average.

It will react to the command to the dummy switch, and it will retrigger the command (and thus the rule) each second as long as the dummy switch is not switched off or the lights are all switched off.

rule "Oscillate Colors Gently"
    //will gently vary lamp colors in a room when dummy switch is set
when 
    Item Wellengang_SZ_Dummy received command
then
    //cycle through lamps and randomly change them by a bit
    gColorSZ.members.forEach[lamp |

        //pick a lamp at random
        if ((rand.nextInt(100) > 75) && (lamp.state !== null)){
            if ((lamp.state as HSBType).getBrightness() > 0) {

                // get base color once
                if (baseColor === null){
                    baseColor = (lamp.state as HSBType)
                }

                //pick a new hue within 10 of the current hue and 45 of the base hue
                newHueOffset =  ((lamp.state as HSBType).getHue() - baseColor.getHue() + rand.nextInt(51) - 25).intValue()
                if (newHueOffset < -45) {newHueOffset = -45}
                if (newHueOffset > 45) {newHueOffset = 45}
                newHue = (baseColor.getHue() + newHueOffset).intValue()
                if (newHue < 1) {newHue = 359 - newHue} 
                if (newHue > 359) {newHue = newHue - 359}

                //pick a new brightness within 10 of the current and 30 below the base brightness, slightly skewed towards staying bright
                newBrightnessOffset = ((lamp.state as HSBType).getBrightness() - baseColor.getBrightness() + rand.nextInt(20) - 9).intValue()
                if (newBrightnessOffset < -30) {newBrightnessOffset = -30}
                if (newBrightnessOffset > 0) {newBrightnessOffset = 0}
                newBrightness = (baseColor.getBrightness() + newBrightnessOffset).intValue()
                if (newBrightness < 12) {newBrightness = 12} //no switching off
                if (newBrightness > 100) {newBrightness = 100}

                //pick a new saturation within 10 of the current and 20 above the base saturation
                newSaturationOffset = ((lamp.state as HSBType).getSaturation() - baseColor.getSaturation() + rand.nextInt(30) - 16).intValue()
                if (newSaturationOffset < 0) {newSaturationOffset = 0}
                if (newSaturationOffset > 20) {newSaturationOffset = 20}
                newSaturation = (baseColor.getSaturation() + newSaturationOffset).intValue()
                if (newSaturation < 70) {newSaturation = 70} //always use color
                if (newSaturation > 100) {newSaturation = 100}

                var newHSB = new String (newHue +","+ newSaturation +","+ newBrightness)
                
                    sendCommand( lamp, newHSB )
            }
        } 
    ]

    if ( Wellengang_SZ_Dummy.state == ON) {
        //If lights go off, or switch goes off then end the show. Otherwise retrigger in 1 s    
        wellengangTimer = createTimer(now.plusSeconds(1)) [|
            val totalBright = gColorSZ.allMembers.map[(state as HSBType).getBrightness()].reduce[ sum, n | sum + n ]
            if  ( totalBright == 0 ) {
                gColorSZ.sendCommand( "30,40,0")
                baseColor = null
                Wellengang_SZ_Dummy.postUpdate(OFF)
            }
            else if ( Wellengang_SZ_Dummy.state == OFF) {
                //reset lamps
                sendCommand(gColorSZ, baseColor)
                baseColor = null
            }
            else if ( Wellengang_SZ_Dummy.state == ON) {
                Wellengang_SZ_Dummy.sendCommand( ON )
            }

        ]
    }
end 

To understand how the rules “Looping Timers with Expire Binding” works, I’ve created a program flowchart. (Hopefully without mistakes)
Maybe it will help others too.

Design Pattern - Looping Timers with Expire Binding.pdf (222.9 KB)

Is this still true? I thought I had seen a more recent post from you that indicated that not blocking Rules would still be a problem with JSR223/NGRE.

If it is still the case that it’s not a problem, the above approach would still be a viable solution for JavaScript which, IIRC, has no sleep function.

By ‘threading issues’, I was referring to the thread pool limitations, which do not exist for the NGRE. This means you could potentially have 1000 rules sleeping at the same time and other rules will continue to trigger.

However, a rule’s action must complete before it can be executed again. So, if a rule is triggered again before the action has completed, like if there is a long sleep in the action, the triggers will occur but the actions will just queue up. For example…

from core.rules import rule
from core.triggers import when
from core.utils import validate_uid

from time import sleep

@rule("Test overlapping rule actions")
@when("Time cron 0/1 * * * * ?")
def test_overlapping_actions(event):
    uid = validate_uid(None)
    test_overlapping_actions.log.warn("Start sleep: [{}]".format(uid))
    sleep(10)
    test_overlapping_actions.log.warn("Sleep complete: [{}]".format(uid))
2019-08-20 20:08:18.349 [DEBUG] [org.openhab.core.automation.module.script.rulesupport.internal.loader.ScriptFileWatcher] - Script loaded: python/personal/test/testScript1.py
2019-08-20 20:08:19.348 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:19.367 [WARN ] [jsr223.jython.Test overlapping rule actions] - Start sleep: [c7f9c35ec3a711e985f5001bb952f560]
2019-08-20 20:08:20.357 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:21.360 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:22.360 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:23.361 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:24.362 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:25.362 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:26.363 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:27.364 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:28.365 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:29.365 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:29.368 [WARN ] [jsr223.jython.Test overlapping rule actions] - Sleep complete: [c7f9c35ec3a711e985f5001bb952f560]
2019-08-20 20:08:29.369 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is executed.
2019-08-20 20:08:29.371 [WARN ] [jsr223.jython.Test overlapping rule actions] - Start sleep: [cdf2b19ec3a711e9bc7e001bb952f560]
2019-08-20 20:08:30.366 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:31.367 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:32.368 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:33.368 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:34.369 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:35.369 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:36.370 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:37.371 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:38.372 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:39.372 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:39.372 [WARN ] [jsr223.jython.Test overlapping rule actions] - Sleep complete: [cdf2b19ec3a711e9bc7e001bb952f560]
2019-08-20 20:08:39.373 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is executed.
2019-08-20 20:08:39.378 [WARN ] [jsr223.jython.Test overlapping rule actions] - Start sleep: [d3e92ee1c3a711e9a1c4001bb952f560]
2019-08-20 20:08:40.373 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:41.373 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:42.374 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:43.375 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:44.376 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:45.377 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:46.377 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:47.413 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:48.413 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The trigger 'Time_cron_0_1_c75fcd51c3a711e9ad56001bb952f560_c75fcd52c3a711e9b522001bb952f560' of rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is triggered.
2019-08-20 20:08:49.380 [WARN ] [jsr223.jython.Test overlapping rule actions] - Sleep complete: [d3e92ee1c3a711e9a1c4001bb952f560]
2019-08-20 20:08:49.381 [DEBUG] [org.openhab.core.automation.internal.RuleEngineImpl] - The rule 'dd203c0f-1cd2-416a-8141-4bd3c4c94ba2' is executed.
2019-08-20 20:08:49.385 [WARN ] [jsr223.jython.Test overlapping rule actions] - Start sleep: [d9e0214fc3a711e9a200001bb952f560]

I’ve never had an issue with this behavior, but IMO this is a weak point in the NGRE that deserves some attention at some point.

Hi there,
I’m asking this question here, because I think it’s related.
I need to have a timer that I need to show the progress. Say a 30second timer, and every second I need to update a message. I have it working with a thread, but I doubt this is the best approach.

	while(Alarm_State.state==STATUS_ARMING && Exit_Countdown.state as Number > 0 && gAlarmAway.state==CLOSED) {
    	logInfo("sss","count down -1")
        Exit_Countdown.postUpdate((Exit_Countdown.state as Number)-1)
        Thread::sleep(1000)
    }

Thanks.

You’re right, that’s the worst possible approach because it is a system resource hog (the sleep).

Why not use a looping timer instead?

JSR223 JavaScript

TODO… I believe JavaScript lacks a sleep command so the looping timer approach would work for it.

The ECMA Script 5.1 engine used in OH3 does not support setTimeout or Promise. Both of which would give you the possibility to do this natively with javascript.

But you can use the same approach as with sleep or timers. Here are my examples:

'use strict';

var ZonedDateTime = Java.type("java.time.ZonedDateTime");
var ChronoUnit = Java.type("java.time.temporal.ChronoUnit");
var CompletableFuture = Java.type("java.util.concurrent.CompletableFuture");
var Thread = Java.type("java.lang.Thread");

var ScriptExecution = Java.type("org.openhab.core.model.script.actions.ScriptExecution");
var logger = Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.core.model.script.FutureTest");

function waitWithTimer(timeInMilliSeconds) {
  var future = new CompletableFuture();
  var timer = ScriptExecution.createTimer(ZonedDateTime.now().plus(timeInMilliSeconds, ChronoUnit.MILLIS), function() {
    future.complete(null);
  });
  
  future.get();
}

function waitWithThread(timeInMilliSeconds) {
  Thread.sleep(timeInMilliSeconds);
}

logger.info("Waiting with timer ...");
waitWithTimer(5000);
logger.info("Timer done");

logger.info("Waiting with thread ...");
waitWithThread(5000);
logger.info("Thread done");

It’s way worth to get an idea, of what can be done with using the java.util.concurrent package of the JVM.

2 Likes

@franks Question: Via DSL Rules i use a global variable and set timer to null. If the rule will triggered again, i must check if the timer is set or null. How i do this with Javascript? I missing this in you post. How can i set the timer global and check on next trigger if its running or not.

Thanks.

That’s two different questions.

  1. How do I set a variable as a global? The answer is the same way you do is in .rules files. You define it outside the Rule. But, as with Rules DSL, in the UI there is no way to define a variable outside the rule so there is no way to create a global variable.

  2. How do I keep the value of a variable from one run of a Script Action to the next?

    this.myTimer = (this.myTimer === undefined) ? null : this.myTimer;

1 Like

Is it possible to create a Timer without immediately starting it? I’d like to define the timer and its function globally at the top and occasionally reschedule()ing it while checking its status with isRunning() rather than != null. I could create the Timer with an initial timeout of 10 years or something, but then isRunning() will report true after the rules are initially loaded even though it wasn’t actually started.

No, when you create the timer you’ve created the timer and it’s going to run.

Keep in mind that when you declare things globally there is no context. It can’t see any of your other global variables. Therefore such a timer wouldn’t really be able to do much in the long run.

Also note, per the docs isRunning only tells you if the function passed to the Timer is actively executing code. It will return false if the timer has been scheduled but has not yet started running the function. Since most of the times the function takes all of a few milliseconds to execute, you’d usually be very lucky to ever get true back from a call to isRunning.

You can check for hasTerminated which will return true if the timer has completed running the function and is no longer scheduled to run. But that will require the timer to finish running the function first.

Therefore, in short, there is no shortcut here. You have to account for the timer not having ever been created (timer === null), the timer is scheduled (timer.hasTerminated == false), and the timer executed the function (timer.hasTerminated == true).

Okay, thanks for the clarification.

The Thread.sleep is such a simple approach, I wish I’d spotted this before now!

It fits with my planned use cases perfectly - a series of steps in lighting punctuated by brief pauses - and tidier than the do/while loop I had contemplated. Have added it to my growing toolkit, thank you.