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');