This tutorial post is very out of date and ultimately did not make it into the official docs. Do not use this except for historic purposes. See the Getting Started Tutorial sections on rules for how to get started with rules. Rules - Introduction | openHAB
Writing Scripts
As you can see a lot can be accomplished in the NGRE without writing scripts at all. Much more than is possible with just Items. However at some point writing a script will be required. We know that the scripts entered into the Script box need to be JavaScript but how do we do all the stuff we are used to in Rules DSL like creating Timers, calling Actions, working with Groups and Items?
JSR223
Remember when I wrote that users of JSR223 Rules are already using the NGRE? This is important because that means:
- the default environment is the same as the default JSR223 Rules environment as documented here with a few additions described below
- any JSR223 JavaScript library can be imported and used in our PaperUI scripts
- we donât have to write Rules in PaperUI at all, and we can mix and match if we want to (writing JSR223 Rules on their own is beyond the scope of this document)
- features like enabling and disabling Rules should also work for JSR223 written Rules, not just PaperUI created Rules // TODO test this is actually true
JavaScript Resources
As previously mentioned, the only supported language is JavaScript. There are many tutorials and documents available as a reference to users to learn JavaScript. A few resources I found useful include:
Tutorial | Why |
---|---|
Java 8 Nashorn Tutorial | How to work with Java Objects inside the JavaScript. Most of the openHAB Objects we will be working with are Java, not JavaScript. |
Java 8 Stream API | Group memberâs operations |
TODO | Need more JavaScript resources. |
openHAB JSR223 Libraries We Can Use
We can rely upon the existing documentation for JSR223 Rules, in particular JSR223 JavaScript Rules and we can learn from and use openHAB JSR223 JavaScript Libraries. The library linked to through the docs is @lewieâs library located here and it provides many conveniences for writing Rules in JavaScript, almost all of which we can use in PaperUI created JavaScript Rules.
This library makes the JavaScript we write a lot more convenient and uses similar names and conventions as is used in the Rules DSL which will be useful for those users looking to migrate from Rules DSL. Note that there will not be a requirement to migrate from Rules DSL. Eventually Rules DSL will be one of the supported languages to run on the NGRE.
However, although the documentation for JSR223 is informative, there are certain things we donât have to deal with in PaperUI defined Rules. Anything that deals with creating Rules or Rule Triggers is already handled for us. We just need to provide the body of the Rule to execute.
Using Libraries
First create the $OPENHAB_CONF/automation/jsr223
folder if it hasnât been created already. This is where the libraries you need to use will go. This is also where jsr223 Rules will go.
Next download lewieâs library from here and place the files in $OPENHAB_CONF/automation/jsr223
. You may want to remove all the files not in the jslib folder. The former are tests and examples which will start running and likely start throwing errors because you do not have certain Items defined. You may also want to remove the jslib/triggersAndConditions.js file. This file conflicts with the rule started trigger with PaperUI created Rules.
If you are running a non-installed OH (e.g. Docker, manual install, QNAP, Synology. etc) you may need to edit the library with the full path to the library. Search through the files for â/etc/openhab2â and replace that with $OPENHAB_CONF where $OPENHAB_CONF is the full path to your OH conf folder.
Watch openhab.log and you should see it loading the scripts. If you copied over the test and example files you may see them start executing in the logs. Until the path gets corrected as described above you will see errors as these try to run.
As you write your Scripts, you will want to reference the examples in the library to learn how to accomplish what you want to do. You will also probably want to learn a bit of JavaScript to learn how to do language specific things like how to construct while loops, process Strings, etc. See the resources above.
Once the library is successfully loading you can import it into your Script using a single line. Then all of the convenience functions created to let us write code that is a bit easier and a bit more familiar.
Here is the library version of the Hello World! script above.
load(Java.type("java.lang.System").getenv("OPENHAB_CONF")+'/automation/jsr223/jslib/JSRule.js');
logInfo("Hello World!");
The above assumes that the environment variable $OPENHAB_CONF
is set to the full path to your conf folder. For installed OH that environment variable will be set for you.
Implicit Variables
Unlike with Rules DSL, not as much is made available to you by default. For example, we need to import the logger and we have to pull Items out of the Item Registry by name instead of having a pre-created variable we can use to access the Items. This is why convenience libraries like the previously mentioned one are so important.
On-the-other-hand, in some ways there is a lot more that is made available to us for use even without libraries. For example, if we write a Script that assumes that the Rule was triggered by a Group changing, we can write a Condition that doesnât even attempt to run the Rule unless it is triggered by a GroupItemStateChangedEvent
.
For the remainder of this document, both a version with and without the previously mentioned library will be shown.
What is Available in the Script?
The default environment provided matches the same default environment provided in JSR223 JavaScript rules. This includes all of the variables listed here. Notice that most of these are the enum type Commands and States like ON, OFF, UP, DOWN, etc. But there are a few variables that give us access to core parts of openHAB that we need.You can identify those as they are all lowercase.
There are also a few variables not listed in that table that are available to us in PaperUI defined Scripts. Those are documented in the following table:
Object | Purpose |
---|---|
event |
Describes the event that caused the Rule to trigger. This variable does not exist unless the Rule was triggered by an Item or Channel event. |
command |
The command that caused the Rule to trigger. This is equivalent to receivedCommand in Rules DSL. This variable does not exist unless the Rule is triggered by a command. |
oldState |
The previous state of the Item. If it is UNDEF or NULL the variable will be blank when logged. This is equivalent to previousState in Rules DSL. It does not exist unless the Rule was triggered by an item changed trigger. |
newState |
The current state of the Item. If it is UNDEF or NULL the variable will be blank when logged. This is equivalent to triggeringItem.state in Rules DSL. Only exists when the Rule was triggered by an item changed trigger. |
state |
The current state of the Item. If it is UNDEF or NULL the variable will be blank when logged. This is equivalent to triggeringItem.state in Rules DSL. Only exists when the Rule was triggered by an item updated trigger. |
There are a few things to note.
- Time triggers will not have any of these variables defined.
- When creating a Rule that has Item event triggers that calls another Rule as itâs action, these variables will be populated in the called Rule. For example, if I have the Rule âHello Worldâ triggered by âItem Foo received commandâ and the Rule âCalls Hello Worldâ triggered by âItem Bar received updateâ which calls âHello Worldâ as its Action then
command
will be undefined and event will be âItem âBarâ received update ONâ in the âHello Worldâ Rule when âCalls Hello Worldâ is triggered.
event
is going to be one of the more useful variables. The event
, when logged takes the following forms:
Event | Form when Logged |
---|---|
a trigger channel fires |
<channel ID>#<event> triggered <EVENT TYPE> , for example: astro:sun:local:set#event triggered START
|
an item changes state |
<Item> changed from <Previous State> to <Current State> , for example: Test changed from ON to OFF . NOTE, if the state is UNDEF the state will be blank. If the Item is a Group then the form will be <Group> changed from <Previous State> to <Current State> through <Item> , for example: TestGroup changed from ON to OFF through Test
|
an item received a command |
Item '<Item>' received command <Command> , for example: Item 'Test' received command ON
|
an item state is updated |
<Item> updated to <State> , for example: Test updated to ON
|
it is a fixed time of day | event will be undefined |
But how do you use event
? The event
Object has the following methods/data members.
Method/Data Member | Value |
---|---|
event.toString |
Prints the text described in the table above |
event.itemName |
The name of the Item that triggered the Rule. undefined when triggered by a channel event. In Rules DSL this is triggeringItem.name . |
event.itemState |
The state of the Item that triggered the Rule. undefined when triggered by a channel event or received command events. In Rules DSL this is triggeringItem.state . |
event.oldItemState |
The previous state of the Item that triggered the Rule. Only has a value for changed triggered Rules. |
event.memberName |
The member of the Group that caused the change to the Group that triggered the Rule. Only has a value for item changed triggers that use a Group. |
event.type |
The event type that triggered the Rule. Types include ChannelTriggeredEvent , ItemStateChangedEvent , ItemCommandEvent , ItemStateEvent , and GroupItemStateChangedEvent . Note event doesnât exist for non-Item and non-Channel triggered Rules. |
event.payload |
JSON formatted description of the event. |
event.source |
Appears to be always null
|
event.topic |
The event bus topic where the event was published: smarthome/channels/<channelID>/triggered , smarthome/items/<Item>/command , smarthome/items/<Group>/<Item>/statechanged
|
You will notice that most of the state related properties have an equivalent environment variable defined.
Beyond the state values, to get the name of the Item that triggered the Rule look at event.itemName
. To get the Item Object associated with the event (e.g. triggeringItem
in Rules DSL) use
// No library
var triggeringItem = ir.getItem(event.itemName);
// Library version
var triggeringItem = getItem(event.itemName);
You will also notice there are a lot of variables that only exist when the Rule is triggered by certain types of triggers. You can test in a script to see if a variable exists using:
// Both with and without library
if(command == undefined) // we know the Rule wasn't triggered by a command if true
Comparisons to Rules DSL
For those used to the Rules DSL, here is a table mapping what we have available to the implicit variables available in Rules DSL Rules.
Rules DSL | NGERE Native | NGRE Library | Notes |
---|---|---|---|
receivedCommand |
command , event.command
|
command , event.command
|
Only if the Rule was triggered by a command |
previousState |
oldState , event.oldItemState
|
oldState , event.oldItemState
|
Only if the Rule was triggered by a changed |
triggeringItem |
ir.getItem(event.itemName) |
getItem("event.itemName") |
Only if the Rule was triggered by an Item event, if the Rule was triggered by a Group then triggeringItem is the Group |
triggeringItem for a Member of triggered Rule |
ir.getItem(event.type.startsWith("Group") ? event.memberName : event.itemName); |
getItem(event.type.startsWith("Group") ? event.memberName : event.itemName); |
Only if the Rule was triggered by an Item event, if the Rule was triggered by a Group then triggeringItem is the member of the Group. There is no true Member of trigger so this will only work with Group changed and updated Rule triggers. |
NA |
state , newState , event.itemState
|
state , newState , event.itemState
|
state only if the Item is triggered by an update, newState only if the Item is triggered by a change. |
now |
var DateTime = Java.type("org.joda.time.DateTime");var now = function() { return DateTime.now(); }; , |
now(); if using the library |
See DateTimeType below |
Items Operations
Accessing Item Objects and States
Unlike in the Rules DSL where all the Items are available by name, in the NGRE (without using a library) we must pull the Item Object from the Item Registry (ir
variable). The following data are available on the Item Object pulled from the Item Registry.
// No library
var MyItem = ir.getItem("MyItem");
// Library
var MyItem = getItem("MyItem");
However, if all you need is the Itemâs state, there is a Map of Item names and their corresponding States available in the items
Object.
// Both with or without library
var MyItemState = items["MyItem"];
The Item Object has a number of data members and methods available. The following table lists them along with their purpose. They can all be accessed in the usual way: e.g. MyItem.groupNames
.
Data/Method | Purpose |
---|---|
category | The name of the icon(s) used to represent the Item on sitemaps. |
groupNames | Array of all the Groups the Item is a member of. |
label | The label of the Item excluding the [%s] parts. |
name | The name of the Item. |
state | The current state of the Item. |
tags | The tags applied to the Item. |
type | The Item type (e.g. âSwitchâ). |
addGroupName | Adds this Item to the given Group. |
addGroupNames | Adds this Item to the given list of Groups. // not tested |
addTag | Adds the given tag to the Item. |
addTags | Adds the given list of tags to the Item. // not tested |
getStateAs | Returns the state of the given Item as the given type. You must pass the type you want the state as. For example, to get a Color Item as an OnOffType use MyColorItem.getStateAs(OnOffType.class) . |
removeAllTags | Removes all tags from the Item |
removeGroupName | Removes the Item from the given Group |
removeTag | Removes the given tag from the Item |
setCategory | Sets the icon for the Item // not tested |
setLabel | Sets the label for the Item // not tested |
toString | Prints out all the important parameters of the Item as a String |
Some of these will not be useful or functional (e.g. I donât think it works to setLabel from Rules).
TODO: Someone who uses these needs to test. Please try the ânot testedâ and update the table accordingly.
Commanding and Updating Items
Notice there isnât a sendCommand
or postUpdate
method on the Item Object. As of this writing there is no method on the Items to do this. So we need to do this through the events
object.
// No Libary
var it = ir.getItem("MyItem");
events.sendCommand("MyItem", ON);
events.sendCommand(it, ON);
events.postUpdate("MyItem", ON);
events.postUpdate(it, ON);
// Library
var it = getItem("MyItem");
sendCommand("MyItem", ON);
sendCommand(it, ON);
postUpdate("MyItem", OFF);
postUpdate(it, OFF);
// TODO submit a PR to add a sendCommandIfDifferent function to the library
Working with Item States
One of the current weaknesses of the NGRE and one that is sure to cause frustration is that the states carried by Items are Java Objects and the values in Rules you will want to compare them against will be JavaScript Objects and primitives. Occasionally this may cause problems. Consider this when errors occur.
Accessing States and Comparisons
Type | Native Example |
---|---|
StringType |
if(items["TimeOfDay"] == "DAY") |
DecimalType |
if(items["Temperature"] > 65) , val result = items["Temperature"] + 5 . To convert to int, float, et. al. use the âvalueâ methods, e.g. val stAsInt = items["Temperature"].intValue . |
PercentType |
See DecimalType |
HsbType |
items["MyColor"].hue , items["MyColor"].saturation , items["MyColor"].brightness , var asDimmer = ir.getItem("MyColor").getStateAs(DecimalType.class) , see DecimalType above for what you can do with the numbers |
All Enum Types (e.g. OnOffType) | if(items["MySwitch"] == ON) |
DateTimeType
//////////////////////////////////////////////////////////////////////////////// TODO how to convert a DateTimeType to OffsetDateTime
DateTimeType
is a special case that needs further discussion. In the old Rules DSL, most date and time operations were done with the Joda DateTime library. For the NGRE Joda has been replaced by Javaâs OffsetDateTime. You will notice that most if not all of the methods you are used to from Joda are present.
If not using a convenience library (see below) you should add the following two lines to the top of your script.
var OffsetDateTime = Java.type("java.time.OffsetDateTime");
var now = function(){ return OffsetDateTime.now(); };
In order to do operations with OffsetDateTime such as plusMinutes
or isAfter
you need to convert the DateTimeType to an OffsetDateTime.
var asODT = OffsetDateTime.parse(items["MyDateTime"].toString()); // Didn't work
TODO How to I pass the DateTimeType.toString() to the OffsetDateTime.parse
//////////////////////////////////////////////////////////////////////////////////////
DateTimeType
is a special case that needs further discussion. In the old Rules DSL, most date and time operations were done with the Joda DateTime library. We can access the Joda DateTime from our NGRE scripts.
If not using a convenience library (see below) add the following lines to the top of your script.
// No library
var DateTime = Java.type("org.joda.time.DateTime");
var now = function() { return DateTime.now(); };
// Nothing to do with library
In order to do operations with now
and date and time comparisons like plusMinutes
and isBefore
the DateTimeType needs to be converted to a Joda DateTime.
// With and without library
var jdt = new DateTime(items["MyDateTime"].toString());
if(jdt.isBefore(now().minusMinutes(10)) {
...
See the Joda DateTime JavaDoc for the full list of methods available.
There will be times where one needs to take a Joda DateTime back to a DateTimeType.
NOTE: This is going to change in the near future and Joda will no longer be required.
// With and without library
var dtt = new DateTimeType(now().toString());
Accessing Persisted Data
Like the sendCommand
and postUpdate
methods, there are no historic state methods on the Item Object. If not using a library, you need to import the PersistenceExtensions
and make the calls to previousState
, lastUpdate
, etc. through that Object.
Add the following to the top of your script if not using the library.
var pe = Java.type("org.eclipse.smarthome.model.persistence.extensions.PersistenceExtensions");
All of the methods discussed in the Persistence documentation are available on PersistenceExtensions with the primary difference being one must pass the Item as the first argument to the method. For example:
// Without library
var prev = pe.previousState(ir.getItem("MyItem"), true, "influxdb");
// With library
var prev = previousState("MyIem", true, "influxdb");
Remember, many of these return a HistoricItem
so to get the previous state, whether or not it is different from the current state of the Item from the default persistence:
// Without library
var prevState = pe.previousState(ir.getItem("MyItem")).state;
var prevTime = pe.previousState(ir.getItem("MyItem")).timestamp;
// With library
var prevState = previousState("MyItem").state;
var prevTime = previousState("MyItem").timestamp;
Functions
Functions is a unit of code that can be independently called. One can pass arguments to the unit. In Scripts functions server three purposes:
- encapsulate a set of code that needs to be called more than once within a given Script
- pass to another function (see Group Operations for examples)
- creating a library that can be used used across multiple Rules (see above for how to include a library and Writing Libraries later in this tutorial for how to create a library)
Creating a function in JavaScript is through the keyword function
.
var namedFunction = function() {
// some JavaScript code
return returnVal;
};
var functionWithArguments = function(arg1, arg2) {
// some JavaScript code
return returnVal;
};
Anonymous function, see Group Operations below
However, unless you put the functions into a library (see next sections) the function will only be available to the Action script where it is defined. To create reusable functions you must put them into a library and import them into your Action Script as illustrated above.
One thing to note is that in NGRE, functions are not the only way to encapsulate a set of code to be called and reused. Since Rules can trigger Rules, one can encapsulate the code in a Rule.
TODO: These are not working for me right now I may need an update?
Function | Description |
---|---|
rules.runNow("<rule UID>"); |
Executes the Rule with the given UID immediately. The conditions of the Rule will be honored and there will be no variables in the cxt.
|
rules.runNow("<rule UID>", <true/false>, Map<String, Object>) |
Executes the Rule with the given UID immediately. The conditions of the Rule will be honored if true is passed, ignored if false is passed. The Map contains key/value pairs that will be available in the called Rule in the ctx variable. |
Group Operations
One of the more powerful tools for the writing of generic reusable Rules is through the use of Groups. By triggering the Rule based on changes to a Group and looking at the topic as described above, one can determine what Item triggered the Rule and write one Rule to work for many Items.
The NGRE also has an alternative way to write generic Rules. Once can write one Rule and then call that one Rule from other Rules using different triggers.
The first thing to realize is that a Group Item is still an Item. So everything in the Items Operations section applies to Groups.
Once in a Rule, there are a number of operations one may want to do on a Groupâs members. One important thing to note is that members
and allMembers
is a java.util.List
, not a JavaScript array. So standard JavaScript array operations are not available.
Nashorn JavaScript provides access to the stream()
method on collections and JavaScript lets us create anonymous functions inline which lets us do the same sorts of operations with Groups that we are used to. There are more operations than those listed in this table. Search for more examples and tutorials on Javaâs Stream API for complete documentation.
Operation | What it does |
---|---|
filter |
var filtered = group.members.stream().filter(function(i){ return <boolean expression>; }).toArray(); where <boolean expression> is the filtering conditional. |
findFirst |
var first = group.members.stream().filter(function(i) { <boolean expression>; } ).findFirst().get(); , you can test whether or not the filter has any elements using isPresent() on the result of findFirst() . |
forEach |
for each (var i in group.members) { <do stuff>; } alternative: group.members.stream().forEach(function(i) { <do stuff>; } );
|
map |
var names = gr.members.stream().map(function(i){ return i.name; }).toArray(); get a List of all the names of Items in members . If you will be doing a map/reduce you donât need the toArray the result of the map |
reduce |
var names = gr.members.stream().map(function(i){ return i.name; }).reduce(function(acc, next) { return acc + " " + next; }).get(); As with the findFirst, the reduce returns an Optional object and we can test if there was a result using isPresent() . |
There may be times when you want to deal with JavaScript arrays instead of Java Collections.
// With and without library
var jsarray = Java.from(group.members); // converts the Java List<Item> to an Item[ ] JavaScript array
Previous step: Experimental Next-Gen Rules Engine Documentation 3 of : Your First Rule
Next step: Experimental Next-Gen Rules Engine Documentation 5 of : Actions