JRuby OpenHAB Rules System

@joriskofman Glad we are making progress!

Can you provide the full path of the personal folder you are using?
It should be
<OPENHAB_CONF_DIR>/ automation/jsr223/ruby/personal

The directory monitored is handled by the core, not by the JRuby implementation itself. Are you also using Jython? Do those scripts get loaded, etc?

When I create a new script in the openHAB interface and enter some code, pressing the “run now” button, a new line is created under the “rules” section, with the title of the script that was just created.
When I then make changed to the script and deploy it again by pressing the “run now” button a new entry is deployed under the “rules” section, but the old one does not get destroyed. So playing around with the code one can easily end up with many rules that might run at the same time. The only way to destroy these rules is to restart openHAB, as the rules are locked because they are loaded from a file (this is the reason the openHAB interface gives).

I have not played around with the UI method of creating rules, so I will have to experiment with that. When the rules are file based the core takes care of destroying the rules when files are changed / loaded. The scripting libraries don’t handle that part of it. So without looking at it yet I am not sure creating a rule definition within code in the UI is supported. Based on what @rlkoshak said above I think in the UI you are only supposed to put whatever is in the run block above in your rule code and the triggers would be configured in the UI and not in the code blocks. Again though, I haven’t looked at the rule editing in the UI.

Sub scenario 2.1:
Initial state:
NewItem is OFF
Output:
The reponse in the logger is always only “button changed event”, but only for ON commands, not for OFF commands, or changes from ON to OFF.

OFF commands shouldn’t trigger changed if the switch is already OFF (this is a standard openhab). However, they should trigger from ON to OFF. I added a test to make sure that is the case. Can you take a look here and let me know if I am testing the right thing?

If that doesn’t help with logs set to trace can you go from on to off and provide the logs?

Sub scenario 2.2:

This is something that is not clear and I need to add documentation on. Anything outside of a run or triggered block is executed when the rule is being parsed. The reason it fails to deploy or is “stuck” in running state is because you have a loop that is being processed while while NewItem.truthy? during the rule processing phase not the rule execution phase. That while loops is adding delays and run blocks onto the rule queue.

I have made an issue to clarify the docs around rule processing and rule execution.

I will need to think about the use case you are trying here for the best way to tackle it. Even if you move it inside of a run block it won’t work the way you want it too. Give me a bit to think about it.

I have released version 1.1.0 with an update to support ‘channel’ to provide channel triggers.

Changelog
Latest Documentation

Thanks to those that helped me with suggestions on how to test channel events. It turns out you don’t need a ‘thing’ to test channel events. The openhab:things trigger <channel> <event> will send a trigger to any channel.

1 Like

You too can have that next to your name. All that means is I donate to the openHAB Foundation. If you are in Germany is even considered a charitable gift and can be a tax write-off.

The donations pay for web hosting for the forum, the docs, myopenhab.org, and some of the build services.

The shield next to my pic indicates I’m a moderator on the forum. I do have some authority in that respect but it’s limited to keeping the forum clean and functioning.

Finally, I am also a member of the architectural council as"the voice of the user" since I have so much experience helping users on the forum. But the AC only come into play if there is a contaversy among maintainers they can’t resolve themselves, or a decision that impacts lots of openHAB repos which isn’t the case here.

My opinions stem solely from my opinions and experience helping users on the forum.

Rules are and always be an interaction between your script and openHAB. For the stuff that us built into openHAB that means interacting with Java Classes. If that’s a problem, an approach similar to that taken by HABApp with a separate service interacting with OH through it’s REST API world be more to your liking.

That’s one reason why I suggested this approach above already.

Potentially both. Or perhaps the addhons maintainers and the scripters maintainers (if you plan on submitting your library as part of the helper libraries. But it is not the wrist thing to maintain your own helper libraries. One of of the “unfortunate situations where feelings were hurt” so I’ve a modest library with some stuff in Python that makes managing timer based stuff a lot easier (denounce, time of day, Gatekeeper, etc).

I didn’t realize that theses are all implemented in the helper library. That does make it easier. You could probably create your own add-on for the helper libraries and maintain it on your own. My other concerns are still valid. You’ll basically be on your own for support.

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?