Alternative to multiple setTimeout statements in JS rules

I was looking for a way to turn on ceiling spot lights in sequence. My first approach was to use setTimeout with a 500ms delay between each sendCommand statement. That resulted in a “Multithreaded Access Error.” After some searching in the forums it seems multiple setTimout statements aren’t allowed.

I googled around and found another solution that worked. A sleep-like function that uses javascript’s Promise object and setTimeout. You can find it here: https://www.sitepoint.com/delay-sleep-pause-wait/

Here is what my ECMA-11 script looks like:

var trigger = event.itemName.toString();
function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}
if(trigger == "LivingroomSensor_MotionAlarm"){
  items.getItem("HuespotHallway2_Color").sendCommand("5,100,30");
  sleep(600)
    .then(() => items.getItem("HuespotHallway1_Color").sendCommand("5,100,30"))
    .then(() => sleep(600))
    .then(() => items.getItem("HuespotDR4_Color").sendCommand("5,100,30"))
  setTimeout(()=>{if(items.getItem("HueRoomLR_Power").state == "OFF"){
    items.getItem("HuespotHallway2_Color").sendCommand("30,65,0");
    sleep(600)
      .then(() => items.getItem("HuespotHallway1_Color").sendCommand("5,100,30"))
      .then(() => sleep(600))
      .then(() => items.getItem("HuespotDR4_Color").sendCommand("5,100,30"));}},120000)
}
  else {
    items.getItem("HuespotDR4_Color").sendCommand("5,100,30");
    sleep(600)
      .then(() => items.getItem("HuespotHallway1_Color").sendCommand("5,100,30"))
      .then(() => sleep(600))
      .then(()=> items.getItem("HuespotHallway2_Color").sendCommand("5,100,30"))
    setTimeout(()=>{if(items.getItem("HueRoomLR_Power").state == "OFF"){
      items.getItem("HuespotDR4_Color").sendCommand("30,65,0");
      sleep(600)
        .then(() => items.getItem("HuespotHallway1_Color").sendCommand("30,65,0"))
        .then(() => sleep(600))
        .then(() => items.getItem("HuespotHallway2_Color").sendCommand("30,65,0"));}},120000)
  }

Please note: I am not a programmer. I was just luckily able to find a solution on the internet and cobble it into a rule. I can’t promise to answer any technical questions on this technique. Hopefully the link will be able to answer any questions you may have. I just wanted to share my solution that worked.

1 Like

This is a great example for how to use Promise. Thanks for posting!

Not quite. You can create as many as you want, but only one can run at a time. If you are careful to stagger the times so they don’t run at the same time they will work fine.

Another way you can sequence events is to use the Gatekeeper design pattern which I’ve provided as a part of openhab_rules_tools which you can install using npm.

So the above could be:

var {gatekeeper} = require('openhab_rules_tools');

var gk = cache.put(ruleUID+'_gatekeeper', () => new gatekeeper.Gatekeeper());

if(trigger = 'LivingroomSensor_MotionAlarm') {
  gk.addCommand(600, () => { items.getItem('HuespotHallway2_Color').sendCommand('5,100,30'); });
  gk.addCommand(600, () => { items.getItem('HuespotHallway1_Color').sendCommand('5,100,30'); });
  gk.addCommand('PT2m', () => { items.getItem('HuespotDR4_Color').sendCommand('5,100,30'); });
  gk.addCommand(0, () => {  if(items.getItem("HueRoomLR_Power").state == "OFF") {
      gk.addCommand(600, () => { items.getItem("HuespotHallway2_Color").sendCommand("30,65,0"); } );
      gk.addCommand(600, () => { items.getItem("HuespotHallway1_Color").sendCommand("5,100,30"); } );
      gk.addCommand(0, () => { items.getItem("HuespotDR4_Color").sendCommand("5,100,30"); } );
    }
  }; );
}
else { 
  ...

The first number is how long to wait after the passed in command is executed before running the next command. So the first line will command 'HuespotHallway2_Color, wait 600 msec and then run the command to HuespotHallway1_Color, wait 600 msec then run the command to HuespotDR4_Color. Then it'll wait two minutes (see the docs for time.toZDT()` in the JS Scripting addon for details on how that works) and run the if statement. If the if statement evaluates to true it adds those commands to the list with the indicated 600 msec spacing.

I’m not saying this is necessarily better, just pointing out alternatives. The reason why this works is that there is only the one timer in Gatekeeper. So you can never have two running at the same time.

1 Like

Since my rule using Promise has been working, I didn’t bother to change it to use gatekeeper. But over the past two years I have come to appreciate what gatekeeper can do, so I decided to use this rule to practice using it. When I initially changed the rule I ran into some errors, which I was able to solve by changing, “var gk = cache.put…” to “var gk = cache.private.put”, and by changing "if(trigger = “LivingroomSensor_MotionAlarm’)” to "if(trigger == “LivingroomSensor_MotionAlarm’)”

I then ran into some problems that were a bit trickier. The errors in the logs were:

2024-08-22 21:37:11.357 [WARN ] [tion.script.ui.LivingRoom_NightLight] - require Gatekeeper instead of gatekeeper and use Gatekeeper() instead of new gatekeeper.Gatekeeper().
2024-08-22 21:37:11.459 [ERROR] [ipt.javascript.LivingRoom_NightLight] - Failed to execute script: TypeError: Cannot read property "addCommand" from null
        at <js>.:program(<eval>:71)
        at org.graalvm.polyglot.Context.eval(Context.java:399)
        at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:458)
        at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:426)
        at java.scripting/javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:262)
        ... 21 more
2024-08-22 21:37:11.459 [ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID 'LivingRoom_NightLight' failed: org.graalvm.polyglot.PolyglotException: TypeError: Cannot read property "addCommand" from null
2024-08-22 22:44:03.929 [WARN ] [tion.script.ui.LivingRoom_NightLight] - require Gatekeeper instead of gatekeeper and use Gatekeeper() instead of new gatekeeper.Gatekeeper().
2024-08-22 22:44:03.929 [ERROR] [ipt.javascript.LivingRoom_NightLight] - Failed to execute script: TypeError: gk.addCommand is not a function
        at <js>.:program(<eval>:71)
        at org.graalvm.polyglot.Context.eval(Context.java:399)
        at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:458)
        at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:426)
        at java.scripting/javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:262)
        ... 21 more
2024-08-22 22:44:03.930 [ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID 'LivingRoom_NightLight' failed: org.graalvm.polyglot.PolyglotException: TypeError: gk.addCommand is not a function

So this morning I copied the gatekeeper code into a script, and worked on it. The script would just turn on the lights sequentially, then turn them off sequentially. Here’s the script:

var {gatekeeper} = require('openhab_rules_tools');

var gk = cache.private.put('test_gatekeeper', () => new gatekeeper.Gatekeeper());

gk.addCommand(600, () => { items.getItem('HuespotHallway2_Color').sendCommand('5,100,30'); });
gk.addCommand(600, () => { items.getItem('HuespotHallway1_Color').sendCommand('5,100,30'); });
gk.addCommand('PT1m', () => { items.getItem('HuespotDR4_Color').sendCommand('5,100,30'); });
gk.addCommand(600, () => { items.getItem("HuespotHallway2_Color").sendCommand("30,65,0"); });
gk.addCommand(600, () => { items.getItem("HuespotHallway1_Color").sendCommand("5,100,0"); });
gk.addCommand(0, () => { items.getItem("HuespotDR4_Color").sendCommand("5,100,0"); });

This script threw the following errors:

2024-08-23 09:39:20.911 [WARN ] [nhab.automation.script.ui.scratchpad] - require Gatekeeper instead of gatekeeper and use Gatekeeper() instead of new gatekeeper.Gatekeeper().
2024-08-23 09:39:21.003 [ERROR] [omation.script.javascript.scratchpad] - Failed to execute script: ReferenceError: "gk" is not defined
        at <js>.:program(<eval>:3)
        at org.graalvm.polyglot.Context.eval(Context.java:399)
        at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:458)
        at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:426)
        at java.scripting/javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:262)
        ... 75 more
2024-08-23 09:39:21.003 [ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID 'scratchpad' failed: org.graalvm.polyglot.PolyglotException: ReferenceError: "gk" is not defined
2024-08-23 09:39:51.174 [WARN ] [nhab.automation.script.ui.scratchpad] - require Gatekeeper instead of gatekeeper and use Gatekeeper() instead of new gatekeeper.Gatekeeper().
2024-08-23 09:39:51.238 [ERROR] [omation.script.javascript.scratchpad] - Failed to execute script: TypeError: Cannot read property "addCommand" from null
        at <js>.:program(<eval>:4)
        at org.graalvm.polyglot.Context.eval(Context.java:399)
        at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:458)
        at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:426)
        at java.scripting/javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:262)
        ... 75 more
2024-08-23 09:39:51.238 [ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID 'scratchpad' failed: org.graalvm.polyglot.PolyglotException: TypeError: Cannot read property "addCommand" from null
2024-08-23 09:40:34.370 [WARN ] [nhab.automation.script.ui.scratchpad] - require Gatekeeper instead of gatekeeper and use Gatekeeper() instead of new gatekeeper.Gatekeeper().
2024-08-23 09:40:34.470 [ERROR] [omation.script.javascript.scratchpad] - Failed to execute script: TypeError: Cannot read property "addCommand" from null
        at <js>.:program(<eval>:4)
        at org.graalvm.polyglot.Context.eval(Context.java:399)
        at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:458)
        at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:426)
        at java.scripting/javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:262)
        ... 75 more
2024-08-23 09:40:34.470 [ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID 'scratchpad' failed: org.graalvm.polyglot.PolyglotException: TypeError: Cannot read property "addCommand" from null

So, after looking at some other examples of gatekeeper usage, I modified the script to the below, and everything worked fine.

var {gatekeeper} = require('openhab_rules_tools');
var gk = new gatekeeper.Gatekeeper;
cache.private.put('lrNightLight_gatekeeper', time.ZonedDateTime.now())

gk.addCommand(600, () => { items.getItem('HuespotHallway2_Color').sendCommand('5,100,30'); });
gk.addCommand(600, () => { items.getItem('HuespotHallway1_Color').sendCommand('5,100,30'); });
gk.addCommand('PT2m', () => { items.getItem('HuespotDR4_Color').sendCommand('5,100,30'); });
gk.addCommand(600, () => { items.getItem("HuespotHallway2_Color").sendCommand("30,65,0"); });
gk.addCommand(600, () => { items.getItem("HuespotHallway1_Color").sendCommand("5,100,0"); });
gk.addCommand(0, () => { items.getItem("HuespotDR4_Color").sendCommand("5,100,0"); });

I have encorporated the above code into my rule, and will wait till tonight, when the conditionals are met, to see how it works.

FYI the underlying issue with missing synchronisation has been fixed end of 2022.
Now, nearly all entry points into the script are synchronised so the Multithreaded Access Error should be gone.

BTW really nice use of Promises here — I should probably add Promises to the docs.

That’s a change that was made to how the cache is accessed arounf OH 4.1 IIRC which was after the above was written.

That was just a bone headed typo on my part. Of course == is needed here.

That’s just a warning. In place of having you call a constructor and using the awkward gatekeeper.Gatekeeper() I implemented a builder. This warning is just telling you that this new builder function exists and you should use it instead of new. However, it should still work with new.

require {Gatekeeper} = require('openhab_rules_tools');
var gk = Gatekeeper();

You probably want to reuse the Gatekeeper on subsequent runs of the rule so that’s why I used the initializer function in the cache.

var gk = cache.private.get('gatekeeper', () => Gatekeeper());

That line pulls the Object from the private cache, or if it doesn’t exist, it initializes that entry with the result of the passed in function and returns that. So the first time this rule runs a new Gatekeeper is created but on subsequent runs the same Gatekeeper is used.

Just so we are testing with the way it should be done (i.e. with the builder function) can you change the code to match the above? I might need to change that warning to an error if for some reason the constructor doesn’t work anymore. I can’t imagine why that would be the case but the error doesn’t make sense to me otherwise.

The test script based on your latest post:

var {Gatekeeper} = require('openhab_rules_tools');
var gk = cache.private.get('gatekeeper', () => Gatekeeper());
cache.private.put('lrNightLight_gatekeeper', time.ZonedDateTime.now())

gk.addCommand(600, () => { items.getItem('HuespotHallway2_Color').sendCommand('5,100,30'); });
gk.addCommand(600, () => { items.getItem('HuespotHallway1_Color').sendCommand('5,100,30'); });
gk.addCommand('PT2m', () => { items.getItem('HuespotDR4_Color').sendCommand('5,100,30'); });
gk.addCommand(600, () => { items.getItem("HuespotHallway2_Color").sendCommand("30,65,0"); });
gk.addCommand(600, () => { items.getItem("HuespotHallway1_Color").sendCommand("5,100,0"); });
gk.addCommand(0, () => { items.getItem("HuespotDR4_Color").sendCommand("5,100,0"); });

Thanks Rich, your script above worked fine. I’ll use it in my rule and check it out. Thanks for your clarifications Rich, and to be clear, I was only wanting to point out the minor corrections in your original reply in case someone came across it and wanted to use it so as to avoid troubleshooting minor oversights.

1 Like

Worked like a charm.