Openhab JRuby Rule Conversation

Will post my conversations from OpenHAB Rules/Python rules in this thread in case it is helpful to others.

See Main OpenHAB JRuby Thead for more details.

Note: A lot of my rules that I am converting were more than likely poorly written as I was learning the DSL/Python Rules Engine.

I have a very weird electric setup where the condensate pump for the HVAC in the basement is on the same circuit as the garage GFCI. Whenever the garage GFCI trips, the condensate pump loses power which after some period of time causes the water sensor in the HVAC system to trigger causing power to be cut to the HVAC system. I added a lamp module onto the GFCI in the garage and get notified if it is offline.

I was also using a vera instead of the openhab zwave until recently so it was hard to determine if the relay was offline.

Original Rules DSL


var boolean notified = false

rule "Check if Garage outlet is tripped"
when 
    Time cron "0 0/1 * * * ?"   // every minute
then
    val updateInMillis = (LampRelay2LastPollSuccess.state as DateTimeType).zonedDateTime.toInstant.toEpochMilli()
    val minutesSinceUpdate = (now().millis - updateInMillis) / (60 * 1000)
    logDebug("Health Check", "Garage Relay updated {} minutes ago", minutesSinceUpdate)
    GarageRelayUpdate.postUpdate(minutesSinceUpdate)
end


rule "Alert condensate pump circuit"
when 
    Item GarageRelayUpdate received update
then
    logDebug("Health Check", "Notified Value (Before): {}", notified)
    if( notified == false){
        if( GarageRelayUpdate.state > 60){
          sendBroadcastNotification("Check garage GFCI outlet, HVAC may stop working until reset")
          notified = true
        }
    }
    else{
        if( GarageRelayUpdate.state < 60){
          notified = false
        }
    }
    logDebug("Health Check", "Notified Value (After): {}", notified)
end

JRuby Rules

rule 'Notify when garage relay is offline' do
  changed things['zwave:device:aeotec:node5'], :from => :online, :to => :offline
  run do
     logger.warn "Garage Relay went offline - condensate pump may not have power, notifying"
     notify "Check Garage GFCI outlet, Downstairs HVAC may stop working until reset"
  end                                                                                                                                       
end

This was a python rule I had to notify if a garage door was open at 8PM or or when arming the alarm in either night delay or night mode.


from core.rules import rule
from core.triggers import when
from core.log import logging
from core.actions import NotificationAction

def checkGarageDoors(notificationReason):
  logging.warn("Checking Garage Doors")
  openDoors = filter(lambda item: item.state == OPEN, ir.getItem("GarageDoors").members)
  doorsOpen = len(openDoors)
  logging.warn("{} Garage Doors are open".format(doorsOpen))
  for door in openDoors:
    notificationText = " {} and {} is open".format(notificationReason,door.label)
    logging.warn("Sending notification: " + notificationText)
    NotificationAction.sendNotification("broconne@gmail.com", notificationText) 


@rule("Check Garage Doors at night", description="Check Garage Doors at Night")
@when("Time cron 0 0 20 1/1 * ? *")
def garageDoorCronCheck(event):
  checkGarageDoors("It's 8PM")

@rule("Check Garage Doors when in night mode", description="Check Garage Doors in night mode")
@when("Item Alarm_Mode changed to 10")
@when("Item Alarm_Mode changed to 14")
def garageDoorCronCheck(event):
  checkGarageDoors("Night mode set")

JRuby converted

def check_garage_doors(reason)
  open_doors = groups['GarageDoors'].select(&:open?)
  logging.debug("#{open_doors.length} garage doors open")
  open_doors.map{ |door| "#{reason} and #{door.label} is open" }
            .each { logger.warn(text); notify(text) }
end

rule 'Check garage doors at night' do
  every :day, at: '8pm'
  run { check_garage_doors "It's 8PM"}
end

rule 'Check garage doors when alarm armed for night' do
  changed Alarm_Mode, to: [10,14]
  run { check_garage_doors "Night mode set"}
end

This is a conversion of a python rule that notified when the media room TV was turned on during certain hours of the day (so we know if the kids turn on the TV when they aren’t supposed to).

Python rule:


from core.rules import rule
from core.triggers import when
from core.log import logging
from core.actions import NotificationAction
from datetime import datetime, time


def in_between(start,end):
  now = datetime.now().time()
  if start <= end:
      return start <= now < end
  else:
      return start <= now or now < end

@rule("Media Room TV Notify", description="Notifies if Media Room TV is turned on after 8:30 and before 6:00 AM")
@when('Item HarmonyMediaRoomActivity changed from "PowerOff"')
def notifyMediaRoomTV(event):
  notify_start = time(20,30)
  notify_end = time(6)
  if in_between(notify_start,notify_end):
    NotificationAction.sendBroadcastNotification("Media Room TV Turned On")

The JRuby rule makes use of the between guard to simplify the rule a lot

require 'openhab'

rule 'Notify if media room TV is turned on after 8:30PM and before 6:00 AM' do
  changed HarmonyMediaRoomActivity, from: "PowerOff"
  run { notify("Media Room TV Turned On") }
  between '8:30PM'..'6:00AM'
end

It seems like the JRuby versions have quite a few less lines of code
The syntax looks very succinct

1 Like

I have a rule where I generate a URL to display a progress “circle” for the washer and dryer:

Python:

import urllib
import urlparse
from core.rules import rule
from core.triggers import when
from core.log import logging
from core.actions import NotificationAction
from time import sleep


def createRadialChart(progress):
    url_base = "https://quickchart.io/chart?c="
    url_query = urllib.quote(
        "{type:'radialGauge',data:{datasets:[{data:[%s],backgroundColor:'#283593'}]},options:{title:{display:true,text:'Progress',fontSize:20}}}" % progress)
    return url_base + url_query


def updateURL(source, dest):
    logging.debug("Executing Progress URL Update!")
    item = items[source]
    if isinstance(item, UnDefType):
        logging.debug("Undefined type for source")
        progress = 0
    else:
        progress = item.floatValue()

    url = createRadialChart(progress)
    logging.debug(url)
    events.sendCommand(dest, url)


@rule("Washer Progress URL ", description="Generates a URL for showing a progress bar for washer progress")
@when("Item Washer_Progress changed")
def washer_finish_url(event):
    updateURL("Washer_Progress", "Washer_Progress_URL")


@rule("Dryer Progress URL ", description="Generates a URL for showing a progress bar for dryer progress")
@when("Item Dryer_Progress changed")
def dryer_finish_url(event):
    updateURL("Dryer_Progress", "Dryer_Progress_URL")


def sendNotification(appliance):
    if items.Brian_Home == ON and items.Brian_Laundry_Notification == ON:
        NotificationAction.sendNotification(
            "email@gmail.com", "%s is finished" % appliance)

#    if items.Anna_Home == ON and items.Anna_Laundry_Notification == ON:
    if items.Anna_Laundry_Notification == ON:
        NotificationAction.sendNotification(
            "email@gmail.com", "%s is finished" % appliance)


@rule("Washer Finished Notification", description="Sends a notification when washer is finished")
@when("Item Washer_Finished changed to ON")
def washer_finished(event):
    logging.debug("Washer Notification of Finished")
    sendNotification("Washer")


@rule("Dryer Finished Notification", description="Sends a notification when dryer is finished")
@when("Item Dryer_Finished changed to ON")
def dryer_finished(event):
    logging.debug("Dryer Notification of Finished")
    sendNotification("Dryer")

Here is the JRuby version I created:

require 'openhab'
require "erb"


appliances = {
  Washer_Progress => Washer_Progress_URL,
  Dryer_Progress => Dryer_Progress_URL
}

rule "Update appliance chart URLs" do
    changed appliances.keys
    triggered do |item|
        progress = item&.state&.float_value || 0
        appliances[item] << "https://quickchart.io/chart?c=" + ERB::Util.url_encode("{type:'radialGauge', data:{datasets:[{data:[#{progress}],backgroundColor:'#283592'}]}, options:{title:{display:true,text:'Progress',fontSize:20}}}")
    end
end

notifications = {
  Washer_Finished => 'Washer',
  Dryer_Finished =>  'Dryer'
}

rule "Appliance finished notification" do 
    changed notifications.keys, to: ON
    triggered do |item|
      notify("#{notifications[item]} is finished ", email: 'email@gmail.com') if Brian_Home.on? && Brian_Laundry_Notification.on?
      notify("#{notifications[item]} is finished ", email: 'email@gmail.com') if Anna_Home.on? && Anna_Laundry_Notification.on?
    end 
end

Note: This is an active PR underway to fix the ugly .float_value for progress

I replaced one of my UPB lamp modules with a tasmota light bulb and I had a set of python rules to make it operate correctly with the switches in the room, using UPB links.

Python Rule:

"""
This script executes rules for lamp automation
"""
from core.rules import rule
from core.triggers import when
from core.log import logging


@rule("UPB Bedroom Brian Link Triggered", description="UPB Link Fired")
@when("Channel omnilink:controller:home:upb_link_activated_event triggered")
def linkTriggered(event):
  link_number = int(event.event)
  if link_number == 64:
    events.sendCommand("Brian_Bedroom_Light","ON")
  elif link_number == 63:
    events.sendCommand("Brian_Bedroom_Light","ON")
  elif link_number == 97:
    events.sendCommand("Brian_Bedroom_Light","OFF")
  elif link_number == 66:
    events.sendCommand("Brian_Bedroom_Light","OFF")
  elif link_number == 62:
    events.sendCommand("Brian_Bedroom_Light","OFF")
      

@rule("UPB Bedroom Brian Link Deactivated", description="UPB Link Deacgivated")
@when("Channel omnilink:controller:home:upb_link_deactivated_event triggered")
def linkTriggered(event):
  link_number = int(event.event)
  if link_number == 64:
    events.sendCommand("Brian_Bedroom_Light","OFF")
  elif link_number == 63:
    events.sendCommand("Brian_Bedroom_Light","OFF")

@rule("Bedroom Brian Goodnight", description="Goodnight")
@when("Channel omnilink:button:home:goodnight:activated_event triggered")
def linkTriggered(event):
  logging.warn("Goodnight Link activiated")
  events.sendCommand("Brian_Bedroom_Light","OFF")

JRuby equivlent:


require 'openhab'

upb_channel = "omnilink:controller:home:upb_link"

rule "UPB Bedroom Link activated" do 
  on_links = %w(63 64)
  off_links = %w(62 66 67)
  channel "#{upb_channel}_activated_event", triggered: on_links + off_links
  run { |trigger|  Brian_Bedroom_Light << (on_links.include?(trigger.event) ? ON : OFF) }
end

rule "UPB Bedroom link deactivated" do 
  channel "#{upb_channel}_deactivated_event", triggered: %w[63 64]
  run { Brian_Bedroom_Light.off }
end

rule "Goodnight Button activated" do 
  channel 'omnilink:button:home:goodnight:activated_event'
  run { Brian_Bedroom_Light.off }
end

I have a set of ESPHome devices with various sensors on them. I am starting my rule conversions for those from python to JRuby.

This rule alerts if any of the sensors go offline for more than hour.

Python

offlineTimers = {}

@rule("Sensor Offline", description="Sensor offline")
@when("Member of ESPStatuses changed")
def offlineAlert(event):
    itemName = event.itemName
    item = ir.getItem(event.itemName)
    if item.state == OPEN:
        logging.warn("{} is offline, starting alert timer".format(itemName))
        timer = Timer(3600, lambda: NotificationAction.sendNotification("email@gmail.com", "%s is offline" % itemName))
        global timer
        offlineTimers[itemName] = timer
        timer.start()
    elif item.state == CLOSED:
        logging.debug("{} is online, disabling any timers".format(itemName))
        timer = offlineTimers.get(itemName)

        if timer is not None and str(timer.getState()) == "TIMED_WAITING":
           timer.stop()
           del offlineTimers[itemName]
           logging.debug("Delete timer for {}".format(itemName))

I think this JRuby rule really shows some of conciseness of the library - abstracting away the common case with creating timers, and tracking, updating, cancelling, etc.

rule 'Notify if ESPHome devices go offline' do 
    changed ESPStatuses.members, from: CLOSED, to: OPEN, for: 1.hour
    triggered { |item| notify("#{item.label} is offline", email: 'email@gmail.com') }
end
1 Like

I just finished converting my DSL rules for managing closet doors to Ruby taking advantage of the new timed commands for JRuby.

DSL:

rule "Closet Door Status Changed"
when 
    Member of ClosetDoors changed
then
    val lightOnTime = 10
    val door = triggeringItem
    logInfo(logName, "Closet Door State Changed: {} {}", door.name, door.state)
    val lightTimer = DoorLights.members.findFirst[ t | t.name == door.name+"_LightTimer" ] as NumberItem
    val light = DoorLights.members.findFirst[ t | t.name == door.name+"_Light" ] as SwitchItem
    if( lightTimer === null) {
        logError(logName, "No Matching Light Timer")
    }
    if( light === null) {
        logError(logName, "No Matching Light")
    }
    logInfo(logName, "Light: {} and Light Timer: {}", lightTimer.name, light.state)
    if(door.state == OPEN){
        lightTimer.sendCommand(lightOnTime) 
    }
    else {
        light.sendCommand(OFF) 
    }
end

Ruby:

rule 'Closet Door Lights' do
  changed ClosetDoors.members
  run do |event|
     light = items["#{event.item.name}_Light"]
     case event.state
     when OPEN   then light.on for: 10.minutes
     when CLOSED then light.off
     end
  end
  not_if { |event| items["#{event.item.name}_Light"].nil? }
  otherwise { |event| logger.error "No matching light found for closet door #{event.item.name}"  } 
end

Continuing my ESP conversions. I had a python rule that would turn on the LED on the ESP sensor whenever motion was detected and then turn it off after there was no motion for 2 seconds.

I didn’t post my python rule because I was using a proxy item there rather than just send the MQTT message which I do in the ruby rule.

Ruby

motion_mappings = {
    Office_Motion => OpenStruct.new(lux_item: Office_Lux, lux: 100, check_item: Lights_Office_Scene, light: Lights_Office_Reccessed, led: Office_LED_String) 
}

rule 'Turn on motion LED' do
  changed ESPMotions.members, to: ON  
  changed ESPMotions.members, to: OFF, for: 2.seconds  
  triggered do |item| 
    on_off = item.on? ? ON : OFF
    logger.info "Turning #{on_off} motion LED mapped to #{item.name}"
    motion_mappings[item].led << {state: on_off, brightness: 255, color: {r: 255, g: 0, b: 0 }}.to_json
  end
end

Continuing the ESP rule conversion - depending on the lux in the room and several other factors (are any other lights on in that room) and if the alarm is armed or not when motion is detected a light will be turned on.

@rule("Office Motion", description="Turns on light for office motion")
@when("Item Office_Motion changed to ON")
def officeMotion(event):

    lux = ir.getItem("Office_Lux").state.floatValue()

    alarmMode = ir.getItem("Alarm_Mode").state.intValue()
    logging.warn("Lux: {} Alarm_Mode: {}".format(lux,alarmMode))
    if lux < 100 and items.Lights_Office_Scene == DecimalType(0) and alarmMode == 0:
      events.sendCommand("Lights_Office_Reccessed","ON")

@rule("Office Not Occupied", description="Office is not occupied")
@when("Item Office_Occupied changed to OFF")
def officeNoMotion(event):
    events.sendCommand("Lights_Office_Reccessed","OFF")

The python rule was only setup to handle a single room, I made the ruby rule be more generic using a hash. To add an additional room, I just need to add a new line to the hash with the appropriate items and lux values. For the python rule I had to use a virtual switch with the expire command to turn the lights off. The ruby rule uses the new timed command (for:) syntax to only turn the light on for 30 minutes… It will keep extending that by 30 minutes every time motion is detected.

motion_mappings = {
    Office_Motion => OpenStruct.new(lux_item: Office_Lux, lux: 100|'lx', check_item: Lights_Office_Scene, light: Lights_Office_Reccessed, led: Office_LED_String, room: 'Office') 
}

rule 'Turn on light due to motion' do
  changed ESPMotions.members, to: ON  
  triggered { motion_mappings[item].light.ensure.on for: 30.minutes }
  only_if { Alarm_Mode.zero? }
  only_if {|event| motion_mappings[event.item].check_item.zero?}
  only_if {|event| motion_mappings[event.item].lux_item < motion_mappings[event.item].lux}
end

Converting my rules for the harmony remotes in my house:

Rules DSL:

rule "Turn off TVs"
when 
	Item Alarm_Mode changed
then
 if (Alarm_Mode.state == 2 || Alarm_Mode.state == 3 || Alarm_Mode.state == 4 ) {
  if (GuestHome.state != 'On' ) {
     logInfo("Harmony", "Entering Night or Away Modes.  Turning off TVs")
     sendCommand(HarmonyLivingRoomActivity, 'PowerOff')
     sendCommand(HarmonyMediaRoomActivity, 'PowerOff')
     sendCommand(HarmonyBedroomActivity, 'PowerOff')
     }
     else{
     logInfo("Harmony", "Entering Night or Away Modes. Guest in house, not turning off TVs")
     }
  }
end


rule "Harmony - Downstairs Off"
when
    Channel "omnilink:button:home:downstairs_off:activated_event" triggered
then
   	 logInfo("Harmony", "Downstairs off pressed - turning off TV.")
     sendCommand(HarmonyLivingRoomActivity, 'PowerOff')
end

Ruby:

require 'omnipro'

rule 'Turn offs TV when Alarm is armed' do
  changed Alarm_Mode, to: [NIGHT, AWAY, VACATION]
  run do
    logger.info 'Entering Night or Away Modes - Turning off TVs'
    [HarmonyLivingRoomActivity, HarmonyBedroomActivity, HarmonyBedroomActivity].each do |harmony|
      harmony << 'PowerOff'
    end
  end
  not_if GuestHome
end

rule 'Downstairs TV Off when Downstairs Goodnight pressed' do
  channel 'omnilink:button:home:downstairs_off:activated_event'
  run { HarmonyLivingRoomActivity << 'PowerOff' }
end

The require 'omnipro' line pulls in a set of constants that map the omnipro numerical states to names and is put in the rubylib dir and reusable in any my rules that talk to the omnipro

Converted my rules around the astro binding:

Setting my Dark virtual switch based on Sunrise/Sunset:

Rules DSL:

rule "Sunrise" 
when Channel 'astro:sun:home:rise#event' triggered START
then
  logInfo(logName, "Sunrise - Disabling Dark")
  Dark.sendCommand(OFF)
end

rule "Sunset" 
when Channel 'astro:sun:home:set#event' triggered START
then
  logInfo(logName, "Sunset - Enabling Dark")
  Dark.sendCommand(ON)
end

Ruby:
Using the new trigger attachments in 4.10 of the JRuby library.

rule 'Set Dark' do
  channel 'astro:sun:home:rise#event', triggered: 'START', attach: OFF
  channel 'astro:sun:home:set#event', triggered: 'START', attach: ON
  run { |event| Dark << event.attachment }
end

Set dark mode at startup (outside of Sunset/Sunrise events:

Rule DSL:
Note I think this rule is broken because it doesn’t seem to handle the case if being restarted after midnight…

rule "System Started Check Dark" 
when System started 
then
 if(now.isAfter((Sunset_Time.state as DateTimeType).zonedDateTime.toInstant.toEpochMilli)) {
    logInfo(logName, "After Sunset - Enabling Dark")
    Dark.sendCommand(ON)
 }
 else{
    logInfo(logName, "After Sunrise - Disabling Dark")
    Dark.sendCommand(OFF)
 }
end

Ruby version:
Using the between ‘range’ syntax to capture the after midnight case:
The only_if block only executes the block if those items are defined (not UNDEF or NULL) removing the need for checking each item in the run block.

rule 'Set dark on startup' do
  changed [Sunset_Time, Sunrise_Time]
  on_start
  run do 
    if between(Sunset_Time..Sunrise_Time).include? Time.now
        logger.info ("After Sunset and before sunrise - Enabling Dark")
        Dark << ON
    else
        logger.info ("After Sunrise - Disabling Dark")
        Dark << OFF
    end
  end
  only_if [Sunset_Time, Sunrise_Time]
end

I have a rule to set and format the dawn/sunrise and dusk/sunset times so I know when each is:

DSL:

rule "Dawn/Sunrise changed"
when
  Item Dawn_Time changed or
  Item Sunrise_Time changed or
  System started
then
  if( Dawn_Time.state != null && Sunrise_Time.state != null) {
    Dawn_Sunrise.postUpdate(Dawn_Time.state.format("%1$tI:%1$tM%1$Tp") + " / " + Sunrise_Time.state.format("%1$tI:%1$tM%1$Tp"))
  }
end

rule "Dusk/Sunset changed"
when
  Item Dusk_Time changed or
  Item Sunset_Time changed or
  System started
then
  if( Dusk_Time.state != null && Sunset_Time.state != null) {
    Dusk_Sunset.postUpdate(Sunset_Time.state.format("%1$tI:%1$tM%1$Tp") + " / " + Dusk_Time.state.format("%1$tI:%1$tM%1$Tp"))
  }
end

The ruby rule takes advantage of the ability to “open up classes” and add a method to them. Adding a method called ‘simple’ that return the DateTime object in a simple format. the opening up of the class has no impact outside the rule in which it executes:


# Open up the DateTimeItem class and add a simple time format
class OpenHAB::DSL::Items::DateTimeItem
  def simple
    format('%1$tI:%1$tM%1$Tp')
  end
end

rule 'Set Dawn/Sunrise' do
  changed [Dawn_Time, Sunrise_Time]
  on_start
  run { Dawn_Sunrise << "#{Dawn_Time.simple} / #{Sunrise_Time.simple}"}
  only_if [Dawn_Time, Sunrise_Time]
end

rule 'Set Dusk/Sunset' do
  changed [Dusk_Time, Sunset_Time]
  on_start
  run { Dusk_Sunset << "#{Sunset_Time.simple} / #{Dusk_Time.simple}" }
  only_if [Dusk_Time, Sunset_Time]
end
1 Like

Converting some of my door related rules -

I have an alert if there are a set of doors that remain open for 5 minutes. The rules DSL used a combination of rules + items with an expiration on them.

Rules DSL:

 rule "Door Open Alert Status Changed"
 when 
     Member of DoorOpenAlerts changed
 then
     val door = triggeringItem
     logInfo(logName, "Door with Open Alert Status Changed {} {} {}", door.name, door.state, door.label)
     val alertTimer = DoorOpenAlertTimers.members.findFirst[ t | t.name == door.name+"_Timer" ] as SwitchItem
     if( door.state == OPEN){
         alertTimer.sendCommand(ON)
     }
     else{
         alertTimer.postUpdate(OFF)
     }
 end

 rule "Door Open Alert Timer Status Changed"
 when 
     Member of DoorOpenAlertTimers received command OFF
 then
     val timer = triggeringItem
     logInfo(logName, "Door Timer Alert Fired {} {}", timer.name, timer.label)
     val notificationText = timer.label + " has been left open"
      sendBroadcastNotification(notificationText)
 end

The JRuby rules try do do away with virtual/expire items and for the most part manual timer management with its built in ‘for’ syntax.

Ruby:

rule 'Door Open Alerts' do
  changed DoorOpenAlerts.members, to: OPEN, for: 5.minutes
  triggered {|item| notify "#{item.name} has been left open" }
end

More door based automation conversion:

I have a rule that whenever one of the basement doors is opened (regular or workshop) then the backyard lights next to both doors will turn on.

For the rules DSL, I rely on my home automation panel “timed command” feature to turn the light on for 30 minutes and reset it every time it triggers. The annoying part of that is that I have to have a separate item for that timed command and map them to the lights, etc.

DSL:

rule "Backyard Door Opened"
when 
    Member of BackyardDoors changed to OPEN
then
    val door = triggeringItem
    logInfo(logName, "{}={}", door.name, door.state)
    //Only if its dark
    if( Dark.state == ON) {
        if( BasementOutsideDoor_Light.state == OFF) {
            BasementOutsideDoor_LightTimer.sendCommand(30)
        }
        if( WorkshopOutsideDoor_Light.state == OFF) {
            WorkshopOutsideDoor_LightTimer.sendCommand(30)
        }
    }
    else{
      logInfo(logName, "No Action, not after sunset")
    }
end

The Ruby rules use the built in timed command feature available on all items (think of it as a dynamic expire) available with the ‘for:’ syntax. The ensure syntax doesn’t do anything if the light is already on.

rule 'Backyard Door Opened' do
  changed BackyardDoors.members, to: OPEN  
  run do 
     [BasementOutsideDoor_Light, WorkshopOutsideDoor_Light].each do |light|
       light.ensure.on for: 30.minutes
     end
  end
  only_if Dark
end

The following rule conversion turns on a light in the downstairs foyer when the primary bedroom door is opened between 4:00AM and 7:30AM - this provides enough light to see the hallway and also we may be going downstairs between these times.

Rules DSL:


rule "Primary Bedroom Door Opened"
when 
  Item PrimaryBedroomDoor changed to OPEN
then
  logInfo(logName, "Primary Bedroom Door Opened")
  if( Dark.state == ON ){
    logInfo(logName, "Currently Dark, checking for morning")
    val morningTime = new Interval(now().withTime(4,0,0,0),now().withTime(7,30,0,0))
    if( morningTime.containsNow() && DownstairsMorningLights.state == OFF) {
       logInfo(logName, "Morning time, turning on downstairs hallway foyer light.")
       Lights_Hallway_Foyer_Timer.sendCommand(10)
    }
  }
end

The Ruby rules use the readable between syntax to limit the rule execution to the appropriate time:

rule 'Master Bedroom Door Opened' do
  changed MasterBedroomDoor, to: OPEN
  run { Lights_Hallway_Foyer.on for: 10.minutes }
  only_if Dark
  between '4:00AM'..'7:30AM'
end

Converted a set of my audio rules that link my Chromcast to my Nuvo whole home audio system:

Some minor changes between rules is I also moved to the Nuvo binding instead of connecting via MQTT and/or my omnipro.

Updating the display of the Nuvo to match the meta data from my Chromecast

Rules DSL:


rule 'Update Chomecast Track'
when Item ChromecastStatusTrack changed
then
  val state = ChromecastStatusTrack.state
  if( state != UNDEF ) {
  logInfo("Nuvo", "Updating Nuvo Chromecast Track: {}", state)
    Nuvo_Source_5_Display_Line_1.sendCommand(state)
  }
  else{
    logInfo("Nuvo", "Skipping undefined state for Album")
  }
end

rule 'Update Chomecast Album'
when Item ChromecastStatusAlbum changed
then
  val state = ChromecastStatusAlbum.state
  if( state != UNDEF) {
    logInfo("Nuvo", "Updating Nuvo Chromecast Album: {}", state)
    Nuvo_Source_5_Display_Line_2.sendCommand(state)
  }
  else{
    logInfo("Nuvo", "Skipping undefined state for Album")
  }
end

rule 'Update Chomecast Artist'
when Item ChromecastStatusArtist changed
then
  val state = ChromecastStatusArtist.state
  if( state != UNDEF) {
  logInfo("Nuvo", "Updating Nuvo Chromecast Artist: {}", state)
  Nuvo_Source_5_Display_Line_3.sendCommand(state)
  }
  else{
    logInfo("Nuvo", "Skipping undefined state for Artist")
  }
end

rule 'Update Chomecast Title'
when Item ChromecastStatusTitle changed
then
  val state = ChromecastStatusTitle.state
  if( state != UNDEF) {
    logInfo("Nuvo", "Updating Nuvo Chromecast Title: {}", state)
    Nuvo_Source_5_Display_Line_4.sendCommand(state)
  }
  else{
    logInfo("Nuvo", "Skipping undefined state for Title")
  }
end

Ruby (again making use of the new trigger attachments):

This normalizes the item state since Nuvo can’t display special characters (the previous rule didn’t do this).

java_import java.text.Normalizer

def normalize(item)
  Normalizer::normalize(item.state.to_s, Normalizer::Form::NFD).gsub("[^\\p{ASCII}]", "")
end

rule 'Update Nuvo Chromecast source' do
  changed ChromecastStatusTrack, attach: Nuvo_S5_Display_Line1 
  changed ChromecastStatusAlbum, attach: Nuvo_S5_Display_Line2 
  changed ChromecastStatusArtist, attach: Nuvo_S5_Display_Line3
  changed ChromecastStatusTitle, attach: Nuvo_S5_Display_Line4
  run { |event| event.attachment << normalize(event.item) }
  only_if { |event| event.item.state? }
end

Pausing the Chromecast if play/pause pressed on the keypad:

Rules DSL

rule 'PlayPause Triggered'
when Channel "mqtt:homie300:mosquitto:nuvo:zone5#playpause" triggered PRESSED
then
  logInfo("Nuvo", "PlayPause Pressed")
  if( ChromecastMusic.state == PlayPauseType.PLAY){
    logInfo("Nuvo", "Pausing Chromecast")
    ChromecastMusic.sendCommand(PlayPauseType.PAUSE)
  }
  else{
    logInfo("Nuvo", "Playing Chromecast")
    ChromecastMusic.sendCommand(PlayPauseType.PLAY)
  }
end

Ruby

 rule 'Nuvo PlayPause Triggered' do
   updated Nuvo_S5_Button_Press, to: 'PLAYPAUSE'
   run { ChromecastMusic.playing? ?  ChromecastMusic.pause : ChromecastMusic.play }
 end

I like JRuby and I’d like to see more examples of JRuby code in the forum. I use the built in rules editor in the main UI to make my JRuby rules so the trigger and conditions are handled by the UI. I hope Brian doesn’t mind me reviving this older thread and adding my conversions. I’m a complete ruby beginner so feel free to help me with tips to find the best way to use the language.
This rule controls my general lighting based on the outside light level. It uses a Lux sensor in a window to dim the inside lights when it is bright outside and make them brighter when there is less light outside. It was originally written in rules DSL. Here is the original code

Items are created in the UI. CurrentDimmer is an unbound number. OutsideLuxNum is a number return by the Lux sensor as a floating point number.

Rules DSL:

var myDimValue = 0
var Number cur = CurrentDimmer.state as Number // current dimmer setting
var Number oll = OutsideLuxNum.state as Number // outside light level
logInfo("LightLevelrule", "Outside light level: " + oll)
switch true {
  case oll > 10000 : { myDimValue = 10}
  case oll > 900 && oll < 10000 : {
    oll = oll / 100
    myDimValue = 100 - oll.intValue
  }
  case oll <= 900: { myDimValue = 100}
}
if ( cur == myDimValue){
  logInfo("LightLevelrule", "Dimmer unchanged")
} else {
  logInfo("LightLevelrule", "Dimmer set to: " + myDimValue)
  gDimmerLux.sendCommand(myDimValue)
  CurrentDimmer.sendCommand(myDimValue)
}

and here is the JRuby version:

my_lux = OutsideLuxNum.to_i
my_cur = CurrentDimmer
my_val = 0
my_dim = 0
logger.info("Outside light level: #{my_lux}")
if my_lux > 10000
  my_dim = 10
end
case my_lux
  when 900..10000
    my_val = my_lux / 100
    my_dim = 100 - my_val.to_i
end
if my_cur == my_dim
  logger.info("Dimmer unchanged")
else
  gDimmerLux << my_dim
  CurrentDimmer << my_dim
  logger.info("Dimmer set to: #{my_dim} ")
end

Hope more folks post their JRuby examples and give it a try, it is super easy

Hi @Andrew_Rowe here’s a version to consider. Normally I would extract the range conversion into a separate method to keep the code clean and more readable, but afaik, you can’t create a method inside a gui rule (I need to test this to be sure)

But in general, don’t be shy to use the item directly as if it’s a value (i.e. as its intrinsic state). IMO, it makes the code much more readable.

logger.info("Outside light level: #{OutsideLuxNum}")

from_range = 900..10_000
to_range = 100..10

dimmer = case OutsideLuxNum
         when (from_range.end..) then 10 # this is the same as (10_000..) - an end-less range
         when from_range
          # convert a value from_range to to_range
          (OutsideLuxNum - from_range.begin) / (from_range.end - from_range.begin) * (to_range.end - to_range.begin) + to_range.begin
         else 100
         end

if CurrentDimmer.ensure.command(dimmer) # Item.ensure.command returns nil when it already had the same value
  gDimmerLux.ensure << dimmer
  logger.info("Dimmer set to: #{dimmer}")
else
  logger.info('Dimmer unchanged')
end

To see more jruby examples, try Topics tagged jruby

1 Like