openHAB 3.0 my getting started notes: Rules

Edit: I’ve started some more formal tutorials. The first is located at OH 3 Examples: Writing and using JavaScript Libraries in MainUI created Rules

This is somewhat of a continuation of openHAB 3.0 my getting started and notes. I wanted to put my Rules explorations into a separate thread because the first one is already getting kind of long.

Approach

As much as is possible, I plan on moving my rules to JSONDB. This will give me a better feeling for the new user’s experience as well as give me the knowledge and skills to develop Rule Templates that can be easily imported, hopefully from a marketplace of some sort in the future. To have the most flexibility with such rules, I also want them to be as independent from third party dependencies as possible.

Therefore:

  • I’m not going to use Jython as it requires something extra to be installed. Instead I’ll limit myself to JavaScript (preferred) and Rules DSL (secondary) as both come with OH 3 without any additional requirements.

  • I’m going to as much as possible use strictly JSONDB rules. There is one thorny problem I’ve still not figured out that may make this not work for everything but using .js or .rules files will be the exception.

  • This also means I’m not going to use the JavaScript Helper Library. In addition to the fact that it doesn’t appear to have been updated in almost a year and a half it counts as an external dependency. But a whole lot of the helper libraries are dedicated to the creation of rules and triggers which is handled for you when using the UI.

With these restrictions a surprising amount of the information that I documented at Experimental Next-Gen Rules Engine Documentation 1 of : Introduction is still relevant. But there are also a number of important differences so what I’ve documented there is not 100% correct.

Experiments

The first step is to figure out how to do some of the basic things people need to do in Rules. I’ve created the following rule that illustrates some examples for some common actions and tasks one might need to do in a Rule.

// Native logger
var logger = Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.model.script.Rules.Experiments");
logger.error("This is an error log");
logger.warn("This is a warning log");
logger.info("This is an info log");
logger.debug("This is a debug log");
logger.trace("This is a trace log");

// openHAB Logger
logger.info("About to test openHAB logger");
var Log = Java.type("org.openhab.core.model.script.actions.Log");
Log.logError("Experiments", "This is an OH error log");
Log.logWarn("Experiments", "This is an OH warn log");
Log.logInfo("Experiments", "This is an OH info log");
Log.logDebug("Experiments", "This is an OH debug log");
// There is no trace

// Get the state of an Item
logger.info("The state of ServiceStatuses is " + items.ServiceStatuses);
logger.info("The triggeringItem is " + event.itemName);
logger.info("The command received is " + command);
// oldState/newState with changed trigger
// state with update trigger

// Command/update an Item
events.postUpdate(event.itemName, "OFF");
// events.sendCommand(event.itemName, "ON");

// Exec.executeCommandLine
logger.info("About to test executeCommandLine");
var Exec = Java.type("org.openhab.core.model.script.actions.Exec");
var Duration = Java.type("java.time.Duration");
var results = Exec.executeCommandLine(Duration.ofSeconds(1), "echo", "hello");
logger.info("results = " + results);

// ScriptExecution.createTimer
logger.info("About to test createTimer");
var ScriptExecution = Java.type("org.openhab.core.model.script.actions.ScriptExecution");
var runme = function(){ logger.info("Timer expired!"); }
var ZonedDateTime = Java.type("java.time.ZonedDateTime");
var now = ZonedDateTime.now();
var timer = ScriptExecution.createTimer(now.plusSeconds(1), runme);

// Action imports include: https://github.com/openhab/openhab-core/tree/master/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/actions
// - Audio (say)
// - BusEvent (sendCommand, postUpdate, saveStates, restoreStates)
// - Ephemeris
// - Exec
// - HTTP
// - Log
// - Ping
// - ScriptExecution
// - Things
// - Timer
// - Voice

// Ephemeris
logger.info("About to test Ephermeris");
var Ephemeris = Java.type("org.openhab.core.model.script.actions.Ephemeris");
logger.info((Ephemeris.isWeekend()) ? "It's the weekend" : "Get back to work!");

// Sleep
logger.info("About to sleep");
java.lang.Thread.sleep(1000);
logger.info("Done sleeping");

// QuantityTypes
logger.info("Cloudiness = {}", items["vCloudiness"]); logger.info("Coudiness without QuantityType = {}", 
            parseFloat(items["vCloudiness"].toString().split(' ')[0]));
logger.info("New QuantityType = {}",
            new QuantityType("50.0 %"));
logger.info("Comparison to QuantityType = {}", 
            items["vCloudiness"] > new QuantityType("50.0 %"));

Notice, most of the above will not work when the rule is triggered by pressing the play button to run the rule.

TODO:

  • manipulating Groups: See the old NGRE experiments linked to above
    - saving values from one run to another or finding an alternative approach for managing Timers
  • how to create a rule template from a rule: the MainUI doesn’t have a way to import a rule template
    - how to access Item metadata
    - how to create a rule from a rule
    - how to enable/disable/launch a rule from a rule

Edit 1:

I discovered that we can add variables to this that survive between executions of the Rule. Eureka! This will let us save “globals”. But, as with globals in .rules files or any other cases, the global variable gets reset when the rule is modified/reloaded. I’m not yet sure if this occurs only when this rule is reloaded or when any rule is reloaded. Global variables are only cleared when this rule is reloaded. I also don’t yet know if this is global to all rules or just the one rule where it’s added. Global values saved to this are only visible from the one rule where it’s set. The context does not extend to the “but only if…” conditional scripts. That apparently has it’s own context.

Need to test to make sure the global context doesn’t go away over time. Will test tomorrow which should give it enough time to be collected if it will time out. I let it sit all night and then ran the rule again and the variables are still there. So there is at least preliminary evidence that the globals won’t get garbage collected.

Here’s the simple case:

// Saving values from one run to another
logger.info(this);
logger.info("test_count = " + this.test_count);
if(this.test_count === undefined) {
  this.test_count = 0;
}
logger.info("test_count = " + this.test_count);
this.test_count += 1;
logger.info("test_count = " + this.test_count);

Edit 2:

To access Item metadata here is a snippet of an example.

// Accessing Item Metadata
logger.info("Trying to extract a metadata value")
var FrameworkUtil = Java.type("org.osgi.framework.FrameworkUtil");
var _bundle = FrameworkUtil.getBundle(scriptExtension.class);
var bundle_context = _bundle.getBundleContext()
var MetadataRegistry_Ref = bundle_context.getServiceReference("org.openhab.core.items.MetadataRegistry");
var MetadataRegistry = bundle_context.getService(MetadataRegistry_Ref);
var Metadata = Java.type("org.openhab.core.items.Metadata");
var MetadataKey = Java.type("org.openhab.core.items.MetadataKey");
var metadata = MetadataRegistry.get(new MetadataKey("name", "vArgus_Status"));
logger.info("vArgus_Status's name is " + metadata.value);

Note, I’ve not advocating using this instead of the Helper Libraries where all the Java.type and such is done for you. I’m documenting it here per my original goals, to make rules independent of external libraries. Consequently getting to the MetadataRegistry is a bit of a pain, as you can see. But at least this shows how to do it.


Edit: Rule manipulation

The following code shows how to find a rule, disable/enable a rule, and run another rule from a script action.

// Find a rule
logger.info("There are " + rules.getAll().length + " rules");
for each (var r in rules.getAll()) {
  logger.info("Rule name = " + r.getName());
  logger.info("Rule UID = " + r.getUID()); 
}
var thisRule = rules.getAll().stream().filter(function(i){ return i.name == "Experiments" }).findFirst().get();
logger.info("This rules UID is " + thisRule.getUID());

// Disable a rule
logger.info("Disabling this rule")
var FrameworkUtil = Java.type("org.osgi.framework.FrameworkUtil");
var _bundle = FrameworkUtil.getBundle(scriptExtension.class);
var bundle_context = _bundle.getBundleContext()
var classname = "org.openhab.core.automation.RuleManager"
var RuleManager_Ref = bundle_context.getServiceReference(classname);
var RuleManager = bundle_context.getService(RuleManager_Ref);
logger.info("Enabled = " + RuleManager.isEnabled(thisRule.getUID()));
// RuleManager.setEnabled(thisRule.getUID(), false);
logger.info("Enabled = " + RuleManager.isEnabled(thisRule.getUID()));

// Run another rule
RuleManager.runNow("tocall");
var map = new java.util.HashMap();
map.put("test_data", "Passed data to called function!")
RuleManager.runNow("tocall", true, map); // second argument is whether to consider the conditions, third is a Map<String, Object> (way to pass data?)

Notice how you can pass data to that other rule. This could be used to make some generic(ish) Script action rules (Rules without triggers) that multiple rules can call and pass data. For example, a centralized notification Rule.

Note, to access the data in the other rule you can get it using this.test_data.

I mostly figured out how to create a Rule (haven’t figured out the Action part yet) but the resultant rule ends up being a read only rule that disappears when OH restarts. I don’t think this is the direction I want to go in so I’m going to instead look into manipulating rules through the REST API when/if I need to go down this route. For now I’m marking this as done.


Edit: Differences in approach from Jython

In my Jython rules I created a number of libraries that automatically generate Rules based on Item metadata. While I’ve no doubt I can do the same for JavaScript JSONDB rules, I think that’s not the best appraoch here. Instead I’m going back to using Group member of rule triggers instead of dynamically generated triggers. I think this approach will be more portable in general.


Edit: Personal libraries

There will be many cases where a library module is the most appropriate approach instead of a Rule. To follow the standard set by the Helper Libraries these modules should go in $OH_CONF/automation/lib/javascript/personal. Then load the script using:

var OPENHAB_CONF = Java.type("java.lang.System").getenv("OPENHAB_CONF");
load(OPENHAB_CONF+'/automation/lib/javascript/core/mylibrary.js');
15 Likes

This is great Rich! Could you share the rule creation code as well? I’m also thinking about JavaScript because I’m chomping at the bit to start rules in OH3.


Somewhat unrelated: are you having to restart OH to get it to notice changes in the automation/jsr223 directory? I am not getting any watcher events firing for file delete/create/modify.

I didn’t go all the way with rule creation because I discovered that dynamically created rules show up as read only rules which isn’t what I wanted. So this might not be 100% complete.

// Find a rule
logger.info("There are " + rules.getAll().length + " rules");
for each (var r in rules.getAll()) {
  logger.info("Rule name = " + r.getName());
  logger.info("Rule UID = " + r.getUID()); 
}
var thisRule = rules.getAll().stream().filter(function(i){ return i.name == "Experiments" }).findFirst().get();
logger.info("This rules UID is " + thisRule.getUID());

// Disable a rule
logger.info("Disabling this rule")
var FrameworkUtil = Java.type("org.osgi.framework.FrameworkUtil");
var _bundle = FrameworkUtil.getBundle(scriptExtension.class);
var bundle_context = _bundle.getBundleContext()
var classname = "org.openhab.core.automation.RuleManager"
var RuleManager_Ref = bundle_context.getServiceReference(classname);
var RuleManager = bundle_context.getService(RuleManager_Ref);
logger.info("Enabled = " + RuleManager.isEnabled(thisRule.getUID()));
RuleManager.setEnabled(thisRule.getUID(), false);
logger.info("Enabled = " + RuleManager.isEnabled(thisRule.getUID()));

// Run another rule
RuleManager.runNow("tocall");
var map = new java.util.HashMap();
map.put("test_data", "Passed data to called function!")
RuleManager.runNow("tocall", true, map); // second argument is whether to consider the conditions, third is a Map<String, Object> 

// Create a new rule
logger.info("Typing needed classes");
scriptExtension.importPreset("RuleSupport");
scriptExtension.importPreset("RuleSimple");
scriptExtension.importPreset("RuleFactories");
scriptExtension.importPreset("default");
var RuleBuilder = Java.type("org.openhab.core.automation.util.RuleBuilder");
var ModuleBuilder = Java.type("org.openhab.core.automation.util.ModuleBuilder");
var Configuration = Java.type("org.openhab.core.config.core.Configuration");

logger.info("Creating the trigger");
var newTrigger = ModuleBuilder.createTrigger().withId("Item-aTestSwitch-received-update").withTypeUID("core.ItemStateUpdateTrigger").withConfiguration( new Configuration( { "itemName": "aTestSwitch" })).build();
var triggers = [newTrigger];
var runme = function(event) {
  var logger = Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.model.script.Rules.Experiments");
  logger.info("Inside created rule!");
}
logger.info("Building the rule")
var execX = new SimpleRule() { execute: runme };
var rule = RuleBuilder.create("createdrule")
                      .withName("Created Rule")
                      .withDescription("Rule created from Experiments rule")
                      .withTriggers(triggers)
                      .withActions(execX.getActions())
                      .build();
logger.info("Registering the rule");
automationManager.addRule(rule);

var triggers = thisRule.getTriggers();
logger.info("There are " + triggers.length + " triggers on this rule");
logger.info("Typing the Configuration");
logger.info("Creating the trigger")
var newTrigger = ModuleBuilder.createTrigger().withId("Item-vIsCloud-received-update").withTypeUID("core.ItemStateUpdateTrigger").withConfiguration( new Configuration( { "itemName": "vIsCloudy" })).build();
logger.info("Added the trigger to the list");
triggers.add(newTrigger);
logger.infoinfo("Adding the triggers to the Rule");

thisRule.setTriggers(triggers);
logger.info("There are now " + thisRule.getTriggers().length + " triggers on this rule");

These may not be the best ways to do it and, as I said, creating the rule may not be 100% yet using this approach. I’ve sort of abandoned the approach for now as I’m not sure it’s how I want to do things with JSONDB rules.

Notice that it’s possible to pass data to the other rule when you call it. In the example call above I have a rule with the UID “tocall” that all it does is log out this.test_data to show the data was passed.

Also notice, this is a rule creating a rule. I don’t actually know how to define a rule in JavaScript in automation/jsr223/javascript but I bet there is an example on the forum here somewhere and there is an example in the Helper Library docs https://openhab-scripters.github.io/openhab-helper-libraries/Guides/Rules.html.

A lot of the above has been cribbed and translated as best as I was able so far from looking at the JavaScript Helper Libraries (rules.js). The JavaScript Helper Libraries are broken in at least one place (LogAction is now called Log in actions.js) .

I’m writing all my scripts through MainUI so all the rules are in JSONDB and nothing is in automation/jsr223. I’ve not seen any issues with library code in automation/lib being picked up right away. My current workflow is I have a rule that exercises the library. I edit the library and save it (using vim right now, will move to VSCode eventually) and press the play button on the rule in MainUI. The changes are immediately picked up. I don’t even have to touch the rule itself.

Rules making rules is what I’m after, and its actually better that they aren’t editable in the UI. Not my exact use case, but as an example you wouldn’t want the user modifying the expire rule.

I have fixed all the issues I ran into in the JS helper libs. So far all I found were outdated imports. As you pointed out, they are woefully out of date, I’m thinking I will do a full rewrite in the vein of the Jython libraries.

But they can edit the rule that creates the rule so all that does is make it more awkward, it doesn’t prevent the editing. And I’m not confident enough in my own knowledge to think that I can come up with all of the possible use cases and options that can/should be made available on the rules.

Just make sure someone isn’t already working on that. Let me know if you need someone to test it out. I don’t expect a PR would be reviewed and approved anytime soon.

1 Like

This topic was automatically closed 41 days after the last reply. New replies are no longer allowed.