JSScripting Timers & Functions DO's & DON'T's

Hi Folks,

my OpenHAB Version is 3.4.0.1 Stable (Debian)

I am converting most of my DSL rules to JSRule (ECMA2021) in the last few weeks and try to learn a bit more JS while doing so…

Right now I’m dealing with timers (timeout & interval) and try to combine them with functions.

But I have various questions about getting this to act the way I think it should - I attach the code for better understanding what I’m talking about…

generally I’m not sure to use the right logic in the syntax…

  • is this the right way to use multiple cascaded timers in functions
  • can I clear timers/intervals in that way
  • what happens to timers/intervals which are called in a function which is cleared after the call
  • are there any things that I missed or done wrong
  • does it make a difference in which order I’m defining the functions

To explain this a little bit: I’m trying to call a timer in a new function out of the main function, and after the time is over, this function should call a new timeout/interval in another function and quit itself.

(function (event){
  if (some condition) return;
  setTimeout(timer1, 60000);
})(event);

function timer1(){
  do something;
  setTimeout(timer2, 300000);
  clearTimeout(timer1);
}

function timer2(){
  do something;
  setInterval(timer3, 50);
  clearTimeout(timer2);
}

function timer3(){
  if(condition is met){
    do something;
    clearInterval(timer3);
  }
}

Here’s the actual JSRule file I’m working on:

rules.JSRule({
  name: "Actions - Execute Daystart",
  description: "execute daystart when sleepstate changed to awake",
  triggers: [triggers.ItemStateChangeTrigger("SleepState", "ASLEEP", "AWAKE")],
  execute: (data) => {
    const presence = items.getItem("PresenceSwitch");
    const overridelock = items.getItem("OverrideLock");
    const bedtime = items.getItem("Bedtime");
    const player = items.getItem("SqueezePlayer");
    const bedroomaudio = items.getItem("Smartplug03Switch");
    const kitchenaudio = items.getItem("Smartplug07Switch");
    const daystartpl = items.getItem("DaystartPlaylist");
    const power = items.getItem("SqueezeBedroomPower");
    const volume = items.getItem("SqueezeBedroomVolume");
    const shuffle = items.getItem("SqueezeBedroomShuffle");
    const favorite = items.getItem("SqueezeBedroomPlayFavorite");
    const play = items.getItem("SqueezeBedroomPlaypause");

    let event;

    function volumeIncrease() {
      if (volume.state < "20") {
        vol = volume.state;
        increase = vol + 1;
        volume.sendCommand(increase);
      } else clearInterval(volumeIncrease);
    }

    function bootupTimer() {
      console.log("Good Morning!");
      if (overridelock.state !== "OFF") {
        manualoverride.sendCommandIfDifferent("OFF");
        power.sendCommandIfDifferent("ON");
        volume.sendCommandIfDifferent("5");
        shuffle.sendCommandIfDifferent("1");
        favorite.sendCommand(daystartpl);
        play.sendCommandIfDifferent("ON");
        console.log("Squeeze - Start playing Music in Bedroom");
        setInterval(volumeIncrease, 60000);
        clearTimeout(bootupTimer);
      }
    }

    (function (event) {
      if (presence.state === "OFF" || overridelock.state === "ON") {
        return;
      }
      if (bedtime.state !== "OFF") {
        bedtime.sendCommand("OFF");
      }
      player.sendCommandIfDifferent("Bedroom");
      bedroomaudio.sendCommandIfDifferent("ON");
      kitchenaudio.sendCommandIfDifferent("ON");

      setTimeout(bootupTimer, 120000);

      let wakeTimer;
      wakeTimer?.clearTimeout(wakeTimer);
      const sleepstate = items.getItem("SleepState");
      console.log("changing Sleepstate");
      wakeTimer = setTimeout(() => {
        if (sleepstate.state !== "AWAKE") {
          sleepstate.sendCommand("AWAKE");
          console.log("Sleepstate is now", sleepstate.state);
        }
        clearTimeout(wakeTimer);
      }, 60000);
    })(event);
  },
  tags: [],
});

I’m fairly new to the matter of coding JS and would love to hear where I can make some improvements to my code and how to avoid bad habits, and learn more about the logic and syntax of ECMA2021 based scripting…

Thanks and I hope you have a nice day…

Like with createTimer, setTimeout and setInterval return a reference to the created timer. The big difference is these both just return a number and not an Object. But, you need that number to clear it out later same as you’d need to save a reference to an openHAB Timer created with createTimer.

If you’ve already logic like this in Rule DSL, the overall approach will be the same. You need to save what setTimeout returns as the reference to the timer and pass that to clearTimeout later on when you want to cancel it. I’m not sure it’s required to call clearTimeout on a timer that’s already run.

let myTimerRef = setTimeout(myFunc, 120000);
...
clearTimeout(myTimerRef);

Looking at the code

Having said all of that, it looks like you are trying to create a timed sequence of events. The Gatekeeper in my Announcing the initial release of openhab_rules_tools (installable through openhabian-config) would probably be a lot easier way to achieve this. One of the main purposes of almost all of openhab_rules_tools is to get people out of the business of managing timers in the first place.

The code would look something like:

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

rules.JSRule({
  name: "Actions - Execute Daystart",
  description: "execute daystart when sleepstate changed to awake",
  triggers: [triggers.ItemStateChangeTrigger("SleepState", "ASLEEP", "AWAKE")],
  execute: (data) => {
    const gk = cache.private.get('gatekeeper', () => new gatekeeper.Gatekeeper('Daystart')); // uses the private cache instead of a global variable

    const presence = items.getItem("PresenceSwitch");
    const overridelock = items.getItem("OverrideLock");
    const bedtime = items.getItem("Bedtime");
    const player = items.getItem("SqueezePlayer");
    const bedroomaudio = items.getItem("Smartplug03Switch");
    const kitchenaudio = items.getItem("Smartplug07Switch");
    const daystartpl = items.getItem("DaystartPlaylist");
    const power = items.getItem("SqueezeBedroomPower");
    const volume = items.getItem("SqueezeBedroomVolume");
    const shuffle = items.getItem("SqueezeBedroomShuffle");
    const favorite = items.getItem("SqueezeBedroomPlayFavorite");
    const play = items.getItem("SqueezeBedroomPlaypause");

    let event;    

    (function (event) {    
      if (presence.state === "OFF" || overridelock.state === "ON") {
        return;
      }
      if (bedtime.state !== "OFF") {
        bedtime.sendCommand("OFF");
      }
      player.sendCommandIfDifferent("Bedroom");
      bedroomaudio.sendCommandIfDifferent("ON");
      kitchenaudio.sendCommandIfDifferent("ON");

      // Gatekeeper will immediately run a command and then the timeout comes into play, so we'll schedule a noop before calling bootupTimer
      gk.addCommand('PT2M', () => { });  // run a function that doesn't do anything and wait 2 minutes
      gk.addCommand('PT1M', () => {
        console.log("Good Morning!");
        if (overridelock.state !== "OFF") {
          manualoverride.sendCommandIfDifferent("OFF");
          power.sendCommandIfDifferent("ON");
          volume.sendCommandIfDifferent("5");
          shuffle.sendCommandIfDifferent("1");
          favorite.sendCommand(daystartpl);
          play.sendCommandIfDifferent("ON");
          console.log("Squeeze - Start playing Music in Bedroom");
        }
      }); // execute the anonymous function and wait one minute
      gk.addCommand(0, () => {
        if (volume.state < "20") {
          vol = volume.state;
          increase = vol + 1;
          volume.sendCommand(increase);
        }
      }); // execute the passed in anonymous function and wait 0 msec before letting gk run the next.
    })(event);
  },
  tags: [],
});

Without Gatekeeper, you’ll have to do the book keeping involved to keep the reference to the Timers you create if you want to clear them later. However, in this code, as written, *you don’t need to call clearInterval at all. Just get rid of all the lines that call clearInterval and it should be OK. Of course, if you decide later you need to be able to cancel this sequence of timers you won’t be able to but that doesn’t appear to be a requirement.

ahh ok that makes sence…

ok I’ll have a look on how to install the library!

const gk = cache.private.get('gatekeeper', () => new gatekeeper.Gatekeeper('Daystart')); // uses the private cache instead of a global variable

I’m sure all of this is documented somewhere, thanks!

as always I appreciate your help!

See JavaScript Scripting - Automation | openHAB