JRuby OpenHAB Rules System

I’ve only started looking at / using Ruby about 2 years ago when @broconne first introduced his concept jruby helper library here in this very thread (see the first few posts at the beginning!). Prior to that, I had never written a single line of Ruby code. I found that Ruby is really really nice and it is now my most favourite language. I still have a lot to learn.

Check out this article: Ruby Basics | OpenHAB JRuby Script Library

2 Likes

Hi @JimT, I’m having some issues with the metadata. Initially, I get the following error:

[INFO ] [ort.loader.AbstractScriptFileWatcher] - Loading script '/openhab/conf/automation/jsr223/ruby/personal/hue_switch.rb'
[INFO ] [itch.hue_dimmer_switch_key_triggered] - Event [hue:0820:2:diml_bedroom:dimmer_switch_event, 1002.0]; area [bedroom]; scene item [Scene_bedroom, OFF]
[INFO ] [itch.hue_dimmer_switch_key_triggered] - Scene metadata: {}
[ERROR] [obj.OpenHAB.DSL.Rules.AutomationRule] - undefined method `value' for nil:NilClass (NoMethodError)
In rule: Hue Dimmer Switch Key triggered
/openhab/conf/automation/jsr223/ruby/personal/hue_switch.rb:119:in `block in <main>'

because there is no metadata yet (that only happens when actually changing scenes in the other rule).
To quickly test this rule, I added metadata to an item via the Karaf console and tried again. This gives the same error:

[INFO ] [itch.hue_dimmer_switch_key_triggered] - Event [hue:0820:1:dim_toilet:dimmer_switch_event, 1002.0]; area [toilet]; scene item [Scene_toilet, OFF]
[INFO ] [itch.hue_dimmer_switch_key_triggered] - Scene metadata: {"last_state"=>["EVENING",{}]}
[ERROR] [obj.OpenHAB.DSL.Rules.AutomationRule] - undefined method `value' for nil:NilClass (NoMethodError)
In rule: Hue Dimmer Switch Key triggered
/openhab/conf/automation/jsr223/ruby/personal/hue_switch.rb:119:in `block in <main>'

The rule handling the key channel trigger:

rule "Hue Dimmer Switch Key triggered" do
  channel [
    'dim_office:dimmer_switch_event',
    'dim_toilet:dimmer_switch_event',
    'diml_bedroom:dimmer_switch_event'
    # etc ...
  ],
    thing: ['hue:0820:1', 'hue:0820:2'],
    triggered: [Hue::ON_SHORT_RELEASED]
  run do |event|
    area_name = event.channel.to_s.split(":")[3].split("_")[1]
    scene_item = items["Scene_#{area_name}"]
    logger.info "Event [#{event.channel.to_s}, #{event.get_event}]; area [#{area_name}]; scene item [#{scene_item.name}, #{scene_item}]"
    unless scene_item
      logger.info "Scene item not found for area: #{area_name}"
      next
    end
    logger.info "Scene metadata: #{scene_item.metadata}"
    last_scene = scene_item.metadata["last_scene"].value
    logger.info "Scene previous state: #{last_scene}"
    scene_item.update(last_scene)
  end
end

The offending line is last_scene = scene_item.metadata["last_scene"].value.
Do you have any suggestions?

Try this:
last_scene = scene_item.metadata["last_scene"]&.value || Scene::BRIGHT

This will set the scene to BRIGHT as default if there was no previous scene

1 Like

Thank you for all your support. With a minor tweak, I have it working now. Since it would not turn the lights (it always gets updated with the last_scene state) off, I changed the line that updated the scene state item to:

scene_item.update(scene_item == 'OFF' ? last_scene : 'OFF')

My next challenge is to use a ‘long press’ off the ON/OFF key to turn more/other lights on/off in a similar fashion. One thing that I could not figure out yet is how to ‘debounce’ the channel triggers for the ON_HOLD event. This event is fired every second (after a short delay) while the ON/OFF key is pressed, resulting in multiple runs of the same rule. In the old days :o) I would have resorted to an expire timer and ignore additional events for 5 or more seconds, but I think there is a more elegant way possible with JRuby. I’ve seen some things for items but not for channels.

For now I have set the trigger to ON_LONG_RELEASE but that means that no action takes place until you release the key, which is not very user friendly because it requires some sense of timing to know how long to keep the key pressed because there is no feedback while pressing the key.

So this rule is supposed to toggle between OFF and the last scene? I had assumed it was just to turn it on, and another rule reacting to a different button would turn the scene to off.

I’m glad you figured it out though! :slight_smile:

1 Like

If you just want the ON_HOLD to trigger once and ignore it until ON_LONG_RELEASE, try this: (as usual, I haven’t tested this…)

# Stores the current "hold" state of the channel
@current_state = {}

rule "Hue Dimmer Switch Key triggered" do
  channel [
    'dim_office:dimmer_switch_event',
    'dim_toilet:dimmer_switch_event',
    'diml_bedroom:dimmer_switch_event'
    # etc ...
  ],
    thing: ['hue:0820:1', 'hue:0820:2'],
    triggered: [Hue::ON_HOLD, Hue::ON_LONG_RELEASE]
  run do |event|
    next if @current_state[event.channel] == event.event

    @current_state[event.channel] = event.event
    next unless event.event == Hue::ON_HOLD
    
    # operate other lights here?
  end
end
1 Like

With the provided example, I made a lot of progress and most scenario’s work well. Thanks again, @JimT. One issue that pops up is an error with multi-threading. Sometimes it throws the following error:

[INFO ] [e_switch.hue_dimmer_key_4_triggered] - Event [hue:0820:2:dim_office:dimmer_switch_event, key pressed 4002.0]; switch area [office]
[INFO ] [e_switch.hue_dimmer_key_4_triggered] - Determine next scene from [#<Enumerator:0x69e163ad>], current=EVENING
[ERROR] [obj.OpenHAB.DSL.Rules.AutomationRule] - fiber called across threads (FiberError)
In rule: Hall Dimmer Key 4 triggered
uri:classloader:/jruby/kernel/enumerator.rb:95:in `next'
uri:classloader:/jruby/kernel/enumerator.rb:17:in `next'
/openhab/conf/automation/jsr223/ruby/personal/hue_switch.rb:107:in `next_scene'
/openhab/conf/automation/jsr223/ruby/personal/hue_switch.rb:303:in `block in <main>'

Looks like the enumerator module is not thread-safe?

The rule and method/function involved:

module Scene
  OFFICE_LIST = %w[EVENING READ BRIGHT].cycle
  ...
end

def next_scene(current, scenes)
  logger.info "Determine next scene from [#{scenes}], current=#{current}"
  next_scene = ""
  next_scene = scenes.next until current == next_scene
  return scenes.next
end

rule "Hue Dimmer Key 4 triggered" do
  channel [
    'dim_office:dimmer_switch_event',
    ...
  ],
    thing: ['hue:0820:1', 'hue:0820:2'],
    triggered: [Hue::HUE_SHORT_RELEASED, Hue::HUE_LONG_RELEASED]
  run do |event|
    area = event.channel.to_s.split(":")[3].split("_")[1]
    key_pressed = event.get_event
    logger.info "Event [#{event.channel.to_s}, key pressed #{key_pressed}]; switch area [#{area}]"
    scene = items['Scene_'+area].state
    if event.get_event == Hue::HUE_SHORT_RELEASED
      unless scene == 'OFF'
        new_scene = next_scene(scene, Scene::OFFICE_LIST) if area == 'office'
        ...
        items['Scene_'+area] << new_scene
      end
    elsif event.event == Hue::HUE_LONG_RELEASED
      items['Scene_'+area] << 'EVENING' unless scene == 'OFF'
    end
  end
end

Line 107 is next_scene = scenes.next until current == next_scene.

I’m no expert in this, but try using a mutex

@scenes_mutex = Mutex.new

def next_scene(current, scenes)
  @scenes_mutex.synchronize do
    logger.info "Determine next scene from [#{scenes}], current=#{current}"
    next_scene = ""
    next_scene = scenes.next until current == next_scene
    scenes.next
  end
end

And here’s a version that doesn’t involve #cycle nor a mutex

module Scene
  OFFICE_LIST = %w[EVENING READ BRIGHT] # Note I removed cycle here, so it's just a simple array
  ...
end

def next_scene(current, scenes)
  logger.info "Determine next scene from [#{scenes}], current=#{current}"
  scenes = scenes + [scenes.first]
  scenes[scenes.find_index(current) + 1]
end

I also changed your code a bit to (my) preferred style

rule "Hue Dimmer Key 4 triggered" do
  channel [
    'dim_office:dimmer_switch_event',
    ...
  ],
    thing: ['hue:0820:1', 'hue:0820:2'],
    triggered: [Hue::HUE_SHORT_RELEASED, Hue::HUE_LONG_RELEASED]
  run do |event|
    area = event.channel.to_s.split(":")[3].split("_")[1]
    key_pressed = event.event
    logger.info "Event [#{event.channel}, key pressed #{key_pressed}]; switch area [#{area}]"
    scene_item = items["Scene_#{area}"]
    scene = scene_item.state

    next if scene == Scene::OFF # assuming this is defined in the Scene module...

    case key_pressed
    when Hue::HUE_SHORT_RELEASED
      new_scene = next_scene(scene, Scene::OFFICE_LIST) if area == 'office'
      ...
      scene_item << new_scene
    when Hue::HUE_LONG_RELEASED
      scene_item << Scene::EVENING
    end
  end
end
2 Likes

I would recommend the second solution here to avoid the mutex and pre-creation of the iterator.

1 Like

I would make an “Area” module like this


module Area
  AREAS = %w[office living bedroom].freeze
  AREAS.each { |area| const_set(area.upcase, area) }

  def self.item_for(area)
    items["Scene_#{area}"]
  end
end

...

scene_item = Area.item_for(area)

........ if area == Area::OFFICE

While we’re at it, I’d rename Area to Zone

Taking it further, I’d use the semantic model Location instead of using “areas”

1 Like

I agree that it can be improved a lot. It’s age is showing (originally created a long time ago with DSL rules, moved to Jython at some point and partly to JS). :grinning:

I’ll try to find some time to restructure the code a bit (and the naming). I already made it more flexible by using hashmaps (dicts) for the different zones and applicable scenes.

Ultimately I would like to make it integrated with Hue scenes and groups so it will be much more interactive and responsive, but that will be quite a task…

@CrazyElectron I was just reading the docs and came across this:

trigger-event-string profile: Trigger String This profile can be used to link a trigger channel to a String item. The item’s state will be updated to the string representation of the triggering event (e.g. PRESSED).

So you could convert your channel triggers into item triggers, if you like.

1 Like

Thanks @JimT , that sounds promising! I’m going to have a look (and it just might solve the issue where channel trigger events for Hue are sometimes missed).

I’m currently playing with JRuby, since the RulesDSL meanwhile don’t offer me enough options to develop more complex “processes”. It works, but it’s no fun. So I’m looking for an alternative and ended up with JavaScript or JRuby. I really like JRuby, it’s slim and offers a few more options than RulesDSL.
However.
RulesDSL with the openHab Extension for Visual Studio are very comfortable to implement and to maintain/debug.
E.g. if you move the mouse over an item you get a tool tip of the items status.

So I’m wondering, what editor/IDE are you using for your JRuby rules?

I use vscode. I don’t however, utilise the openhab extension for vscode. I usually run the karaf console inside vscode’s terminal window, and would just type status ItemName but I don’t need to do this very often.

What I do more often is have a “test.rb” file that I would frequently wipe clean and just do short scripts to test something, and issue logger.warn / info to tell me various things that I’d like to know during runtime.

Thanks for the reply @JimT ! To use the karaf console is a good advise.

I modified the VSCode Extension to use rb-files like rules-files. Now I am able to access the values of the items directly.

I found another problem. I’m not really sure if this is a problem on my side.
I try to make a simpel calculation in a rule, but it wont work.

rule "eMobility - Stop" do
    received_command TestSwitch
    run do
        logger.info "Start"

        # next if GoEChargerPwmSignal != "CHARGING" || ChargingMode != "pvMode"

        basicPowerConsumption = 300
        forcast1h = WeatherLocalHours01SolarHarvest - basicPowerConsumption
        minute = Time.now.min
        averageSolarPower = RCT_Power_Solar.average_since(minute.minutes)
        logger.info "Value #{forcast1h}"

        caluclation = (averageSolarPower / 60 * minute)
        logger.info "Calculation #{caluclation}"

        forcast1h = 5000 - caluclation

        logger.info "Result #{forcast1h}"
        #logger.info RCT_Power_Solar.average_since(15.minutes)
    end
end

My problem is the calculation of this simple subtraction

forcast1h = 5000 - caluclation

I received this error:

2023-03-03 08:25:20.857 [INFO ] [on.jruby.helperfncs.emobility_-_stop] - Start
2023-03-03 08:25:20.879 [INFO ] [on.jruby.helperfncs.emobility_-_stop] - Value 733.107
2023-03-03 08:25:20.880 [INFO ] [on.jruby.helperfncs.emobility_-_stop] - Calculation 70.382572372135166666666666666666680743181141093700 W
2023-03-03 08:25:20.883 [ERROR] [obj.OpenHAB.DSL.Rules.AutomationRule] -  (Java::JavaLang::NullPointerException)
In rule: eMobility - Stop
RUBY.<main>(/etc/openhab/automation/jsr223/ruby/emobility/helperfncs.rb:23)
java.util.concurrent.Executors$RunnableAdapter.call(java/util/concurrent/Executors.java:515)
java.util.concurrent.FutureTask.run(java/util/concurrent/FutureTask.java:264)
java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(java/util/concurrent/ScheduledThreadPoolExecutor.java:304)
java.util.concurrent.ThreadPoolExecutor.runWorker(java/util/concurrent/ThreadPoolExecutor.java:1128)
java.util.concurrent.ThreadPoolExecutor$Worker.run(java/util/concurrent/ThreadPoolExecutor.java:628)

It works, if I change the substraction to:

forcast1h = caluclation - 5000

I have no idea, what I’m doing wrong…

In case of this horrible long result (70.382572372135166666666666666666680743181141093700) I tried to use

calculation.round(2)

This also wont work. Is this not supported in JRuby?

Looking forward to get help from you!

Looks like there ist a problem with BigDecimals…

caluclation = (averageSolarPower / 60 * minute).to_f

#calculation = 1.234

Made it work.

also in addition with .round(1)

caluclation = (averageSolarPower / 60 * minute).to_f.round(1)

#calculation = 1.2

@NikM Can you tell me what you’re trying to achieve? I’m not sure I can deduce that from your code.

First I’d like to clarify this code:

        minute = Time.now.min
        averageSolarPower = RCT_Power_Solar.average_since(minute.minutes)

Are you trying to get the average value of RCT_Power_Solar within the hour? So if it’s currently 13:25, you only want to know the average for the past 25 minutes, and if it’s 13:01, you only want to know the average for the past 1 minute?

Questions:

  • I’m curious to know what averageSolarPower / 60 * minute means, given the above.
  • What is basicPowerConsumption - does it have a unit? Is it Watt, or something else?
  • What is WeatherLocalHours01SolarHarvest - I assume it’s an item. Is it a plain item, or one with a dimension?

Are you trying to get the average value of RCT_Power_Solar within the hour? So if it’s currently 13:25, you only want to know the average for the past 25 minutes, and if it’s 13:01, you only want to know the average for the past 1 minute?

Yes indeed, that’s my target.

Before I will answer your questions.
The main target of this rule should be to calculate how much power the solar-plant has produced and compare it to the forecasted solar-“harvest”.

I’m curious to know what averageSolarPower / 60 * minute means, given the above.

The result is the energy(Wh) calculated up to 1 hour.

The item

Number WeatherLocalHours01SolarHarvest "Solarharvest in 1 hour [%.0f Wh]"  <solarplant>

gives me the forecasted solar-harvest (Wh) for the next hour.
And now I’m able to calculate/compare the two values.

basicPowerConsumption = 300

Is not used yet. You can ignore, but to feed you curiosity: This is round about the basic power consumption of or house.