Static constants of Java classes in JS Scripting

Maybe there is an obvious answer to that but I just stumbled upon two cases where I tried to use a static constant/enum of a Java class (in one case it was ChronoUnit.DAYS) in JS Scripting. However when I use

var { ChronoUnit } = require("@runtime")

ChronoUnit.DAYS is undefined/null. When I use

var ChronoUnit = Java.type("java.time.temporal.ChronoUnit")

I can use ChronoUnit.DAYS as expected.

Why is that so?

I can not really answer your question, but you can directly use time.ChronoUnit.DAYS without any Java mangling.

1 Like

That is so because I don’t think ChronoUnit is included in the simple rule context (i.e. all the stuff that gets put into @runtime).

But, as @laursen indicates, if you are using JS Scripting, you should be using time.<something> for all your Date Time needs. A lot of work has been done in openhab-js to make it so you almost never have to mess with Java Classes and the joda-js library that is used is very similar to the Java classes.

Also, since this smells like an XY Problem, why do you need to use ChronoUnit? What are you trying to accomplish?

2 Likes

If this is the case then this should be corrected in the js scripting documentation. The ChronoUnit appears in the table of „ useful utilities and types“ at the end of the page (JavaScript Scripting - Automation | openHAB). Also you get an object out of the context, no idea what it is though…

Actually with my question I was trying to understand why this difference exists in general. My use case for the ChronoUnit was nothing too complicated: I was using the truncatedTo method of ZDT which requires a temporal unit to get rid of hours, minutes, seconds, nanos in one go. Since there are other methods to achieve what I needed it was really not about the ChronoUnit in particular.

But I learned that I can simply use „time“ and will try to avoid the @runtime at least for all time related stuff.

1 Like

It is, I have double checked both the DefaultScriptScopeProvider in core and the class name of what you get from @runtime.
However it doesn’t work as well for me, which is interesting but IMO not relevant.

1 Like

I stand corrected. It appears to have been there at some point perhaps but it is no longer listed at JSR223 Scripting | openHAB which is the authoritative docs for what would be in @runtime.

@florian-h05, do you think it make sense to replace the table with a link to the jsr223 table? Then we won’t get stuck needing to maintain that list in two places.

You really should avoid @runtime for pretty much everything in JS Scripting if you are using the openhab-js library which is the case by default. If you do encounter something you cannot do through the openhab-js provided capabilities that requires @runtime please open an issue so openhab-js can be enhanced to support the use case.

One of the goals of openhab-js is to ensure that you are always working with JS stuff in rules and never Java stuff. As soon as you start messing with @runtime you need to know at all times whether you are dealing with a JS entity or a Java entity and know that and how the two behave differently.

All of js-joda is available under time as well as some additions like time.toZDT(), isBetweenTimes(), et. al. The OH provided additions are documented at JavaScript Scripting - Automation | openHAB.

Small off-topic question, can PercentType be avoided here?

var { PercentType } = require("@runtime");
actions.Audio.playSound("sonos:Move2:move2", "GoodMorning.mp3", new PercentType(5));

That’s a good question. I don’t know.

I don’t use audio in my setup.

Looking at the code a number of the built in actions have been wrapped so they work with JS arguments (e.g. ScriptExecution, ThingActions Sematics, etc.), or they just work with JS arguments naturally (e.g. Ephemeris, Exec, etc.). However, it does appear that Audio and Voice do still require a PercentType. I don’t think there is any auto-conversion magic in the add-on that will handle converting a JS number to a PercentType for you (e.g. the way js-joda ZonedDateTimes are auto converted to Java ZonedDateTimes behind the scenes) so this may be one of the few places left where the Java classes need to be imported and used.

You can get PercetType from @runtime or import it directly using Java.type.

Maybe this can be wrapped too? It’s a little more challenging with PercentType than most though given it’s constraints. There is no natural equivalent type in JS.

I also still use

var { ON, OFF } = require("@runtime");

I guess I should just use strings “ON”, “OFF” instead then?

I think that’s what most of us do. The only way to get an OnOffType in a JS rule are:

  • the event Object in managed rules as we haven’t found a way to wrap that Java Object the same way as it is wrapped for file based JSRule and rule builder rules (think there are a couple of ideas but nothing implemented yet)
  • the rawState of an Item
  • you’ve created one yourself

You’ll find almost universally that the String version is used for most of the enum types in OH in JS Scripting rules since the item.MySwith.state is a String itself.

Yeah I still use this a lot as well to be able to compare states properly. As someone with a Java development background it just does not feel right to do everything string based I guess :sweat_smile: But I will try to adapt :ok_hand::blush:

Just realised that numbers from custom metadata namespaces are actually BigDecimal Java types in JS Scripting… That’s another thing where I need to deal with Java types then :confounded_face:

Simply use:

actions.Audio.playSound("sonos:Move2:move2", "GoodMorning.mp3", (5/100));

works just fine.

3 Likes

That should be converted automatically.

Though I can’t remember the last time I put a number into metadata. Can you show your code and how you set the metadata (from a rule, the UI, or .items file)? It used to be the case that if you put something into metadata from a rule, it keeps whatever type it was in that rule regardless of the language.

Really? Cool! I don’t have a sync set up so I couldn’t try it. That’s good to know that numbers can be converted to PercentTypes automatically too.

In that case, I think that all of the Actions should work as is with JS types then. I only saw a few that took PercentType and everything else was String, ZonedDateTime, int and float or have been wrapped to work with the JS Thing and JS Item classes.

Sure! I set the metadata on the item via Main UI like this:


value: " "
config:
   devicewaittime: 10
   deviceoff: 0.1
   devicerunning: 3

In the code I used this line to get devicewaittime:

var waitTime = statusConfig.configuration["devicewaittime"];

When I later use this to setup a timer:

var timeoutId = setTimeout(deviceFinished, waitTime, outlet);

This fails miserably (you will find the * 1000 in the complete code below. It wasn’t there when this exception popped up. I added it later, I simply forgot it before):


2024-07-22 19:50:00.699 [ERROR] [omation.script.javascript.d1edc3008c] - Failed to execute script: TypeError: invokeMember (setTimeout) on org.openhab.automation.jsscripting.internal.threading.ThreadsafeTimers@3453db4d failed due to: Cannot convert '10.0'(language: Java, type: java.math.BigDecimal) to Java type 'java.lang.Long': Invalid or lossy primitive coercion.
        at <js>.globalThis.setTimeout(@jsscripting-globals.js:167)
        at <js>.main(<eval>:70)
        at <js>.:program(<eval>:74)
        at org.graalvm.polyglot.Context.eval(Context.java:399)
        at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:458)
        ... 23 more

So I changed it now to:

var waitTime = statusConfig.configuration["devicewaittime"].longValue()

This works but maybe I should just use strings for metadata. It’s more transparent than to deal with a BigDecimal.

Complete code (there still might be other bugs since I changed a few things while migrating from my old to my new OH instance):


function deviceFinished(outlet) {
  items[outlet.name + '_status'].sendCommandIfDifferent('FINISHED');
  actions.notificationBuilder("Gerät ist fertig").withTitle(outlet.label).withReferenceId(outlet.name).send();
}

function deviceSwitchedOff(outlet) {
  items[outlet.name + '_status'].sendCommandIfDifferent('OFF');
  actions.notificationBuilder("Gerät wurde ausgeschaltet").withReferenceId(outlet.name).hide().send();
}

function cancelTimer(outlet) {
  if (cache.private.exists(outlet.name)) {
      var timeoutId = cache.private.get(outlet.name);
      console.debug("Removing existing timer for outlet", outlet.name, "(", timeoutId, ")");
      // This will also clear a timer added by setInterval
      clearTimeout(timeoutId);
      cache.private.remove(outlet.name);
  }
}

function updateRuntime(outlet) {
  var runtimeItem = items[outlet.name + "_runtime"];
  var currentRuntime = runtimeItem.numericState;
  runtimeItem.sendCommand(currentRuntime + 1 + " min");
}

function main(itemName, newState, oldState) {
  
  var outlet = items[itemName].semantics.equipment;
  if (outlet == null) {
    console.warn("Trigger item is not part of an equipment");
    return;
  }
  if (outlet.semantics.equipmentType != "PowerOutlet") {
    console.warn("Expected Equipment type PowerOutlet. Got" + equipment.semantics.equipmentType + "instead.")
    return;
  }

  var statusConfig = outlet.getMetadata("status");
  if (statusConfig == null) {
    console.warn("No status configuration found for outlet", outlet.name);
    return;
  }

  var deviceOff = statusConfig.configuration["deviceoff"];
  var deviceRunning = statusConfig.configuration["devicerunning"];
  var waitTime = statusConfig.configuration["devicewaittime"].longValue() * 1000;
  
  if (deviceOff == null || deviceRunning == null || waitTime == null) {
    console.warn("Incomplete status configuration", deviceOff, deviceRunning, waitTime);
    return;
  }

  if (newState <= deviceOff) {
    cancelTimer(outlet);
    var previousStatus = items[outlet.name + '_status'].state;
    items[outlet.name + '_status'].sendCommandIfDifferent('OFF');
    if(previousStatus == "RUNNING") {
      deviceSwitchedOff(outlet);
    }
  } else if (newState >= deviceRunning && oldState < deviceRunning) {
    cancelTimer(outlet);
    items[outlet.name + '_status'].sendCommandIfDifferent('RUNNING');
    items[outlet.name + '_runtime'].sendCommandIfDifferent("0 min");
    var timeoutId = setInterval(updateRuntime, 60 * 1000, outlet);
    cache.private.put(outlet.name, timeoutId);
  } else if (newState < deviceRunning && oldState >= deviceRunning) {
    var timeoutId = setTimeout(deviceFinished, waitTime, outlet);
    cache.private.put(outlet.name, timeoutId);
  }
}
main(this.event.itemName, this.event.itemState, this.event.oldItemState);

I know another type I would not know how to deal with without Java types: HSBType. I use it in one of my rules and I don’t think that it would be transformed automatically.

That is something worth filing an issue over on the js scripting add-on in the openhab-addons repo. It shouldn’t be that big of a deal to add support for BigDecimal in this context.

In the mean time you can probably avoid the issue in one of three ways:

  1. set the value in metadata to Strings and parse them in the rule (yuck)
  2. you should be able to call waitTime.longValue() to convert the BigDecimal to a primitive as you’ve done
  3. use actions.ScriptExecution.createTimer(time.toZDT(waitTime), devicedFinished)

time.toZDT() definitely can handle BigDecimal. It will add the number as milliseconds from now.

Personally I like to use ISO Duration Strings to define lengths of time to schedule timers and the like. The metadata value would be PT10S. I prefer duration strings becuase they are better at self documenting. “P1M2W3DT4H5M6S” would be one month, two weeks, three days, four hours, five minutes, and six seconds. Of course usually it’s much simpler than that.

If you wanted to use a duration string and setInterval you can use time.toZDT(waitTime).getMillisFromNow(). I like using OH timers though as they tend to be easier to manage, reschedule, etc. And in fact I always use TimeMgr from OHRT so I can pass the duration string as is when creating the timer and don’t even need to worry about calling time.toZDT()

In either case, you never have to worry about multiplication by 1000 or keeping track of units and such. The whole BigDecimal issue goes away too. That’s probably why I’ve not encountered this problem before.

Ah yes, if you want to use HSBType that won’t be converted automatically.

I agree that this is a very nice approach as it gives a lot of flexibility and removes unit handling in the code altogether. Thanks for the idea, I think I will go down that route :blush:

Also pay attention to JavaScript Scripting - Automation | openHAB. time.toZDT() can make a lot of complicated time code easier to write and read. For example:

if(time.toZDT().isBetweenTimes("8:00 am", "11:00 pm"))

if(time.toZDT().isAfterTime(items.Sunrise)))

etc. When using any of the additions to js-joda (i.e. everything explicitely covered in the docs linked to above like isBetweenTimes, isAfterTime, etc.) you can pass in anything supported by time.toZDT(). No more of .plusMinutes this and withHour that.

2 Likes

I don’t think that the number is converted to PercentType, we don’t have code for that.
But there are overloads for the audio actions that accept floats, which makes this code work.

1 Like