JRuby OpenHAB Rules System

conf/rules is just for RulesDSL

conf/automation/jsr223/ is for jsr223 scripts (depending on which automation addon is enabled). The type of script depends on the file extension, so .rb files will be executed by jruby, .js will be executed by jsscripting, .py. will be executed by jython, etc.

Traditionally these scripts are organised inside a subdirectory that correspond to the actual language, so jruby scripts are usually placed in /conf/automation/jsr223/ruby, jsscripting in conf/automation/jsr223/js and so on. This is purely for organisational purposes, done by convention. So it is advisable to follow the convention.

You can make subdirectories below this to organise your scripts even further. For example I have conf/automation/jsr223/ruby/personal/lights/ for all my light-related scripts.

conf/automation/lib/ruby is by convention used for storing shared code/libraries that you could require from your script. It is also added to Ruby’s $LOAD_PATH (by default). You are not required to use this, you can put it in /any/place/you/like/that/is/accessible as long as you set the RUBYLIB config so that the path gets added to $LOAD_PATH, but it’s easier to stick to the convention.

Lastly, GEM_HOME is set to conf/scripts/lib/ruby also by default - you can change this in the addon config if you so desire.

@jimtng @boconnor - thank you so much for this.

It’s very, very nice to be able to write code that writes rules. I found a couple of issues - should they go into the GH issues in the addons repo or do you prefer them here?

1 Like

@peterhoeg the ideal place is an issue on GitHub https://github.com/boc-tothefuture/openhab-jruby/issues

1 Like

4.44.0 is now released.

Notable changes since 4.39.1:

  • Make file-based rules viewable through the UI (read-only) (openhab 3.4+).
  • Add support for persistence between methods (openhab 3.3+)
  • Support tags for file-based rules
  • Many minor fixes and improvements

Thanks to @ccutrer for the fixes and improvements.

As always, for more details on the changes go here and latest docs are always here

2 Likes

Hope this is the right forum to post this. I have a generic rule which dynamically gets a related item to update a date/time. It works unless the dynamic item is NULL. code is below

event_motion_tripped = items[event.item.name + "_Tripped"]
        event_motion_lasttrippeddate = items[event.item.name + "_LastTrippedDate"]
          
        case newVariable
        when "Tripped"
            if (newTripped == 1)
                event_motion_tripped.on
                event_motion_lasttrippeddate.update(newDateTime)
            else
                event_motion_tripped.off
            end
        end

So if event_motion_tripped is NULL, the event_motion_tripped.on throws an error (undefined method `off’ for nil:NilClass (NoMethodError).

My question is, can I handle this?

Use the safe navigation operator, i.e. event_motion_tripped&.on

Or if you feel it’s appropriate, add a guard line above your case statement

next unless event_motion_tripped

That will just skip setting the item to ON correct? Is there a way to set the item to ON when it’s NIL?

The error message says that the object event_motion_tripped itself is nil, i.e. it isn’t referring to an actual Item. This could happen if there is no such item with the name event.item.name + "_Tripped"

The item exists in my item file, it’s just NULL.

so event_motion_tripped = items[event.item.name + “_Tripped”] returnd a NIL item, hence the .on throws and error, but if I reference one of the items directly from the script like SplitSystemUnit_Spare_Motion_Tripped.on - that works even if the item is currently NIL.

Try adding logger.info "#{event.item.name}_Tripped" and see what the name really is when you got the error. I am sure it would say something that doesn’t exist.

You can always do AValidItem.on regardless of its current state, so even when its state is currently NULL (AValidItem.state == nil && AValidItem.null? == true). As I said above, event_motion_tripped is the Ruby nil object (not to be confused with an Item object having NULL as its state). And it’s nil because items["a non existent item"] will return nil.

1 Like

Thanks @jimtng ,

I have done what you suggested. results below.

script:

        event_motion_tripped = items[event.item.name + "_Tripped"]
        event_motion_lasttrippeddate = items[event.item.name + "_LastTrippedDate"]
        logger.info "#{event.item.name}_Tripped"
        logger.info "#{event.item.name}_LastTrippedDate"

        case newVariable
        when "Tripped"
            if (newTripped == 1)
                event_motion_tripped.on
                event_motion_lasttrippeddate.update(newDateTime)
            else
                event_motion_tripped.off
            end
        end

log output:

2022-11-28 09:57:47.720 [INFO ] [utomation.jruby.motion.aeotec_motion] - SplitSystemUnit_Spare_MotionSensor_Tripped

2022-11-28 09:57:47.723 [INFO ] [utomation.jruby.motion.aeotec_motion] - SplitSystemUnit_Spare_MotionSensor_LastTrippedDate

2022-11-28 09:57:47.869 [ERROR] [obj.OpenHAB.DSL.Rules.AutomationRule] - undefined method `on' for nil:NilClass (NoMethodError)

In rule: Aeotec Motion

/etc/openhab/automation/jsr223/personal/motion.rb:20:in `block in <main>'

2022-11-28 09:57:47.880 [INFO ] [utomation.jruby.motion.aeotec_motion] - SplitSystemUnit_Spare_MotionSensor_Tripped

2022-11-28 09:57:47.881 [INFO ] [utomation.jruby.motion.aeotec_motion] - SplitSystemUnit_Spare_MotionSensor_LastTrippedDate

2022-11-28 09:57:47.914 [ERROR] [obj.OpenHAB.DSL.Rules.AutomationRule] - undefined method `on' for nil:NilClass (NoMethodError)

In rule: Aeotec Motion

/etc/openhab/automation/jsr223/personal/motion.rb:20:in `block in <main>'

items file:

Switch                  SplitSystemUnit_Spare_Motion_Tripped               "Spare Room Motion_Tripped Sensor [%s]"
DateTime                SplitSystemUnit_Spare_Motion_LastTrippedDate       "Timestamp of TriSensor Trip [%1$tY-%1$tm-%1$tdT%1$tH:%1$tM:%1$tS]"        <time>

As you can see, I did infact have the wrong name in the script
thanks very much for you help :slight_smile:

Let me start by saying that I really like the compact way of writing rules with JRuby. Thanks for all the hard work put into that!

I’m in the process of migrating my rules (mostly Jython) to JRuby and I can’t figure out how to get the channel name as string, so I can extract part of the channel name with .split(). Since Ruby is new for me as a programming language, I just might miss something very obvious

For the record: running OH 3.3.0 in a container (on Kubernetes).

I am converting a rule that handles different Hue dimmer switch events to turn lights on/of or select a predefined scene. The sample rule for testing that:

rule 'Hue Dimmer Switch Key triggered' do
    channel [
        'dim_office:dimmer_switch_event',
        # ... etc ...
        ],
        thing: 'hue:0820:0016852dc5a3'
    run do |event|
        logger.info("Hue Dimmer Trigger event: #{event}")
        logger.info("Hue Dimmer Trigger channel: #{event.channel}")
        sw = event.channel.split(':')
        logger.info("Hue Dimmer Trigger switch: #{sw}")
    end
end

The log shows:

[INFO ] [rulesupport.loader.ScriptFileWatcher] - Loading script '/openhab/conf/automation/jsr223/ruby/personal/hue_sw_office.rb'
[INFO ] [fice.hue_dimmer_switch_key_triggered] - Hue Dimmer Trigger event: hue:0820:0016852dc5a3:dim_office:dimmer_switch_event triggered 1000.0
[INFO ] [fice.hue_dimmer_switch_key_triggered] - Hue Dimmer Trigger channel: hue:0820:0016852dc5a3:dim_office:dimmer_switch_event
[ERROR] [obj.OpenHAB.DSL.Rules.AutomationRule] - undefined method `split' for hue:0820:0016852dc5a3:dim_office:dimmer_switch_event:Java::OrgOpenhabCoreThing::ChannelUID (NoMethodError)
In rule: Hue Dimmer Switch Key triggered
/openhab/conf/automation/jsr223/ruby/personal/hue_sw_office.rb:29:in `block in <main>'

How can I convert the Java class ChannelUID to a string in JRuby? Could not find an example in the documentation.

On a side note: I noticed that the error logging used a backtick as opening for the method name and a single quote as closing in the undefined method error message
 :o)

I believe event.channel.to_s.split(':') should do the trick!

As for the backtick in the error message, that’s just how ruby’s error messages are written (you can see it in the stack trace location as well). Not sure about the exact reason for it, but I’ve seen it elsewhere as well.

1 Like

Ok, this is a bit embarrassing
 I should have tried to use Ruby conversions. But since it is a Java Class, I though that would not work.

Thanks!

JRuby creates some convenient alias methods for all java classes, e.g to_s => toString(), and also any getSomething() => something, setSomething() => something= etc.

A few things to help:

logger.info "#{something.class} #{something.methods}"

will tell you what class something is, and what its methods are.

Also ChannelUID has some helper methods that you might find useful instead of splitting and interpreting it yourself.

Thanks for all the help. Making some progress, but struggle with the previous_state method.

The defined item has persistence (assigning group gPersist that is defined in the MAPDB config):

String Light_Scene_office "Lights Office" <light> (gOffice,gPersist)

The mapdb.cfg for completeness:

Strategies {
    default = everyUpdate
}
Items {
    // persist all items added to group gPersist on every change and restore them from the database at startup
    gPersist* : strategy = everyChange,restoreOnStartup
}

I changed the Item state a few times through Karaf to make sure it has a previous state before testing the rule.
The rule (simplified for testing):

rule 'Hue Dimmer Switch Key triggered' do
    channel [
        'dim_office:dimmer_switch_event',
        # ... etc ...
        ],
        thing: 'hue:0820:0017682fc1b3',
        triggered: ['1000.0', '1002.0']
    run do |event|
        lightScene = items['Light_Scene_' + event.channel.to_s.split(':')[3].split('_')[1]]
        logger.info "#{lightScene.class} #{lightScene.methods}"
        logger.info "Hue #{lightScene.name} state is #{lightScene} with trigger #{event.get_event}"
        logger.info "Previous state: #{lightScene.previous_state}"
    end
end

Gives an error:

[INFO ] [ort.loader.AbstractScriptFileWatcher] - Loading script '/openhab/conf/automation/jsr223/ruby/personal/hue_sw_office.rb'
[INFO ] [fice.hue_dimmer_switch_key_triggered] - Java::OrgOpenhabCoreLibraryItems::StringItem [:casecmp, :valid_encoding?, :acceptedDataTypes, :to_java_bytes, :getAcceptedDataTypes, :squeeze!, [**SNIP**] :previous_state, :delta_since, [**SNIP**] :state=, :]
[INFO ] [fice.hue_dimmer_switch_key_triggered] - Hue Light_Scene_office state is OFF with trigger 1000.0
[ERROR] [obj.OpenHAB.DSL.Rules.AutomationRule] - undefined method `state' for nil:NilClass
Did you mean?  state? (NoMethodError)
In rule: Hue Dimmer Switch Key triggered
uri:classloader:/META-INF/jruby.home/lib/ruby/stdlib/delegate.rb:83:in `method_missing'
/openhab/conf/automation/jsr223/ruby/personal/hue_sw_office.rb:35:in `block in <main>'

As can be seen from the methods dump, :previous_state should be available for this StringType item. Any suggestions how I can get the previous state?

From a quick glance, I don’t see anything wrong with your rule. This may be an error in the library. While we do test many of the persistence methods, for some reason pervious_state isn’t one of them.

I can try and reproduce this tonight (US Eastern time) and let you know what I find.

MapDB can only store one state for each Item, which is the current state (its main purpose is to restore states on startup), which means there isn’t any previous state, so null is returned. If you need this you have to use a different persistence service.

Good catch. Our error message could be a bit less confusing as it is not clear why invoking previous_state invokes state on a nil object.