This tutorial is going to be somewhat of a living document as there is a lot changing even as I type this. I will return and update it as things change and try to keep it up to date. It’s a little all over the place and not intended to be a complete tutorial on how to use JSScripting yet.
For now, only if you want to be on the bleeding edge consider following this path. In a few weeks a lot of the below will change a bit, though I tried to cover that stuff too. For most consider this a preview of what is to come and a first draft of my personal notes.
Introduction
A Little Bit of History
As long as OH has existed, or at least since OH 1.6, there has been available an implementation of JavaScript embedded in the Java Runtime Environment. This implementation is called Nashorn and it currently implements ECMAScript 5.1 which is quite old and lacking a number of features.
Early on in OH 1 and OH 2 there was a concept called JSR223 (since renamed Java Scripting API I believe but you will see JSR223 all over the OH docs and forum even still) that allowed the use of Nashorn (and other languages) to write rules. A nice and robust Helper Library was developed to make things easier. Use of these other languages in OH 1 was rare and in OH 2 they were “experimental”.
In OH 3 the rules engine that supported these other language became the rules engine and Rules DSL (the language built by and for OH) moved to run on it. Thus, as of OH 3.0 the set of rules languages that OH comes with out of the box include:
- Rules DSL
- Nashorn JavaScript ECMAScript 5.1
- Blockly (which gets converted to Nashorn behind the scenes).
But a growing list of Automation add-ons are being developed and added to OH to support other languages. One of those is JSScripting.
The Future
Unfortunately, Nashorn is deprecated in Java 11 and will be completely removed once openHAB moves off of Java 11 over the next year or two. Thus, there needs to be an alternative and JSScripting is that alternative.
JSScripting implements the latest ECMAScript 2021 with all the features and it is being built to be as close to a native JavaScript environment as possible. But that means it differs from how Nashorn works in a number of ways. One of the key differences is that nothing gets injected into the rule’s context by default. One has to import everything that is used (more on this later).
JSScripting is IMHO the most mature and capable of the current set of rules languages and therefore is the best candidate as a new default and replacement for Nashorn. Unlike with Nashorn, the Helper Library that makes interacting with openHAB easier and work in a way that is more JavaScript like comes with the add-on. No more need to clone a git repo and move files around. If you want to be on the bleeding edge, you can install the Helper Library using npm.
Thankfully, there is a way to write code that is compatible with both Nashorn and JSScripting.
Have Your Cake and Eat It Too
Unfortunately, as of this writing, when you install the JSScripting add-on all your Nashorn written rules will break. This section will walk you through how to change your Nashorn rules so they work on both. Soon (hopefully in a matter of days) this will no longer be the case and the two can run side by side (PR has already been submitted, just waiting for review and merge).
What’s Different
In order to support external third party libraries the code that makes up a rule needs to not have variables already defined in it. Especially with variable names like event
and items
the likelihood of conflicts is too great. Therefore all the stuff that is normally injected into a rule (see JSR223 Scripting | openHAB) does not exist and needs to be imported.
In addition, the helper library that comes with JSScripting works differently from the JSR223 Nashorn one and it makes a greater effort to convert and present as much as possible as JavaScript types instead of Java types. Consequently if you have rules that depend heavily on the Nashorn helper library the number of changes is going to be much greater and is out of scope for this tutorial.
However, despite these differences it is possible and relatively easy to write a rule that is compatible with both Nashorn without the JSR223 helper library and JSScripting. Given that the helper library mostly makes it easier to write rules in .js files, it is likely that in most of your UI rules written in Nashorn do not even use the helper library anyway, or do so minimally.
runtime
As mentioned, stuff like events
and items
and other variables that one relies on in a Nashorn rule to interact with OH do not exist by default in JSScripting rules. But they can be imported using
var runtime = require('@runtime');
And then you can access all the variables and types in the “Default Preset” (linked to above) using that imported runtime
Object.
But Nashorn doesn’t understand the keyword require
. So we need to do a little trickery. If require
doesn’t exist, we need to build the require Object with the Default Presets.
var runtime = (typeof(require) === "function") ? require("@runtime") : {
itemRegistry: itemRegistry,
events: events,
items: items,
DateTimeType: DateTimeType
};
Look at your rule/Script Action/Script Condition and include an entry for each of the variables you are using from the Default Presets and add them to the runtime
Object.
With this at the top of your code, you can access all the presets you are used to using through the runtime
Object in either Nashorn or JSScripting.
So Script Action that looks like
if(items["MySwitch"] == OFF) {
events.sendCommand("MyOtherSwitch", "ON");
}
would become
var runtime = (typeof(require) === "function") ? require("@runtime") : {
events: events,
items: items,
};
if(runtime.items["MySwitch"] == runtime.OFF) {
runtime.events.sendCommand("MyOtherSwitch", "ON");
}
That second version of the Script Action will work in either Nashorn or JSScripting.
Convert all your Script Actions and Script Conditions as shown above and verify they work. Then you can install the JSScripting add-on. At this time I recommend installing the jar file posted at Release JSScripting and OHJ · digitaldan/openhab-addons · GitHub, though all those changes might be included in the SNAPSHOTS already.
You can tell if you have the latest and greatest by looking in MainUI → Settings and if you see a JSScripting entry in the bottom right you have the latest, or at least a recent enough version to follow the rest of this tutorial.
Alternative to runtime
If you’d rather make your Nashorn Script Actions/Conditions work in JSScripting with minimal changes and don’t mind the injection of all the variables from the Default Preset instead of creating/importing a runtime
Object, you can import all the variables so they are just there the same as they are in Nashorn.
if(typeof(require) === "function") Object.assign(this, require('@runtime'));
Only if require
is defined as a function pull in all the Default Presets. However it does not set up you up well to use the Helper Library as there are some variables that have the same name but different meanings in the Default Preset and the Helper Library (e.g. items
and things
). So only use this approach if you do not intend to use the Helper Library.
Other Differences
Once all the presets are available using either the runtime or the Object.assign approach above there are a few additional differences between the two that need to be sorted.
Comparing Types
The use of Object.assign appears to work slightly different from the way Types are injected in Nashorn requiring a slightly different approach. The following example will show a way to compare an Item state’s type to UnDefType in both.
var undefType = (typeof(require) === "function") ? runtime.UnDefType : runtime.UnDefType.class;
if(event.itemState.class == undefType){
Thread.sleep()
This simply will not work in JSScripting. You will need to find an alternative approach if you use sleeps.
Cancelling a Timer Inside the Timer
This is a rare and frankly aberrant thing to do. But Nashorn used to let you get away with it. An example will be helpful here.
var ScriptExecution = Java.type("org.openhab.core.model.script.actions.ScriptExecution");
var ZDT = Java.type("java.time.ZonedDateTime");
this.timer = (this.timer === undefined) ? null : this.timer;
this.timer = ScriptExecution.createTimer(ZDT.now().plusSeconds(5), function() { this.timer.cancel(); });
Even though that is kind of a nonsense thing to do, in Nashorn there is no problem with that code. In JSScripting an exception will be thrown.
JSScripting Helper Library
As I mentioned, the helper library will come with the add-on. No need to install it separately.
However, there are lots of changes being made to the helper library as we lead up to the release so it’s moving at a faster pace than the add-on. So you might want to install the latest using NPM. See GitHub - openhab/openhab-js: openHAB Javascript Library for instructions on doing that as well as the docs for the Helper Library itself. tl;dr:
cd $OH_CONF/automation/lib/javascript/personal
npm i openhab
NOTE: There is a yet to be merged PR that will move this to $OH_CONF/automation/js
Writing rules in the UI is a different experience than writing rules in .js files. For the most part we are really just writing little code snippets in Script Actions and Script Conditions. Needing to import each part of the Helper Library in each and every one is a pretty significant burden on UI Rules writers. Therefore the add-on has a single configuration parameter to automatically inject the Helper Library into your Script Actions/Conditions. By default it will be enabled.
This will give you access to all the Classes and Objects in the API: GitHub - rkoshak/openhab-js: openHAB Javascript Library. If you turn off that setting, you will want to add the following to the top of your Script Actions/Conditions
Object.assign(this, require('openhab'));
This will import all the Helper Library to that Script Action/Condition.
From this point you can start to migrate the code to using the Helper Library. Look at the API docs linked above and pay special attention to items
and actions
Objects as those will be the ones you use the most.
As a simple example this
var runtime = (typeof(require) === "function") ? require("@runtime") : {
events: events,
items: items,
};
if(runtime.items["MySwitch"] == runtime.OFF) {
runtime.events.sendCommand("MyOtherSwitch", "ON");
}
will become
if(items.getItem("MySwitch").state == "OFF") {
items.getItem("MyOtherSwitch").sendCommand("ON");
}
Stay Tuned
The above migration process is a little awkward and it kind of forces you to “burn your ships”. Soon (PR submitted but not yet merged) Nashorn and JSScripting will be able to run side-by-side. When this happens I recommend the following approach.
- Open a rule that you are ready to migrate.
- Open the Code tab and copy the contents.
- Disable the Nashorn rule by tapping the pause button.
- Create a new rule, fill out the identifying info and then open the Code tab and paste.
- Change the “type” under the Script Action from “application/javascript” to “application/javascript;version=ECMAScript-2021”
- Using the docs above convert the code to be compatible with JSScripting and the helper library.
- Test that the rule works. Once you are satisfied delete the old Nashorn version. If you need to you can disable the JSScripting rule and reenable the Nashorn one until you can make it work.
Advantages of JSScripting
- The
cache
provides a key/value store that will let you store data that persists from one rule to the next and can be used by more than one Script Action/Condition. So for example, one can share a Timer between multiple rules. - Most of the Java type stuff is handled for you. Almost everything you work with will be a JavaScript Object. For example,
items.getItem("MyGroup").members
returns a JavaScript Array of OHItem Objects (which are JavaScript). - IMHO it is likely going to be the future default for OH. Nashorn is going to go away and Blockly will be updated to support JSScripting.
- Supports the latest JavaScript language features. I particularly like the
=>
operator. - It’s possible to install and use almost any standard JavaScript library using the standard package manager (i.e. npm) and you can therefore distribute your own libraries in that way too.
Current Disadvantages
- As of this writing it’s really bleeding edge. Lots of PRs are in work and already submitted that will change things considerably. If you don’t want to mess with a lot of rework, hold off for awhile.
- Currently cannot support
Thread.sleep()
. The way the engine works that won’t work nor will the usual JavaScriptPromise
andawait
. Alternatives are being investigated. - For UI rules, the Script Actions/Conditions are reused from one run to the next. Unfortunately that results in errors when using
let
orconst
. There is work being done to look at how to address this. - The JSScripting helper library is a great first start but stay tuned for lots of more great additions. in the near future.