Working with Date Times in JS Scripting (ECMAScript 11)

This is all in the docs but I don’t think many go back and review the docs when installing an update. This tutorial is just to provide a heads up about this feature that was added to openhab-js not too long ago to make creating, working with, and processing date times easier. It is not intended to be a replacement for the docs, just a place to have a few more examples.

Traditionally, working with and converting date times in openHAB has been a pain. Really, it’s a pain in any programming environment because there are few concepts that most people use every day that is as complex as dates and time.

First let’s set some groundwork.

joda-js

The JS Scripting add-on uses the js-joda library for all date time types. openHAB uses java.time.* internally. The openhab-js library and add-on has a bunch of stuff built into it that automatically converts between the JavaScript and the Java versions of these classes so that you can use the pure JavaScript classes in your rules.

This means that you can use a JS-Joda ZonedDateTime in any call to an openHAB Java method that expects a java.time.ZonedDateTime. Therefore, use the JS-Joda classes in your rules which are available via time (e.g. time.ZonedDateTime).

Time Utilities

openhab-js’s time includes a number of additional utilities to make implementing the most common use cases of using dates and times in OH easier. Some of these are stand-alone features while others are additions to the ZonedDateTime class (monkey patched).

Creating a ZonedDateTime with an Offset

This is probably the most common use cases in all of openHAB because this is usually what one needs to do create an openHAB Timer which takes a ZonedDateTime as the time for the timer to run. So the offset is almost always from now.

time.toZDT(); // returns a ZonedDateTime of now, shorter to type than time.ZonedDateTime.now()
time.toZDT(12345); // passing any JavaScript or Java representation of a number will be treated as the number of milliseconds to add to now
time.toZDT(items.getItem('MyNumberTime')); // It will add the state of a Number:Time Item (i.e. QuantityType<Time>) to now
time.toZDT(items.getItem('MyNumberItem'); // Adds the state of the Item to now as milliseconds
time.toZDT('PT1h2m3.4s'); // Parses the ISO8601 duration string and adds it to now
time.tiZDT(time.Duration.ofSeconds(123)); // Adds the passed in Duration to now

I particularly like the duration string version because it makes the code very clear. For example:

ScriptExectution.createTimer(time.toZDT('PT5s'), ...

will schedule the timer for five seconds from now.

Using the state of a DateTime Item in a Rule

In order to compare or manipulate the state of a DateTime Item in a rule its state needs to be converted to a ZonedDateTime.

time.toZDT(MyDateTimeItem); // Converts the state of the DateTime Item to a ZonedDateTime (can handle both the openhab-js Item Object or the native openHAB Java Item Object)
time.toZDT(items.getItem('MyDateTimeItem')); // if all you have is the name of the Item

Once you have it converted you can use all the standard JS-Joda operations to compare and manipulate it.

Other Conversions to ZonedDateTime

time.toZDT() will also convert java.time.ZonedDateTime, and native Java Date to JS-Joda ZonedDateTime. It can also parse the RFC formatted string produced by java.time.ZonedDateTime.toString() but it cannot handle an ISO8601 formatted string with the timezone at this time.

Static Times of Day

Often we will want to create a ZonedDateTime at a specific time but with today’s date.

time.toZDT('8:30:00 AM'); // 8:30 AM today
time.toZDT('13:02'); // 1:02 PM today

Moving a ZonedDateTime to Today’s Date

Sometimes one will have a date time where the time needs to be kept static but the date needs to be adjusted to today (e.g. an alarm clock). Calling toToday() on a ZonedDateTime will move the date to today but preserve the time, taking into account for DST change overs. For example, if the time is 13:02 with a date from three days ago and the DST changeover happened in between, calling toToday() will result in 13:02 today, not 12:02 or 14:02`.

ScriptExecution.createTime(time.toZDT(items.getItem('AlarmClock')).toToday(), ...

Between Times

One of the more common but frustratingly common comparisons is to see if a ZonedDateTime is between two times (ignoring the date). For example, one might want to do something different between 10:00 PM and 8:00 AM in a rule than what it does the rest of the time.

time.toZDT().betweenTimes('10:00 PM', '8:00 AM') // true if now is between 10:00 PM today and 8:00 AM tomorrow

time.toZDT(items.getItem('MyDateTime')).betweenTimes(items.getItem('MyDateTimeStart'), '22:00') // true if the state of MyDateTime is between the time carried by MyDateTimeStart and 10:00 PM

betweenTimes() will call time.toZDT() on both of its arguments so anything accepted by toZDT() can be passed as an argument.

If the time in the first argument is later than the second argument, the time period is assumed to span midnight and the second argument will be advanced to tomorrow.

Are Two ZonedDateTimes Close to Each Other?

There are sometimes cases where we want to see whether or not two DateTime Items are within a certain amount of time of each other. For example, perhaps you want to see if a rule is triggered within a second of the last trigger and do something different.

timestamp.isClose(time.toZDT(), time.Duration.ofSeconds(1)); // true if the timestamp is within 1 second of now
time.toZDT(items.getItem('DoorOpenedLastUpdate').isClose(time.toZDT(), time.Duration.parse('PT1d12h')); // the state of DoorOpenedLastUpdate is no more than 36 hours ago

Note that it will return true if timestamp is one second before or one second after now.

Milliseconds from Now

The more native looking way to create timers in JS Scripting is by calling setTimeout(). However this function only supports the number of milliseconds from now to run the timer. But it’s easy to convert a ZonedDateTime to the number of milliseconds from now.

setTimeout(toZDT(items.getItem('AlarmClock').millisFromNow(), () => { consol.log('Alarm clock has gone off!'); });

Conclusion

With these utilities, only rarely should one need to directly manipulate date time Objects or use a long string of method calls to get to something that can be converted to a ZonedDateTime. With support for Duration strings and time of day strings your code should be just a little more human readable.

Common complicated comparisons like checking to see if a ZonedDateTime is between two times of day, accounting for spanning midnight, or testing to see if two ZonedDateTimes are within a certain amount of time of each other are now just a single function call.

Hopefully this post has made you aware of these utilities and shown you just a few of the many things you can do with them.

Note that openhab-rules-tools, my library of utilities that implement many of the design patterns, make heavy use of these functions. For example:

// Create a timer for the triggering Item to go off in 5 seconds and call runme, 
// if a timer already exists reschedule it
timerManager.check(event.itemName, 'PT5s', runme, true);

// Create a timer that calls a function every 10 seconds until that function returns true
loopingTimer.loop(10000, loopFunction);

// If it's within a time specified by a Number:Time Item, queue up the calls until that amount of time has passed 
// e.g. if you don't want to send commands to a bunch of devices too fast
items.getItem('HueLights').forEach( (i, index, arr) => {
    gatekeeper.addCommand(time.toZDT(items.getItem('HueDelay')), () => { i.sendCommand(OFF) });
});

6 Likes

Really good overview and explanation.
It took me too long to find this post :slight_smile:

From my point of view, it would be a complete overview, if you add a chapter how to convert the ZDT to a DT, like already discussed here:

There is no reason you should ever need to convert a ZonedDateTime to a DateTimeType. All the operations that require a date time require a ZonedDateTime. Commanding or updating the Item requires a String.

What use case are you trying to solve where you need to create a DateTimeType in your rule?

Ok, how to save now() into a datetime item?

items.getItem('MyDTItem').postUpdate(time.toZDT().toLocalDateTime().toString());

NOTE: This is hopefully a temporary work-around. There is a known problem with parsing the type of string that joda-js ZonedDateTime produces without importing the proper locale libraries of Joda. Doing that be default is huge so right now there isn’t a technical solution. But if you convert the ZDT to LDT, the current timezone of the system is assumed. When/if that gets fixed the solution will change to

items.getItem('MyDTItem').postUpdate(time.toZDT().toString());

I’ve not tried it, but it might work to just pass the ZDT.

items.getItem('MyDTItem').postUpdate(time.toZDT());

It doesn’t always work with Numbers though so I always convert to a String when updating or commanding an Item. For DT Items, I use the timestamp profile most of the time so rarely need to update one from a Rule.

This might be a VSC issue but I tried using
time.toZDT(Sunset_Time)
and it identified an error in VSC
“The method or field time is undefined (org.eclipse.xtext.diagnostics.Diagnostic.Linking)”

There is no openHAB specific plugin for writing JavaScript openHAB rules. You’ll need to set up a generic JS plugin and point it at the openhab node library (which you’ll probably have to install yourself using npm.

That error is basically telling you that it’s trying to parse the code as it if were Rules DSL and of course, JavaScript is a completely different language so it’s not going to work. Indeed, there is no such thing as time.toZDT() in Rules DSL.

So if I understand your correctly, this will only work if I use the create rule or script from the OH3 MainUI

No, this will only work if you install the JS Scripting add-on and write your rules using JavaScript instead of Rules DSL. See the add-on’s docs for details on how to write JS Scripting rules. The syntax is quite different and the files go in another folder.

Both Rules DSL and JS Scripting can also be used in MainUI, but again the syntax is quite different. See the Rules sections of the Getting Started Tutorial for more details.

1 Like

How do i get the last day of current month?
current day i get from java.time.ZonedDateTime.now().getDayOfMonth()
I have tried java.time.ZonedDateTime.now().dayOfMonth().getMaximumValue() but it does not work.
I want responce like 30 for November.

.plusMonths(1).withDayOfMonth(1).minusDays(1).getDayOfMonth ?

Thanks that worked.

##   Release = Debian GNU/Linux 11 (bullseye)
##    Kernel = Linux 5.10.0-14-amd64
##  Platform = VMware Virtual Platform/440BX Desktop Reference Platform
##    Uptime = 9 day(s). 10:39:27
## CPU Usage = 2% avg over 2 cpu(s) (1 core(s) x 2 socket(s))
##  CPU Load = 1m: 0.02, 5m: 0.03, 15m: 0.00
##  openHAB 3.3.0 - Release Build

I have a BetweenTimes question when running in a JS script. I understand that the right syntax is “isBetweenTimes” but I’m unable to execute the time command if I append .isBetweenTimes(‘10:00 AM’, ‘8:00 PM’) to time.toZDT(). The error is

TypeError: time.toZDT is not a function.  

What am I’m doing wrong.

console.log(time.toZDT()); //Works.  Gives me system time.
console.log(time.toZDT().isBetweenTimes('10:00 AM', '8:00 PM')); //Complains saying TypeError: time.toZDT is not a function

What version of OH?

Have you installed the openhab-js library manually using npm at any point in the past?

Does console.log work with Java boolean true/false objects?

I would expect a different complaint, but always a bit wary of “do multiple things in one line and guess which part is failing” :smiley:

I don’t believe I have.

openhabian@cj323:~$ npm list
/home/openhabian
└── (empty)

I’ll test it but I was using an example from this thread

It would be located under $OH_CONF/autmation/js/node_modules.

If you see an openhab folder under there than you have overridden the version of the library that comes with the add-on.

If not, you are using the version of the helper library that came with the add-on. The latter is the most likely case.

I don’t remember exactly when the toZDT() and isBetweenTimes() were added to the helper library but given the date of the OP I’m guessing it was after OH 3.3 release. This means, to pick up the latest and greatest you need to install the latest version of the helper library.

From the $OH_CONF/automation/js folder run npm install openhab.

Note, you’ll probably want to remove that when you update to OH 3.4+ or else your helper library won’t keep up (unless you remember to run npm to update the libraries manually).

Thanks @rlkoshak. Yes, there were no modules under the js folder so I “sudo npm install openhab.” After a couple of minutes, I reran the test script and it ran successfully. I appreciate your help.

@rossko57, yes, the console.log does work with Java Boolean true/false objects. The Between Times command just simplified my JavaScript code to a single line. This is particular useful in rules where actions are executed only during certain times of day (conditional script).

1 Like