JRuby OpenHAB Rules System

Hello again indeed making progress. Let’s put the UI defined rules off to the side for a minute and continue on with getting the file based rules working. From what I have observed in the UI it should indeed work as you state, the triggers are translated into a rule and the rest of the script is then called when the trigger is activated.

I think the test you introduced for sub scenario 2.1 would do the trick. As for sub scenario 2.2 it totally makes sense that that rule would never really work as I intended. I think I may have been working on the indentation assumptions from python, where the while loop is inside the rule not outside.

After reading through Installation — openHAB Helper Libraries documentation on a hunch I put a test rules file in this location:
/openhab/conf/automation/jsr223/ruby/personal/
which does get the rule file loaded, while this location:
/openhab/userdata/automation/jsr223/ruby/personal does not

So this is pretty exciting for me. But what I am trying to achieve may still be out of my reach. What I am looking for is something like the example:

rule "Execute rule when item is changed for specified duration" do
  changed Alarm_Mode, for: 20.seconds
  run { logger.info("Alarm Mode Updated")}
end

But I need the rule to fire every time the item stays changed for X seconds, for as long as the item is changed. So if I try to make an example for what that would look like, then it would be something along the lines of:

rule "Execute rule when item is changed a multiple of a duration" do
  changed Alarm_Mode, for: every: 20.seconds
  run { logger.info("Alarm Mode Updated")}
end

I would suggest that seconds not be limited to being a integer type, but be a double type so that units of time less than one second can be measured, or used in timing. This could be useful for blinking status lights, or increasing a dimmer when a button is held.

I think it would seem more natural if the operator every would be every: like at:, and if the time units were week instead of :week, and thereby one could simply make multiples of each of the time units by adding an ‘s’ to the end of each as in weeks

rule 'Log an entry at 11:21' do
  every :day, at: '11:21'
  run { logger.info("Rule #{name} run at #{TimeOfDay.now}") }
end

would then become

rule 'Log an entry at 11:21' do
  every: day, at: '11:21'
  run { logger.info("Rule #{name} run at #{TimeOfDay.now}") }
end

or if that is too often

rule 'Log an entry at 11:21 every second day' do
  every: 2.days, at: '11:21'
  run { logger.info("Rule #{name} run at #{TimeOfDay.now}") }
end

Thanks for pointing out the documentation issue for rules placement. I have updated the docs for future users.

I will have to think about if I can abstract your use case to a high level construct. If it is very common it is something we can add. Right now what you want to achieve is done with timers. However, my ruby wrappers around timers are only partially implemented in the released version.

If you wanted to implement it today, you would have to do something like this:

rule 'Execute something while a switch is set to ON' do
  changed TestSwitch
  run { logger.info("button changed event")}
  run do
    java_import java.time.ZonedDateTime
    timer = after(1.second) do
      if TestSwitch.on?
        logger.info("button still changed to ON")
         timer.reschedule(ZonedDateTime.now.plus(Java::JavaTime::Duration.ofSeconds(1)))
       end
     end
  end
end

The after code has been implemented and creates timer objects. However, I have not added a ruby wrapper around the timer object yet, so you have to interact with it using Java now, which I feel is messy. So you can do this, but it may break in the future when I wrap a ruby delegator around the Timer object. I have not fully decided, but the future syntax around timers will probably look like this:

rule 'Execute something while a switch is set to ON' do
  changed TestSwitch
  run { logger.info("button changed event")}
  run do
    timer = after(1.second) do
      if TestSwitch.on?
        logger.info("button still changed to ON")
         timer.reschedule
       end
     end
  end
end

reschedule will permit you to pass an optional duration (like the after method), if you don’t pass an optional duration it just reschedule with the previous duration.

Thanks for the other syntax suggestions. Unfortunately, as far as I know they are not possible. When you see every above that is a method name and method names can’t end with a colon. The reason ‘at’ has a colon is because at is a named parameter of the method. The reason :day and others are preceded by a colon is because they are symbols (sort of like unchangeable strings - but they don’t need to be quoted).

So when you see
every :day, at: '11:21'

What that really is, is a method call:
every(:day, at: '11:21')

It is just that ruby doesn’t require parentheses for method calls.

Thank you for your suggestion, I did implement it today and it works a charm I would most probably not have been able to come up with that on my own. Quite simple rules for something that works more or less like the design pattern for long and short press

rule 'long/short press button rule button up' do
  changed button_up, to: ON

  run { logger.info("button UP changed event")}
  
  run do
    if button_up.on? and lamp_cluster.off?
      logger.info("turning light on")
      lamp_cluster << 30
    end

    java_import java.time.ZonedDateTime
    timer = after(2.second) do

      if button_up.on?
        logger.info("button still ON, sending increase command, rescheduling")
        lamp_cluster.brighten(10)

        timer.reschedule(ZonedDateTime.now.plus(Java::JavaTime::Duration.ofSeconds(2)))
       end
     end
  end
end

rule 'long/short press rule button down' do
  changed button_down, to: ON
  
  run { logger.info("button DOWN changed event")}
  
  dimming = false
  
  run do
    java_import java.time.ZonedDateTime
    timer = after(2.second) do
      if button_down.on?
       logger.info("button still ON, sending decrease command, rescheduling")
       lamp_cluster.dim(10)
       dimming = true
       timer.reschedule(ZonedDateTime.now.plus(Java::JavaTime::Duration.ofSeconds(2)))
      else
        if dimming == false
          logger.info("button OFF, sending off command")
          lamp_cluster.off
          dimming = false
        end
      dimming = false
      end
    end
  end
end

all of this achieved without casting or converting a single type very slick indeed. With the wrappers around the timer objects it would be even more slick.
But if one could have a trigger function that works for repeated time intervals, as well as time intervals that are shorter than a certain period of time, that could compress this rule very much indeed. Like I suggested, but something along the lines of changed ... for: less than 1.second

next is to try implementing a state machine with a state machine gem. I have done some preliminary testing to show that the gem file is installed, loaded, I have initialised the class and it all works, now I just have to rework (and understand) the example to work in my situation. Will report back in due time.

Glad you got it working!

Two quick things I noticed.
It might be working, but the if statements for button_up should change to:
if button_up.on?
Right now you are just asking ruby to execute this if button_up is not nil which it never is. I would like it to work the way you laid it out but there is no way to override what ruby considers truthy. Ruby truthyness is described here.

This one is different than python, what you have works, but in almost all cases you want to use && instead of and. More details on the reasoning is here but it can lead to some unexpected behavior unless you really know what you are doing in Ruby.

Which leads to one more thing. I highly recommend you install in Rubocop. It will check your code against a lot of the official ruby style guide and more importantly if you execute it with rubocop -a it will autocorrect your code and tell you what it changed.

What state machine gem are you using? I am curious what you working on there, sounds neat.

But if one could have a trigger function that works for repeated time intervals, as well as time intervals that are shorter than a certain period of time, that could compress this rule very much indeed. Like I suggested, but something along the lines of changed ... for: less than 1.second

I am not fully understanding this in the context of what I see in the code? In the code I see above it’s more like you want a while_true block that takes a delay and has a check then executes code if its true. Maybe some more examples would help me better understand the use cases here.

This should work too, right?

I have released version 2.0.0 with a breaking change to support timers using the after method.

Changelog
Latest Documentation

I had wanted to finish up the rule syntax by adding support for things, however I am encountering a weird JRuby issue with things. While I wait for help from the JRuby team and because timers were being discussed I added initial support for timers. Full documentation is in the link above.

Please note this syntax is highly likely to change as it doesn’t provide a good solution to reentrant timers that are pretty common in rules.

Examples:

after 5.seconds  do
  logger.info("Timer Fired")
end
# Timers delegate methods to OpenHAB timer objects
after 1.second do |timer|
  logger.info("Timer is active? #{timer.is_active}")
end
# Timers can be rescheduled to run again, waiting the original duration
after 3.seconds do |timer|
  logger.info("Timer Fired")
  timer.reschedule
end
# Timers can be rescheduled for different durations
after 3.seconds do |timer|
  logger.info("Timer Fired")
  timer.reschedule 5.seconds
end

Right, I did some cleanup of the code before posting it here, and there I introduced the error (or unintentional behaviour) that you noticed, I went back and rectified my mistake. I will look into Rubocop to check rules, that is probably a more robust way to do it then just loading the rule into openhab and hoping for the best.

The behaviour that I am actually looking for is this;

  1. if a button receives a short pulse input (off to on to off again in a very short period of time) the output would be a change in state of the recipient item (on to off in this case). Like and oldschool latching relay.
  2. if that same button receives a long pulse input then while the button is still on for each (user settable) increment in time an action occurs.

In my case I can then use a button that is non latching to change the output state of an other item like a latching relay (with functionality in 1) and furthermore extend that button to have different behaviour when long pressed.
In other examples one can use the minimum and maximum time in the latching relay functionality to blink a status light while something is ongoing, or change an item if something happens for a given period of time. It is true that timers and while loops could achieve this functionality, but the ‘for: duration’ almost does the job already.

So I think that if there was a way to extend the ‘for: duration’ formulation with a minimum amount of time, and a repeated interval my above rule example would be much more trimmed down. In effect I would have probably 4 rules, but each containing only one line of code.

Regarding the state machine I looked into AASM, which seemed like a relatively easy to implement version of a state machine, but it turned out to not be JRuby compatible (I have no idea why). So I am now looking into the gem called ‘state_machines’.
What I am looking to implement is functionality that does occupancy monitoring, and controls lights accordingly, but also looks at the amount of ambient light and uses it to control when occupancy monitoring changes the states of the lights. Probably a state machine is overkill, but there is no kill like overkill. I have seen some examples of state machines on the forum already, but not really liked the way that one has to formulate all the conditional statements to make the transitions from and to states robust (that is more robust than I would be able to formulate them on my own).

I will look into updating my rules with timers for the release version 2 and post my findings when I do.

after changing my rules to suite the new (or what i perceive to be the new syntax) as follows:

require 'openhab'
  
rule 'long/short press button rule button up' do
  changed button_up, to: ON

  run { logger.info("button UP changed event")}

  run do
    if button_up.on? and livingroom_lamp_cluster.off?
      logger.info("turning light on")
      lamp_cluster << 30
    end

    after(2.second) do |timer|
      if button_up.on?
        logger.info("button still ON, sending increase command, rescheduling")
        lamp_cluster.brighten(10)

        timer.reschedule
       end
     end
  end
end

I get the following error:

java.lang.RuntimeException: Fail to execute action: 1
	at org.openhab.core.automation.internal.RuleEngineImpl.executeActions(RuleEngineImpl.java:1187) ~[bundleFile:?]
	at org.openhab.core.automation.internal.RuleEngineImpl.runRule(RuleEngineImpl.java:987) [bundleFile:?]
	at org.openhab.core.automation.internal.TriggerHandlerCallbackImpl$TriggerData.run(TriggerHandlerCallbackImpl.java:89) [bundleFile:?]
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515) [?:?]
	at java.util.concurrent.FutureTask.run(FutureTask.java:264) [?:?]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) [?:?]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) [?:?]
	at java.lang.Thread.run(Thread.java:834) [?:?]
Caused by: org.jruby.exceptions.NoMethodError: (NoMethodError) undefined method `after' for #<OpenHAB::Core::DSL::Rule::RuleConfig:0x235e5cdc>
	at org.jruby.RubyBasicObject.method_missing(org/jruby/RubyBasicObject.java:1715) ~[?:?]
	at RUBY.method_missing(/openhab/conf/automation/lib/ruby/lib/openhab/core/dsl/entities.rb:62) ~[?:?]
	at RUBY.<main>(<script>:14) ~[?:?]
	at RUBY.process_queue(/openhab/conf/automation/lib/ruby/lib/openhab/core/dsl/rule/rule.rb:206) ~[?:?]
	at RUBY.execute(/openhab/conf/automation/lib/ruby/lib/openhab/core/dsl/rule/rule.rb:180) ~[?:?]

does this have anything to do with the jar file not being updated? and if it needs to be updated at some point will you consider putting a version indication in the file name so that they are easier to tell apart?

The jar-file shouldn’t need to be updated, only the ruby files from here.

okay thanks, I just copied the file from the git repo, so no harm done there. But even so then copying a file that need not be updated should not result in the error above.

The error comes from ruby, saying that there’s no after method. Have you tried restarting openhab after you copied the updated ruby files? I know that for jython, files in the lib folder doesn’t get reloaded automatically, so you have to trigger a reload manually. In the jython helper libraries they have added a reload function for this, but otherwise oh need to be restarted for the files to be loaded.

1 Like

Hello Anders, I did just that, tried restarting openhab, which did not solve the problem. I tried looking in the source files for the after method, which as far as I can see is implemented in the “timers.rb” file. But from there to actually being loaded there may be any number of things that go wrong, either on my end or somewhere else.
I will try some few things more and report back.

Hmm, looking at the source i doesn’t seem like the Timer class is included in the RuleConfig or Rule classes. @broconne does it get included with some other module or is this a bug?

It’s a defect and all of my test cases for (simplicity) tested it outside the rule.
I will add a test case inside the rule and release a new version.

I have released version 2.0.1 with a fix to support the after method inside of rules.

Changelog
Latest Documentation

Very cool i will have a go this evening.

Edit: it works very well now

I’ve just started trying this out. When there’s an error in my rule/script, I usually got this in the karaf console:

19:50:20.523 [ERROR] [re.automation.internal.RuleEngineImpl] - Failed to execute rule '371e870c-1ce8-4b9a-9c53-2bb887d915fc': Fail to execute action: 1

Is there a way to have a more detailed error message to know exactly which line of code within the rule caused the error?

You can log into the openhab-cli interface (via ssh) and use the log:set command to adjust the log levels of the individual modules.
I think there is a post above where some log levels are suggested.

But in general I have never seen a log message stating the line on which the error originates. Not sure how to achieve just that.

Maybe posting the contents of your rule would help?

When I am testing the cause of the error is sent to stdout… I have opened an issue to see if I can configure JRuby to redirect that so we can log it.

1 Like

You will get the error messages with the lines if you are set to a minimum of DEBUG for org.openhab.core.automation