OH 3 Examples: Writing and using JavaScript Libraries in MainUI created Rules

I’ve managed to port a few of my rules over to OH 3 using MainUI and JavaScript and this article will have a couple of examples and some lessons learned. I’ve only experimented thus far using JavaScript. Some of this may apply to Rules DSL, Jython, and Groovy too when rules are created through MainUI of the REST API. Some of this may also apply to rules created through text files. But all of those are out of scope for this article, which only focuses on using ECMAScript in rules created through MainUI.

Lifecycle of a rule

The first time a MainUI created rule that rule is given a context. Each subsequent time the rule is executed, it’s given the same context.

In a script action or a script condition the context can be obtained using the keyword this. So if you want to add a variable to the context it’s as simple as assigning a value to this.variablename.

    this.SOME_CONSTANT = 12345;

If you want to add a function you do it the same way.

    this.myFunc = function() { 
        // do something
    }

If you change the rule the context will become reset back to the default (e.g. all your timers will go away).

But, those lines of code run every time the rule triggers, so how do I save a value across multiple runs of a rule?

Saving a variable

In JavaScript, if you attempt to reference a variable that has not been defined the value will be undefined. Therefore, before assigning the variable, check to see if it’s already been defined and only if it is undefined should it be assigned.

this.myTimer = (this.myTimer === undefined) ? null : this.myTimer;

Given the above code, myTimer will be initialized to null if it’s not already defined. If it is defined, it retains what ever value it was set to prior to the rule running. So you can set a timer in one run of the rule and check to see if it’s still active in the next run.

Important note: the context is not shared across rules nor is it shared across script actions and script conditions for the same rule. Each gets their own so you can’t share variables between them in this way.

Loading a library

To load a library into a rule’s context one uses the load function. You need to pass this function the full path to the .js file you want to load. When it loads the library file it’s just like that code was written as part of your rule in the first place.

So how do we get the full path to the library? There is a property openhab.conf that gets set with the location of your openHAB’s conf folder (e.g. /etc/openhab). By convention, your personally written JavaScript libraries go in openhab.conf + "/automation/lib/javascript/personal". So if you have a library file named utils.js, you would load that into your rule using:

this.OPENHAB_CONF = (this.OPENHAB_CONF === undefined) ? java.lang.System.getProperty("openhab.conf") : this.OPENHAB_CONF;
load(OPENHAB_CONF + "/automation/lib/javascript/personal/utils.js")

Again, by testing to see if it’s already defined we can avoid reloading it every time the rule runs, saving a little bit of extra work. However, that means that if it changes it won’t be reloaded.

Libraries are different

Everything that gets defined in the script action or script condition that loads the library gets saved to the context whether or not you define it using this.. When a library is loaded, its as if all that code were just typed into the script action or script condition so everything it defines also gets added to the context. This can lead to problems when the library defines variables with the same name as a variable defined in the script action or script condition, the library will overwrite that variable. The library might also define a bunch of stuff that only it cares about. How can we avoid this?

When defining a library, put it in a function and pass that function the context.

(function(context) {
    // your library stuff goes here
})(this)

When wrapped in this way, only stuff that you explicitly add to context will exist for the script action or script condition that loaded it. This lets you limit how much pollution and potential conflicts that will occur between multiple libraries, particularly libraries written by more than one author.

When openHAB executes a rule it injects a whole bunch of stuff into the context. Important stuff like events (used to postUpdate and sendCommand), ir (the image registry to get access to an Item by name), actions (used to access binding actions), etc. But this stuff doesn’t automatically exist in the loaded library now because we’ve isolated it from the context. Therefore, in your library functions, you need to access that stuff using context., for example, context.ir.getItem("MyGroup").

A library few examples

NOTE: there is some overlap with the Helper Libraries in the below code. This is because:

  • as of this writing the helper libraries for JavaScript are broken on OH 3
  • I want all the libraries I publish to openhab-rules-tools to be stand alone with no external dependencies (dependencies among themselves is OK for now)

timeUtils.js

I will be maintaining this library at https://github.com/rkoshak/openhab-rules-tools/tree/main/time_utils where you can download and copy this to $OPENHAB_CONF/automation/lib/javascript/community and load and use in your scripts. As of this writing the library looks like:

(function(context) {

  'use strict';
  var log = Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.model.script.Rules.time_utils");

  // Import the Java Classes
  var ZonedDateTime = (context.ZonedDateTime === undefined) ? Java.type("java.time.ZonedDateTime") : context.ZonedDateTime;
  var LocalDateTime = (context.LocalDateTime === undefined) ? Java.type("java.time.LocalDateTime") : context.LocalDateTime;
  var ZoneId = (context.ZoneId === undefined) ? Java.type("java.time.ZoneId") : context.ZoneId;
  var ChronoUnit = (context.ChronoUnit === undefined) ? Java.type("java.time.temporal.ChronoUnit") : context.ChronoUnit;
  var Duration = (context.Duration === undefined) ? Java.type("java.time.Duration") : context.Duration;
  var DateTimeType = (context.DateTimeType === undefined) ? Java.type("org.openhab.core.types.DateTimeType") : context.DateTimeType;
  var DecimalType = (context.DecimalType === undefined) ? Java.type("org.openhab.core.types.DecimalType") : context.DecimalType;
  var PercentType = (context.PercentType === undefined) ? Java.type("org.openhab.core.types.PercentType") : context.PercentType;
  var QuantityType = (context.QuantityType === undefined) ? Java.type("org.openhab.core.types.QuantityType") : context.QuantityType;


  /** 
   * Parses a duration string returning a Duration object. Supports:
   *  - d days
   *  - h hours
   *  - m minutes
   *  - s seconds
   *  - z milliseconds
   * The unit is followed by an integer (decimals are not supported).
   * Examples:
   *  - 5d 2h 7s
   *  - 5m
   *  - 1h23m
   * 
   * @param {string} timeStr 
   * @return {java.time.Duration} the string parsed to a Duration
   */
  context.parseDuration = function(timeStr) {
    var regex = new RegExp(/[\d]+[d|h|m|s|z]/gi);
    var numMatches = 0;
    var part = null;

    var params = { "d": 0, "h": 0, "m":0, "s":0, "z":0 };
    while(null != (part=regex.exec(timeStr))) {
      log.debug("Match = " + part[0]);
      numMatches++;

      var scale = part[0].slice(-1).toLowerCase();
      var value = Number(part[0].slice(0, part[0].length-1));
      params[scale] = value;
    }

    if(numMatches === 0){
      log.warn("Could not parse any time information from '" + timeStr +"'. Examples of valid string: '8h', '2d8h5s200z', '3d 7m'.");
      return null;
    }
    else {
      log.debug("Days = " + params["d"] + " hours = " + params["h"] + " minutes = " + params["m"] + " seconds = " + params["s"] + " msec = " + params["z"]);
      return Duration.ofDays(params["d"]).plusHours(params["h"]).plusMinutes(params["m"]).plusSeconds(params["s"]).plusMillis(params["z"]);
    }
  }

  /** 
   * Adds the passed in Duration to now and returns the resultant ZonedDatetime. 
   * @param {string | java.time.Duration} dur the duration to add to now, if a string see parseDuration above
   * @return {java.time.ZonedDateTime} instant that is dur away from now
   */
  context.durationToDateTime = function(dur) {
    if(dur instanceof  Duration) {
      return ZonedDateTime.now().plus(dur);
    }
    else if(typeof dur === 'string' || dur instanceof String){
      return durationToDateTime(parseDuration(dur));
    }
  }

  /** 
   * @return {Boolean} Returns true if the passed in string conforms to ISO 8601. 
   */
  context.isISO8601 = function(dtStr) {
    var regex = new RegExp(/^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?(Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$/);
    return regex.test(dtStr);
  }

  /** 
   * Converts a number of supported types to a ZonedDateTime including:
   *  - ISO80601 formatted String
   *  - Duration String (see parseDuration above) which is added to now
   *  - Number types which are treated as milliseconds to add to now
   *  - openHAB Number Types which are treated as milliseconds to add to now
   *  - openHAB DateTimeType
   * @param {string|int|long|java.time.Duration|java.lang.Number|org.openhab.core.types.DateTimeType|org.openhab.core.types.DecimalType|org.openhab.core.types.QuantityType|java.time.Duration|java.time.ZonedDateTime} when the representation of time converted to ZonedDateTime
   * @return {java.time.ZonedDateTime} when converted to a ZonedDateTime
   */
  context.toDateTime = function(when) {
    var dt = null;

    if(when instanceof ZonedDateTime) {
      log.debug("Already ZonedDateTime " + when.toString());
      dt = when;
    }
    else if(typeof when === 'string' || when instanceof String){
      if(isISO8601(when)){
        log.debug("Converting ISO80601 local date time " + when);
        dt = ZonedDateTime.of(LocalDateTime.parse(when), ZoneId.systemDefault());
      }
      else {
        log.debug("Converting duration " + when);
        dt = durationToDateTime(when);
      }
    }
    else if(typeof when === 'number' || typeof when === "bigint") {
      log.debug("Converting number " + when);
      dt = ZonedDateTime.now().plus(when, ChronoUnit.MILLIS);
    }
    else if(when instanceof DateTimeType){
      log.debug("Converting DateTimeType " + when.toString());
      dt = when.getZonedDateTime();
    }
    else if(when instanceof DecimalType || when instanceof PercentType || when instanceof QuantityType || when instanceof Number){
      log.debug("Converting openHAB number type " + when.toString());
      dt = ZonedDateTime.now().plus(when.longValue(), ChronoUnit.MILLIS);
    }
    else {
      log.warn("In toDateTime, cannot convert when, unknown or unsupported type: " + when);
    }

    return dt;
  }

  /** 
   * Moves the passed in ZonedDateTime to today. 
   * @return {java.time.ZonedDateTime} when converted to a ZonedDateTime and moved to today's date
   */
  context.toToday = function(when) {
    var now = ZonedDateTime.now();
    var dt = toDateTime(when);
    return dt.withYear(now.getYear()).withMonth(now.getMonthValue()).withDayOfMonth(now.getDayOfMonth());
  }
  
  })(this);

Things to notice.

  • Only those methods added to context will be available to the script that loads this library.
  • Everything defined inside the wrapping function is available. This is why the functions can call each other without using context.
  • The openHAB types should be in the context already but they are not. I need to experiement some more to see why the default profile is not being loaded.

timerMgr.js

This is a class I wrote to handle the use case that appears over and over again where one needs to create and manage one timer per Item across a large set of Items. The whole purpose of libraries is to avoid needing to rewrite code like that over and over again. As with timeUtils, this is made available and will be maintained at https://github.com/rkoshak/openhab-rules-tools/tree/main/timer_mgr.

/**
 * Constructor, initializes the logger, imports the needed stuff and creates an empty timers dict.
 */
var TimerMgr = function() {
  'use strict';
  var OPENHAB_CONF = java.lang.System.getProperty("openhab.conf");
  this.log = Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.model.script.Rules.TimerMgr");
  this.log.debug("Building timerMgr instance.");
  this.timers = {};
  this.log.debug("Loading timeUtils");
  load(OPENHAB_CONF+'/automation/lib/javascript/community/timeUtils.js');
  this.ScriptExecution = Java.type("org.openhab.core.model.script.actions.ScriptExecution");
  this.log.debug("Timer Mgr is ready to operate");
}

/**
 * Private function that gets called when the timer expires. It does some cleanup and executes
 * the passed in timer lambda, if there was one.
 * @param {*} the unique name for the timer, can be anything supported by a dict but will usually be a string
 */
TimerMgr.prototype._notFlapping = function(key) {
  this.log.debug("Timer expired for " + key);
  if (key in this.timers && "notFlapping" in this.timers[key]) {
    this.log.debug("Calling expired function " + this.timers[key]["notFlapping"]);
    this.timers[key]["notFlapping"]();
  }
  if (key in this.timers){
    this.log.debug("Deleting the expired timer");
    delete this.timers[key];
  }
},

/**
 * Private function that does nothing. Used when the user didn't pass in a function to call
 * when the timer expires.
 */
TimerMgr.prototype._noop = function() { },

/**
 * Call when one wants to create a timer or check to see if a timer is already created. 
 * Depending on the arguments, a new timer may be created, an existing timer rescheduled,
 * and if the timer already exists, a flapping function called. This lets one do something
 * when the timer already exists or when the timer expires.
 * 
 * @param {*} key the unique ID for the timer, usually an Item name
 * @param {*} when any representation of time supported by timeUtils.toDateTime
 * @param {*} func function called when the timer expires
 * @param {*} reschedule defaults to false, when true if the timer already exists it wilol be rescheduled
 * @param {*} flappingFunc optional function to call when the timer already exists.
 */
TimerMgr.prototype.check = function(key, when, func, reschedule, flappingFunc) {
  this.log.debug("Timer manager check called");
  if (reschedule === undefined) reschedule = false;

  var timeout = toDateTime(when);
  this.log.debug("Timer to be set for " + timeout.toString());

  // Timer exists
  if (key in this.timers){
    if (reschedule){
      this.log.debug("Rescheduling timer " + key + " for  " + timeout.toString());
      this.timers[key]["timer"].reschedule(timeout);
    }
    else {
      this.log.debug("Cancelling timer " + key);
      this.cancel(key);
    }
    if (flappingFunc !== undefined){
      this.log.debug("Running flapping function for " + key);
      flappingFunc();
    }
  }
  
  // Timer doesn't already exist, create one
  else {
    this.log.debug("Creating timer for " + key);
    var timer = this.ScriptExecution.createTimerWithArgument(timeout, this, function(context) { context._notFlapping(key); });
    this.timers[key] = { "timer": timer,
                         "flapping": flappingFunc,
                         "notFlapping": (func !== undefined) ? func : this._noop }
    this.log.debug("Timer created for " + key);
  }
},

/**
 * @param {*} key unique name for the timer
 * @return true if the timer exitst, false otherwise
 */
TimerMgr.prototype.hasTimer = function(key) {
  return key in this.timers;
},

/**
 * Cancels the timer by the passed in name if it exists
 * @param {*} key  unique name for the timer
 */
TimerMgr.prototype.cancel = function(key) {
  if (key in this.timers) {
    this.timers[key]["timer"].cancel();
    delete this.timers[key];
  }
},

/**
 * Cancels all the timers.
 */
TimerMgr.prototype.cancelAll = function() {
  for (key in this.timers) {
    if (!this.timers[key]["timer"].hasTerminated()) {
      this.log.debug("Timer has not terminated, cancelling timer " + key);
      this.cancel(key);
    }
    delete this.timers[key];
    this.log.debug("Timer entry has been deleted for " + key);
  }
}

In this case TimerMgr is a class. As such all the definitions are kept inside the class definition with the only symbol exposed to the context being TimerMgr itself. Therefore wrapping the above in the function would be redundant.

Things to notice:

  • See how timerUtils.js is loaded here showing how libraries can import other libraries.
  • Because you are inside a class, this now refers to the context inside the class, not the context from the script that loaded it.
  • However, if you have statically defined methods, the this is the calling rule, not the class. You can pass it a different context by using apply, e.g. callback.apply(this, [list, of, arguments]);.

utils.js

This is a personal library I’m writing with some random functions I reuse across various rules.

(function(context) {
  'use strict';

  /**
   * Sends the message to email and notifications based on time of day.
   * @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){
      // TODO add push notification
      context.actions.get("mail", "mail:smtp:gmail").sendMail("rlkoshak@gmail.com", "openHAB 3 Alert", message);
    }
    else {
      // TODO Only send emails at night
      context.actions.get("mail", "mail:smtp:gmail").sendMail("rlkoshak@gmail.com", "openHAB 3 Night Alert", message);
    }
  }

  /**
   * Sends the message to email.
   * @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 3 Info", message);
  }

  /**
   * Returns the metadata on the passed in item name with the given namespace.
   * @param {string} itemName name of the item to search the metadata on
   * @param {string} namespace namespace of the metadata to return
   * @return {Metadata} the value and configuration or null if the metadata doesn't exist
   */
  context.getMetadata = function(itemName, namespace) {
    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");
    return MetadataRegistry.get(new MetadataKey(namespace, itemName));
  }

  /**
   * Returns the value of the indicated namespace
   * @param {string} itemName name of the item to search the etadata on
   * @param {string} namespace namespace to get the value from
   * @return {string} The value of the given namespace on the given Item, or null if it doesn't exist
   */
  context.getMetadataValue = function(itemName, namespace) {
    var md = getMetadata(itemName, namespace);
    return (md === null) ? null : md.value;
  }

  /**
   * Returns the configuration of the given key in the given namespace
   * @param {string} itemName name of the item to search for metadata on
   * @param {string} namespace namespace of the metadata
   * @param {string} key name of the value from config to return
   * @return {string} the value assocaited with the key, null otherwise
   */
  context.getMetadataKeyValue = function(itemName, namespace, key){
    var md = getMetadata(itemName, namespace);
    if(md === null){
      return null;
    }
    return (md.configuration[key] === undefined) ? null : md.configuration[key];
  }

  /**
   * Returns the value of the name metadata, or itemName if name metadata doesn't exist on the item
   * @param {string} itemName name of the Item to pull the human friendly name metdata from
   */
  context.getName = function(itemName) {
    var name = getMetadataValue(itemName, "name");
    return (name === null) ? itemName : name;
  }

  /**
   * Filters the members of the passed in group and generates a comma separated list of
   * the item names (based on metadata if available).
   * @param {string} groupName name of the group to generate the list of names from
   * @param {function} filterFunc filtering function that takes one Item as an argument
   */
  context.getNames = function(groupName, filterFunc) {
    var Collectors = Java.type("java.util.stream.Collectors");
    return context.ir.getItem(groupName)
                     .members
                     .stream()
                     .filter(filterFunc)
                     .map(function(i) {
                       return context.getName(i.name);
                     })
                     .collect(Collectors.joining(", "));
  }

})(this)

Things to notice:

  • this shows how to access binding actions from a library (see sendAlert and sendInfo)
  • I define a name metadata on many of my Items so I can send alerts and reports using a human friendly name instead of the Item name
  • getNames shows one way to filter and map/reduce a list of Items into a comma separated string listing the Item’s names. This is using Java’s Stream API. There might be a more JavaScript way to do this but that would require converting the Java List one gets when calling members on the Group to a JavaScript array (can be done with var jsarray = Java.from(context.ir.getItem(grouName).members);).

A few rule examples that load and use libraries

debounce

I’m running out of space in this post so see https://github.com/rkoshak/openhab-rules-tools/blob/main/debounce/javascript/debounce.yml which shows loading and using TimerMgr from above. This rule implements a debounce on Item states based on Item metadata.

ephem_tof

See https://github.com/rkoshak/openhab-rules-tools/blob/main/ephem_tod/javascript/ephemTimeOfDay.yml which shows loading and using TimerMgr and timeUtils. This rule implements the Time of Day design pattern using Ephemeris so one can define a different set of times of days based on the type of day it is (e.g. have a different set for weekends and for weekdays).

Offline Alert Report

triggers:
  - id: "1"
    configuration:
      time: 08:00
    type: timer.TimeOfDayTrigger
conditions:
  - inputs: {}
    id: "2"
    configuration:
      itemName: ServiceStatuses
      state: ON
      operator: "!="
    type: core.ItemStateCondition
actions:
  - inputs: {}
    id: "3"
    configuration:
      type: application/javascript
      script: >-
        var logger =
        Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.model.script.Rules.OfflineReport");

        scriptExtension.importPreset("default");

        this.OPENHAB_CONF = (this.OPENHAB_CONF === undefined) ? java.lang.System.getProperty("openhab.conf") : this.OPENHAB_CONF;

        load(OPENHAB_CONF + "/automation/lib/javascript/personal/utils.js")


        var nullItems = getNames("ServiceStatuses", function(i) { return i.state.class == UnDefType.class; });

        logger.info(nullItems);


        var offItems = getNames("ServiceStatuses", function(i) { return i.state == OFF; });

        logger.info(offItems);


        var msg = "";


        if(nullItems.length > 0) {
          msg = "The following sensors are in an unknown state: " + nullItems;
        }

        if(offItems.length > 0) {
          if(msg.length > 0) msg += "\n";
          msg += "The following sensors are known to be offline: " + offItems;
        }


        sendInfo(msg);
    type: script.ScriptAction

This rule sends me an infoAlert (using utils.js) every morning with a list of all my home automation relevant services that are in an unknown state or are offline. The rule only runs if there is one or more members of ServiceStatuses that are in this state.

27 Likes

Whilst this is a great explanation of how to use the current JS capability, I really hope to get GraalJS committed to OH3, as much of this would change.

The currently library mechanism is terrible: having to explicitly locate the filesystem directory with library files then ask the runtime to load (inline) them:

  • Awkward boilerplate to load a library
  • Awkward boilerplate to define a library
  • No namespacing of library exports (e.g. a library just dumps it’s exports into the global namespace, potentially overwriting other libraries, or your variables - used to happen regularly to me)
  • No framework-level tracking of library files (so no reloading of a script when it’s libraries change)
  • Incompatibility with every other 3rd party JS library (which use CommonJS or ES6 modules) and unfamiliarity for every existing JS dev

None of these issues exist in the GraalJS OH code. :crossed_fingers: it makes it in.

4 Likes

When? We need to generate docs now. OH 3 is still on track I think for a December release right? We need at least some docs in place before that.

I can’t write default docs based on Jython because it doesn’t come with OH out of the box (and currently doesn’t work anyway). I don’t want to do so using Rules DSL because it’s too limited. So I’m suck with Nashorn. Unless:

  • GraalJS is going to come with openHAB 3 out of the box (i.e. no addons or external dependencies required)
  • I can get access to it right now so docs can be generated right now
  • I have assurances that it will be part of OH 3.0 release

I have no choice but to use Nashorn as the basis of getting started tutorial and the docs.

When?

I guess that’s a question for the maintainers. I’ve not had much success in getting traction for PR discussions. I was not aware that the 3.0 release is Dec; if that is the case (and with the current speed at which I can actually make changes) it won’t make it.

I completely understand why you are going with Nashorn (I would too in the circumstances), it’s just a shame that OH is starting JS support in such a way that much of it will change as soon as we get onto a modern version of javascript, and create a second transition for JS authors.

3 Likes

@jpg0 I came across your library in my reading on JS and imports in particular. I think it would not actually have a huge impact on the user side since you would change from load to require and (most) of the remainder or the rule would stay the same. Libraries would have to be refactored, but the way we’re forced to write them now with the wrappers nearly feels like a hack so I am all for ditching that in favour of proper exports!

While I am not a maintainer so I cannot help speed up the process, let me know if I can help with your libraries or any testing.

1 Like

In addition to what Michael mentions (which is beyond my experience and knowledge), I’ll add that the main focus of this post is for those users who will be writing rules through MainUI only. I’m not referencing those users who will be writing .js file rules. As such I agree with the impact, it looks like it will be relatively minimal. These users will only be slightly more technical than a true beginner (who would probably be using Blockly anyway) but definitely not oriented towards a front end or Node.js experienced developer. Given that, I would expect the only impact would be as Michael points out, swapping the load for require.

For more advanced users who are building up their own frameworks for rules based on libraries pulled form all over the place I would expect the impact to be much greater.

Also, I want to make it clear, I’m not exactly happy to be “stuck” with Nashorn for this. I can’t tell you how many time’s I’ve looked something up that elegantly solves a problem only to find it’s only supported in ECMAScript 6 or later. And I worry about how much is going to have to change in the docs when that changes. I’ve watched what you are doing with GraalJS with interest. It’s just that we’ve run out of time. And I’ve some serious concerns about basing the docs and especially the getting started tutorial on anything with external dependencies. It looks bad to basically say “ignore the fact that OH comes with Nashorn, go install the X add-on to get started.”

I generally agree, although having ported almost all the JS helper libraries to ES6 it was a bit more work than switching the load for require. This was because it changes the scope of variables imported: with the load method all the symbols just appeared (and were not generally discoverable), whereas with ‘require’ they are namespaced and explicit. The porting was a pretty tedious and error-prone process to switch imports, try to figure out what actually came from that import and adjust, run to find the errors, repeat…

Saying this, there’s not really any other option at this point anyway! I agree that we cannot ask for installation of another plugin. At least this conversation has spurred me on to push more PRs to at least try to get things going faster!

5 Likes

@jpg0 can you give us a link to your PR? so the rest of us can follow along
and it is appreciated the work you are doing… big :+1:
I think we need to give a nudge to maintainers (who have been admittedly very busy)

1 Like

The parent PR is here https://github.com/openhab/openhab-addons/pull/8516

There are a few child PRs that I need to get through as prereqs before I can add the core GraalJS code to the parent one.

2 Likes

thanks Jonathan
:+1:

This might be the wrong place to ask, and if it is let me know. But I was wondering, is Jython planned to be working with OH3? Either when launched or in the future? I’ve spent an awful lot of time on Jython rules as I really like the power of it. I really look forward to OH3 and would love to migrate my rules. I don’t mind holding off as things settle, just want to see if there are any thoughts among the developers.

Thanks!

I think it’s planned for OH 3. I can’t say when (tomorrow, next week, sometime after OH 3 release). The current PRs have it being made available as an add-on similar to the Groovy add-on that is already available in OH 3 M2. There is some drama among the developers but I do not expect that to prevent it’s ultimate support.

1 Like

Found this great thread.
I am a noob in javascript. I am looking to use this library here. How do i go about doing it? I was looking for a .js file but obviously its not there. do i need to install the library using npm and then somehow transfer the .js file to openhab? pls advise. thanks

Those sorts of third party libraries I do not know how to install and use. You’ll need to get the .js files for it and all the libraries it depends upon and then load them.

As @jpg0 indicated, he’s working on GraalJS which works in a more standard way. But as far as I can tell, with my limited JS skills and knowledge, those ts files are JavaScript.

1 Like

You’d have a (really) hard time trying to get this to work on the standard Openhab distribution; as @rlkoshak says, you’d need GraalJS support which I’m working on getting into OH.

This should work fine with GraalJS once it’s available; I am already using a number of conversion libraries like this (such as cronstrue, chroma-js, humanize-duration, parse-duration) and they all work fine. With GraalJS it’s as simple as npm i chrono to get it installed, and you can use it just as it says in the docs.

ps. a .ts file is a typescript file, which is a typed dialect of JavaScript, needing conversion to standard JS. But you don’t need to know that; if you use npm to install it, you’ll get the .js version, which is created as part of the build.

3 Likes

In really hope there will be helper libraries that reduce the required code to a minimum. Comparing the current example codes in JS or Python with those in DSL i am surprise that one ist willing to write so much more code than in DSL for most rules.
Most rules will deal with timers, if statements, item filter and groups etc.
These things should be usable as easy as possible with minimum of code to reduce errors in writing rules. This is the case for beginners and pros.
A large and well documented library of helper functions is essential for quick and error free development. Using predefined and easy to use functions will lead to the required acceptance.
Are there plans to work in this direction? Or does every developer pick their stuff on their own? This will make ist hard for the community to help when everyone uses their own stuff when basics are missing.

Has anybody tried HABapp - a Python version of a rule engine? Writing rules in Python with minimum code is shown there. It would be great to see this done similar in openhab Python or JS rules.
I liked many things of DSL and the good support from the community. But more options like functions and reusable components of a language like JS or Python will be great!
I really like the stability and quality of openhab including all bindings, the great and helpful community and hope this will continue for openhab 3, too. Thanx a lot to all involved developers for their great work!

Thank you for the work on this. It is very informative and helpful! I am having trouble getting the log function to work when called from the .js files as written. I am interested in this functionality for the development of my own libraries.

I can see the log entries from the testing scripts on GitHub when run from the UI, but not the the loaded.js libraries. I am actually testing on your loopingTimer.js code https://github.com/rkoshak/openhab-rules-tools/tree/main/looping_timer. The logger is being loaded / called as prescribed:

var log = this.log = Java.type(“org.slf4j.LoggerFactory”).getLogger(“org.openhab.model.script.Rules.LoopingTimer”);

this.log.debug(“Looping timer - loop called”);

I am monitoring the log from the Karaf Console, but I have also checked the openhab.log / events.log files manually for entries. I have tried changing the logging level in the .js file log code to “log.info(…)”, this did not work. I have also tried the following and spent a while looking around for a solution.

log:set DEBUG
log:set DEBUG org.openhab.model.script.Rules.LoopingTimer
log:set DEBUG jsr223

Do you have any ideas? Thanks!

Actually that was submitted by someone else. I did a brief reverie review for any obvious problems but not a line by line review. In short, I don’t know exactly how it works.

By default debug level is suppressed. Does it work with info?

Guess not.

Setting the log to this might be a problem if that’s how loopingtimer does it. Libraries should not have stuff like their own internal logger to this as it risks overwriting stuff defined in the rule.

Hi! I just figured it out. The .getLogger value, when the logger was loaded, was the same in both the test code and the .js file. I changed the name of one of them and now it is working great:

Test code:

this.logger = (this.logger === undefined) ? Java.type(“org.slf4j.LoggerFactory”).getLogger(“org.openhab.model.script.Rules.LoopingTimerTest”) : this.logger;

.js file

this.log = Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.model.script.Rules.LoopingTimer");