JRuby OpenHAB Rules System

Thanks @pacive -
It is really based around the ‘zip’ install of OpenHAB which is what it gets tested against. For the docs, as you go, any PRs for improvement would be welcome.

Done!

A couple of releases:

2.12.0 (2021-02-02)

Bug Fixes

  • return nil for items[‘nonexistent’] instead of raising an exception (4a412f8)

Features

  • add Item.include? to check for item’s existence (1a8fd3a)

2.11.1 (2021-02-01)

Bug Fixes

  • group: support for accessing triggering item in group updates (6204f0a)

2.11.0 (2021-02-01)

Features

As usual, check changelog and latest documentation for details.

Changelog
Latest Documentation

1 Like

I liked it when the original post at the top included some sample scripts. It gave me a quick overview of the syntax and what the library is capable of, without having to delve into the documentation. It’s what piqued my interest in the first place.

1 Like

There have been quite a few updates in the past two weeks with @jimtng and @pacive doing most of the new feature work while I have been focusing on removing technical debt.

The rules system is moving towards full feature compatibility with the addition of the DateTime item type from @pacive , which works very well in the ruby context. Usage details are here

@jimtng added persistence support, which works directly on the items, docs are here. It is easy to work with and you can do things like this:

Item1.average_since(12.hours, :influxdb)

Changelog is below containing other additional features, performance enhancements and bug fixes.

Thanks for the great contributions @jimtng and @pacive

2.18.0 (2021-02-14)

Features

2.17.0 (2021-02-12)

Features

  • units: import OpenHAB common units for UoM (351a776)

2.16.4 (2021-02-12)

Bug Fixes

  • changed_duration: timer reschedule duration bug (6bc8862)

2.16.3 (2021-02-12)

2.16.2 (2021-02-11)

Bug Fixes

  • decorate items[itemname], event.item, and triggered item (ce4ef03)

2.16.1 (2021-02-11)

Performance Improvements

  • timeofdayrangeelement: subclass Numeric to make comparisons more efficient (c2482e8)

2.16.0 (2021-02-10)

Features

  • support comparisons between various numeric item/state types (510d6db)

2.15.0 (2021-02-09)

Features

  • add Persistence support (9cab1ff)

2.14.3 (2021-02-09)

Bug Fixes

  • multiple delayed triggers overwrite the previous triggers (6f14429)

2.14.2 (2021-02-08)

2.14.1 (2021-02-05)

Bug Fixes

  • number_item: make math operations and comparisons work with Floats (3b29aa9)

2.14.0 (2021-02-03)

Features

  • logging: append rule name to logging class if logging within rule context (00c73a9)

2.13.1 (2021-02-02)

2.13.0 (2021-02-02)

Features

  • dimmeritem: dimmeritems can now be compared (aa286dc)
2 Likes

I want to say thank you for all the work you guys have put into this. I just recently moved to OH3 and was really trying to do everything in the MainUI and ECMAScript but I was struggling with anything a bit more complicated than just a basic rule. I know rikoshk has put a lot of effort working on the documentation, but if I couldn’t find what I was looking for there I’d come back from stackoverflow with some code to try only to find out most of the time it wasn’t supported with the version of ECMAScript used.

Even in this early state this was easy to install and the rule structure just makes so much more sense to me. In fact I’d say it is easier than the Rule DSL.

Small things that probably should be noted in the installation documents. Currently it does not enable the info logging when installing the addon. And at least on linux the user needs to remember to change the permissions to the folders created to be owned by the openhab user. The permission one is pretty obvious as that will give you an error in the console, but it took me a while to figure out why I wasn’t seeing anything from the ‘Simple’ rule example.

Only coding that wasn’t immediately obvious was working with groups.

Sending a command to a group is:

GroupItem.group << YOUR_COMMAND

Whereas for an item it is just:

Item << YOUR_COMMAND

Also currently something like:

run { |event| logger.info(event.item.groups) } #Will list all groups not just the item's groups.

The rule I was struggling with in ECMAScript was trying to create a virtual color item that had the average color of a group of lights. Since groups can’t have a base type of HSBType. And even with HSBType and ColorItem not implemented in the library yet it was still way easier with Ruby’s map, sum, and string functions to accomplish it. While still having readable code.

1 Like

Thank you for the feedback. Glad to hear that it is working out well for you so far!

Currently it does not enable the info logging when installing the addon.

Info logging should be the default, what did you need to do to enable it?

I will open documentation issues on the other install items.

As for groups, we realize there are some issues and are working through changes for them:

GroupItem.group << YOUR_COMMAND

That is a defect (well my poor design) and will be something we will fix.

run { |event| logger.info(event.item.groups) } #Will list all groups not just the item's groups.

That also seems like a defect and not sure what would cause that to happen.

The rule I was struggling with in ECMAScript was trying to create a virtual color item that had the average color of a group of lights. Since groups can’t have a base type of HSBType. And even with HSBType and ColorItem not implemented in the library yet it was still way easier with Ruby’s map, sum, and string functions to accomplish it. While still having readable code.

Can you share what you did? It may influence our design of the ruby implementation of those item types.

I think that info level logging is disabled for all scripting add-ons in OH3 for some reason. Jython doesn’t output info logging either.

Sure, here is my color item rules. Most of the actual code here is just trying to find the related group and virtual item that needs to be updated so I can use one rule to work with multiple rooms. Though that is where Ruby really shines. I’m still not quite happy with how I have it. Not a Ruby expert. Probably could just be a class instead of two methods and a struct. But it works!

require 'openhab'

rule 'Pass commands from virtual Lights Color' do
  received_command virtual_Lights_Color.items
  triggered { |item| logger.info("Group item #{item.name} received command")}
  run { |event| groups[event.item.name.from(8)].group << event.command }
end

rule 'Update Virtual Lights Color item' do
  changed All_Lights_Color.items
  triggered { |item| logger.info("Group item #{item.name} updated")}
  run do |event|
    item = event.item
    virtual_item = get_virtual_item(item)
    group_item = get_group_item(item)
    colors = group_item.map(&:color)
    average = [:h,:s,:b].map{|k| colors.sum(&k)/colors.count}.join(',')
    virtual_item.update(average)
    logger.info("Updating virtual Item to #{average}")
  end
end

class String
  def from(position)
    self[position, length]
  end
end

NamingScheme = Struct.new(:room, :equipment, :number, :type) do
  def virtual_name
    ["virtual", room, "#{equipment}s", type].join('_')
  end

  def group_name
    [room, "#{equipment}s", type].join('_')
  end
end

def get_virtual_item(item)
  items[NamingScheme.new(*item.name.split('_')).virtual_name]
end

def get_group_item(item)
  groups[NamingScheme.new(*item.name.split('_')).group_name]
end

class Java::OrgOpenhabCoreLibraryItems::ColorItem
  def color
    state.hsb
  end
end

class Java::OrgOpenhabCoreLibraryTypes::HSBType
  HSB = Struct.new(:h,:s,:b)
  def hsb
    HSB.new(constituents["h"].to_f, constituents["s"].to_i, constituents["b"].to_i)
  end
end
1 Like

Version 3.0.0 (Breaking Change) is released with many features, performance improvements and bug fixes. Thanks to @jimtng and @pacive they put into here.

Features are below, the big breaking change is the complete rewrite of groups by @pacive. Please see the updated docs.

@sovapatr This should address many of your issues that you noted

@pacive feel free to do another post going into detail on the changes and updates to groups.

Rollershutters and Players are also now supported.

BREAKING CHANGES

  • groups: items no longer acts as a indicator to rules to
    trigger on member changes, it has been replaced with members

Features

  • groups: groups now act as items (210e507)

  • player: add support for player items (70418ab)

  • add stack trace to errors (572114e)

  • groups: support command and << (dd140aa)

  • groups: adds supports for item groups (127ab17)

  • event: add event.state for update trigger (d4eb4f7)

  • add conversion operator for DecimalType to Quantity (42bc5de)

  • persistence: automatically convert to quantity for dimensioned items (7e352d4)

  • add RollershutterItem (f5801d9)

Performance Improvements

  • logging: use block syntax to log method calls (9657e72)

  • datetime: delegate more methods directly to ZonedDateTime (ea31954)

Bug Fixes

  • persistence: selective conversion to Quantity (3187de7)

  • metadata: convert value to string before assignment (dba5db7)

  • changed: for parameter with thing chaged trigger (cd08922)

  • changed_duration: stringitem from/to comparison didn’t work (21721e7)

  • items: to_s did not include UNDEF and NULL (71f3de4)

  • add dig-method to top level metadata (2975cd5)

  • rule: otherwise blocks are always executed (dd5d5e5)

  • changed_duration: guards not evaluated for changed duration (48a63e8)

  • changed_duration: cancel ‘changed for’ timer correctly (1bf4aa3)

2 Likes

The updates for Groups makes them behave basically like any other item, and the state (as long as the group has one) can be accessed directly.

If the groups have a type (and an aggregation function, such as Group:Switch:OR(ON, OFF) Switches) they also supports all methods that the base type does, e.g. Switches.on? for checking if the state is ON, and Switches.on to send an ON-command.

Even without a type, you can send a command using Group << ON which then gets sent to all members of the group.

You can also work with the members of the group using Group.each { |item| ... } or any of the methods provided by the ruby Enumerable module. These methods work on the direct members of the group.

To work with all descendants (i.e. members of sub-groups) you can use the all_members method. This by default returns all non-group Items (as per default OpenHAB behaviour of the corresponding GroupItem method), but you can either pass :all as an argument (all_members(:all)) to include the groups as well, or :groups to gen only sub-groups. You can also pass a block to create a custom filter.

Since this is a major version change the library will not upgrade automatically on restart of the bundle if you have followed the installation instructions and put

org.openhab.automation.jrubyscripting:gems=openhab-scripting=~>2.x

in your jruby.cfg file. This means that if you have rules that depends on the old group syntax, nothing will break until you manually change that line.

To upgrade, just change to:

org.openhab.automation.jrubyscripting:gems=openhab-scripting=~>3.0

and restart the bundle or OpenHAB. You might then get some errors until you have updated your affected rules.

Required changes to rules

  • If you have used Group.items in triggers, this needs to be changed to Group.members
  • If you access a groups state or properties using Group.group[.method] change this to Group[.method]
3 Likes

Thank you for your effort here. I am not a ruby guy, but I like the looks of the rule examples shown here and your docs.
I have spent a couple hours trying to figure out between but the log only shows the else satisfied; what am I missing?

require 'openhab'

rule 'Log the rule name every minute' do
  every :minute
  run do 
    logger.info "Rule #{name} executed"    
    case Time.now
    when between('00:00'..'05:59')
      logger.info("Night")
    when between('06:00'..'11:59')
      logger.info("Morning")
    when between('12:00'..'17:59')
      logger.info("Afternoon")
    when between('18:00'..'19:59')
      logger.info("Evening")
    when between('20:00'..'23:59')
      logger.info("Night")
    else
      logger.info("Not in time range")
    end
  end
end

2021-03-07 16:20:01.073 [INFO ] [jruby.Log_the_rule_name_every_minute] - Not in time range


This seems to be a bug due to namespace conflicts. The “between” that you need is OpenHAB::DSL::TimeOfDay.between but the between without specifying the full namespace resolved to the wrong one (from the rule_config).

I’ll raise an issue and get this fixed ASAP

Until a fix is in place, changing Time.now to TimeOfDay.now should work i think.

That won’t fix it, because the range created is (String…String). One way to get it working for now is to use fully qualified path i.e. OpenHAB::DSL::TimeOfDay.between

1 Like

Version 3.1.2 is released which fixes the issue noted by @carlyler on accessing between within an execution block.

Your rule with some minor updates below, should now work:

require 'openhab'

rule 'Log the rule name every minute' do |rule|
  every :minute
  run do 
    logger.info "Rule #{rule.name} executed"    
    case Time.now
    when between('00:00'..'05:59') then logger.info("Night")
    when between('06:00'..'11:59') then logger.info("Morning")
    when between('12:00'..'17:59') then logger.info("Afternoon")
    when between('18:00'..'19:59') then logger.info("Evening")
    when between('20:00'..'23:59') then logger.info("Night")
    else logger.info("Not in time range")
    end
  end
end

Changelog is below. In addition we now support Image items.

3.1.2 (2021-03-09)

Bug Fixes

  • scope: change execution block binding to be object that based a block to rule (b529684)

3.1.1 (2021-03-08)

Bug Fixes

  • rollershutter_item: add safe navigation and nil checks (1e98464)

3.1.0 (2021-03-08)

Features

  • image: support for image items (dacc7a8)
2 Likes

The case/when statements can be simplified to:

rule 'Log the rule name every minute' do |rule|
  every :minute
  run do 
    logger.info "Rule #{rule.name} executed"    
    case Time.now
    when between('20:00'...'06:00') then logger.info('Night')
    when between('06:00'...'12:00') then logger.info('Morning')
    when between('12:00'...'18:00') then logger.info('Afternoon')
    when between('18:00'...'20:00') then logger.info('Evening')
    else logger.info("Not in time range")
    end
  end
end
2 Likes

Good use of the open range in Ruby. Thanks for sharing.

Just to highlight some ruby features, case statements return the value from the when statement, so you would not need to repeat the logging statement (if you didn’t want to).

rule 'Log the rule name every minute' do |rule|
  every :minute
  run do
    logger.info "Rule #{rule.name} executed"
    part_of_day = case Time.now
                  when between('20:00'...'06:00') then 'Night'
                  when between('06:00'...'12:00') then 'Morning'
                  when between('12:00'...'18:00') then 'Afternoon'
                  when between('18:00'...'20:00') then 'Evening'
                  else 'Not in time range'
                  end
    logger.info(part_of_day)
  end
end

If you aren’t going to need the result anywhere else, you can instead just ‘tap’ into the result.

rule 'Log the rule name every minute' do |rule|
  every :minute
  run do
    logger.info "Rule #{rule.name} executed"
    case Time.now
    when between('20:00'...'06:00') then 'Night'
    when between('06:00'...'12:00') then 'Morning'
    when between('12:00'...'18:00') then 'Afternoon'
    when between('18:00'...'20:00') then 'Evening'
    else 'Not in time range'
    end.tap { |part_of_day| logger.info(part_of_day) }
  end
end
1 Like

There have been a couple of fixes and features since the last post:

3.4.2 (2021-04-02)

Bug Fixes

  • metadata: convert loaded metadata config into Ruby objects (aa8e2b7)

3.4.1 (2021-04-02)

Bug Fixes

  • dependency: swapped mimemagic for marcel for mime type detection (b1ec891)

3.4.0 (2021-03-22)

Features

  • thing: add boolean methods for checking thing’s status (58bda12)

3.3.0 (2021-03-16)

Features

  • persistence: convert HistoricItem methods to directly return its state (942d7ea)

3.2.1 (2021-03-11)

Bug Fixes

  • handle native java exceptions in clean_backtrace (a6f7be4)

3.2.0 (2021-03-10)

Features

  • support more comparisons (3898d2d)

3.1.2 (2021-03-09)

Bug Fixes

  • scope: change execution block binding to be object that based a block to rule (b529684)

3.1.1 (2021-03-08)

Bug Fixes

  • rollershutter_item: add safe navigation and nil checks (1e98464)
1 Like

Oh my gosh, this is EXACTLY what I need. I’m a ruby expert, and I write lots of stuff in ruby that interacts with OpenHAB (mostly via MQTT), but the Rules DSL is severely lacking, and every time I try to use javascript it’s just such a mess of documentation and differing versions (OH 2.x? 3.x? built in or Graal?). And I’m just not as familiar with javascript. And your approach is (mostly) exactly how I would have done - a gem with the ruby support code, that’s easy to install, and having support for gems, etc., a DSL that relatively closely matches the Rules DSL, etc.

But my question now, is the docs at Installation | OpenHAB JRuby Script Library link to an openhab2-addons repo, which then doesn’t seem to have a viable jar. https://github.com/boc-tothefuture/openhab-jruby/releases seems much more appropriate and up to date, but only seems to have a source archive. Do I need to compile it myself for OH 3.x?

Once I have it up and running, I’d be more than happy to help maintain the ruby side libraries.

2 Likes