JRuby OpenHAB Rules System

You can’t really have “global variables” in the traditional manner. Each script file is isolated from one another under the default threading option, so a Ruby $global in one file isn’t visible in another file.

In any case it’s usually not recommended to use a global variable, but if you happen to need one, you could use the Shared Cache feature. It uses the built in mechanism from openHAB, so beware that openHAB will invalidate/remove the stored value when all scripts accessing that key was unloaded (e.g. reloaded?).

I usually use item’s metadata to store a global “flag” that I need in my rules.

Hoping someone can help me please :slight_smile: I recently moved my openhab to docker. I am running the stable release. Recently my JRuby Scripting rules stopped working and I am at a loss as to why.

I have pasted the error from the logs below.

2023-08-10 10:07:27.434 [INFO ] [ort.loader.AbstractScriptFileWatcher] - (Re-)Loading script '/openhab/conf/automation/jsr223/personal/keba.rb'

2023-08-10 10:07:27.812 [WARN ] [ernal.JRubyScriptEngineConfiguration] - Error evaluating `require 'openhab'`

org.jruby.exceptions.LoadError: (LoadError) no such file to load -- openhab

Did you mean?  open3

	at org.jruby.RubyKernel.require(org/jruby/RubyKernel.java:1057) ~[?:?]

	at RUBY.require(uri:classloader:/META-INF/jruby.home/lib/ruby/stdlib/rubygems/core_ext/kernel_require.rb:85) ~[?:?]

	at RUBY.<main>(<script>:1) ~[?:?]

2023-08-10 10:07:27.827 [ERROR] [ipt.internal.ScriptEngineManagerImpl] - Error during evaluation of script '/openhab/conf/automation/jsr223/personal/keba.rb': Error during evaluation of Ruby in org/jruby/RubyKernel.java at line 1057: (LoadError) no such file to load -- openhab

Did you mean?  open3

2023-08-10 10:07:27.828 [WARN ] [ort.loader.AbstractScriptFileWatcher] - Script loading error, ignoring file '/openhab/conf/automation/jsr223/personal/keba.rb'

If you’ve recently upgraded your jruby helper library to version 5.x, you’ll need to make some changes.

  • Remove the require 'openhab' line at the top of your script. It is no longer required, because the addon now injects the require line by default. If you do want to have the explicit require statement, that’s fine too, because requiring the same file multiple times are simply ignored. In any case the new require line is require 'openhab/dsl'.
  • You will also be affected by other breaking changes from 4.x to 5.x so please read through the v5.0.0 changelog

If you encounter any other issues, feel free to post here again.

Thanks Jim. I downgraded to 4.x at the moment. Will read through the changelog and upgrade later

Could you please share your configuration file. What did you change?

No Problem, I can push you in the right direction.

  1. You will find the extension settings in your user-folder:
    USER/.vscode/extensions/openhab.openhab-1.0.0

2.Within this folder you can open the json-file /meta/openhab.tmLanguage.json

3.Here you should add “rb” to the fileTypes part. Then you can study the file and maybe change some other settings.

After saving the file, you have to restart VSCode, i think. And now the coloring of the text in the rb-files should be DSL-Rules like. And you should have access to the value of an item by howering the mouse pointer over it.

2 Likes

Hello,

I have an issue with simple class. I’m completely new with jRuby.

#file utils.rb
@can_send_pushover_message = true

class PushoverMessenger
  def initialize
    @can_send_pushover_message = true
  end

  def send_pushover_html_message(pushover_message, wait_time_for_next_message)
    if @can_send_pushover_message
      pushover_account = things["pushover:pushover-account:xxxxxxxxxx"]
      pushover_account.sendHtmlMessage(pushover_message, "openHAB")
      @can_send_pushover_message = false
      after wait_time_for_next_message.minutes do
        @can_send_pushover_message = true
      end
    end
  end
end

and i call method:

require "utils.rb" # here is class definition

pushover_messenger = PushoverMessenger.new

rule "test" do
  on_load
  every 10.seconds
  run do
    pushover_messenger.send_pushover_html_message("Hello: <font color='#EDBB99'><b>World</b></font>",15)
  end
end

and when I create a class instance and call method I get error:

undefined local variable or method `things’

What should I add as require or what other trick should I do.

Marcin

things is not available by default inside a class context, only at top level. You either need to call OpenHAB::DSL.things explicitly, or include OpenHAB::DSL into the class to make it available there.

Note also that your @can_send_pushover_message at top level is a different variable than the one inside your class. You don’t (currently) assign to the top level one anywhere, so it will work fine as is. Just warning you if you try to modify it from something not inside the class, and can’t figure out why the class instance isn’t seeing it.

1 Like

This is it. Thank you. Rest of code needs my attention, it is only a draft.

I have an issue using delay. I receive this error: undefined method 'delay' for main:Object (NoMethodError) with the following code snippet:

brightness = 0
while(brightness <= 100)
      brightness = brightness + 1
      prevBrightness = brightness
      light << brightness
      delay 15.seconds
      next if prevBrightness != brightness # User changed brightness in the meantime
end

It should be a simple wakeup light which increases brightness over time and aborts if the user changed the brightness in the meantime.
The script works nice with sleep but I would like to avoid using it.

If I’m not mistaken, delay creates a timer, meaning it schedules the code after it to run later. This is only available in the rule context, so when you’re in another code block, like a loop, it doesn’t exist. If it had been working, I think this rule would have spun out of control anyway, creating tons of new timers.

delay is not meant to be used like that. It is not the same as “sleep” or pause. It is actually an execution block, similar to run and triggered. The link will show you some examples of how to use it properly.

Firstly, in Ruby there are better ways than looping with for, while, do, e.g.:

0.upto(100).each do |brightness|
  # code here to use brightness
end

Secondly, the next if .... above won’t do anything. I think you meant to use break?

Lastly, to implement what you are trying to do, you should use timers instead of some form of sleep.

Something like this perhaps:

rule "Brighten my morning!" do
  every :day, at: "5am"
  run do
    next if light.on? # don't do the gradual thing if light is already on

    level = 0

    after(15.seconds) do |timer|
      next unless light.state.to_i == level # Stop the routine if it got changed manually
      # Note to_i above to round the state to integer in case it has fractions

      level = level + 1
      light << level
      timer.reschedule if level < 100 && !timer.cancelled?
    end
  end
end

Jim
I use UI rules not files so the portion of the rule I would use is below correct?
Can I ask a few questions?

is light an item?
is level a variable?
So we start out checking if the light is on

    next if light.on?

What does next do? Does it require the end
Is this not required in UI rule?
then we set level to 0

 after(15.seconds) do |timer|

ok, we are creating a timer here?

timer.reschedule if level < 100 && !timer.cancelled?

now we are resetting the timer if level isn’t 100 yet or the timer got cancelled? How would the timer have gotten cancelled?

      level = level + 1
      light << level

inside the loop, every 15 seconds the level gets incremented by 1 until we reach 100?
sooo… if the light is not on, we set level (a local variable we just created on the fly?) to 0 then loop every 15 seconds until level reaches 100

Sorry, if you could just break it down a little for us noobs. Some times ruby is so succinct it is hard to follow

Yes to both. You’d need to point light (or rename it) to an actual item.

It’s an early exit from the enclosing block. If you’re using UI rule, you’ll need to replace that with return instead.

Yes.

It’s a paranoid failsafe. It will work even without it, but I have been caught with a runaway self repeating timer that keeps executing / rescheduling itself even after being cancelled.

It’s because you can cancel a timer, and then reschedule it again. So if you happen to already be executing inside your timer handler and you cancel the timer from somewhere else, your code keeps going to the next line and the next line…and eventually reschedule itself thus nullifying your cancel call.

Correct.

level is indeed a local variable there.

I’m just a more experienced noob :smiley: but still a noob. Happy to help if I can.

so in UI my rule looks like this?

if light.on?

    level = 0
    after(15.seconds) do |timer|
      next unless light.state.to_i == level
      level = level + 1
      light << level
      timer.reschedule if level < 100 && !timer.cancelled?

end

next is unneeded
so is the end for if or the timer?
Where does the return go?
I think the file example trying to convert it to UI, I think that confuses me as far as the structure

No this will give you the opposite logic. We want the code to run if light is off, not on. Also your code is missing one closing end:
if ... end on the outside,
after(xxx) do ..... end on the inside. The do and end are a pair, and it is called a block in Ruby. Blocks are a fundamental construct in Ruby that is crucial to know/understand.

In this case after(....) is a method that takes a block. The stuff inside the do…end is the block being passed to the after() method

I literally meant: replace next with return like this:
The only thing different to the original is s/next/return/


    return if light.on? # don't do the gradual thing if light is already on

    level = 0

    after(15.seconds) do |timer|
      next unless light.state.to_i == level # Stop the routine if it got changed manually
      # Note to_i above to round the state to integer in case it has fractions

      level = level + 1
      light << level
      timer.reschedule if level < 100 && !timer.cancelled?
    end

PS don’t forget to point light to the actual item.

as the very first line:
light = MyActualLightDimmerItem

This works as intended! (But 5am… hell no :smiley: )

OK, sorry, let me see if I got this straight.

return if light.on?

This is a stand alone statement. It needs no end because this one line is it
It means ‘if the light is on bail out’ early exit, none of the following code runs

    level = 0

This is a local variable being created and set to 0

ok so here we create a block with after

after (<some period of time goes here>) do
        ###do whatever here
end

the end is the completion of the do block

|timer|

does timer have a name? local timer, disappears after code runs?

      next unless light.state.to_i == level

How does this work? level is a local variable right?
How would our item ‘light’ have a state the same as level
and this is the same thing as return, an early exit from the block?
Sorry for dumb questions

|timer| is the argument list for the block. So essentially timer is a local variable with the scope of (only visible in) the block. When a method calls a block, it can pass arguments just like any other method. In this case, Timer yields itself to the block, specifically to allow constructs like this.

Correct, level is a local variable with scope of (in your case) the entire “file” of the UI rule. Ruby is “cool” in that every block is a closure, and “captures” any local variables that are referenced within it, but exist outside it. This is just a really fancy way of saying that even though the block from the timer is executed asynchronously after the rule was initially executed, level is a single variable that will continue to live on, and be a single value - i.e. if you change it within the block, its effects are visible outside the block to anyone else referencing the same value, including multiple executions of the same block.

The logic here is:

  • light has state 0, level is 0
  • set level to 1
  • command light to change to level (1)
  • wait 15 seconds, and repeat (unless we’ve reached 100)

If during that 15 seconds, the light was commanded to something different, like if someone turned it off locally, the state would no longer match level, and we want to stop our “loop” of slowly dimming up. Essentially it was cancelled.

return returns from the current method (or file, which a UI rule is) immediately. next returns from the current block, but allows the method calling the block to continue executing, and possibly call the block again. This is analogous to continue in C/C++/Java etc., except that it is far more general since it applies to blocks, not just basic loops. In terms of loops, it means “skip this single iteration”. break returns from the current block and the method calling the block, so that it (likely) cannot be called again. For file-based rules, the actual execution part of a rule is just another block, so we use next to skip the execution of the rule, instead of return which would mean “return from the enclosing method” (which would actually raise a LocalJumpError because there usually is no enclosing method, and return at file level cannot be used from inside a block). Using break will also raise an error in a file-based rule, or in your timer case, because break is invalid unless the block is being called immediately from within the method you’re passing it to. For rules, we save the block off and call it when the rule is triggered. For timers, we save the block off and call it when the timer expiries.

I hope all that makes sense. Happy to clarify any points I was too vague or dense on.

Inorite?!?! I swear that guy never sleeps!

1 Like

Sorry to have beat this to death
so here is what I have in UI rule

logger.info("jruby fading rule run")
### BigRedDimmer is a item - a Lifx bulb

return if BigRedDimmer.on? # if the light is on, nevermind, do nothing
level = 0
after(1.seconds) do |timer|
  next unless BigRedDimmer.state.to_i == level
      # they should be the same unless someone/something changed it
  level = level + 1
  BigRedDimmer << level
  timer.reschedule if level < 100 && !timer.cancelled?
end

So I have a rule, if the light is off, it will fade the light on one step a second until 100 unless at some point during that process, someone or something charges the dimmer, then the process just aborts

How do I pass in which light item I want to fade, say if I wanted to make it work on any light? Can I use it in a script to fade any item? (that works like a dimmer)

How about what if it isn’t off and it just fades from what ever it is now to a new setting either up or down?