Yet another DateTime question (epoch seconds to DateTime in ECMA 2021)

I’ve been trying for 2 hours now to get this frustrating thing over and done with, to no avail :face_with_steam_from_nose:

I have a timestamp in epoch seconds from my Ring doorbell that comes in via MQTT as a string, into an item. I want to convert this to a DateTime.

In Nashhorn I had:

var logger = Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.model.script.Rules.RingDevices");
scriptExtension.importPreset("default");
var myEpochMilliseconds = items["ringSnapshotTimestampEpoch"];
var resultDateTime = new Date(myEpochMilliseconds * 1000);
var isoDateTime = resultDateTime.toISOString();
events.postUpdate("ringSnapshotTimestampLocal", isoDateTime)

This worked well. I am now trying to convert all my rules to the ECMA 2021 format. This is as close as I got:

var myEpochMilliseconds = Number(items.getItem("ringSnapshotTimestampEpoch").state)*1000;
var instant = time.Instant.ofEpochMilli(myEpochMilliseconds).atZone(time.ZoneId.systemDefault());
console.log(instant)
//var dt = time.toZDT(instant);
//var dt = time.ZonedDateTime.ofInstant(instant, tz);
//var dt = time.toZDT(instant)
//console.log(dt)
items.getItem("ringSnapshotTimestampLocal").postUpdate(instant.toString())

You can see by the commented out lines, I tried a lot (and more variants). Nothing seems to work to get this into my DateTime item. This last variant is the closest (as in: produces the least errors) and comes up with the error:
State '2025-03-11T10:21:47+01:00[SYSTEM]' cannot be parsed for item 'ringSnapshotTimestampLocal'.

Maybe someone can lend me a helping hand with this. Read a lot of topics but can’t get it to work.

Try

var dt = time.toZDT(instant.toString());

Thanks for the reply!

var myEpochMilliseconds = Number(items.getItem("ringSnapshotTimestampEpoch").state)*1000;
var instant = time.Instant.ofEpochMilli(myEpochMilliseconds).atZone(time.ZoneId.systemDefault());
console.log(instant)
//var dt = time.toZDT(instant);
//var dt = time.ZonedDateTime.ofInstant(instant, tz);
var dt = time.toZDT(instant.toString());
console.log(dt)
items.getItem("ringSnapshotTimestampLocal").postUpdate(dt)

Gives me:

2025-03-11 11:51:19.162 [INFO ] [cript.ui.RingSnaphotTimestampToLocal] - "2025-03-11T11:51:18+01:00[SYSTEM]"

2025-03-11 11:51:19.188 [INFO ] [cript.ui.RingSnaphotTimestampToLocal] - "2025-03-11T11:51:18+01:00[SYSTEM]"

2025-03-11 11:51:19.192 [ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID 'RingSnaphotTimestampToLocal' failed: java.lang.UnsupportedOperationException: Unsupported operation identifier 'toFullString' and  object '[object Object]'(language: JavaScript, type: ZonedDateTime). Identifier is not executable or instantiable.

Or did you mean to implement is somewhere else in the code?

Yes, this is what I meant. I thought a postUpdate might work with a ZDT value.
Probably not…
Need to check what kind of value is expected for a datetine item.

How about

items.getItem("ringSnapshotTimestampLocal").postUpdate(dt.toString());

I just now managed to get it to work with:

var myEpochMilliseconds = Number(items.getItem("ringSnapshotTimestampEpoch").state)*1000;
var instant = time.Instant.ofEpochMilli(myEpochMilliseconds).atZone(time.ZoneId.systemDefault());

items.getItem("ringSnapshotTimestampLocal").postUpdate(instant.toString().slice(0, -8))

So indeed, toString(), which I tried before but I needed to slice the [SYSTEM] part off (see earlier log output). Don’t understand just yet why that gets added but then subsequently it can’t be dealt with.

Yep, it needs to converted to ISO-8601 format first

Funny enough, somewhere along all the versions I tried, the log showed:

"2025-03-11T12:10:48+01:00[SYSTEM]"

2025-03-11 12:10:49.191 [ERROR] [b.automation.script.javascript.stack] - Failed to execute script:

org.graalvm.polyglot.PolyglotException: Error: ISO 8601 strings are not yet supported

Which would have you think the output is an ISO-8601 string, right? But subsequently it could not be parsed to my item ringSnapshotTimestampLocal. Weird.

I still don’t like the slice-bit. It’s to prone for future errors. Any other approach without the slice will do it for me. So if anyone can give further hints, it’s appreciated. For now it’s working

This is a ZonedDateTime, not an Instant. You need only an Instant, so you should be able to simplify:

var instant = time.Instant.ofEpochMilli(myEpochMilliseconds);

Which version of openHAB are you running? 4.3.2 should be able to handle that.

I was replying out of the top of my head and wasn’t at my PC.

you need to change it as follows:

var myEpochMilliseconds = Number(items.getItem("ringSnapshotTimestampEpoch").state)*1000;
var instant = time.Instant.ofEpochMilli(myEpochMilliseconds);
var dt = time.toZDT(instant.toString()).toLocalDateTime();
items.ringSnapshotTimestampLocal.postUpdate(dt);

The problem was “.atZone” was adding “[System]”.
In case you want to provide a different time zone as the server’s time zone you need to replace .toLocalDateTime() by .atOffset()

In this case it looks like:

var myEpochMilliseconds = Number(items.getItem("ringSnapshotTimestampEpoch").state)*1000;
var instant = time.Instant.ofEpochMilli(myEpochMilliseconds).atOffset(time.ZoneOffset.ofHours(-8)).toString();
items.ringSnapshotTimestampLocal.postUpdate(instant);

(no conversion to zdt)

This gives me the Failed to execute script: org.graalvm.polyglot.PolyglotException: Error: ISO 8601 strings are not yet supported error. I am on 3.3 which could be the problem. Trying to convert most of my rules so my setup keeps working when upgrading to 4+

In JRuby this would’ve been done like this (although there are many other ways to do it)

time = Time.at(ringSnapshotTimestampEpoch.state)
ringSnapshotTimestampLocal.update(time)

EDIT:

Actually this also works:

ringSnapshotTimestampLocal.update(ringSnapshotTimestampEpoch.state)

Here’s how it was tested:

Welcome to JRuby REPL. Press Ctrl+D to exit, Alt+Enter (or Esc,Enter) to insert a new line.
JRuby> items.build { date_time_item ringSnapshotTimestampLocal; number_item ringSnapshotTimestampEpoch}
=> nil
JRuby> ringSnapshotTimestampEpoch.update Time.now.to_i
=> #<OpenHAB::Core::Items::NumberItem ringSnapshotTimestampEpoch nil state=NULL>
JRuby> ringSnapshotTimestampEpoch.state
=> 1741706723
JRuby> time = Time.at(ringSnapshotTimestampEpoch.state)
=> #<Java::JavaUtil::Date: Wed Mar 12 01:25:23 AEST 2025>
JRuby> ringSnapshotTimestampLocal.update(time)
=> #<OpenHAB::Core::Items::DateTimeItem ringSnapshotTimestampLocal nil state=NULL>
JRuby> ringSnapshotTimestampLocal.update(ringSnapshotTimestampEpoch.state)
=> #<OpenHAB::Core::Items::DateTimeItem ringSnapshotTimestampLocal nil state=2025-03-12T01:25:23.000+1000>

It’s designed so you don’t have to struggle when doing date/time operations / conversions / calculations.

I tested the code on my side and it works. I am on 4.3.
No need to convert all scripts before updating to oh4. You can still install nashorn and let the old script run as before. You just need to change the application type of the rule.

Does this code work on 3.3?

Yeah, this is what I expect in the year 2025: simple code to convert seconds into a date which I can store in a datetime item.

no epoch, no millis, no instant, no java.time.whatever, no zdt, joda, no zoneddatetime and whatelse is available around the time conversion.
Flexibility is nice, but this is insane.

Why is ringSnapshotTimestampEpoch storing a epoch in the first place? It seems like it would be best all the way around to use a transformation to convert that to a DateTime and store it in a DateTime Item.

Is that in fact what this rule is doing? If so you could implement this as a JS Script transformation and save the extra Item. I see you are still on OH 3 so that won’t be an option. After you upgrade it will be though.

Unfortunately time.toZDT() has no way to tell the difference between a number that should be interpreted as “milliseconds from now” and an epoch. Since “milliseconds from now” is the more common use case, that’s what’s supported.

Converting the number to a string poses a similar problem. If it’s not one of the supported String formats time.toZDT() doesn’t know how to handle it. Handling epoch as a String might be something that could be supported but according to the docs it’s not currently supported.

It would be good to add support for Instant too.

It should. But the string I’m seeing in the log isn’t any valid and supported date time format.

I don’t thing this works, or if it does something has been added to OH since 4.3.3 to make this work.

The code should read:

// Convert the number to an Instant
var instant = time.Instant.ofEpochSeconds(items.getItem.ringSnapshotTimestampEpoch.numericState);
// Convert the Instant to a ZonedDateTime with the default time zone
var dt = time.ZonedDateTime.ofInstant(instant, time.ZoneId.systemDefault());
// assuming this Item is a DateTime Item
items.ringSnapshotTimestampLocal.postUpdate(dt); 

Nothing changed in 4+ with Nashorn. You can upgrade first and then convert your rules after. Then you can take advantage of all that has been added and improved to the helper library since 3.3.

You can also install the openhab-js library manually using npm. See the docs for details. I think openHABian has an option to install it. But that’s not guaranteed to work because some changes are made to stay in sync with the add-on.

My version of the code above might work in 3.3. I can’t remember when “numericState” and the ability to get the Item without calling “getState()” was added. But I recommend upgrading first and then come back and change your rules.

1 Like

It‘s a value coming in via MQTT.

It works. I tested it before posting.
Thanks for your code. I‘ll add it to my RCS collection (Rich‘s Code Snippets) :slight_smile:

Needs to be

var instant = time.Instant.ofEpochSecond(items.getItem("ringSnapshotTimestampEpoch").numericState);

Would you reference a transformation file in the MQTT-channel under Incoming Value Transformations?
I’ve used these files a lot but only for stateDescription. But that is just what it is, a display value. The type could be something well different…

I was under the impression it had to be installed separately. I want to clean stuff up a bit. Less is more :slight_smile: But if it is shipped with the package, I might as well…

Or what I originally meant to write

var instant = time.Instant.ofEpochSecond(items.ringSnapshotTimestampEpoch.numericState);

At some point, all the Items were added as members of the items Object so you can just get the Item by name without needing to call getItem().

I was going to add the getItem() calls to my example but decided not to but clearly didn’t fully back out my changes.

Good catch!

I think I misread your original code. I thought the toString was being called on epoch, but it’s being called on the instant. Indeed that will work because it outputs an ISO8601 string using GMT as the timezone.

I think it would be good to add support time.toZDT() for Instants. It would not be hard to detect and we can assume the systemDefault() for the timezone. If you need a different timezone it’s just a matter of using the code above to supply the timezone.

@arjan-io , once you are on OH 4, you can use the following inline JS script transform to convert the epoch on the fily and eliminate the extra Item and the rule.

| time.ZonedDateTime.ofInstant(time.Instant.ofEpochSeconds(parseInt(input)), time.ZonedId.systemDefault()).toString()

That basicially does it all on one line. You can put that into the incoming state transformation field for the MQTT Channel and link that to your DateTime Item.

On OH 3 I don’t think inline script transformations are supported. I don’t think any script transformation outside of Nashorn JS is supported.

If this were defined as a separate transform (which would be smart if you have more than one Channel that needs this transformation) you can create a transformation in the UI under Settings → Transformations or in a .js file in the conf/transformations folder that looks like this:

(function(input){
  let inst = time.Instant.ofEpochMilli(parseInt(input));
  return time.ZonedDateTime.ofInstant(inst, time.ZoneId.suystemDefault());
})(input)

Then you can select that transformation.

But again, you apply this to the state transformation on the Channel, or as a profile on the link between the Channel and the Item.

You could also use this in the state description to only change how it shows in the UI, but I think a DateTime Item is way more useful.

It does but it’s just an add-on. No big deal to install nor to remove later when you are done.

A lot has changed in JS Scripting between 3.3 and 4.3 and some of those changes were breaking. If you rewrite your rules to JS SCripting 3.3, you’ll have to make adjustments after you upgrade. If you upgrade first, you’ll be rewriting your rules using the latest JS Scripting and won’t have to adjust your rules twice.