Need help with DSL rule with loop

  • Platform information: Intel NUC
    • Hardware: x86/8gb/200gb SSD
    • OS: Windows 10 x64
    • Java Runtime Environment: Zulu x64
    • openHAB version: 3.4.4
  • Issue of the topic: Configure a loop to turn on → wait 20 minutes ->turn off → wait 20 minutes → loop (turn on again and so) until a proxy item switch is turned off.

Can you help me make a rule that controls a sonoff simple switch to turn on and off a heating fan. The idea is to switch on a proxy switch (Calefaccion) that turns on the sonoff (Calefactor) → wait 20 minutes ->turn off → wait 20 minutes → loop (turn on again and so) until the proxy item (Calefaccion) switch is turned off, breaking the loop and turning the sonoff OFF.

Here is what I have since I could not make the timers work as intended, creating an uncontrolable loop that kills my server by sending constant ON/OFF commands like crazy.

rule "Calefactor ON"
when
    Item Calefaccion changed from OFF to ON 
then
    if (Calefaccion.state == ON) {
        while (Calefaccion.state == ON) {
        Calefactor.sendCommand(ON)
        Thread::sleep(1200000)
        Calefactor.sendCommand(OFF)
        Thread::sleep(1200000)
        }} else { Calefactor.sendCommand(OFF)}
end

The only proper way to implement this will be to use a Timer. While this rule is running it could be triggered again but those subsequent triggers will just queue up and cause the rule to run again once the rule finally ends (hours later perhaps). With Timers, the rule only spends a few milliseconds running meaning that triggers of the rule don’t get queued up.

Furthermore, with the sleeps, if you change Calefaccion’s state to OFF, it could be up to 40 minutes before that rule will exit. With a timer, you can immediately cancel the timer.

I might recommend looking into Blockly going forward. There’s a looping timer block available on the marketplace that implements this pretty straight forwardly with minimal work. Trigger the rule on any change to “Calefaccion”.

image

If Calefaccion changed to ON we immediately command the Calefactor Item to ON (I use BoundSwitch here as that’s the name of a test switch In happened to have handy). Then create a looping timer which runs every 20 minutes that toggles the Calefactor Item. The loop will continue until the ProxySwitch (Calefaccion Item in this case) is no longer ON.

That last part is there because the block requires it. But in fact it’s not strictly needed because if Caledaccion changed to anything other than ON, we cancel the looping timer so it’s no longer running and we immediately command the Calefactor Item to OFF.

Doing this in Rules DSL is largely the same logic.

rule "Calefactor Changed"
when
    Item Calefaccion changed
then
    if(newState == ON) {
        privateCache.put('MyTimer', createTimer(0, [ |
            Calefactor.sendCommand(if(Calefactor.state == ON) OFF else ON)
            privateCache.get('MyTimer').reschedule(now.plusMinutes(20))
        ])
    }
    else {
        privateCache.get('MyTimer')?.cancel
        Calefactor.sendCommand(OFF) 
    }
end

I strongly encourage the use of the cache over global variables. The logic above is pretty much the same as the Blockly. If Calefaccion changed to ON we create a Timer that runs immediately which toggles Calefactor and then. reschedules itself to run in 20 minutes when it toggles Calefactor again. If Calefaccion changed to OFF, we cancel the Timer and command Calefactor to OFF.

Hi Rich, thanks for your help.

Is the DSL rule missing one parenthesis in privateCache.put('MyTimer'?

I’ve closed it in ('MyTimer') but maybe should be closed at the end?

 privateCache.put('MyTimer'), createTimer(0, [ |
            Calefactor.sendCommand(if(Calefactor.state == ON) OFF else ON)
            privateCache.get('MyTimer').reschedule(now.plusMinutes(20))
        ])

Is this rule working with OH3? I cannot make it work, double checked my things/items but no luck. There are things that I don’t know how to work with like privateCache.get and new things to me.

Can Blockly be backed up? I only use DSL rules because its easy to backup and restore the whole OH configuration.

No, the cache is a map. The first argument is the key i.e. 'MyTimer' and the second argument is the Timer itself (createTimer...).

It should. The overall concept is the same as would be used from OH 1 forward.

However, the privateCache and sharedCache I’m not sure existed in OH 3, or if it does which version of OH 3. But the docs are at Textual Rules | openHAB. Look at the version of the docs for 3.4 to verify.

Of course. All managed configs are included when you use openhab-cli backup and if you do your own backups they are located in /var/lib/openhab/jsondb.

Without using the cache the rule would look something like:

var Timer myTimer = null;

rule "Calefactor Changed"
when
    Item Calefaccion changed
then
    if(newState == ON) {
        myTimer = createTimer(0, [ |
            Calefactor.sendCommand(if(Calefactor.state == ON) OFF else ON)
            myTimer.reschedule(now.plusMinutes(20))
        ])
    }
    else {
        myTimer?.cancel
        Calefactor.sendCommand(OFF) 
    }
end

I’m using OH 3.4.4 and I couldn’t find references to privateCache on the docs for that version.

I don’t know how to replace privateCache from your code above to test, can you lend me a hand there? sorry for bothering

You could write that rule in JRuby (which supports openhab 3.4) like this:

rule "Cycle the heater" do
  received_command Calefaccion
  run do |event|
    if event.on?
      Calefactor.on
      after(20.minutes, id: Calefactor) do |timer|
        Calefactor.toggle
        timer.reschedule unless timer.cancelled?
      end
    else
      timers.cancel(Calefactor)
      Calefactor.ensure.off
    end    
  end
end

Thanks, tried it but it didn’t work either. Items are correct, but the rule fails to do anything (doesn’t turn on the sonoff).

Your trigger is item changed, not received command.
Although I prefer received command (and therefore you need to send a command to trigger it), you can make it trigger the same way as your original rule:

rule "Cycle the heater" do
  changed Calefaccion
  run do |event|
    if event.on?
      Calefactor.on
      after(20.minutes, id: Calefactor) do |timer|
        Calefactor.toggle
        timer.reschedule unless timer.cancelled?
      end
    else
      timers.cancel(Calefactor)
      Calefactor.ensure.off
    end    
  end
end

Still not working, I don’t know what is wrong, the item can be directly controlled and if I test the proxy switch with the ‘real’ heater item (making it point to a simple command ON and toggling from the sitemap) it works but obviously, no rule is taking place and its not the desired behaviour.

EDIT:

I tested the rule above, it works with event.on? to check the changed state.

Can you try this with added logging:

rule "Cycle the heater" do
  changed Calefaccion
  run do |event|
    if event.on?
      logger.info "Starting"
      Calefactor.on
      after(2.seconds, id: Calefactor) do |timer|
        logger.info "Toggling"
        Calefactor.toggle
        timer.reschedule unless timer.cancelled?
      end
    else
      logger.info "Stopping"
      timers.cancel(Calefactor)
      Calefactor.ensure.off
    end
  end
end

Have you installed and configured jruby automation addon?

for the log part:

'Calefactor.rules'
2024-05-23 22:55:06.867 [WARN ] [el.core.internal.ModelRepositoryImpl] - Configuration model 'Calefactor.rules' has errors, therefore ignoring it: [1,25]: mismatched input 'do' expecting 'when'

For your question, no I didn’t; I thought OH could interpret jruby by default. But after installing it, the error was the same

Your DSL rules are in conf/rules - that’s not the correct place for JRuby scripts.

You need to save the file in this directory: conf/automation/ruby/ - note the file extension is .rb

This is noted in the document I linked above, specifically about File based scripts

2024-05-23 23:42:33.841 [INFO ] [ort.loader.AbstractScriptFileWatcher] - Loading script '/C:/openhab/conf/automation/ruby/Calefactor.rb'
2024-05-23 23:42:36.515 [ERROR] [ipt.internal.ScriptEngineManagerImpl] - Error during evaluation of script 'file:/C:/openhab/conf/automation/ruby/Calefactor.rb': Error during evaluation of Ruby in /C:/openhab/conf/automation/ruby/Calefactor.rb at line 1: (NoMethodError) undefined method `rule' for main:Object

Did you follow the installation instructions?

Note in openhab 4 it’s much simpler, just install and done. In openhab 3 you need to manually configure the gems and the require settings

I really, really thank you for your time and support but this is going to be a headache for contingency reasons (if I have to reinstall OH for whatever reason in the future) only for one rule to work. All my rules are DSL ‘vanilla’ as much as possible for this reason, I even use DSL for easy backup too; so I prefer to stick to DSL scripts, until maybe someday I migrate to blockly.

Again thank you for your time and patience.

tldr: Code in DSL:

// Definition of global vars at start of file!
var Timer tCalefactor = null 

rule "Calefactor timed ON/OFF or OFF"
when
    Item Calefaccion changed
then
    tCalefactor?.cancel                                                      // dismiss timer, if present
    if(newState == OFF) {
        if(Calefactor.state != OFF) Calefactor.sendCommand(OFF)              // switch Item OFF if not OFF
    } else
        tCalefactor = createTimer(now, [|                                    // create Timer and start immediately
            Calefaccion.sendCommand(if(Calefaccion.state != ON) ON else OFF) // toggle Item
            tCalefactor.reschedule(now.plusMinutes(20))                      // reschedule Timer
        ])
end
1 Like

Thanks Udo, that rule was almost perfect for my needs; I just had to change the items on:
Calefaccion.sendCommand(if(Calefaccion.state != ON) ON else OFF)

for:
Calefactor.sendCommand(if(Calefactor.state != ON) ON else OFF)

and everything works as expected. I tested it by changing the (now.plusMinutes(20)) to (now.plusSeconds(3)) and worked like I needed. Now I will put back the (now.plusMinutes(20)) and its done.

Thanks to everybody here for the help.

This is the rule… No solution without typo… :wink:

1 Like

It’s how we know you’re paying attention. :smile:

3 Likes