Design Pattern: Looping Timers

Thanks for your reply. Sorry i just posted here when i saw you responded to my original post.

What is the best way to implement a daemon process which runs always? I don’t want to start it in the “system startup” event because if I develop it I don’t want to restart the system in every minute. So I have created this cron below to start it. But if I change the code, I got I huge exception in the logs. I don’t have problem with that, but is this the best solution for it?

var Timer timer = null

rule "cron process"
when
	Time cron "0/10 * * * * ? *"
then
	if (timer === null)
	{
		logInfo("cron", "start timer")
		timer = createTimer(now, [ |
        	//do the job here....
			
			timer.reschedule(now.plusMillis(200))
    	])
	}
end
2 Likes

The exception in the logs is because the timer will still be running when you save the code, but the code that is supposed to run is recompiled. So this creates a mess when the timer expires and tries to run the code which is no longer where the timer thought it was. So this is quite normal when developing with timers.

I guess this is a possible way to always have some code triggered, but the question always is: Is there not a better way, like reacting to a more specific event to execute some code.

If it is some piece of code that evaluates something and then does nothing in 99% of cases, I would try to make it as efficient as possible in evaluating the case of “nothing to do”.

1 Like

Arno has nailed the answer. But I have my own additional concerns.

OH is not a real time system. It really is not equipped to handle polling something from Rules as fast as you are trying to do. Personally, I’d say anything faster than 750 msec should be moved outside of OH Rules and placed in an external service that polls whatever is being polled and reports changes to OH through the REST API or MQTT.

I don’t think JSR223 Rules will necessarily have the same issue but even there I’d consider polling something this fast as inappropriate. Rapid polling like this really needs to be in the binding or in the device that is reporting to OH.

I suppose you don’t realize that if you change a .rules file, OH will rerun the System started Rules when it reloads the file. There is no need to restart anything to trigger System started Rules. Furthermore, for testing while you are developing, you can always add a testing Switch as a trigger and manually kick off the Rule.

I usually recommend against creating permanent solutions to temporary problems.

Basically what I want to do is to create some kind of “RGB led daemon” for xiaomi gateway which handles and executes the different kind of lightning effects coming from the different rules. For example if I ask this service (via a virtual Item) to dim the light with red color, then this service would turn on the light, change the color, and change the brightness of the light in every 200ms (or 500ms, we will see). This is the reason why I want to implement this logic in OpenHAB. Embedding and copy-pasting this logic for several rules is not the best way, I think.
On the other hand, I would like to implement another service (which runs e.g. in every seconds) which is responsible to handle the “light requests” in the gateway. For example I have a yellow light effect, which means a window is open (remains yellow, until the window is closed). But I have another light effect (blinkling 5 times the blue light), which means the entrance door has been opened. So if the yellow light is on, but someone enters the door, then the gateway should blink the blue light 5 times and then it should switches back to yellow. And this scenario should be more convenient to be implemented with a daemon process, which “remembers” the previously used light color.
These scenarios are only in my theory yet. We will see, what the xiaomi gateway let me to do. :slight_smile:

I’m not sure that Xiaomi gateway can accept and process commands that fast. I know Hue can’t based on what others have reported.

But, based on what you describe, you should trigger the rule when your virtual Item receives a command, then run the looping timer only until the light reaches the desired state. Then the timer exits. Don’t have the CPU sit there spinning and doing nothing if you can help it.

You don’t need to loop for this. You just need to trigger a Rule when the entrance door is opened. Save the current state of the light to a variable. Then blink the light how ever you want. Finally restore the light to the state stored in the variable.

It is absolutely NOT more convenient to implement with a looping timer.

OH is event driven. Write your Rules to respond to events. Don’t have it sitting there looping waiting for things to happen. Let is sleep and react when something does happen.

Thank you for the answer and the suggestion. I also would like to approach most of the scenarios in event-based way. Basically only the first example (light dimming) lead me to this “Looping timers” pattern, but I also have fears that if I force the gateway to dim the whole night, then probably next day I have to order a new device :slight_smile:
If the gateway does not support native dimming effect, then probably I should not force it to do.

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.