Easy Groovy Rules (long)

I want to make it easy to write rules in Groovy by creating a small set of base classes that add functionality found in the standard rules engine.

The base class contains methods for logging (logError, logWarn, logInfo, and logDebug), for interacting with bus (sendCommand and postUpdate), for interacting with the item registry (getItem, getItems), getting actions (getAction). There are some other convenience functions for things like creating triggers for different events.

In addition to the base classes, I have a small set of classes covering some of the common use cases for rules. For example, “do something when a contact opens”, “do something if a reading is outside a certain range”, “do something if a door has been open too long”, “do something when a door opens”, “do something when an item changes state”, etc. Generally, these classes have one or two methods with default implementations (logging only) that can be overridden to implement what the user wants to do. The event handler also traps and logs any exceptions thrown by those methods.

I implemented this for my own use in OpenHab1 and found it to work well.

I’m new to OSGi and the SmartHome framework, so this has been a lot of trial-and-error to get things working. I tried a lot of different things to get my external Groovy classes loaded and usable in JSR223 groovy scripts, and repeatedly failed. So for now I’ve added them directly to the RuleSupport bundle in the smarthome codebase. I’m not sure what the best or optimal approach should be, but at least this has allowed me to make progress on implementing what I want. I had to make some adjustments to my Java code such as passing and returning Object instead of types to allow the scripts to be a little more Groovy-like.

Anyway, please share your thoughts on this approach and how to proceed. Assuming the response is positive, my next step is to open an Issue in the appropriate repository and begin working on getting my code to a state that can be merged.

Thanks,

Doug

Some simple demonstration examples:

“do something when a switch is thrown”
“do something when a contact/door opens”
“do something if a door has been open too long”

This example uses a switch to simulate a door opening and closing. A text field is updated when the door opens and closes, and also if the door is left open longer then 5 minutes.

import org.eclipse.smarthome.automation.module.script.rulesupport.shared.simple.helper.*
scriptExtension.importPreset('RuleSupport')

def r1 = new OnOffRule('TestSwitch2') {
    def on(event)  { postUpdate('TestContact1', 'OPEN'); }
    def off(event) { postUpdate('TestContact1', 'CLOSED'); }
}

def r2 = new OpenClosedRule('TestContact1') {
    def open(event) {
        logWarn('Door open');
        postUpdate('TestString2', 'Door open')
    }
    def closed(event) {
        logInfo('Door closed');
        postUpdate('TestString2', 'Door close')
    }
}

def r3 = new OpenTooLongRule('TestContact1', 'Back door', 5) {
    def alert(itemName) {
        logWarn('Door open too long!!!');
        postUpdate('TestString2', getFriendlyName(itemName) + ' open longer than ' + minutes + ' minutes!')
    }
    def closedAfterAlert(itemName) {
        infoMsg(getFriendlyName(itemName) + ' has been closed.')
    }
}

automationManager.addRule(r1)
automationManager.addRule(r2)
automationManager.addRule(r3)

“do something if a reading is outside a certain range”

This example updates a text field based on a value in a range. It uses a more Java-like approach by defining a new class instead of using an anonymous one. This is more lines of code, but has the advantage that log messages with have a category of “UserRules.RangeDemo” instead of just “UserRules”.

import org.eclipse.smarthome.automation.module.script.rulesupport.shared.simple.helper.*
scriptExtension.importPreset('RuleSupport')

class RangeDemo extends RangeRule {
    public RangeDemo() {
        super('TestSlider', 'Temp', 30, 80);
    }

    def low(event) {
        def msg = getFriendlyName(event) + ' low: ' + event.getItemState() + ' degrees'
        logWarn(msg)
        postUpdate('TestString3', msg)
    }

    def high(event) {
        def msg = getFriendlyName(event) + ' high: ' + event.getItemState() + ' degrees'
        logWarn(msg)
        postUpdate('TestString3', msg)
    }

    def backInRange(event) {
        def msg = getFriendlyName(event) + ' has returned to normal.'
        postUpdate('TestString3', msg)
        logInfo(msg)
    }
}

def r = new RangeDemo()
automationManager.addRule(r)

“do something when a switch is thrown”
“get and use an openhab action”

This uses the pushover to send a message when a switch turns from OFF to ON

import org.eclipse.smarthome.automation.module.script.rulesupport.shared.simple.helper.*
scriptExtension.importPreset('RuleSupport')

def r = new OnOffRule('TestSwitch5') {
    def on(event) {
        getAction('pushover').pushover('TestSwitch5 ON')
    }
}

automationManager.addRule(r)

“turn on the outside lights at sunset and off at sunrise”

import org.eclipse.smarthome.automation.module.script.rulesupport.shared.simple.helper.*
scriptExtension.importPreset('RuleSupport')

def r1 = new ChannelEventRule('astro:sun:local:set#event', 'START') {
    def triggered(event) {
        sendCommand('OutsideLight', 'ON')
    }
}

def r2 = new ChannelEventRule('astro:sun:local:rise#event', 'START') {
    def triggered(event) {
        sendCommand('OutsideLight', 'OFF')
    }
}

automationManager.addRule(r1)
automationManager.addRule(r2)

“turn the hall light on at 7:30 PM and off at 11:00 PM”

import org.eclipse.smarthome.automation.module.script.rulesupport.shared.simple.helper.*
scriptExtension.importPreset('RuleSupport')

def r1 = new TimerRule('0 30 19 * * ?') {
    def timeFor() { sendCommand('HallLight', 'ON') }
}

def r2 = new TimerRule('0 0 23 * * ?') {
    def timeFor() { sendCommand('HallLight', 'OFF') }
}

automationManager.addRule(r1)
automationManager.addRule(r2)

“turn a switch on when the daylight event starts and off when it ends”

import org.eclipse.smarthome.automation.module.script.rulesupport.shared.simple.helper.*
scriptExtension.importPreset("RuleSupport")

def r1 = new RangeEventSwitchRule("astro:sun:local:daylight#event", "Day_Event");
automationManager.addRule(r1)
3 Likes

Hi @torpex77,
I following a similar approach but based on Jython. I would like to switch to Groovy for several reasons but so far I didn’t understood how I can integrate my own packages into the class path like modules in python.

Did you make any progress on your project?

In addition, I ask myself the following questions:

  1. Groovy JS223 scripts getting compiled automatically. What about my own classes outside the script path? Do I need to compile those manually before use?
  2. Do I have to restart OpenHab when I make changes to my classes?

Cheers,
Philipp

Hello Philipp,

Good timing on your question. I haven’t done much at all since my initial post, since I didn’t get much feedback on how many people were interested in this. However I spent some time yesterday on this, mainly figuring out where I left everything, rebasing my changes on the current master, etc, in preparation for picking it back up.

My current plan is to open an issue and try to get some feedback that way. Hopefully this week.

As for your questions:

I never did find a way to get my classes added on the script path. I did a lot of reading and tried a lot of different things without success. It could be something simple that I just don’t have enough OSGi / Groovy / OpenHab knowledge to actually get working.

Because of that, I took the approach of incorporating my changes directly into the smarthome rule support code. Then I just extend/reuse those classes in my scripts (see the examples above). Those are automatically reloaded. But if I have to change my base classes, I need to rebuild the rule support jar and update my openhab instance with it. Of course, the idea is that those should be fairly complete and not require changes.

Hope that helps. If I get my issue created I’ll update this thread with a link to it.

Doug

Thank you for the explanation. I am lucky that you are still committed to Groovy. When I took my first steps with Groovy/ESH a few months ago, I quickly gave up and wrote my classes in python. @steve1 wrote some very helpful foundation classes. One more reason why I chose Python.

Your approach is clear to me now. If possible, I would like to avoid additional compiling steps and keep my classes in the JS223 scope. With Jython, I can manage that by adding

 -Dpython.path=/openhab/conf/lib/python

to the EXTRA_JAVA_OPTS environment variable. I guess (hope) that there is a similar option for Groovy. So far, I only found this workaround, to load class manually from the file path:

def workspace = build.properties.get('envVars').get('WORKSPACE');
def cl = new GroovyClassLoader(this.class.classLoader);
cl.addURL(new File("${workspace}/").toURL()); // whatever path you need to add
def shell = new GroovyShell(cl, getBinding());
def myScript = shell.parse(new File("${workspace}/StaticBuildJobAnalysis.groovy")); // whatever your script is
myScript.run();

Also not really comfortable solution…