Timed command repetition

Can anyone share a clever idea how to accomplish this in rulesDSL:

On an item receiving a command, I need to repeat it for a variable number of times “n” with a variable interval “t” inbetween . n and t are variables or items.

Would like to avoid sleep() so looking for a clever timer based implementation.
Note while unfinished the item receives a different command it must stop eventually running timers so it does not override the new command, and the n x t cycle needs to be restarted with the new command.
Thanks for any half way elegant solution !

I have done this in rules dsl, but it was somewhat buggy and messy. That’s why I looked at java directly to create rules. You could run jrule for just doing this and keep your other rules in dsl or similar.

In case you are interested look at example 9

What do you want to do when either n or t changes? Should these changes be taken into account in the running timer instance, or should they be ignored?

If they should be ignored then I guess something like this should work. Note that I haven’t done any rules DSL for a long, long time so it may need some additional love.

var Timer recurringTimer = null

var delay = 0
var iterations = 0

rule "Markus special recurring timer"
when
    Item Some_Item changed
then

    val ruleName = "Markus special recurring timer"

    // save delay and iterations to global variable so that modifying the items has
    // no impact on the running timer (if any)
    delay = item_t.state
    iterations = item_n.state

    // Note while unfinished the item receives a different command it must stop eventually running timers so it does not override the new command
    if (recurringTimer != null) {
        recurringTimer.cancel()
    }


    recurringTimer = createTimer(now.plusSeconds(t)) [|
        logInfo(ruleName, "Timer triggered")

        // do whatever needs to happen here

        iterations = iterations - 1
        if (iterations > 0) {
            logInfo(ruleName, "Rescheduling timer")
            recurringTimer.reschedule(now.plusSeconds(t))
        } else {
            logInfo(ruleName, "All done")
            recurringTimer.cancel()
            recurringTimer = null
        }
      ]

end

Elegant may not be much of an option here but it should be doable with a cascading timer I think. Design Pattern: Cascading Timers. I never really consider timers to be elegant but they sure are handy.

I do have an elegant solution in JavaScript and Python with my Gatekeeper library. But that’s not going to work in Rules DSL.

Indeed, sleep() won’t work if you need to be cancel it on a new command because in OH 3 only one instance of the rule can run at a time so the new command won’t start processing until the previous one completes processing.

From the description these commands are being sent to the same Item though and that will be the challenge. How do we tell the difference between a command that came from somewhere, or the repeated command generated by this rule? Do you need to support the case where the Item receives and ON and starts looping and sending ON again, and then something else sends ON and the loop needs to stop and start over again?

This is really tricky to do with only one Item. But with a proxy it seems more doable. The initial command that triggers the rule comes from the proxy. Then as we send and repeat the command to the “real” Item the rule doesn’t get retriggered and we don’t have to figure out how to tell the difference between a rule generated command and an external to the rule generated command.

So maybe something like this would work (just typing this in, there will be typos):

var Timer repeatTimer = null;

rule "Repeat command"
when
    Item ProxyItem received command
then
    // Cancel the looping timer if it's already running
    repeatTimer?.cancel()

    repeatTimer = createTimer(now.plus(<initial delay>, [ |
        if(<not done>){
            RealItem.sendCommand(receivedCommand)
            repeatTimer.reschedule(now.plus(<next delay>)
        }
        else {
            repeatTimer = null
        }
    ]
end

OK, that’s one problem solved. The next problem is to handle that list of different delays. Is it safe to assume that “t” is the same each time through the “n” loops? Put another way, we don’t need a different “t” for each iteration of the loop?

If so it could be as simple as:

var Timer repeatTimer = null;

rule "Repeat command"
when
    Item ProxyItem received command
then
    // Cancel the looping timer if it's already running
    repeatTimer?.cancel()

    var loopCount = LoopCount.state as Number
    val delaySecs = LoopDelay.state as Number
    repeatTimer = createTimer(now.plus(<initial delay>, [ |
        if(loopCount > 0){
            RealItem.sendCommand(receivedCommand)
            loopCount = loopCount - 1
            repeatTimer.reschedule(now.plus(delaySecs)
        }
        else {
            repeatTimer = null
        }
    ]
end

To make this generic you’ll probably want to use Associated Items DP. If you need a variable delay between each iteration you’ll need to devise some sort of math algorithm to calculate it or a look-up table.

That should get you pretty close and it should be pretty stable and reliable, assuming that I understand the requirements correctly. If not I can explore other options.

In your second rule, shouldn’t both loopCount and delaySecs be global variables? I assumed that they would not be in scope of the lambda when run by the timer.

A lambda inherits the scope that exists at the time that it’s created.

When you create a global lambda, there is no context yet to inherit.

When you create a lambda inside a rule, the lambda inherits the current context in that rule. So that means it gets all the globals and all the variables defined in the rule up to that point. Even after the rule exits, the lambda will keep those variables alive for it’s use.

Because we are not trying to share information between multiple contexts (e.g. we don’t have two timers both reading and changing these two variables, we don’t have two separate rules reading and changing these two variables) the two variable only need to exist in the context of this one rule. There is no need to make them globals.

NOTE: the LSP may complain about attempting to use/change a non-final variable inside the lambda. You can ignore that error/warning. The code will still work.

3 Likes