Managing Multiple Timers in Javascript OH3

I have a generalised rule which updates the Status of a room based on triggers from a motion sensor and a timer. Any number of room triggers can be added, without modifying the rule, as the rule works out which room status to update based on the trigger name. This of course means that at any time multiple timers may be running and when each one expires the correct room status has to be updated. In order to make it work as I want I have one hurdle to get over and that is the function called by an expired timer needs to determine the ID of the expired timer in order to update the status of the correct room. So I would appricate a pointer on how a function called by a timer can determine the ID of the timer it called. The code as it stands at the moment (with logger code remove to make it more readable) is:

/*
Triggers for this rule MUST take the format Room_* Where: Room is the location being monitored and is
used to determine which status should be changed.
The motion sensor being used stays ON until there is no motion for 1 minute, when it changes to OFF.
*/
//
//---------------------------Variable Declarations-----------------------------------------------------------
//
var ScriptExecution = Java.type(“org.openhab.core.model.script.actions.ScriptExecution”);
var logger = Java.type(‘org.slf4j.LoggerFactory’).getLogger(‘org.openhab.rule.’ + ctx.ruleUID);
var ZonedDateTime = Java.type(“java.time.ZonedDateTime”);
//
// Enable/Disable Logging
var LogingEnabled = true;
//
// Define room states
var Occupied = ‘Occupied’;
var Unoccupied = ‘Unoccupied’;
var NoMotionDetected = ‘No Motion Detected’;
var Unknown = ‘Unknown’;
//
// Time since last motion dected before room status changed to Unoccupied
var DelayPeriod = 1; // Minutes
//
// Get Trigger and Room details
var Trigger = event.itemName
var Room = Trigger.slice(0, Trigger.indexOf(“_”));
//
// Array and Object to track timers
var OccupancyTrackersCreated;
//
if (OccupancyTrackersCreated !== true) // Make sure OccupancyTimerObject is only created first time script called
{
eval(‘var TimerArray =;’);
eval(‘var TimerArrayLength = 0;’);
eval(‘var OccupancyDelayTimer = {};’);
OccupancyTrackersCreated = true;
}
//
//-------------------------- Functions Used By The Rule ---------------------------------------------
//
// Function used by timers to change occupancy status
//after period of no motion
//
function waitToChangeOcupancy()
{
var CallingTimerID = 123; //This is what I need to work out how to get actual timerID
//
for (ArrayIndex = 0; ArrayIndex < TimerArrayLength; ArrayIndex = ArrayIndex + 2)
{
if (TimerArray[ArrayIndex] == CallingTimerID)
{
events.postUpdate(TimerArray[ArrayIndex+1], Unoccupied);
TimerArray.splice(ArrayIndex,2);
break;
}
}
};
//
//-------------------------- Main Body of The Rule ---------------------------------------------
//
if (items[Trigger] == ‘ON’) // Motion detected
{
events.postUpdate(Room, Occupied); // Set room status to occupied
//
if (this.OccupancyDelayTimer[Room] !== undefined) // Cancel timer so state not changed TO Unoccupied
{
this.OccupancyDelayTimer[Room].cancel(); // Cancel timer
this.OccupancyDelayTimer[Room] = undefined; // Ensure timer ID cleared
}
}
else if (items[Trigger] == ‘OFF’) // No motion detected so set status NoMotionDetected for timer period after which function called
{
this.OccupancyDelayTimer[Room] = ScriptExecution.createTimer(ZonedDateTime.now().plusMinutes(DelayPeriod), waitToChangeOcupancy);
TimerArrayLength = this.TimerArray.push(this.OccupancyDelayTimer[Room], Room);
events.postUpdate(Room, NoMotionDetected);
}
else
{
events.postUpdate(Room, ‘Unknown’);
}

Please use code fences, not quotes, for code. How to use code fences

In answer to your question, use a function generator that you can pass values to.

function waitToChangeOccupanceGenerator(callingTimerId) {
  return function() {
    for(ArrayIndex ...
  }
}
...
ScriptExecution.createTimer(ZonedDateTime.now().plusMinutes(DelayPeriod), waitToChangeOcupancy(123));

But, for reference, I’ve a TimerMgr JavaScript library that odes all that management of multiple timers for you. openhab-rules-tools/timer_mgr at main · rkoshak/openhab-rules-tools · GitHub

Or, if you don’t want to rely on an external library, at least use a dict instead of an array to manage the Timers. Then you can just use the Item name as the index instead of needing to loop through the array.

1 Like

@rlkoshak Thanks for the response. I now have the code working with multiple room etc both in the original form:

/*
Triggers for this rule MUST take the format Room_*  Where: Room is the location being monitored and is
used to determine which status should be changed. 

The motion sensor being used stays ON until there is no motion for 1 minute, when it changes to OFF.
*/

var ScriptExecution = Java.type("org.openhab.core.model.script.actions.ScriptExecution");
var logger          = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + ctx.ruleUID);
var ZonedDateTime   = Java.type("java.time.ZonedDateTime");
 
// Enable/Disable Logging
var LogingEnabled = true;

// Define room states
var Occupied = 'Occupied';
var Unoccupied = 'Unoccupied';
var NoMotionDetected = 'No Motion Detected';
var Unknown = 'Unknown';

// Time since last motion dected before room status changed to Unoccupied
var DelayPeriod = 1;  // Minutes

// Get Trigger and Room details
var Trigger = event.itemName
var Room = Trigger.slice(0, Trigger.indexOf("_"));

// Flag to ensure Delay Timers Object is only created once - on first call
var OccupancyDelayTimersObjectCreated;

// Function used by timers to change occupancy status
//after period of no motion

function waitToChangeOccupancy(Room, Trigger, LogingEnabled)
{

  return function() 
  {
    if (LogingEnabled)logger.info("Motion Delay Timer Callback Routine - Room: " + Room +"  Trigger: " + Trigger) ;
    if (Trigger != 'ON')events.postUpdate(Room, Unoccupied);  // Make sure hasn't re-triggered during function call
  }
}

//----------------------------------------------------------------------------------------------------

if (OccupancyDelayTimersObjectCreated !== true)      // Make sure OccupancyTimerObject is only created first time script called
{
 eval('var OccupancyDelayTimer = {};');
 OccupancyDelayTimersObjectCreated = true;
 if (LogingEnabled)logger.info("Motion Detection (" +Room +") - Occupancy Delay Timer Object Created");
}
if (LogingEnabled)logger.info("Motion Detection (" +Room +") Entered - TimerID: " + this.OccupancyDelayTimer[Room]);
if (items[Trigger] == 'ON')  // Motion detected
{
  if (LogingEnabled)logger.info("Motion Detection (" +Room +") - Motion State ON");
  events.postUpdate(Room, Occupied);  // Set room status to occupied

  if (this.OccupancyDelayTimer[Room] !== undefined)  // Cancel timer so state not changed TO Unoccupied
  {
    this.OccupancyDelayTimer[Room].cancel();         // Cancel timer
    this.OccupancyDelayTimer[Room] = undefined;      // Ensure timer ID cleared
    if (LogingEnabled)logger.info("Motion Detection delay timer for: " +Room +" - CANCELLED");
  }
  else
  {
    if (LogingEnabled)logger.info("Motion Detection delay timer for: " +Room +" - UNDEFINED");  
  }

}

else if (items[Trigger] == 'OFF')     // No motion detected so set status NoMotionDetected for timer period after which function calle
{
  if (LogingEnabled)logger.info("Motion Detection (" +Room +") - Motion State OFF");
  this.OccupancyDelayTimer[Room] = ScriptExecution.createTimer(ZonedDateTime.now().plusMinutes(DelayPeriod), waitToChangeOccupancy(Room, Trigger, LogingEnabled));
  events.postUpdate(Room, NoMotionDetected);
  if (LogingEnabled)logger.info("Motion Detection (" +Room +") TimerID: " + this.OccupancyDelayTimer[Room]);
  
} 
else 
{
  events.postUpdate(Room, 'Unknown');
}

and also using your timer library:

/*
Triggers for this rule MUST take the format Room_*  Where: Room is the location being monitored and is
used to determine which status should be changed. 

The motion sensor being used stays ON until there is no motion for 1 minute, when it changes to OFF.
*/

var ScriptExecution = Java.type("org.openhab.core.model.script.actions.ScriptExecution");
var logger          = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + ctx.ruleUID);
var ZonedDateTime   = Java.type("java.time.ZonedDateTime");
var OPENHAB_CONF = java.lang.System.getenv("OPENHAB_CONF");
load(OPENHAB_CONF+'/automation/lib/javascript/community/timerMgr.js');
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');

// Enable/Disable Logging
var LogingEnabled = true;

// Define room states
var Occupied = 'Occupied';
var Unoccupied = 'Unoccupied';
var NoMotionDetected = 'No Motion Detected';
var Unknown = 'Unknown';

// Time since last motion dected before room status changed to Unoccupied
var DelayPeriod = "30s";  // Minutes

// Used to ensure timers only created once
var OccupancyDelayTimersCreated;

// Get Trigger and Room details
var Trigger = event.itemName;
var Room = Trigger.slice(0, Trigger.indexOf("_"));

// Function used by timers to change occupancy status
//after period of no motion
function waitToChangeOccupancy(Room, Trigger, LogingEnabled)
{
  return function() 
  {
    if (LogingEnabled)logger.info("Motion Delay Timer Callback Routine - Room: " + Room +"  Trigger: " + Trigger) ;
    if (Trigger != 'ON')events.postUpdate(Room, Unoccupied);  // Make sure hasn't re-triggered during function call
  }
}

//----------------------------------------------------------------------------------------------
if (OccupancyDelayTimersCreated !== true)      // Make sure OccupancyTimerObject is only created first time script called
{
 eval('var timers = new TimerMgr();');
 OccupancyDelayTimersCreated = true;
 if (LogingEnabled)logger.info("Motion Detection (" +Room +") - Occupancy Delay Timers Created");
}
if (LogingEnabled)logger.info("Motion Detection (" +Room +") Entered");


if (items[Trigger] == 'ON')  // Motion detected
{
  if (LogingEnabled)logger.info("Motion Detection (" +Room +") - Motion State ON");
  events.postUpdate(Room, Occupied);  // Set room status to occupied
  timers.cancel(Room);         // Cancel timer
  if (LogingEnabled)logger.info("Motion Detection delay timer for: " +Room +" - CANCELLED");

}
else if (items[Trigger] == 'OFF')     // No motion detected so set status NoMotionDetected for timer period after which function calle
{
  if (LogingEnabled)logger.info("Motion Detection (" +Room +") - Motion State OFF");
  timers.check(Room, DelayPeriod, waitToChangeOccupancy(Room, Trigger, LogingEnabled));
  if (LogingEnabled && timers.hasTimer(Room))logger.info("Motion Detection (" +Room +") - Timer Created");
  events.postUpdate(Room, NoMotionDetected);
} 
else 
{
  events.postUpdate(Room, 'Unknown');
}

However, just to let you know that while getting it working with your timer library I noticed that the Documentation has got out of step with the code. In particular:

  1. The example in the hasTimer Section uses has_timer instead of hasTimer

  2. The order of the arguments for check are given as: key, when, function, flapping_function, reschedule. However the test code seems to have the order of: key, when, function, reschedule, flapping_function.

Once again thank you for you for the work and effort you put into OpenHAB and the excellent advise you provide novices like me.

~~That’s the docs for the Python version of the library. If you look up in the JavaScript section you will see the docs present hasTimer. https://github.com/rkoshak/openhab-rules-tools/tree/main/timer_mgr#hastimer~~ Never mind. I see it now.

Again, I think you are looking at the Python docs, not the JavaScript. Now that I’m looking at it, the JavaScript section is completely missing the section for the check function which needs to be corrected. The order of the arguments is different between Python and JavaScript because in Python you can “skip” arguments to the function so I was able to put them in what I thought was a more reasonable order. But in JavaScript 5.1 you can’t skip arguments so I had to change the order a little so the most commonly used arguments are first.

EDIT: Updates have been made. In case you were referring to the order of the arguments in the table that describes their meaning, I reordered the rows in the table. The rest of the docs for check for JavaScript and the examples appear to be correct.

Hello Rich,

I want to thank you first for all the great tutorials and the knowledge sharing :slight_smile:

I tried to use your timerMgr function, but somehow its not working.
I used the example just for testing and get the following error.

Did I miss something? :frowning:

2021-03-23 20:00:48.386 [ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID '1fadfabf78' failed: ReferenceError: "TimerMgr" is not defined in <eval> at line number 7
this.OPENHAB_CONF = (this.OPENHAB_CONF === undefined) ? java.lang.System.getenv("OPENHAB_CONF") : this.OPENHAB_CONF;
load(OPENHAB_CONF+'/automation/lib/javascript/personal/timerMgr.js');
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;

Here are the relevant lines from one of the rules I have that is using it.

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

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

Also, which version of OH are you running? And how did you define the rule?
I don’t see anything significantly different so the question becomes is timerMgr.js at that location? What does it look like? Maybe there was a mistake in copying or something like that.