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

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