How to upload JavaScript rules?

Does anyone have a nice means to upload ECMAScript JS rules other than copy’n’paste via UI ?

They are text by nature, they get stored as text, but so far I cannot upload them non-interactively like I can do with DSL rules files, so for now I cannot use my favorite editor, cannot modularize sources well, cannot use git to do versioning etc, at least not without a need to copy’n’paste every time which is annoying.

There is an OH2 method shown here, should be something like?

It’s really a multipart question with a multipart answer.

I assume you are referring strictly to UI created rules. If writing your rules in .js files then there’s no. upload required. You just put your .js files in $OH_CONF/automation/jsr223/javascript/personal and they get picked up just like .rules files.

I also assume you are referring to Nashorn JavaScript and not the JavaScript plugin.

OK, with the assumptions out of the way, most of what I’m about to write is language independent. Unless otherwise noted this is how it works for all languages.

When working with UI rules, only part of the rules are actually written in a scripting language: the Script Actions and the Script Conditions. The Rule proforma (tags, name, UID, etc.), and triggers and the overall structure of the rule is pure JSON and not written in any of the rules languages.

Any give UI rule can have 0 or more Actions and 0 or more Conditions. Each Script Action and Each Script Condition can be written in any supported language. So it’s possible (maybe not recommended) to have a single rule that uses all the scripting languages at the same time. For example, I could use Blockly for a Script Condition and three Script Actions, one in Rules DSL, one in Jython and another in ECMAScript 5.1. That would be a perfectly valid and working rule.

Given the above, a complete rule has two representations. In the UI it’s shown in a YAML format. Here is a relatively simple rule that I have that includes one JS Script Action and one JS Script Condition as it appears in the MainUI Code tab as YAML.

triggers:
  - id: "1"
    configuration:
      itemName: Large_Garagedoor_Opener
    type: core.ItemCommandTrigger
  - id: "2"
    configuration:
      itemName: Small_Garagedoor_Opener
    type: core.ItemCommandTrigger
conditions:
  - inputs: {}
    id: "3"
    configuration:
      type: application/javascript
      script: items["vCerberos_Status"] ==  OFF ||
        items["Cerberossensorreporter_Onlinestatus"] == OFF
    type: script.ScriptCondition
actions:
  - inputs: {}
    id: "4"
    configuration:
      type: application/javascript
      script: >-
        var OPENHAB_CONF = java.lang.System.getenv("OPENHAB_CONF");

        load(OPENHAB_CONF+'/automation/lib/javascript/personal/alerting.js');


        sendAlert("Attempting to trigger a garage door but cerberos is not online!");
    type: script.ScriptAction

Here is that same rule as it’s saved in $OH_USERDATA/jsondb/automation.json or as the rule is returned by the REST API.

{
  "status": {
    "status": "IDLE",
    "statusDetail": "NONE"
  },
  "editable": true,
  "triggers": [
    {
      "id": "1",
      "configuration": {
        "itemName": "Large_Garagedoor_Opener"
      },
      "type": "core.ItemCommandTrigger"
    },
    {
      "id": "2",
      "configuration": {
        "itemName": "Small_Garagedoor_Opener"
      },
      "type": "core.ItemCommandTrigger"
    }
  ],
  "conditions": [
    {
      "inputs": {},
      "id": "3",
      "configuration": {
        "type": "application/javascript",
        "script": "items[\"vCerberos_Status\"] ==  OFF || items[\"Cerberossensorreporter_Onlinestatus\"] == OFF"
      },
      "type": "script.ScriptCondition"
    }
  ],
  "actions": [
    {
      "inputs": {},
      "id": "4",
      "configuration": {
        "type": "application/javascript",
        "script": "var OPENHAB_CONF = java.lang.System.getenv(\"OPENHAB_CONF\");\nload(OPENHAB_CONF+'/automation/lib/javascript/personal/alerting.js');\n\nsendAlert(\"Attempting to trigger a garage door but cerberos is not online!\");"
      },
      "type": "script.ScriptAction"
    }
  ],
  "configuration": {},
  "configDescriptions": [],
  "uid": "garage_alert",
  "name": "Garage Alert",
  "tags": [
    "doors",
    "Alarm"
  ],
  "visibility": "VISIBLE",
  "description": "Send an alert when the garage door is triggered by cerberos is offline"
}

Hopefully this helps to illustrate what we are working with. The big thing to notice is that the JS is embedded into a larger structure. Consequently, you will be hard pressed to find any editor (unless someone writes a new and improved VSCode extension) that will let you write a full rule that does anything useful with the code.

So I think we are placed in a situation where what we are really talking about is importing Script Actions and Script Conditions. That’s really the only way you’ll be able to write in the pure scripting language and take advantage of IDEs and linting and stuff like that. I’ve had that idea before but don’t think I ever opened an issue to request it. I don’t think it would be too hard to add but it’s not going to be quite what you are after because it would only support importing individual Script Actions and Script Conditions. It’s not going to let you import one whole rule in one go. For that you’ll need the JSON with JS embedded in it or the YAML, again with the JS embedded in it.

So unfortunately, I don’t think we can get there from here. At best you could code you rules as JSON and use the REST API to submit it to OH. But you won’t get much IDE support that way. So if managing your rules in this way is important to you, you should probably stick to using text files for your rules and not use the UI rules. But that doesn’t mean giving up on the use of JavaScript for your rules. You can write whole rules complete in text files if that’s your desire.

This part deserves to be addressed separately. If you truly have code that you want to extract out of the rules to generate modularization then you can use libraries. You can place .js files into $OH_CONF/automation/lib/javascript/personal and import that code into your Script Actions and Script Conditions.

For example, I have the following alerting.js library module:

(function(context) {
  'use strict';

  var NotificationAction = Java.type("org.openhab.io.openhabcloud.NotificationAction");

  /**
   * Sends the message to email and notifications based on time of day.
   * If logger is already defined, the message will be logged as a warn level to that logger.
   * Otherwise a sendAlert logger is created and logged to.
   * @param {string} message string to send out as an alert message
   */
  context.sendAlert = function(message) {
    var log = (context.logger === undefined) ? Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.model.script.Rules.sendAlert") : context.logger;
    log.warn("ALERT: " + message);

//    var night = context.items["TimeOfDay"] == "NIGHT" || context.items["TimeOfDay"] == "BED";
//    if(!night){
//      NotificationAction.sendNotification("rlkoshak@gmail.com", message, "alarm", "alert");
//      context.actions.get("mail", "mail:smtp:gmail").sendMail("rlkoshak@gmail.com", "openHAB Alert", message);
//    }
//    else {
//      // Only send emails at night
//      context.actions.get("mail", "mail:smtp:gmail").sendMail("rlkoshak@gmail.com", "openHAB 3 Night Alert", message);
//    }
//    NotificationAction.sendNotification("rlkoshak@gmail.com", message, "alarm", "alert");
    NotificationAction.sendBroadcastNotification(message, "alarm", "alert");
    context.actions.get("mail", "mail:smtp:gmail").sendMail("rlkoshak@gmail.com", "openHAB Alert", message);
  };

  /**
   * Sends the message to email.
   * If logger is already defined, the message will be logged as a warn level to that logger.
   * Otherwise a sendAlert logger is created and logged to.
   * @param {string} message string to send out as an info message
   */
  context.sendInfo = function(message) {
    var log = (context.logger === undefined) ? Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.model.script.Rules.sendInfo") : context.logger;
    log.warn("INFO: " + message);
    context.actions.get("mail", "mail:smtp:gmail").sendMail("rlkoshak@gmail.com", "openHAB Info", message);
  };

})(this)

It’s a simple little library to centralizing my alerting logic.

To use this library in a Script Action I’d use:

var OPENHAB_CONF = java.lang.System.getenv("OPENHAB_CONF");
load(OPENHAB_CONF+'/automation/lib/javascript/personal/alerting.js');

this.sendAlert("This is an alert!);
this.sendInfo("This is an info!);

Most of my library stuff like this I’ve published to GitHub - rkoshak/openhab-rules-tools: Library functions, classes, and examples to reuse in the development of new Rules. which has implementations of most of the Design Patterns (time of day, debounce, gatekeeper, etc) that you can just clone and use. The one I get the most use out of personally is the Timer Manager which does all the management and book keeping when you have one rule that has to work with lots of timers (e.g. one per Item).

If you are really needing modularization this is probably the most proper way to implement it.

This could also be a work around for the lack of the an import feature in MainUI. You could write all your code as library modules. Then your Script Actions and Script Conditions in your UI rules would just be an import and function call. You’d have to manage stuff like the triggers for the rule separately from the scripts but it’d let you write in JavaScript directly and get all that IDE goodness.

Another way to modularize rules is to take advantage of the fact that rules can call other rules. But that’s not going to get you to using an IDE so I’m not going to go into that here. For the curious, the raw code to call a rule from another rule is:

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

If using the Helper Library (or you can put that into your own) it’s just one function call. Notice how you can pass Objects to the rule you are calling.

You can also enable/disable rules from other rules which raises interesting possibilities.

You can. JSONDB is a text file and like any text file it works quite well with git. And as of the most recent milestone, the order of the entries in the .json file is guaranteed so you won’t run into problems where it looks like the whole file changed when you’ve just added one new entry. I have the git history of my system going back to when I first started using OH 1.6 and in OH 3 I’ve used MainUI for everything that MainUI can be used for.

Finally, it looks very promising that we will have a marketplace again when OH 3.2 is released. Based on the discussion on GitHub that marketplace will support UI Widgets, Add-ons, and at least Rule Templates, hopefully Rule Libraries as well (perhaps whole combinations of all three). There may be some alternative modularization opportunities that arise with that. At the very least it should get us to where each OH user is not wholly required to write their own rules.

Sorry for the long post. It’s a big topic.

2 Likes

Is that also possible for complete rules (with triggers and conditions, eventually) ? If so what’s the syntax for that ? In Jython there’s decorators but in JS ?
Or did you mean to say that is just for the actions script part like you go on to explain in your code example ?

Yes but that’s not what I’m looking for (I guess you know).
Just like probably every programmer, I would like to have git apply to all the source (input) file(s).
Including the triggers/conditions part if possible.

Yes and it’s not well documented. You’ll have to dig through the Helper Library source code and at times even the openHAB JavaDocs. There are a few examples shown at Examples — openHAB Helper Libraries documentation. A bunch of those have the JavaScript tabs populated. Though I don’t know if those docs are up to date. Michael is attempting to rewrite the JS Helper Libraries so they have parity with the Jython but I doubt the docs have been kept up to date.

Most people use the Helper Libraries when writing text based rules and the JavaScript libraries are not as complete and nice to use as the Jython Helper Libraries yet. There have been some recent threads started by @LordLiverpool IIRC that explored the syntax for that. I’ve done it in Jython but never done it in JS. I’ve only done UI rules with JS.

If I were to go back to text based rules I’d use Jython as it’s a better experience over all compared to JS for now. A Jython rule using the Helper Library looks a whole lot more like a Rules DSL Rule over all.

I posted examples of triggers and other rules with the new Helper Libraries syntax that I got working, starting here:

1 Like

I’m not getting there yet.
I have installed JS Scripting in UI and copied Core/ to /etc/openhab as Ivan’s install docs say.
Any .js in /etc/openhab/automation/jsr223/javascript/personal does not get picked up though.
Only in /etc/openhab/automation/jsr223/ it does (as the OH docs say on JS Scripting so that part is working).
Whenever I insert any when(...) like in your example, OH complains in logs so I guess the rule template or whatever when() is defined as does not take effect. Can you suggest how to get that going?

Oh, wait. You are not using the default Nashorn ECMAScript 5.1 that comes with OH by default? Yea, the JS add-on totally breaks Nashorn, does not work much like Nashorn rules do and is incompatible with the Helper Libraries.

There is very little documentation yet on it and most of the documentation you will find in the OH docs do not apply.

If you installed the JS addon you are largely on your own for now. There are a couple of threads (All my ECMAScript scripts stopped working throwing exceptions and Usage of new ECMA2021 Automation Scripting (GraalVM)) and @jpg0 has a helper library referenced in that second thread.

But what I’ve been talking about is the default built in Nashorn JavaScript. The two work differently from each other and rules written for one are not compatible with the other. Essentially, the add-on doesn’t import anything by default whereas the default inserts a whole bunch of stuff (e.g. a dict of the Items and their states, utilities like events that let you sendCommand and postUpdate, access to Actions, etc.).

Having said all that, just about everything I talked about above concerning what is possible should apply. But the specific syntax you’ll use will be different. And the syntax David linked to won’t apply either nor will the Helper Libraries. So you’ll have to choose which one to pursue.

1 Like

Uh, then that was by mistake. I installed it after there was no rule to trigger at all.
Yep I was wondering about some GraalVM error already.

Ok so uninstalled it again. Placing something.js in personal/ is detected now, however as described the when() decorator (or whatever it is) is still unknown.

2021-09-08 21:00:59.538 [INFO ] [rt.internal.loader.ScriptFileWatcher] - Loading script '/etc/openhab/automation/jsr223/javascript/personal/test.js'
2021-09-08 21:00:59.539 [DEBUG] [ipt.internal.ScriptEngineManagerImpl] - Added ScriptEngine for language 'js' with identifier: file:/etc/openhab/automation/jsr223/javascript/personal/test.js
2021-09-08 21:00:59.546 [ERROR] [ipt.internal.ScriptEngineManagerImpl] - Error during evaluation of script 'file:/etc/openhab/automation/jsr223/javascript/personal/test.js': ReferenceError: "when" is not defined in /etc/openhab/automation/jsr223/javascript/personal/test.js at line number 7
markus@devpc:~ $ cat /etc/openhab/automation/jsr223/javascript/personal/test.js
function Rule_testLog(event)
{
  var logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + ctx.ruleUID);

  logger.info([itemRegistry.getItem(event.itemName).getLabel(), 'geändert von:', event.oldItemState ? event.oldItemState : 'Null'].join(' '));
}
when("Item Testschalter changed")(Rule_testLog);
//when("Item SecondBedroomAeotecZW100Multisensor6_SensorRelativeHumidity changed")(Rule_LastUpdate);
rule
(
    "Rule_testLog",
    "Does stuff"
)(Rule_testLog);


function OnTimer_EveryTenSeconds(event)
{
        // Do stuff
  var logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + ctx.ruleUID);

  logger.debug('debug-Output EverytenSeconds:');
}
// @ts-ignore
when("Time cron 0/10 * * * * ?")(OnTimer_EveryTenSeconds);
// @ts-ignore
rule
(
    "On timer:  (every ten seconds)",
    "Does stuff"
)(OnTimer_EveryTenSeconds);

Just to cover the basics, you’ve cloned and installed the Helper Library? Pay special care that the main branch isn’t the one you need. You need js-rewrite branch. I think the default branch (ivans-rewrites) doesn’t support OH 3 for JS.

As I said, I’ve never written JS rules in text files, but I know in Jython one has to import the rule decorators. I suspect the same is the case here. Maybe the examples at David’s link are skipping the imports. In Jyhton you’d import rule from core.rules and import when from core.triggers.

I don’t think JS has the ability to import just what you need like that so I’d try adding the following to the top of your file.

var OPENHAB_CONF = java.lang.System.getenv("OPENHAB_CONF");
load(OPENHAB_CONF+'/automation/lib/javascript/core/rules.js');
load(OPENHAB_CONF+'/automation/lib/javascript/core/triggers.js');

That should import what you need. However, the when decorator only exists in the js-rewrite branch so make double sure you’ve cloned the right branch.

1 Like

getting closer. Yes the imports were missing.
Replaced with the js-rewrite branch but now I’m getting

2021-09-08 22:51:50.915 [INFO ] [rt.internal.loader.ScriptFileWatcher] - Loading script '/etc/openhab/automation/jsr223/javascript/personal/test.js'
2021-09-08 22:51:50.917 [DEBUG] [ipt.internal.ScriptEngineManagerImpl] - Added ScriptEngine for language 'js' with identifier: file:/etc/openhab/automation/jsr223/javascript/personal/test.js
2021-09-08 22:51:51.488 [ERROR] [ipt.internal.ScriptEngineManagerImpl] - Error during evaluation of script 'file:/etc/openhab/automation/jsr223/javascript/personal/test.js': TypeError: TriggerBuilder.create is not a function in /etc/openhab/automation/lib/javascript/core/triggers.js at line number 131
2021-09-08 22:51:51.489 [DEBUG] [rt.internal.loader.ScriptFileWatcher] - Script loaded: /etc/openhab/automation/jsr223/javascript/personal/test.js

At this point we have reached the end of my knowledge. TriggerBuilder is used all over the place in triggers.js. But it’s not defined there. I think it’s actually referring to org.openhab.core.automation.util.TriggerBuilder which is an OH Java class.

triggers.js imports Triggerbuilder as part of the RuleSupport ScriptExtension. See JSR223 Scripting | openHAB for what is supposed to be imported and you can see on line 35 of https://github.com/CrazyIvan359/openhab-helper-libraries/edit/js-rewrite/Core/automation/lib/javascript/core/triggers.js that it’s imported.

Out of curiosity, did you restart OH after removing the GraalVM addon? There have been reports that the uninstall doesn’t always clean everything up properly and a restart may be required.

If you have restarted, or a restart doesn’t fix this I’m at a loss for what could be wrong. It doesn’t make sense and should work.

Somethings you can try:

  • add that line 35 to your file which should cause it to be loaded twice, but maybe there is something weird going on with scope

  • import TriggerBuilder into your script using

    var TriggerBuilder = Java.type(org.openhab.core.automation.util.TriggerBuilder);

NOTE: That’s how you’ll “import” any Java Class you may need or want to use.

  • modify triggers.js and import TriggerBuilder there.

Seems I have not, and when I did, voilà. Nothing else, just the restart was needed.

Now while I’m unsure about the state of the lib (Michael himself says in some file it’s very much unfinished), it’s working for now, at least for the ‘decorators’ so I finally have my JS-rule-in-one-file working.

Thanks for your help.

1 Like

Glad you got it working!

Even though I’m mostly working with UI rules these days in the hopes that I’ll be able to distribute rule templates on the up and coming marketplace, I can probably help if you get stuck again.

I also encourage you to look at my openhab-rules-tools. They might save you a good deal of effort as you convert your rules doing some of the common stuff. It’s pretty awesome to test to see if a timer for an Item exists, reschedule it if it does or create a new one if it doesn’t in just one line of code (minus the import and instantiation of the class of course, see the TimerMgr library). :slight_smile: The other one I’ve found most useful is the rate_limit library where you can ignore a command if it takes place too soon after the last one, again with one line of code. I use that one to control how often I receive alerts for certain things.

Both support Expire type time strings, DateTimeTypes, or number of milliseconds so you never have to deal with ZonedDateTimes either (though those are supported too).

rl.run(function() { sendAlert("My alert message"); }, "8h15m");

Gatekeeper also makes a great replacement for cascading timers if you used those. You can just call addCommand on the Gatekeeper with how much time to wait before running the next one.

I was able to cut my rules down by about 50% from using these libraries, which isn’t really fair because I had to write most of them. But you don’t. :wink:

1 Like

Sorry, I arrived late to the discussion.

The OH Javascript zoo has even more species in it than I thought… it’s no wonder people get confused.