Migrate from Nashorn to JSScripting for UI Rules

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.

  1. Open a rule that you are ready to migrate.
  2. Open the Code tab and copy the contents.
  3. Disable the Nashorn rule by tapping the pause button.
  4. Create a new rule, fill out the identifying info and then open the Code tab and paste.
  5. Change the “type” under the Script Action from “application/javascript” to “application/javascript;version=ECMAScript-2021”
  6. Using the docs above convert the code to be compatible with JSScripting and the helper library.
  7. 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 JavaScript Promise and await. 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 or const. 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.
12 Likes

Great write up! I would also like to add my favourite feature of the new version: modules.

If you want to use existing 3rd party Javascript libraries (or write your own), then this is now possible (although not explained here yet, but it will come). Maybe you want crazy control over your light colours, readable descriptions for those cron expressions, or simple to use an existing library to interact with something else. All this is now possible.

1 Like

Thanks! I plan on continuing to refine the article as those outstanding PRs get merged and as I learn more. I’ll definitely add that to the advantages.

I found this nice writeup through google when I ran into problems installing JSScripting because I could not get the ECMAScript 11 module to appear like it does in the documentation which I now learned to be outdated:

It’s really super duper mega convenient if you can leave all your scripts in the old Nashorn ECMA 5 and rewrite them one at a time to ECMA 11/JSScripting. Why was this changed?

-edit- As @hmerk pointed out, I was running 3.1.1 of the add-on, and this feature is available from 3.2.0.

I don’t understand your issue, just installed JS scripting and now it looks like
image

Just as in the docs…

You do understand my issue, it just doesn’t affect you. Are you running Stable or Beta?
I’m running the 3.2 stable and the plugin identifies itself as JSScripting 3.1.1.

openHAB 3.2 Release version

This is puzzling…

It is as if you’re experiencing the post-merge and I’m experiencing the pre-merge of the folowing:

when you install the JSScripting add-on all your Nashorn written rules will break. (…) 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).

Even though we’re running the same version.

You seem to have an older version installed


This is the one coming with the release build…

1 Like

That must be it! Do you need to install it manually? I click settings > automation > + and there I see the following: oooh…you know what? My upgrade to 3.2 must have failed. I’m still running 3.1.1. The plugin version number should have been a hint, but I thought they were not in tandem.

My apologies for not investigating this more thoroughly. Thank you for that last comment, it made me see the light. :+1:t2:

No worries, glad we could sort it out :+1:

Is there a way to check if or where certain Java features are available?

Specifically, I’m looking if I can still execute other scripts, which in Nashorn worked like this:

var BundleCtx = Java.type('org.osgi.framework.FrameworkUtil').getBundle(scriptExtension.class).getBundleContext()
var RuleMgr = BundleCtx.getService(BundleCtx.getServiceReference('org.openhab.core.automation.RuleManager'))
RuleMgr.runNow('123456789a')

which will result in:

org.graalvm.polyglot.PolyglotException: ReferenceError: “scriptExtension” is not defined

Although Java.type is working fine and scriptExtension throws the error so technically my question may be incorrect. Note that I am importing the suggested scopes, i.e.:

Object.assign(this, require('@runtime'))
Object.assign(this, require('openhab'))

I have also some minor problems with rounding :slight_smile:

i tried this little function:

var num = 5.56789;
var n = num.toFixed(2);

Error:
failed: org.graalvm.polyglot.PolyglotException: TypeError: [object Object] is not a function

Same when using
toPrecision(2);

I agree with the question where there is an overview of functions. @Redsandro

@rlkoshak @jpg0

EDIT:
my fault, it works but i had heap errors like other people here with JSS

Anything Nashorn can do JSScripting can do too except for the equivalent of Thread.sleep.

Sometimes the best source of examples will be by looking at the code for the helper library openhab-js. In this case, if you go to openhab-js and look at the part of the library that works with rules, you’ll see that osgi has a getService function.

 ​let​ ​RuleManager​ ​=​ ​osgi​.​getService​(​"org.openhab.core.automation.RuleManager"​)​;

If you don’t want to use the helper library, you need to use something besides scriptExtension. For example:

this.ScriptHandler = Java.type("org.openhab.core.automation.module.script.rulesupport.shared.ScriptedHandler");
    var _bundle = FrameworkUtil.getBundle(ScriptHandler.class);

Please be sure to read the docs for the add-on. Everything you need is imported by default, unless you turn that off in the bibding’s settings. Also note that runtime and the helper libraries are not compatible. For example, in runtime items is a dict of item name/current states. But in the helper library it’s your one stop shop for all things item related including sending commands and such.

You never want to include both as they will overwrite parts of tech other. You only want to include runtime if you have a Nashorn script you want to run on JS Scripting with minimal changes. In that case, I’d argue just leave it as Nashorn. You really are not getting much benefit from JSScripting in that case.

The docs for the add-on has a decent reference and you can click through to reach comprehensive reference docs and even get to the code for the helper library. Everything else is going to be standard GraalVM JavaScript. I’d guess that this might be a place where GraalVM differs from native. I bet that your num is not a standard JavaScript Number but something else that can work for both JS and Java at the same time (this is running in Java after all) but I’m far from an expert on that part of it.

1 Like

Thank you for showing me how to import RuleManager in GraalJS.

I think I’m doing something wrong. I’m getting the following message when trying to execute a rule:

[ERROR] [internal.handler.ScriptActionHandler]
Script execution failed:
org.graalvm.polyglot.PolyglotException:
TypeError:
invokeMember (runNow) on org.openhab.core.automation.internal.RuleEngineImpl@35d1667c failed due to:
no applicable overload found
(overloads:
[Method[public void org.openhab.core.automation.internal.RuleEngineImpl.runNow(java.lang.String,boolean,java.util.Map)],
Method[public void org.openhab.core.automation.internal.RuleEngineImpl.runNow(java.lang.String)]],
arguments: [123456789a (String), DynamicObject (Nullish), DynamicObject (DefaultLayout)])


For the people to came here using search: The answer to “Is there a way to check where certain Java features are?” is “not as a list, but check the helper source.”

I’ve not done this from JSScripting so there might be something different in how it works. All my rules that call other roles are written to be compatible with both Nashorn and JSScripting so I’m not using the helper library osgi for that part yet.

It should work though but I can’t tell you why it didn’t work nor how to fix it. But it does look like you called runNow with the wrong number of arguments. The error isn’t saying that you didn’t get the Rule manager, but that the runNow call you tried to make doesn’t match the runNow functions available.

1 Like

Thank you, I know I had the right amount of arguments, but you provided some helpful perspective on where to look. This is how I figured out I can no longer use undefined as a second parameter. In GraalJS it (whether or not to assess rule conditions) needs to be explicitly true or false when set.

I recommend adding this in the topic start post to a list of things to look out for when migrating.

1 Like

How can I get context, getAttribute or similar?

I’m looking for a way to read the attributes passed to a rule or “script” as specified using the runNow() command. In Nashorn this would be context.getAttribute('someAttribute'). However, in GraalJS, context is not available, and searching in openhab-js yields no helpful hints.

The map is injected straight into the rule. So if you pass a map to the rule with a key “foo”, the called rule will have a variable “foo” available.

However, that behavior is likely to change in the future. This isn’t in the docs yet since it is likely to change.

1 Like

Thank you. That’s convenient.

I understand.

Is there a roadmap or something more abstract than the OH release notes to watch in order to stay apprised of such specific developments, feature freeze or finalized behavior?