Groovy: debugging/how to get access to 'events'

Tags: #<Tag:0x00007f2fbc7b1568> #<Tag:0x00007f2fbc7b0f28> #<Tag:0x00007f2fbc7b0aa0>

I am currently migrating from OpenHAB 1.8.x to 2.4.
I added Groovy Script Support as described here https://github.com/eclipse/smarthome/wiki/Scripted-Rule-Support#groovy.
The provided example.groovy works fine, I also managed to add a logger.
However, I am unable to send a command… this is my script (so far):

import org.eclipse.smarthome.automation.*
import org.eclipse.smarthome.automation.module.script.rulesupport.shared.simple.*
import org.eclipse.smarthome.config.core.Configuration

import org.eclipse.smarthome.model.script.actions.LogAction

scriptExtension.importPreset("RuleSupport")
scriptExtension.importPreset("RuleSimple")

import org.slf4j.*

def sRule = new SimpleRule() {
    Object execute(Action module, Map<String, ?> inputs) {
        def Logger logger = LoggerFactory.getLogger('ex.groovy')
        logger.warn("Hello World from Groovy")
        //logger.warn("get" + getTypes())
        //logger.warn(events)
        events.sendCommand('DI_LED16_FRONT_DOOR_SAFE', "ORANGE")
    }
}

sRule.setTriggers([
    TriggerBuilder.create().withId("aTimerTrigger").withTypeUID("timer.GenericCronTrigger")
        .withConfiguration(new Configuration([cronExpression: "0/10 * * * * ?"])).build()
    ])

automationManager.addRule(sRule)

If the statement events.sendCommand('DI_LED16_FRONT_DOOR_SAFE', "ORANGE") is active, my script fails with Failed to execute rule '3252e495-2105-4c6d-9382-b9f8800914cb': Fail to execute action: 1.

Two questions:
1.) where can I find more information regarding the error? openhab.log and events.log do not carry enough information
2.) since logger.warn(events) yields the same error, I wildly guess that events is unavailable… how do I get access to events?

I am going to guess this is related to all the work being done by @5iver on JSR232 and scripting - there are changes that likely affected Groovy. I use Jython so I don’t know for sure.

Thanks for the reply, I hope @5iver or someone else will provide a helpful answer.

I thought about using Jython, but I am currently not willing to spend the time to convert my Groovy Rules to Jython … they are complex but unit- and family-tested.

I figured something else: using the “events” variable in “file” scope works, it is just not available in the sRule instance:

import org.eclipse.smarthome.automation.module.script.rulesupport.shared.simple.*
import org.eclipse.smarthome.config.core.Configuration
import org.eclipse.smarthome.model.script.actions.LogAction

scriptExtension.importPreset("RuleSupport")
scriptExtension.importPreset("RuleSimple")

import org.slf4j.*
def Logger logger = LoggerFactory.getLogger('ex.groovy')

def sRule = new SimpleRule() {
    Object execute(Action module, Map<String, ?> inputs) {
        logger.warn("Hello World from Groovy")
        //logger.warn("in class events" + events) ---> ****ERROR during execution****
    }
}

sRule.setTriggers([
    TriggerBuilder.create().withId("aTimerTrigger").withTypeUID("timer.GenericCronTrigger")
        .withConfiguration(new Configuration([cronExpression: "0/10 * * * * ?"])).build()
    ])

automationManager.addRule(sRule)
logger.warn("global log events " + events) //****--> works fine****

The last line logs global log events org.eclipse.smarthome.automation.module.script.defaultscope.internal.ScriptBusEvent@3f1c3d67

… now how do I get access to events in my script class?

Ok, now I am passing the events (and ir) instance during Rule instantiation, store them in fields and access the fields.

import org.eclipse.smarthome.automation.*
import org.eclipse.smarthome.automation.module.script.rulesupport.shared.simple.*
import org.eclipse.smarthome.config.core.Configuration

import org.eclipse.smarthome.model.script.actions.LogAction

scriptExtension.importPreset("RuleSupport")
scriptExtension.importPreset("RuleSimple")

import org.slf4j.*
import org.eclipse.smarthome.core.items.ItemRegistry
import org.openhab.core.library.types.OpenClosedType

class DisplayHallRule extends SimpleRule {
	private static final String LED_OFF = "OFF"
	private static final String LED_RED = "RED"
	private static final String LED_GREEN = "GREEN"
	private static final String LED_ORANGE = "ORANGE"

	private Object events
	private ItemRegistry ir
	def ledItemName
	def String[] ctItemNames
	def Logger logger = LoggerFactory.getLogger('DisplayHallRule')

	DisplayHallRule(Object events, ItemRegistry ir, String ledItemName, String... ctItemNames) {
		logger.warn("<init> got events: " + events)
		this.events = events
		logger.warn("<init> stored events: " + this.events)
		logger.warn("<init> got ir: " + ir)
		this.ir = ir
		logger.warn("<init> stored ir: " + this.ir)
		this.ledItemName = ledItemName
		this.ctItemNames = ctItemNames
	}

    Object execute(Action module, Map<String, ?> inputs) {
		logger.warn("events " + events)
		logger.warn("ir " + ir)

		def String newColor = LED_ORANGE //undefined?!
		try {
			for (ctItemName in ctItemNames) {
				logger.warn("ctItemName " + ctItemName)
				def ctItem = ir.getItem(ctItemName)
				logger.warn("ctItem " + ctItem)
				logger.warn("ctItem.state " + ctItem.state)
	//			logger.warn("ctItem.state.toString " + ctItem.state.toString)
				if (ctItem.state == OpenClosedType.CLOSED) {
				//if (ctItem.state.toString == "CLOSED") {
					logger.warn(ctItemName + " is closed")
					newColor = LED_GREEN
				} else if (ctItem.state == OpenClosedType.OPEN) {
				//} else if (ctItem.state.toString == "OPEN") {
					logger.warn(ctItemName + " is opened")
					newColor = LED_RED
					break
				}
			}
			events.sendCommand(ledItemName, newColor)
		} catch (Error e) {
			logger.warn("Error: " + e + ": " + e.getMessage())
		}

        logger.warn("done executing")
    }
}


def sRule = new DisplayHallRule(events, ir, 'DI_LED16_FRONT_DOOR_SAFE', 'DI_CT_DOOR')
sRule.setTriggers([  /*...*/  ])

automationManager.addRule(sRule)

Passing the context (events, ir) via the constructor is a clumsy workaround, but I could go on like that, if there is no other solution.
However, it also seems I cannot access OpenClosedType. Executing the version pasted above leads to Error: java.lang.NoClassDefFoundError: org/openhab/core/library/types/OpenClosedType ....

How do I address this?
(You might notice that I also used Object as the type of events because of a similar Error when using the appropriate ScriptBusEvent type. But since ScriptBusEvent resides in an internal package I thought this is unavoidable.

All of my changes have been after the 2.4 release. I’ve done very little with Groovy, but I’m aware of some issues. I have an idea of what the problem is and plan to dig into them sometime this year… possibly over the summer. I plan to build out Groovy and JS helper libraries with the same functionality as the ones for Jython, but I may just put that effort into building out a scripting API that provides that functionality.

You’re migrating from 1.8, but are your rules currently in Groovy? That would be great, as there are very few people using Groovy and your experience could help a lot. @vbierrecently reported a similar issue, so he might have some insight for you.

This is working for me in OH 2.4 and Groovy 2.4.12…

import org.slf4j.*

import org.eclipse.smarthome.automation.*
import org.eclipse.smarthome.automation.module.script.rulesupport.shared.simple.*
import org.eclipse.smarthome.config.core.Configuration

scriptExtension.importPreset("RuleSupport")
//scriptExtension.importPreset("RuleSimple")
//scriptExtension.importPreset("RuleFactories")

def log = LoggerFactory.getLogger('jsr223.groovy')

log.warn("Virtual_Number_1 [{}], Virtual_Switch_1 [{}]", items["Virtual_Number_1"], items["Virtual_Switch_1"])

// initilize a value
events.sendCommand("Virtual_Number_1", "5")
events.sendCommand("Virtual_Switch_1", "ON")

def testRule1 = new SimpleRule() {
    Object execute(Action module, Map<String, ?> inputs) {
        log.warn("Virtual_Number_1 [{}]", items["Virtual_Number_1"])
        log.warn("ir.getItem(\"Virtual_Number_1\").state [{}]", ir.getItem("Virtual_Number_1").state)
        events.sendCommand("Virtual_Number_1", (items["Virtual_Number_1"] + 5).toString())
        log.warn("Virtual_Switch_1 [{}]", items["Virtual_Switch_1"])

        if (items["Virtual_Switch_1"] == ON) {
            events.sendCommand("Virtual_Switch_1", "OFF")
        } else {
            events.sendCommand("Virtual_Switch_1", "ON")
        }
        log.warn("items[\"Virtual_Switch_1\"] == OnOffType.ON [{}]", items["Virtual_Switch_1"] == OnOffType.ON)
        log.warn("items[\"Virtual_Switch_1\"] == ON [{}]", items["Virtual_Switch_1"] == ON)
    }
}
testRule1.setName("Test sendCommand");
testRule1.setTriggers([
    TriggerBuilder.create()
        .withId("aTimerTrigger")
        .withTypeUID("timer.GenericCronTrigger")
        .withConfiguration(new Configuration([cronExpression: "0/10 * * * * ?"]))
        .build()
    ])

automationManager.addRule(testRule1); 

In the example I posted, these are being loaded into the context properly. Could you please provide a stripped down example where you cannot access them?

I’ve been trying but I cannot reproduce it anymore?!

I will focus on your script first. I can’t see why your testRule1 is able to access ir and events but mine can’t.

This one was missing an import and had a couple extras, but this works…

import org.eclipse.smarthome.automation.*
import org.eclipse.smarthome.automation.module.script.rulesupport.shared.simple.*
import org.eclipse.smarthome.config.core.Configuration
//import org.eclipse.smarthome.model.script.actions.LogAction

scriptExtension.importPreset("RuleSupport")
//scriptExtension.importPreset("RuleSimple")

import org.slf4j.*
def Logger logger = LoggerFactory.getLogger('ex.groovy')

def sRule = new SimpleRule() {
    Object execute(Action module, Map<String, ?> inputs) {
        logger.warn("Hello World from Groovy")
        logger.warn("in class events" + events)// ---> ****ERROR during execution****
    }
}

sRule.setTriggers([
    TriggerBuilder.create().withId("aTimerTrigger").withTypeUID("timer.GenericCronTrigger")
        .withConfiguration(new Configuration([cronExpression: "0/10 * * * * ?"])).build()
    ])

automationManager.addRule(sRule)
logger.warn("global log events " + events) //****--> works fine****

Regarding access to events: it seems that a Rule can access events only if an anonymous class is used, as in all examples found in the docs. However, I prefer named classes which typically get parameters (mostly item names) passed.

access to events works with the following code (anonymous class)

import org.eclipse.smarthome.automation.module.script.rulesupport.shared.simple.*
import org.eclipse.smarthome.config.core.Configuration
import org.eclipse.smarthome.model.script.actions.LogAction

scriptExtension.importPreset("RuleSupport")
scriptExtension.importPreset("RuleSimple")

import org.slf4j.*

def sRule = new SimpleRule() {
	def Logger logger = LoggerFactory.getLogger('anonClass')

    Object execute(Action module, Map<String, ?> inputs) {
		logger.warn("executing")
		events.sendCommand('Virtual_Switch_1', 'ON')
        logger.warn("done executing")
    }
}
sRule.setTriggers([
    TriggerBuilder.create().withId("aTimerTrigger").withTypeUID("timer.GenericCronTrigger")
        .withConfiguration(new Configuration([cronExpression: "0/10 * * * * ?"])).build() ])

automationManager.addRule(sRule)

access to events does not work with the following code (named class NamedClass)

import org.eclipse.smarthome.automation.*
import org.eclipse.smarthome.automation.module.script.rulesupport.shared.simple.*
import org.eclipse.smarthome.config.core.Configuration

import org.eclipse.smarthome.model.script.actions.LogAction

scriptExtension.importPreset("RuleSupport")
scriptExtension.importPreset("RuleSimple")

import org.slf4j.*

class NamedClass extends SimpleRule {
	def Logger logger = LoggerFactory.getLogger('NamedClass')

    Object execute(Action module, Map<String, ?> inputs) {
		logger.warn("executing")
		events.sendCommand('Virtual_Switch_1', 'ON')
        logger.warn("done executing")
    }
}

def sRule = new NamedClass()
sRule.setTriggers([
    TriggerBuilder.create().withId("aTimerTrigger").withTypeUID("timer.GenericCronTrigger")
        .withConfiguration(new Configuration([cronExpression: "0/10 * * * * ?"])).build() ])

automationManager.addRule(sRule)

Openhab.log entries related to the named class example:

2019-06-02 19:55:32.588 [INFO ] [rt.internal.loader.ScriptFileWatcher] - Loading script 'namedClass.groovy'
2019-06-02 19:55:40.010 [WARN ] [NamedClass                          ] - executing
2019-06-02 19:55:40.011 [ERROR] [omation.core.internal.RuleEngineImpl] - Failed to execute rule 'bdbe1453-b270-4cc2-8cce-b6edaa3739c3': Fail to execute action: 1

I could switch to anonymous classes for simple use cases, but that would force me to pass my parameters with setters or the like. From a design perspective, this is undesirable: constructor parameters enforce passing (mandatory) values.
Additionally, this does not work for complex cases, where I created my own class hierarchies (MyXyzRule implements Rule with OpenHab 1.8).

1 Like

I’ve done a lot with Groovy rules, but not a much lately.

IIRC, I bumped into the same issue. I just couldn’t get my rules to do enough common stuff easily to make it worth it. Which led me down a path of putting all my base classes directly into the the RuleSupport bundle. Which was kind of a pain, but worked for me. They do a lot of things the various Python libraries do and generally make it easy (or easier at least) to do the common things you want to do in a rule script.

It’s been on my list for a while to port these from 1.x to 2.x, and was mostly done. But with upcoming org/package changes in 2.5 (which is a good thing) plus potential rules changes in 3.x I decided to hold off on that for now. Plus, work has been really busy and my free time has been limited.

The Python rules have come a long way in the past couple of years (thanks @5iver) and I’ve had “learn Python” on my to-do list for a while. So I decided to just go ahead and port my rules to Python. It took a bit longer than I wanted, since I was learning the language at the same time. And moving from OH1 to OH2. But I’ve got most of my rules converted now.

Sorry that doesn’t help with your problem right now. But I thought I should at least chime in as you seem to be bumping into much of the same issues.

I wish there was a way to dynamically add Groovy classes to the Rules Engine at runtime like you can the way JSR support is set up for JPython. I never was able to figure out a way to do that to my satisfaction (I could dynamically compile objects, but not use them to in extending a base class.)

Good luck,

Doug

:slightly_smiling_face::+1:
Post for any questions/issues you haven’t figured out and I’ll be glad to help!

I still have some tabs in my browser open to circle back to you when looking into building a scripting API to provide helper library functionality for all languages! I probably won’t get to that for a couple months yet.

There’s definitely something wrong with how OH is adding the default scope variables for Groovy. NashornScriptEngineFactory.scopeValues() does something special for Javascript (compare it to AbstractScriptEngineFactory.scopeValues()), and I suspect something similar is needed for Groovy. None of the imports in the rules in this thread should be needed, as those should already be in the script context. This is a different issue than the one with base classes though (maybe not?).

Thanks for your input. Due to a bunch of new requirements I felt it was the right time to go from OH 1.8 to 2.4… but I am starting to look for alternative ways to implement my complex rules. One idea is a rule generator which generates XText rules based on Groovy/Kotlin code.
I need OH 2.4 since I need (or at least “want to have”) features introduced in Homematic Binding 2.x.

Learning Python was on my list too, it never made it to the top :wink: .

Maybe there is a quick answer to this one:

how do I implement a StartupTrigger?

Docs (https://www.openhab.org/docs/configuration/jsr223.html#triggertype-objects-all-jsr223-languages) do not seem to answer this?!
I cannot find an appropriate TriggerHandler subclass.

Thanks

You can look at how I did it in the Jython helper libraries, but you will also need S1566 or newer due to this. If you get it working in Groovy, please let me know or submit a PR for it!

hmmmm… okaaay … I wasn’t expecting that I need a snapshot and lots of code for that.

I was thinking about the following workaround:

  • define a virtual switch item VIRTUAL_STARTUP
  • define a XText Rule triggered by System Startup
  • let this XText Rule change the VIRTUAL_STARTUP from OFF to ON
  • trigger the groovy/phython/whatever script using an ItemStateChangeTrigger on VIRTUAL_STARTUP

sounds quirky but it should work… what do you think?

That should work as long as the old rule engine is around. If you want to stay on a stable release, then when 2.5 rolls out, setup Jython and the helper libraries (StartupTrigger is included). The StartupTrigger will then be available in the UI or any language that you are using.