JRuby Scripting Official Helper Library

The JRuby Scripting Helper Library has now been included as an official part of the openHAB repository.

Changelog

Installation Instructions
Documentation
CHANGELOG
GitHub Repository
Converting JS / RulesDSL / Jython Rules to JRuby
Some Examples in JRuby

The official library starts at version 5.0 which includes major new features, fixes and some breaking changes from version 4.x. For more details, see the changelog for version 5.0.0.

The helper library will be installed automatically by the jrubyscripting addon in openHAB 4.0. For openHAB 3.4.x it can be installed by following the installation instructions. For convenience, an alternative version of the addon that performs the automatic installation is available from the marketplace.

Thanks to @broconne for creating this library and to @ccutrer for his major contributions.

Discussion thread: JRuby OpenHAB Rules System

5 Likes

Some quick examples of rules written with JRuby Scripting:

rule 'Control light based on door state' do
  changed Door_Sensor, to: [OPEN, CLOSED]
  run { |event| Cupboard_Light << event.open? } # Send a boolean command to a Switch Item
end
# Assumption: Motion sensor items are named using the pattern RoomName_Motion
# and Light switch items are named with the pattern RoomName_Light
rule 'Generic motion rule' do
  changed Motion_Sensors.members, to: ON
  run do |event|
    light = items[event.item.name.sub('_Motion', '_Light')] # Lookup item name from a string
    light&.ensure&.on for: 2.minutes
  end
end
rule 'Warn when garage door is open a long time' do
  changed Garage_Door, to: OPEN, for: 15.minutes
  run { Voice.say "Warning, the garage door is open" } # call TTS to the default audio sink
end

If you love one-liners, this is an entire rule:

changed(Door_Sensor) { |event| Cupboard_Light << event.state.open? }

Some of my recent rules:

# I have an item I turn ON when I'm waiting for someone, and want to be notified immediately based on my cameras. I have too many cameras and too many notifications to get this all the time
changed Driveway_Cars, Driveway_Persons do |event|
  next unless ExpectingSomeone_Switch.on?
  next unless event.state?

  if event.state.positive?
    only_every(:minute) do
      notify("Someone is in the driveway")
    end
  end
end
# frozen_string_literal: true

# this rule saves a snapshot from my outdoor cameras every day at solar noon, so I can build a Timelapse without shadows shifting every day

require "cgi"
require "fileutils"
require "open-uri"
require "uri"
require "yaml"

CAMS_TO_SNAPSHOT = %w[
  back_yard_south
  fire_pit
  ...
].freeze

OUTPUT_DIR = "/home/cody/docker/frigate/storage/snapshots"

# astro.items:
# DateTime Sun_Noon { channel="astro:sun:home:noon#start" }
every :day, at: Sun_Noon do
  frigate_config = YAML.load_file("/home/cody/docker/frigate/config.yml")
  snapshot_urls = frigate_config.dig("go2rtc", "streams").to_h do |cam, url|
    url = url.first if url.is_a?(Array)
    url = URI.parse(url)
    url.scheme = "http"
    url.path = "/Streaming/channels/1/picture"
    # have to re-parse so it will be the correct type since the scheme changed
    [cam, URI.parse(url.to_s)]
  end

  today = Date.today
  CAMS_TO_SNAPSHOT.each do |cam|
    snapshot_url = snapshot_urls[cam]

    FileUtils.mkdir_p(File.join(OUTPUT_DIR, cam))

    # credentials have to be passed to #open separately
    creds = [CGI.unescape(snapshot_url.user), CGI.unescape(snapshot_url.password)]
    snapshot_url.userinfo = ""
    logger.info("Downloading snapshot from camera #{cam}")

    snapshot_url.open(http_basic_authentication: creds) do |snapshot|
      IO.copy_stream(snapshot, File.join(OUTPUT_DIR, cam, "#{today}.jpg"))
    end
  rescue => e
    logger.warn("Failed to fetch snapshot from #{cam}: #{e}")
    notify("Failed to fetch snapshot from #{cam} camera")
  end
rescue
  notify("Failed to fetch snapshots from cameras")
  raise
end

This sets up a series of rules for exterior light. The lights come on in the evening to a relatively low level, then even lower at night. But if the cameras detect a person in the vicinity, they’ll immediately brighten. When the cameras no longer detect a person, it takes 15 seconds before they go back to their prior level. It’s a good demonstration of using a method to set up variations on a base rule.

def setup_person_light(dimmer, person_items, evening_level:, night_level: nil, respect_halloween: false)
  night_level ||= evening_level

  OpenHAB::DSL.rule "Set #{dimmer.name} based on occupancy and house mode" do
    on_load
    changed HouseMode_String, *person_items
    run do
      halloween = respect_halloween && Date.today == MonthDay.parse("10-31")

      OpenHAB::DSL.timers.schedule([dimmer, :state_based_on_occupancy_and_mode]) do |timer|
        timer&.cancel

        occupied = person_items.map(&:state).compact.sum.positive?
        case HouseMode_String.state
        when "Day"
          dimmer.ensure.off
        when "Evening", "LateEvening"
          if occupied && !halloween
            dimmer.ensure << 100
          elsif !dimmer.state || dimmer.state < evening_level
            dimmer << evening_level
          else
            next after(15.seconds) { dimmer.ensure << evening_level }
          end
        when "Night"
          if occupied
            dimmer.ensure << 100
          elsif !dimmer.state || dimmer.state < night_level
            dimmer << night_level
          else
            next after(15.seconds) { dimmer.ensure << night_level }
          end
        end
        nil
      end
    end
  end
end

setup_person_light(PorchCans_Dimmer,
                   [Porch_Persons],
                   evening_level: 20,
                   night_level: 0,
                   respect_halloween: true)
setup_person_light(PorchLights_Dimmer,
                   [Porch_Persons],
                   evening_level: 20,
                   respect_halloween: true)
setup_person_light(CoachLights_Dimmer,
                   [Porch_Persons, Driveway_Persons, Basketball_Persons],
                   evening_level: 50,
                   night_level: 25,
                   respect_halloween: true)
setup_person_light(BasketballCoachLights_Dimmer,
                   [Basketball_Persons],
                   evening_level: 100,
                   night_level: 25)

Version 5.0.1 released:

  • Support the latest openhab 4.0 snapshot.
  • Include convenient aliases to UI scripts and UI Scenes (OpenHAB 4.0) handy for triggering them

Version 5.2.0 released :tada:

  • Support adding semantic tags in openhab 4 and getting tag attributes
  • Support regexes for from/to/command filters on triggers
  • Various bug fixes

To use version 5.2 of the library, be sure to update the gems setting in the JRubyScripting addon to >~ 5.2 . The default setting is ~> 5.0.0 which will not upgrade the version past 5.0.x.

Full Changelog

Version 5.6.0 has just been released :tada:

Notable new features since 5.2.0:

  • New item type predicates, to easily check item’s type e.g. Item.dimmer_item? which extends to checking a group item’s base type.
  • Support openHAB 4’s unit in Items Builder.
  • Support openHAB 4’s custom Semantic tag creation through the registry.
  • Scenes can be created in file-based rules.
  • Context variables can be passed to and accessed by scenes and rules.
  • New persistence methods all_states_since and all_states_between.
  • Support event.cron_expression, event.time, event.item for time-based events.

Full Changelog

1 Like

Version 5.7.0 has just been released :tada:

  • Sitemap builder to create sitemaps dynamically through Ruby code (thanks, @ccutrer!)
  • Fixes for QuantityType arithmetics

The Sitemap builder joins the existing family of builders: Thing Builder and Item Builder. These entities can all be created / generated in Ruby code dynamically.

sitemaps.build do
  sitemap "default", "My Residence" do
    frame label: "Control" do
      text label: "Climate", icon: "if:mdi:home-thermometer-outline" do
        frame label: "Main Floor" do
          text item: MainFloor_AmbTemp
          switch item: MainFloorThermostat_TargetMode, label: "Mode", mappings: %w[off auto cool heat]
          setpoint item: MainFloorThermostat_SetPoint, label: "Set Point", visibility: "MainFloorThermostat_TargetMode!=off"
        end
        frame label: "Basement" do
          text item: Basement_AmbTemp
          switch item: BasementThermostat_TargetMode, label: "Mode", mappings: { OFF: "off", COOL: "cool", HEAT: "heat" }
          setpoint item: BasementThermostat_SetPoint, label: "Set Point", visibility: "BasementThermostat_TargetMode!=off"
        end
      end
    end
  end
end
1 Like

Version 5.8.0 has been released with several minor bug fixes and minor improvements:

  • Support removing custom Semantic tags to complement the ability to create/add them.
  • Support dynamically recreating/updating items, things, and sitemaps during the lifetime of a script.
  • Fix the properties of PointType latitude, longitude and altitude.

Full changelog

New contributor: @uqs

Example: Adding and removing semantic tags:

# Add a new semantic Equipment `Thermometer` as a subtype of `Sensor`
Semantics.add(Thermometer: Semantics::Sensor) 

# Add a new semantic Property `Illuminance` as a subtype of `Light`
Semantics.add(Illuminance: Semantics::Light) 

# Remove the `Illuminance` semantic
Semantics.remove(Semantics::Illuminance)

# Add a new Semantic Property `Luminosity`
Semantics.add(Luminosity: Semantics::Light) 
1 Like

Version 5.10.0 has been released.

Changes:

  • New event attribute event.group which gives you the triggeringGroup.
  • Add Item.link / Item.unlink to make it easier to link/unlink an item to a channel.
  • Minor improvements and bug fixes.

Changelog

1 Like

Version 5.12.1 has been released.

New features since the previous post:

  • Add ensure_states! and bang version of command shortcuts
  • Make profile usable in UI
  • Several minor bug fixes

Changelog

Example of a JRuby-created profile:

Step 1: Create the profile in JRuby:

Create a rule with a system start trigger. In file-based rules, simply create the profile in the file without setting up any triggers. It will be loaded as soon as the file is loaded on start up.

Step 2: Use the profile from the UI (or from a file-based rule)

2 Likes

Version 5.15.0 has been released.

New Features since the last post:

  • Allow specifying tags and description when creating a script and a scene in file-based rules.
  • Support staticIcon and dynamic icons in sitemap builder
  • Support buttongrid in sitemap builder
  • Add Enumerable#toggle
  • Add more helper methods to access linked channels
  • Improvements to rspec for testing user’s rules.
  • Many bug fixes

Example for creating a sitemap with a buttongrid widget:

# This creates a buttongrid to emulate a TV remote control
sitemaps.build do
  sitemap "rc", label: "TV Remote Control" do
    buttongrid item: LivingRoom_TV_RCButton, buttons: [
      [1, 1, "BACK", "Back", "f7:return"],
      [1, 2, "HOME", "Menu", "material:apps"],
      [1, 3, "YELLOW", "Search", "f7:search"],
      [2, 2, "UP", "Up", "f7:arrowtriangle_up"],
      [4, 2, "DOWN", "Down", "f7:arrowtriangle_down"],
      [3, 1, "LEFT", "Left", "f7:arrowtriangle_left"],
      [3, 3, "RIGHT", "Right", "f7:arrowtriangle_right"],
      [3, 2, "ENTER", "Enter", "material:adjust"]
    ]
  end
end
2 Likes

Version 5.17 is out!

What’s new since 5.15:

  • Add support for TimeSeries (in openHAB 4.1+).
  • Add support for icons in sitemap builder switch mappings
  • Support selecting multiple types of location and equipment in the semantic model
  • Several minor features, improvements, and bug fixes

Full changelog v5.15.0…v5.17.0

Main documentation for openHAB JRuby Library

How to Install

Enjoy writing rules in JRuby!

1 Like

Version 5.19.0 is out!

What’s new since 5.17:

  • Support openHAB 4.2 enhancements to persistence methods:

    • New _until methods in addition to the _since and _between methods
    • #persist supports persisting a past/future state and a time series data
    • Support new methods: next_state, next_update, remove_all_states_since (_between, _until)
    • Deprecation of some persistence methods:
    • openHAB 4.2 persistence methods now return a QuantityType for dimensioned items. However, it has always been done this way in the jruby library for all the supported openHAB versions (3.4, 4.0, 4.1). So this change in core doesn’t impact existing jruby scripts.
    • Bug fix: the persistence #evolution_rate returns a DecimalType instead of QuantityType, because it represents a percent change value.
  • Add rule!, scene!, and script! to create a rule without generating multiple rules with unique IDs when existing uid is encountered.

    • rule (without a bang) with an explicit uid will avoid creating a new rule when a rule with the same uid already exists
    • rule! with an explicit uid will remove the previous rule with the same uid and create apply the new rule.
    • Without an explicit uid, rule will generate a unique suffix to avoid uid clashes, whereas rule! will overwrite (remove) the previous rule when the inferred uid is the same.

    This behaviour allows creating UI rules with file-based syntax, without affecting most existing file-based rules.

  • TimeSeries#add: accept a Ruby String or a Numeric value for convenience.

  • Support filtering in item_added, thing_added, channel_linked triggers, as well as in their _removed, _updated, and _unlinked counterparts.

  • Linking a channel to an item can now be done just as easily as linking an item to a channel.

Full changelog v5.17.0…v5.19.0

1 Like