Blockly - concurrent rule execution - creating timer for each group member

Hi,

running OH3.2.0 I try to dynamically create timers for group members with blockly, but it is not working as expected:

Situation:
I have a group with items representing window open state.
Whenever a window item from this group changes from CLOSED to OPEN I want to start a timer, that will remind me to close the window.
Of cause I do not want to have one rule (or one expire item) per window, but I only want to have one rule for the entire group.

Whats working:
I have created a rule, triggered by “state of a member of an item group changes”.
I have created a blockly script that will do the following:
if new state == OPEN, than create a timer
If new state == CLOSED, cancel the timer
Once the timer is running, it will send a notification and reschedule.

The rule is working perfect if I just open one window

Whats not working:
As in blockly I can also give the timer a name, if have setup a dynamic generated name as: triggeringItemName+"_timer"
Expecting that I can have multiple timers running in parallel. However when opening multiple windows at the same time (e.g. first opening one window, than while first window is still open, I will open a second window and expectation is to have two timers & receiving two notifications) the script is not working as expected.

Based on the logs I see the following:
Window 1 changes from CLOSED to OPEN
Timer for window 1 is scheduled
Window 2 changes from CLOSED top OPEN
Timer for windows 2 is scheduled
Both windows will remain open, but only timer for window 2 is executed. Once the 2nd (or 3rd…) timer is scheduled, any previous timers created by the same rule (even with different timer name) are not executed anymore.

Question: Is this expected? Whats the purpose of giving the timer a name, if it will always get overwritten?

Code generated by blockly:

var intialDuration, rescheduleDuration, notificationText;

var logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + ctx.ruleUID);

var scriptExecution = Java.type('org.openhab.core.model.script.actions.ScriptExecution');

var zdt = Java.type('java.time.ZonedDateTime');

if (typeof this.timers === 'undefined') {
  this.timers = [];
}


intialDuration = 10;
rescheduleDuration = 5;

if (event.itemState == 'OPEN') {
    logger.info((String(event.itemName) + '_Timer scheduled'));
    if (typeof this.timers[(String(event.itemName) + '_Timer')] === 'undefined' || this.timers[(String(event.itemName) + '_Timer')].hasTerminated()) {
      this.timers[(String(event.itemName) + '_Timer')] = scriptExecution.createTimer(zdt.now().plusMinutes(intialDuration), function () {
        logger.info((String(event.itemName) + '_Timer running'));
                    
          //send notificaiton - code removed

          logger.info((String(event.itemName) + '_Timer reschedule'));
          if (typeof this.timers[(String(event.itemName) + '_Timer')] !== 'undefined') { this.timers[(String(event.itemName) + '_Timer')].reschedule(zdt.now().plusMinutes(rescheduleDuration)); }
        })
    }
} else if (event.itemState == 'CLOSED') {
  logger.info((String(event.itemName) + '_Timer cancel'));
  if (typeof this.timers[(String(event.itemName) + '_Timer')] !== 'undefined') {
    this.timers[(String(event.itemName) + '_Timer')].cancel();
    this.timers[(String(event.itemName) + '_Timer')] = undefined;
  }
}

Log:

2022-01-03 10:21:49.400 [INFO ] [org.openhab.rule.b49c1e12c3         ] - OGBadezimmerFenster_Contact_Timer scheduled
2022-01-03 10:21:57.186 [INFO ] [org.openhab.rule.b49c1e12c3         ] - OGSchlafzimmerFenster_Contact_Timer scheduled
2022-01-03 10:31:49.606 [INFO ] [org.openhab.rule.b49c1e12c3         ] - OGSchlafzimmerFenster_Contact_Timer running
2022-01-03 10:31:49.780 [INFO ] [org.openhab.rule.b49c1e12c3         ] - OGSchlafzimmerFenster_Contact_Timer reschedule
2022-01-03 10:34:15.678 [INFO ] [org.openhab.rule.b49c1e12c3         ] - OGSchlafzimmerFenster_Contact_Timer cancel
2022-01-03 10:34:23.533 [INFO ] [org.openhab.rule.b49c1e12c3         ] - OGBadezimmerFenster_Contact_Timer cancel

Thanks in advance for any help :slight_smile:

Do you mind providing the blocklies as well?

I have already nailed the issue down and the issue is about the event context not being passed to the timer:

If I use “event.ItemName” (in JS) or “contextual info triggering item name” (in blockly) my expectation would be, that the event is passed to the timer and therefore the timer always keeps the event that initially scheduled the timer. However in real life it seems that for a normal timer the event is always the event that last happened, as the event variable is updated.

To reproduce the issue, you can use the following setup & rule:

  • 1 Group item
  • 2 switch items, that are part of the group
{
  "members": [
    {
      "link": "XXX",
      "state": "OFF",
      "type": "Switch",
      "name": "Test_Item1",
      "label": "Test Item 1",
      "category": "",
      "tags": [],
      "groupNames": [
        "Test_Group"
      ]
    },
    {
      "link": "XXX",
      "state": "ON",
      "type": "Switch",
      "name": "Test_Item2",
      "label": "Test Item 2",
      "category": "",
      "tags": [],
      "groupNames": [
        "Test_Group"
      ]
    }
  ],
  "link": "XXX",
  "state": "NULL",
  "editable": true,
  "type": "Group",
  "name": "Test_Group",
  "label": "Test Group",
  "category": "",
  "tags": [],
  "groupNames": []
}

and the following rule:

configuration: {}
triggers:
  - id: "1"
    configuration:
      groupName: Test_Group
    type: core.GroupStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      blockSource: '<xml xmlns="https://developers.google.com/blockly/xml"><block
        type="oh_log" id="CeTmCgg+%A|,-r(!@YVD" x="43" y="39"><field
        name="severity">info</field><value name="message"><shadow type="text"
        id="TgG;cqia||Wr8Q8/|bzy"><field name="TEXT">abc</field></shadow><block
        type="text_join" id="p)l8Mq_w[eMBPILb[_/k"><mutation
        items="2"></mutation><value name="ADD0"><block type="text"
        id="4AdA7L81~HV+np]T%*b/"><field name="TEXT">Rule context:
        </field></block></value><value name="ADD1"><block type="oh_context_info"
        id="k%W6S)i9UZ~;PhTAC16l"><field
        name="contextInfo">itemName</field></block></value></block></value><next><block
        type="oh_timer" id="lpowAQx]1%a;A?mvGwI+"><field
        name="delayUnits">plusSeconds</field><value name="delay"><shadow
        type="math_number" id="C7DaC]#Mz?KDNdCPY7Uj"><field
        name="NUM">20</field></shadow></value><value name="timerName"><shadow
        type="text" id="SA.@5cb;%R=qGJ{/ipXn"><field
        name="TEXT">MyTimer</field></shadow><block type="oh_context_info"
        id="MOYe!${sPs)__#Jva+sz"><field
        name="contextInfo">itemName</field></block></value><statement
        name="timerCode"><block type="oh_log" id="J~[,4#2wUQdr)+qzy`r?"><field
        name="severity">info</field><value name="message"><shadow type="text"
        id="AGCPmr=[8w%!F(?ruTkv"><field name="TEXT">abc</field></shadow><block
        type="text_join" id="~].z`MjC-VJLjSmU.oTO"><mutation
        items="2"></mutation><value name="ADD0"><block type="text"
        id="WezD4!wQXyfyz)+=sZ-:"><field name="TEXT">Timer context:
        </field></block></value><value name="ADD1"><block type="oh_context_info"
        id="Q)#=%2k%..gr;z}ZQC|v"><field
        name="contextInfo">itemName</field></block></value></block></value></block></statement></block></next></block></xml>'
      type: application/javascript
      script: >
        var logger =
        Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' +
        ctx.ruleUID);


        var scriptExecution = Java.type('org.openhab.core.model.script.actions.ScriptExecution');


        var zdt = Java.type('java.time.ZonedDateTime');


        if (typeof this.timers === 'undefined') {
          this.timers = [];
        }



        logger.info(('Rule context: ' + String(event.itemName)));

        if (typeof this.timers[event.itemName] === 'undefined' || this.timers[event.itemName].hasTerminated()) {
          this.timers[event.itemName] = scriptExecution.createTimer(zdt.now().plusSeconds(20), function () {
            logger.info(('Timer context: ' + String(event.itemName)));
            })
        }
    type: script.ScriptAction

Test Case:

  1. Toggle Item 1
  2. wait 10 seconds
  3. Toggle Item 2
  4. wait 20 seconds that both timer will execute

Expected log output of the rule:

  1. “Rule context: Test_Item1” (when toggling Item 1)
  2. “Rule context: Test_Item2” (when toggling Item 2)
  3. “Timer context: Test_Item1” (20 seconds after toggling item 1)
  4. “Timer context: Test_Item2” (20 seconds after toggling item 2)

Actual log output of the rule:

  1. “Rule context: Test_Item1”
  2. “Rule context: Test_Item2”
  3. “Timer context: Test_Item2”
  4. “Timer context: Test_Item2”

However if I adjust the rule as JS rule and replace the createTimer with createTimerWithArgument and pass the original event as argument to the timer it is working well:

configuration: {}
triggers:
  - id: "1"
    configuration:
      groupName: Test_Group
    type: core.GroupStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript
      script: >-
        var logger =
        Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' +
        ctx.ruleUID);


        var scriptExecution = Java.type('org.openhab.core.model.script.actions.ScriptExecution');


        var zdt = Java.type('java.time.ZonedDateTime');


        if (typeof this.timers === 'undefined') {
          this.timers = [];
        }


        logger.info(('Rule context: ' + String(event.itemName)));

        if (typeof this.timers[event.itemName] === 'undefined' || this.timers[event.itemName].hasTerminated()) {
          this.timers[event.itemName] = scriptExecution.createTimerWithArgument(zdt.now().plusSeconds(20),event, function (e) {
            logger.info(('Timer context: ' + String(e.itemName)));
            })
        }
    type: script.ScriptAction

The ultimate question and answer to my issue is, if it would make sense to have an additional timer-block that will pass the current event to the timer or better update the current block and what should the be default option? I guess there are use cases for both, if you use event within a timer, that you want to access the latest event, or the event that initially scheduled the timer.

I suspected that was the case but wanted to test it first. You beat me to it.

That’s a common problem actually.

There is a way to force that to happen when writing the code directly. Use a function generator. Call a function with events as an argument that returns your runme function that gets called by the timer. Here’s an example from my Debounce Rule Template.

/**
 * Called when the debounce timer expires, transfers the current state to the 
 * proxy Item.
 * @param {string} state the state to transfer to the proxy Item
 * @param {string} name of the proxy Item
 * @param {Boolean} when true, the state is sent as a command
 */
var end_debounce_generator = function(state, proxy, isCommand) {
    return function() {
        logger.debug("End debounce for " + proxy + ", new state = " + state + ", curr state = " + items[proxy] + ", command = " + isCommand);
        if(isCommand && items[proxy] != state) {
          logger.debug("Sending command " + state + " to " + proxy);
          events.sendCommand(proxy, state.toString());
        }
        else if (items[proxy] != state) {
          logger.debug("Posting update " + state + " to " + proxy);
          events.postUpdate(proxy, state.toString());
        }
      }
}

But I couldn’t figure out how to create a function that returns a function so that would require some new blocks.

Another option could be to use createTimerWithArgument but that will require some new blocks, as you guessed, as well.

Ultimately the problem is that all the variables are declared at the top of the script action so all the timer functions are using the same global variables. So we can’t define a variable inside the function called by the timer to limit the scope. As you’ve seen, event gets rewritten each time the rule runs too.

This is a thorny problem I don’t have a solution to.

Don’t think we need to overengineer on this, as within JS the createTimerWithArgument is already fine.
Will add it to my todo just to create a block for it

Except you can only pass one argument.

Yes, but as in my JS example I can pass the entire event object as argument, which will hopefully fit to multiple usecases.

Is it worth to log an enhancement request, to get a createTimerWithArguments function? :slight_smile:

@rlkoshak I have posted a new timer block, that will also pass the event to the timer to the market place here

However even I put my complete block definition into a<code> </code> block as per the template, it is not recognized correctly. Do you have any recommendation on this?

Use MD code blocks.

```yaml
code goes here
```
1 Like