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

Version 5.21.0 is out!

What’s new since 5.19.0:

  • Sitemap builder: support release_only option for slider, and release option for switch mappings
  • Persistence: support last_change and next_change
  • Item builder: support CallItem and StringListType
  • Cloud/App Notification: support enhanced notifications (title, on_click, url attachment, response buttons) in notify

Example:

rule "Doorbell" do
  changed Doorbell, to: ON
  run do
    notify "Someone pressed the doorbell!",
      title: "Doorbell",
      attachment: "http://myserver.local/cameras/frontdoor.jpg",
      buttons: {
        "Show Camera" => "ui:/basicui/app?w=0001&sitemap=cameras",
        "Unlock Door" => "command:FrontDoor_Lock:OFF"
      }
  end
end

The enhanced notification will be supported in ios/android. Currently being discussed/tracked in [openhabcloud] Support new notification features · Issue #16934 · openhab/openhab-addons · GitHub

Full Changelog : v5.21.0…v5.19.0

3 Likes

FYI @JimT @ccutrer I have just pinned this topic to the Scripts & Rules category, so it always appears at the top.

3 Likes

Version 5.22.0 is out!

What’s new:

  • Persistence: support performing direct arithmetics (+, -, *, /) against PersistedState objects. You no longer need to (but still able to) access the .state property of the PersistedState object when performing calculations. This was designed so PersistedState objects can be treated the same way as a normal DecimalType or QuantityType.
    Example: Number1.update(Number1.persisted_state(1.hour.ago) * 2)

  • Add StartlevelEvent, ItemChannelLinkAddedEvent, ItemChannelLinkRemovedEvent to the list of event objects with Ruby improvements.

  • Support creating a changed trigger that triggers when any (i.e. wildcard) Thing changed status, e.g.

    rule "Alert when any Thing goes offline" do
      changed things, to: :offline
      run do |event|
        Notification.send("Warning, Thing #{event.uid} became offline", email: "admin@mysystem.com")
      end
    end
    

    Note: things is a special method that returns the openHAB Thing registry. It can be used to access a specific Thing e.g. things["astro:sun:home"], create new things, enable/disable things, etc. Thing actions are directly available on the Thing object, e.g. things["astro:sun:home"].get_event_time("SUN_SET", ZonedDateTime.now, "START")

  • Changed notify method to Notification.send. It now supports the new enhanced notification features that are added in openHAB 4.2.

  • Added Notification.hide and Notification.log

  • Add TimedCommand parameter only_when_ensured. This enables the timed command to start/extend the timer only when the item was not already in the desired state.

    Example scenario: Turn on a security light and set it on timer to turn off. When only_when_ensured is true, it will not start a timer if the security light was already turned on. Otherwise, if the timer was started and more motion was detected, extend the timer. This way, if you’ve turned on the light manually, it will stay on and not get turned off by the timer just because motion was detected.

    This following rule / command would’ve taken many more lines to implement without using the TimedCommand feature:

    rule "Motion Detected" do
      changed Motion_Sensor, to: ON
      run do
        FrontPorchLight.on for: 30.minutes, only_when_ensured: true
      end
    end
    

    With Terse rules syntax, it can be written in one line:

    changed(Motion_Sensor, to: ON) { FrontPorchLight.on for: 30.minutes, only_when_ensured: true }
    

Full Changelog : v5.21.0…v5.22.0

2 Likes

Version 5.22.1 is out!

What’s new:

  • Fix version checking code to support milestone and RC versions.

If you’ve recently upgraded to RC1, please restart openHAB so that your JRuby Helper gem gets upgraded to the latest version.

Thanks @berni288, for reporting this issue!

1 Like

Version 5.23.0 is out!

What’s new:

  • Notification.send can accept an Image Item as attachment, e.g.
    Notification.send("Doorbell pressed", attachment: DoorBell_Snapshot_Item)
    # Alternatively:
    Notification.send("Doorbell pressed", attachment: "item:DoorBell_Snapshot_Item")
    
  • Add TimeSeries#<< method. Example:
    time_series = TimeSeries.new
    time_series << [Time.now, 100]
    # same as:
    time_series.add(Time.now, 100)
    
  • Support resuming and rescheduling timed commands from its completion block.
  • Support using attachment for every trigger in file-based rules.
  • Support multiple days for every trigger, e.g.:
    rule "Weekly reminders" do
      every :sunday, :wednesday, :friday
      run { logger.info "Remember to water the plants" }
    end
    
  • Support the enhanced Button widgets for Buttongrid when creating sitemaps.
  • Fix every day-of-week trigger. Previously it would create a cron job that triggers every second on that day. It now triggers only once at midnight for that day, unless an extra at is specified.
  • Add call_item? predicate to GroupItem.

Full Changelog : v5.22.1…v5.23.0

2 Likes

Version 5.24.1 is out!

What’s new:

  • Support creating a profile for a trigger channel. See example
  • Logging prefix for file-based rules has been slightly changed so it is now possible to set the log level for all the rules in the file. See example in the PR.

Full Changelog : v5.23.0…v5.24.1

Version 5.26.0 is out!

What’s new:

  • Support creating a rule in an initially disabled state.
  • Support adding group members with GroupItem.members.add.
  • Add Thing#bridge? method to check whether the thing is a bridge.
  • Add median persistence methods which was recently merged in 4.3

Full Changelog : v5.24.1…v5.26.0

2 Likes