OH3 and JS-Scripting (ECMA 2021): time functions

OH3.3-release and using JS-Scripting gives me a baaaad headache concerning time.

Say, you want to work with time and what all of that concerns. currently I’m completely stuck, when it comes to “getHours, getDay, …” stuff.

so, code looks like this:

var nowISO = new Date().toISOString();     // To be able to use in "history" of items
var nowZoned = time.ZonedDateTime.now();   // To be able to calculate with it

// I'd like to calculate an average of say the last 10minutes:
var ago10mins = time.ZonedDateTime.nowISO().minusMinutes(10);
var UeberschussAVG = items.getItem("EMS_Ueberschuss").history.averageSince(ago10mins );
                                                                           ^works

// I also want to built an date-item, which in turn I want to send to a DateTime item
// nowISO does not work with either getYear() or year()
var todayAt13hours = new Date(nowZoned.year(), nowZoned.month(), nowZoned.day(), "13", "00", "00", "000");
                                       ^works           ^works            ^error
var tomorrowAt13hours = new Date(nowZoned.getYear(), nowZoned.getMonth(), nowZoned.getDay() +1, "13", "00", "00", "000");
                                          ^doesn't work AT ALL

So, how is it even possible to do, what I tried to do in the code above:

  1. use some “10 minutes back”-interval for persistence (works, btw)
  2. AND do some basic JS-functions like “getYear” or “getDay” with the same (or maybe other) Date-object?

Don’t use Date. That’s a relatively limited JS only class. The purpose of rules is to interact with openHAB and openHAB always wants a ZonedDateTime. The helper library for JS Scripting imports Joda-JS which is close but not exactly the same as java.time.ZonedDatetime.

So use Manual | js-joda as your reference (note there is a link to this page in the JS Scripting docs).

In addition, I added a bunch of helper stuff related to time to the helper library. See JavaScript Scripting - Automation | openHAB with some more examples at Working with Date Times in JS Scripting (ECMAScript 11).

var nowZoned = time.toZDT();

var UeberschussAVG = items.getItem("EMS_Ueberschuss").history.averageSince(nowZoned.minusMinutes(10));
// or
// var UeberschussAVG = items.getItem("EMS_Ueberschuss").history.averageSince(time.toZDT(-10*60*1000));

The JS Scripting add-on will convert your Joda ZonedDateTime to a Java ZonedDateTime where necessary. It that isn’t working then it needs to be fixed.

I don’t know what the nowISO() is all about but it’s not needed here. That looks like it might be some new function recently monkeypatched onto the Joda ZonedDateTime class by the library but I don’t see it reflected in the docs. It might not be intended for use outside the library.

var todayAt13hours = time.toZDT("13:00");
var tomorrowAt13hours = tocayAt13hours.plusDays(1);

You can pass these as the update/command in sendCommand and postUpdate I’m pretty sure. If not use:

    items.getItem('MyItem').postUpdate(todayAt13hours.getLocalDateTime().toString());

There is some localization issues converting between a Java ZonedDateTime and Joda-JS ZonedDateTime with the timezone but if you strip off the timezone it assumes the system timezone and can parse the string.

See above. Don’t use Date. Stick with the Joda-JS classes as much as possible. Use the time.X utilities.

See the reference docs for Joda-JS (again linked to in the JS Scripting docs). In particular it’s year() and month() and dayOfMonth().

You don’t want to mix classes here so avoid going outside of Joda-JS as much as possible. But if you happen to have a JavaScript Date Object, you can convert it to a Joda-JS ZonedDateTime with time.toZDT(myJSDateObject).

1 Like

Thanks as always, Rich!

I’m maximum confused right now, because I think, we wanting to get rid of Joda-time within openHAB? only zonedTime for OH-interaction. And I wanted to do some JS-magic for logic within the rule only.
So, to be clear: more often than not do I rely on “what hour is it” - and that’s something joda-js doesn’t offer, does it?

Man, that time-stuff is getting more and more frustrating! :wink:

I found some code-snippet here in the forum on this and used it. :wink:

But yeah, this afternoon after my obligatory trailrun with the dog I’ll get to it and try to comprehend and understand this once and for all… :wink:

They did get rid of it, no Joda time used in openHAB.
Just as no javascript used in openHAB.

But of course you can write your own rules and scripts in javascript or jython or node red etc., and each of those has its own ways to handle datetimes.

Key part

so use whatever is convenient in your rules. “Convenient” should include an easy means to convert to/from zonedDateTime and whatever.

Joda is convenient in javascript rules.

1 Like

Joda for Java was no longer maintained and the java.time classes that come with Java do everything that Joda does (including class names) so OH moved to java.time.

The helper libraries go to great lengths to keep you only using JavaScript. The closest thing to Java.time.X in JavaScript is the Joda-JS library. So the add-on and helper library imports and makes available to you that library for working with dates and time. And because it’s really similar, it works mostly the same as we’ve been used to for the last decade.

What sort of magic? I did show above and in the tutorial how to convert a JS Date to a Joda-JS ZonedDateTime using time.toZDT() so you could do your magic and then convert it when you are ready to send it to an Item or create a timer or the like

But I find it much much easier to use the Joda classes. There’s a reason they exist in the first place. The capabilities of Date are pretty primitive.

This is going to sound harsh but I can’t figure out a better way to say it with it coming across as snarky, which I don’t intend. Did you click on the links in my reply? Did you look at the reference for ZonedDateTime?

Literally everything you can do in Rules DSL you can do in JS Scripting in pretty much the same ways. Sometimes the code is nearly identical. But there are a few minor differences so if it doesn’t work, refer to the reference docs to see what’s different. In this case, I think java.time.ZonedDateTine calls it getHourOfDay() where, as you can see, Joda-JS calls it just hour().

Java’s time classes originally came from Joda (that’s why Joda was no longer maintained). So they look the same, act the same, and have the same capabilities. There are only minor differences but there is nothing you can do in one that you can’t do in the other.

1 Like

Sorry, wasn’t meant for you. It’s just, that “.hour” didn’t work on it.
but after some consultation with the docs and your help I’m now with this:

var interval10min = time.toZDT().minusMinutes(10);
var UeberschussAVG = items.getItem("EMS_Ueberschuss").history.averageSince(interval10min);
                                                                           ^ works!

if (time.toZDT().hour() < 13) Start = time.toZDT("13:00");
else Start = time.toZDT("13:00").plusDays(1);
                        ^ works also!

items.getItem("EMS_SpMaStart").sendCommand(Start);
                                           ^ doesn`t work
items.getItem("EMS_SpMaStart").sendCommand(Start);
                                           ^ doesn`t work

“Start” is now: 2022-10-29T18:29:34.039+02:00[SYSTEM]

sending only “Start” to a DateItem throws a warning - without changing the item:

2022-10-29 18:30:05.459 [WARN ] [ab.core.internal.events.EventHandler] - Creation of event failed, because one of the registered event factories has thrown an exception: Error getting class for simple name: '$Proxy1Type' using package name 'org.openhab.core.library.types.'.
java.lang.IllegalArgumentException: Error getting class for simple name: '$Proxy1Type' using package name 'org.openhab.core.library.types.'.
	at org.openhab.core.items.events.ItemEventFactory.parseSimpleClassName(ItemEventFactory.java:183) ~[?:?]
	at org.openhab.core.items.events.ItemEventFactory.parseType(ItemEventFactory.java:158) ~[?:?]
	at org.openhab.core.items.events.ItemEventFactory.createCommandEvent(ItemEventFactory.java:109) ~[?:?]
	at org.openhab.core.items.events.ItemEventFactory.createEventByType(ItemEventFactory.java:78) ~[?:?]
	at org.openhab.core.events.AbstractEventFactory.createEvent(AbstractEventFactory.java:53) ~[?:?]
	at org.openhab.core.internal.events.EventHandler.createEvent(EventHandler.java:131) ~[?:?]
	at org.openhab.core.internal.events.EventHandler.handleEvent(EventHandler.java:106) ~[?:?]
	at org.openhab.core.internal.events.EventHandler.handleEvent(EventHandler.java:84) ~[?:?]
	at org.openhab.core.internal.events.ThreadedEventHandler.lambda$0(ThreadedEventHandler.java:67) ~[?:?]
	at java.lang.Thread.run(Thread.java:829) [?:?]
Caused by: java.lang.ClassNotFoundException: org.openhab.core.library.types.$Proxy1Type cannot be found by org.openhab.core_3.3.0
...

using your “Start.getLocalDateTime().toString()” throws an error

2022-10-29 18:39:34.107 [ERROR] [b.automation.script.javascript.stack] - Failed to execute script:
org.graalvm.polyglot.PolyglotException: TypeError: (intermediate value).getLocalDateTime is not a function
	at <js>.:program(<eval>:49) ~[?:?]

So, I could strip the “[SYSTEM]”-part and use a string for the sendCommand, but I don’t get, how I can then work with a ZDT-object and compare it to a DateTime-item’s value?

var jetzt = time.toZDT();
if (jetzt > time.toZDT(items.getItem('EMS_SpMaStart')).plusMinutes(10))) {

this throws an error?
Isn’t it supposed to work as “zonedDateTime”? How must I send an value to a DateTime-item?

2022-10-29 18:55:27.512 [ERROR] [b.automation.script.javascript.stack] - Failed to execute script:
org.graalvm.polyglot.PolyglotException: JsJodaException: Invalid value for NanoOfDay (valid values 0 - 86399999999999): NaN
	at <js>.assert(webpack://openhab/./node_modules/@js-joda/core/dist/js-joda.esm.js?:290) ~[?:?]
	at <js>.checkValidValue(webpack://openhab/./node_modules/@js-joda/core/dist/js-joda.esm.js?:1489) ~[?:?]

sorry, if it’s currentyl a mess within my head. DSL works and ECMAv5 works also, but JS-Scripting (which I suppose will be the Rule-Engine-Language of choice!) is another world…

What frustrates me the most (and just to be sure: that’s not on you, Rich!) is, that the docs reference some external documentation - but it seems, the most part of the linked reference isn’t the way, openHAB Rules are prepared to do!

example: the date-thingies - all examples straight from https://js-joda.github.io/js-joda/manual/:

  1. error org.graalvm.polyglot.PolyglotException: ReferenceError: "LocalDate" is not defined
var dt = LocalDate.now();
  1. error org.graalvm.polyglot.PolyglotException: ReferenceError: "ZonedDateTime" is not defined
var dt = ZonedDateTime.now().toString(); // e.g. 2016-03-18T12:38:23.561+01:00[SYSTEM]
  1. error org.graalvm.polyglot.PolyglotException: ReferenceError: "LocalTime" is not defined
var dt = LocalTime.now();
  1. error org.graalvm.polyglot.PolyglotException: ReferenceError: "LocalDateTime" is not defined
var dt = LocalDateTime.now();
  1. error org.graalvm.polyglot.PolyglotException: ReferenceError: "ZonedDateTime" is not defined
var dt = ZonedDateTime.now(); 

So, there’s the one thing you posted earlier, which works to create some timestamp:

var nowZoned = time.toZDT();

and the one directly linked in the openHAB docs:

var now = time.ZonedDateTime.now();

So, I figured, we have to use “time.” for all of the above - and it works!

var dt = time.LocalDateTime.now();
console.log("date", dt);

if (dt > items.getItem("EMS_SpMaStart")) {
  console.log("später");
} else {
  console.log("vorher");
}

items.getItem("EMS_SpMaStart").sendCommand(dt.plusMinutes(10).toString());

So - is this now a “good behaviour” I’m using? It seems I can do all my “JS-magic” plus I can easily update DateTime-items with it? (only have to convert it to strings).

of course… something’s missing:

var jetzt = time.LocalDateTime.now();
var interval10min = jetzt.minusMinutes(10);
var UeberschussAVG = items.getItem("EMS_Ueberschuss").history.averageSince(interval10min);

this won’t go through:

2022-10-30 09:39:07.004 [ERROR] [b.automation.script.javascript.stack] - Failed to execute script:

org.graalvm.polyglot.PolyglotException: TypeError: invokeMember (averageSince) on org.openhab.core.persistence.extensions.PersistenceExtensions failed due to: no applicable overload found (overloads: [Method[public static org.openhab.core.library.types.DecimalType org.openhab.core.persistence.extensions.PersistenceExtensions.averageSince(org.openhab.core.items.Item,java.time.ZonedDateTime)], Method[public static org.openhab.core.library.types.DecimalType org.openhab.core.persistence.extensions.PersistenceExtensions.averageSince(org.openhab.core.items.Item,java.time.ZonedDateTime,java.lang.String)]], arguments: [JavaObject[EMS_Ueberschuss (Type=NumberItem, State=2369, Label=Überschuss, Category=, Tags=[Point], Groups=[PL12EMS]) (org.openhab.core.library.items.NumberItem)] (HostObject), 

why? Why do I need to use “ZonedDateTime” for persistence of a DateTime-item and “LocalDateTime” for updating the very same DateTime-item?
is it just me? Do i miss something important? or is it just my OCD… :wink:

Not just .hour, .hour(). It’s. function call. Rules DSL has put people into bad habits. It has something called “syntactic sugar” where it lets you call functions that start with get without the “get” and any function that has no arguments as if they were not a function. So .getHour() and .hour are the same. As far as I know, Rules DSL is the only of the OH rules languages that supports that. In all other languages you have to call a function as a function.

That’s a weird error. I’ve not seen that before. It doesn’t seem to be related to the sendCommand but maybe it is. As with Rules DSL, when in doubt, send the String. But in this case there’s some issues with localization on the timezone part of the toString(). That’s easy enough though.

items.getItem("EMS_SpMaStrt").sendCommnd(Start.toLocalDateTime().toString());

Same as you always have in Rules DSL.

var jetzt = time.toZDT();
if(jetz.isAfter(time.toZDT(items.getItem('EMS_SpMaStart')).plusMinutes(10)) { // I think you had an extra )

When looking at the reference docs for ZonedDateTime (or any of the other classes in the docs), make sure to look at the Inherited Summary section.

Also note that in addition to isBefore() and isAfter(), we’ve added myZDT.betweenTimes(a, b) which returns true if myZDT is between a and b where a and b can be anything supported by toZDT() as well as myZDT.isClose(a, dur) which returns true if myZDT is within dur (a Duration) of the ZonedDateTime a to handle common time based comprisons.

I’m not sure what that error is but suspect it’s trying to convert the ZDTs to numbers so the > will work. And that’s not going to work.

So, ECMAScript 5 uses the java.time.ZonedDateTime like Rules DSL does. JS Scripting attempts to never force you to use the Java classes so that third party node libraries can be supported.

There is a nuance there which is common in documenting JavaScript Libraries as far as I have seen. The examples will show using the library on its own. But in practice, the library will hardly ever be imported into you program “naked” like that because then you run into collisions between libraries. If you have two libraries that both define foo, which ever library is loaded second will overwrite the first loaded library’s foo variable and both libraries are likely to break. So when you import a library, you usually put it into it’s own “namespace”.

In JSS Scripting, that namespace is time. Thats what is intended to be communicated by this in the OH docs:

openHAB internally makes extensive use of the java.time package. openHAB-JS exports the excellent JS-Joda library via the time namespace

So

var dt = time.LocalDate.now(); // note that will only give you a date, not a date time
var dt = time.ZonedDateTime.now().toString();

and so on. Just put time. in front of any reference to a Joda time class.

Yes, that’s how it works. If you at some point import some other third party library, you’ll also have to pay attention to what namespace it’s imported under too.

OK, if the .toString() worked on a full ZonedDateTime, ignore what I showed above with the toLocalDateTime().

I thought that the postUpdate and sendCommand functions were updated to accept more than just String, but maybe I’m miss-remembering. Just keep in mind that ultimately it’s going to be converted to a String anyway so you won’t go wrong by sending a String in the first place.

Hmmm. That should have worked. Essentially the same thing worked above when you put it all on the same line right? var interval10min = time.toZDT().minusMinutes(10)

If so that’s a mystery. It should work whether you do the minusMinutes all on the same line or on separate lines. That’s worth filing an issue on. That’s a bug.

1 Like

OK, I’ll have to gather some info and do that.

But another thing. How can I calculate a timespan from a given DateTime-item?

normally I’d guess, this would work (as both are ZonedDateTime, right?)

var jetzt = time.ZonedDateTime.now();
var lastStart = items.getItem("EMS_WaMaLetzterStart").state;
var difference = jetzt - lastStart;

console.info("Last Update: ", jetzt, lastStart, difference);

but the difference is NaN - and jetzt appears to be a string?:

2022-11-02 18:58:44.643 [INFO ] [utomation.script.ui.EMS_CalcWWStatus] - Last Update:  "2022-11-02T18:58:44.617" 2022-11-02T17:00:00.000+0100 NaN

Nope, jetzt is a ZonedDateTime but lastStart is a String. .state is always a String. You can get at the actual State Object using .rawState but that’s going to be a DateTimeType, not a ZDT. But none of that matters. time.toZDT() is your time swiss army knife.

var jetzt = time.toZDT(); // returns now
var lastStart = time.toZDT(items.getItem("EMS_WaMaLetzterStart")); // returns a ZDT of the Item's state or null if it's state is NULL or UNDEF

The whole reason I wrote and pushed so hard to get time.toZDT() added to the helper library is because you shouldn’t have to know or care what you got. If it can be converted to a ZDT, time.toZDT() can convert it to one. Once you have a ZDT, everything is compatible and works the same. So if you need to do anything with time, unless you already know you have a ZDT, pass it to toZDT() first to convert it. Even if you are not sure, pass it to toZDT() and if it already is a ZDT, it’ll just pass it back unchanged.

This will not and has never worked, in Rules DSL or any other language (maybe it’d work in jRuby). It certainly won’t and can’t work in JavaScript because there’s no such thing as operator overloading. That means assigning a different function to +, -, *, /, et. al. based on the types of the operands. In JavaScript, - is only going to work with with numbers. You don’t have numbers.

But, looking at the Joda-JS docs we see there is a Duration class.

var difference = time.Duration(jetzt - lastStart);

By default that’ll give you a toString in ISO8601 format (e.g. PT1h2m3s for one hour, two minutes and three seconds) or you can get at the time in the units of choice (e.g. difference.seconds()). You can get the absolute value (in case you don’t care whether the duration is negative), do math with Durations (have to call the functions though, +, - , etc won’t work) and, you can pass one to time.toZDT() and get a ZDT that is now + duration. You can even pass the ISO8601 duration String to time.toZDT(). For example time.toZDT('Pt5m30s)is the same astime.ZonedDateTime.now.plusMinutes(5).plusSeconds(30)`.

1 Like

hmm. I figured, as jetzt was in quotes it was a string, though. again what learned (sorry, german pun).

thanks for that!

cool. My long-covid brain must have forgotten, I used it last week or so already. Thanks for keeping me updated and helping!