JS Scripting toLocalDateTime no different from UTC when in GMT+0100

Context: OH 4.1.2 running on Debian 12. In a bash shell the output of date shows the correct local time. I’ve tried setting the timezone in OH settings to GMT and also leaving it blank in the hope that OH would pick up the OS setting. After setting the timezone to blank I see this in the log:

2024-04-01 11:50:20.916 [INFO ] [.core.internal.i18n.I18nProviderImpl] - Time zone is not set, falling back to the default time zone.

I have a rule that reads electricity unit prices from here. The data is in JSON format and looks like this:

{
  "count": 6284,
  "next": "https://api.octopus.energy/v1/products/AGILE-23-12-06/electricity-tariffs/E-1R-AGILE-23-12-06-G/standard-unit-rates/?page=2",
  "previous": null,
  "results": [
    {
      "value_exc_vat": 14.03,
      "value_inc_vat": 14.7315,
      "valid_from": "2024-04-01T21:30:00Z",
      "valid_to": "2024-04-01T22:00:00Z",
      "payment_method": null
    },
etc
etc
etc

The times in the JSOC are UTC, at least that’s my understanding of timestamps that end in the Z character.

The rule was working fine until yesterday when the UK moved from GMT to BST. Now, all the unit prices are skew-whiff, shifted by one hour. I use the time.toLocalDateTime method to convert the time in the valid_from element to the local time. Before Sunday the time in the valid_from element was unchanged when converted with toLocalDateTime. After Sunday the in the valid_from element is… still unchanged. Here’s a test rule:

rules.JSRule({
  name: 'Test UTC vs local time',
  description: 'Test UTC vs local time',
  triggers: [triggers.ItemStateChangeTrigger('DEVTEST_SWITCH')],
  execute: data => {
    var logger = log('DEVTEST')

    logger.info('Test UTC from Octopus agile JSON vs localdatetime rule...')

    var agileprices = String(items.getItem('OctopusAgilePrices_AgilePricesJSON').state)
    var obj = JSON.parse(agileprices)

    for (var i = obj.results.length - 1; i >= 0; i--) {
      var valid_from = obj.results[i].valid_from;
      var valid_from_zdt = time.toZDT(valid_from);
      var valid_from_ldt = valid_from_zdt.toLocalDateTime();                      //get valid_from as LocalDateTime
      logger.info("Index ({}), valid_From time ({}) converted to local date time ({}).", String(i), String(obj.results[i].valid_from), String(valid_from_ldt));
    }

  }
})

And an excerpt from the output in the log:

11:43:53 Index (5), valid_From time (2024-04-01T19:00:00Z) converted to local date time (2024-04-01T19:00).
11:43:53 Index (4), valid_From time (2024-04-01T19:30:00Z) converted to local date time (2024-04-01T19:30).
11:43:53 Index (3), valid_From time (2024-04-01T20:00:00Z) converted to local date time (2024-04-01T20:00).
11:43:53 Index (2), valid_From time (2024-04-01T20:30:00Z) converted to local date time (2024-04-01T20:30).
11:43:53 Index (1), valid_From time (2024-04-01T21:00:00Z) converted to local date time (2024-04-01T21:00).
11:43:53 Index (0), valid_From time (2024-04-01T21:30:00Z) converted to local date time (2024-04-01T21:30).

What do I need to do to convert the UTC timestamp in the JSON to a localdate time?

The documentation of ZonedDateTime.toLocalDateTime:

Gets the LocalDateTime part of this date-time.
This returns a LocalDateTime with the same year, month, day and time as this date-time.

I admit it needs further clarification, but basically it simply strips off the time zone information and returns a LocalDateTime object (which contains no time zone data in it). Do not read “LocalDateTime” and interpret that as “local to your time zone”. LocalDateTime simply means a DateTime object without any time zone data. Contrast that to ZonedDateTime which is a DateTime object WITH time zone data.

To do what you want, you probably should NOT use LocalDateTime, but instead, keep using ZonedDateTime, except that maybe you want to convert the time zone to your local time zone

var valid_from_zdt = time.toZDT(valid_from).withZoneSameInstant(ZoneId.systemDefault())

Assuming your system’s time zone is set correctly to your local time zone.

Thanks for the info, I see that I made an assumption based on the method name rather than the documentation.

I just tried the code above and got an error in the log:

2024-04-01 13:18:44.034 [ERROR] [.script.file.octopusagileforecast.js] - Failed to execute rule Test-UTC-vs-local-time-b1e58143-75e7-4b61-a373-3aa9342693f2: ReferenceError: "ZoneId" is not
defined: ReferenceError: "ZoneId" is not defined
        at execute (octopusagileforecast.js:139)
        at execute (@openhab-globals.js:2)

I’ll have a look at the documentation for withZoneSameInstant.

You might have to import java.time.ZoneId into jsscripting. I don’t use jsscripting and don’t know the syntax to do it.

It is readily available without explicitly importing in jruby, because it’s a part of the default script scope.

I can’t seem find the correct syntax to do that. For the moment I’ve added a dirty hack using plusHours(1). I’m sure I’ll work it out before the last Sunday in October :slight_smile:

It would be nice if the same was true for js scripting (he said selfishly) but maybe there’s a good reason why it isn’t.

Thanks again for your help.

I looked it up for you

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

Many thanks again. More progress. There’s still an error message but now it’s that systemDefault() is unknown:

2024-04-01 14:38:24.152 [ERROR] [.script.file.octopusagileforecast.js] - Failed to execute rule Test-UTC-vs-local-time-607b2b3f-3fb5-44c7-97ef-860f37086f0f: TypeError: invokeMember (systemD
efault) on java.time.ZoneId failed due to: Unknown identifier: systemDefault: TypeError: invokeMember (systemDefault) on java.time.ZoneId failed due to: Unknown identifier: systemDefault
        at execute (octopusagileforecast.js:146)
        at execute (@openhab-globals.js:2)
2024-04-01 14:38:24.152 [ERROR] [e.automation.internal.RuleEngineImpl] - Failed to execute rule 'Test-UTC-vs-local-time-607b2b3f-3fb5-44c7-97ef-860f37086f0f': Failed to execute action: 1(Er
ror: Failed to execute rule Test-UTC-vs-local-time-607b2b3f-3fb5-44c7-97ef-860f37086f0f: TypeError: invokeMember (systemDefault) on java.time.ZoneId failed due to: Unknown identifier: syste
mDefault: TypeError: invokeMember (systemDefault) on java.time.ZoneId failed due to: Unknown identifier: systemDefault
        at execute (octopusagileforecast.js:146)
        at execute (@openhab-globals.js:2))

I found the SystemDefaultZoneId class and have tried a few variations of calling the id() method but all cause a runtime error.

Show your code. Did you have () after systemDefault?

Yup, the rule is now:

rules.JSRule({
  name: 'Test UTC vs local time',
  description: 'Test UTC vs local time',
  triggers: [triggers.ItemStateChangeTrigger('DEVTEST_SWITCH')],
  execute: data => {
    var { ZoneId } = require("@runtime");

    var logger = log('DEVTEST')

    logger.info('Test UTC from Octopus agile JSON vs localdatetime rule...')

    var agileprices = String(items.getItem('OctopusAgilePrices_AgilePricesJSON').state)
    var obj = JSON.parse(agileprices)

    for (var i = obj.results.length - 1; i >= 0; i--) {
      var valid_from = obj.results[i].valid_from;
      var valid_from_zdt = time.toZDT(valid_from);
      var valid_from_zdt_string = valid_from_zdt.toString();
      var valid_from_zdt_zone = valid_from_zdt.zone();
      // var valid_from_ldt = valid_from_zdt.toLocalDateTime();                      //get valid_from as LocalDateTime
      // var valid_from_ldt = valid_from_zdt.plusHours(1);                      //get valid_from as LocalDateTime
      var valid_from_ldt = time.toZDT(valid_from).withZoneSameInstant(ZoneId.systemDefault());                      //get valid_from as LocalDateTime
      logger.info("Index ({}), valid_From time ({}), as string = ({}), converted to local date time ({}).", String(i), String(obj.results[i].valid_from), valid_from_zdt_string, String(valid_from_ldt));
    }

  }
})

I tried this and found the solution:

// var { ZoneId } = require("@runtime") DONT USE THIS
const ZoneId = Java.type("java.time.ZoneId") // Use this instead

You shouldn’t mix up Java Time package with JS-Joda time!!

time.ZoneId

should work.

See Reference | js-joda and Manual | js-joda for JS-Joda docs.

Still doesn’t work, the error is now:

2024-04-01 15:23:48.812 [ERROR] [.script.file.octopusagileforecast.js] - Failed to execute rule Test-UTC-vs-local-time-16c0cf89-e483-40ef-abff-055387676d61: TypeError: invokeMember (rules)
on java.time.ZoneRegion@56cbc9a8 failed due to: Unknown identifier: rules: TypeError: invokeMember (rules) on java.time.ZoneRegion@56cbc9a8 failed due to: Unknown identifier: rules
        at value (@openhab-globals.js:2)
        at value (@openhab-globals.js:2)
        at execute (octopusagileforecast.js:147)
        at execute (@openhab-globals.js:2)
2024-04-01 15:23:48.812 [ERROR] [e.automation.internal.RuleEngineImpl] - Failed to execute rule 'Test-UTC-vs-local-time-16c0cf89-e483-40ef-abff-055387676d61': Failed to execute action: 1(Er
ror: Failed to execute rule Test-UTC-vs-local-time-16c0cf89-e483-40ef-abff-055387676d61: TypeError: invokeMember (rules) on java.time.ZoneRegion@56cbc9a8 failed due to: Unknown identifier:
rules: TypeError: invokeMember (rules) on java.time.ZoneRegion@56cbc9a8 failed due to: Unknown identifier: rules
        at value (@openhab-globals.js:2)
        at value (@openhab-globals.js:2)
        at execute (octopusagileforecast.js:147)
        at execute (@openhab-globals.js:2))

(I really appreciate all your help and patience)

Ahhhh so toZDT returns js-joda (something totally alien to me, not being a jsscripting user))

You shouldn’t mix up Java Time package with JS-Joda time!!

A-ha, that was the secret sauce:

var valid_from_ldt = time.toZDT(valid_from).withZoneSameInstant(time.ZoneId.systemDefault());

For completeness the rule is now:

rules.JSRule({
  name: 'Test UTC vs local time',
  description: 'Test UTC vs local time',
  triggers: [triggers.ItemStateChangeTrigger('DEVTEST_SWITCH')],
  execute: data => {
    // var { ZoneId } = require("@runtime");
    const ZoneId = Java.type("java.time.ZoneId") // Use this instead

    var logger = log('DEVTEST')

    logger.info('Test UTC from Octopus agile JSON vs localdatetime rule...')

    var agileprices = String(items.getItem('OctopusAgilePrices_AgilePricesJSON').state)
    var obj = JSON.parse(agileprices)

    for (var i = obj.results.length - 1; i >= 0; i--) {
      var valid_from = obj.results[i].valid_from;
      var valid_from_zdt = time.toZDT(valid_from);
      var valid_from_zdt_string = valid_from_zdt.toString();
      var valid_from_zdt_zone = valid_from_zdt.zone();
      // var valid_from_ldt = valid_from_zdt.toLocalDateTime();                      //get valid_from as LocalDateTime
      // var valid_from_ldt = valid_from_zdt.plusHours(1);                      //get valid_from as LocalDateTime
      var valid_from_ldt = time.toZDT(valid_from).withZoneSameInstant(time.ZoneId.systemDefault());                      //get valid_from as LocalDateTime
      logger.info("Index ({}), valid_From time ({}), as string = ({}), converted to local date time ({}).", String(i), String(obj.results[i].valid_from), valid_from_zdt_string, String(valid_from_ldt));
    }

  }
})

Which gives the following output:

<snip>
15:26:05 Index (5), valid_From time (2024-04-01T19:00:00Z), as string = (2024-04-01T19:00Z), converted to local date time (2024-04-01T20:00+01:00[SYSTEM]).
15:26:05 Index (4), valid_From time (2024-04-01T19:30:00Z), as string = (2024-04-01T19:30Z), converted to local date time (2024-04-01T20:30+01:00[SYSTEM]).
15:26:05 Index (3), valid_From time (2024-04-01T20:00:00Z), as string = (2024-04-01T20:00Z), converted to local date time (2024-04-01T21:00+01:00[SYSTEM]).
15:26:05 Index (2), valid_From time (2024-04-01T20:30:00Z), as string = (2024-04-01T20:30Z), converted to local date time (2024-04-01T21:30+01:00[SYSTEM]).
15:26:05 Index (1), valid_From time (2024-04-01T21:00:00Z), as string = (2024-04-01T21:00Z), converted to local date time (2024-04-01T22:00+01:00[SYSTEM]).
15:26:05 Index (0), valid_From time (2024-04-01T21:30:00Z), as string = (2024-04-01T21:30Z), converted to local date time (2024-04-01T22:30+01:00[SYSTEM]).

Many thanks to both of you for helping me! :slight_smile:

The good thing about JS-Joda is that its API is very similar to Java Time (I guess the authors of JS-Joda chose Joda from the Joda Java time package).
So if you know Java time, most stuff will work exactly the same with JS-Joda, only difference is that JS-Joda is plain JS and doesn’t pollute JS Scripting with Java types.

:+1:

For complete completeness (?) I had to tweak the above code slightly when updating the rule I was having problems with. In that rule some DateTime items are updated, these can’t be updated with timestamps that include the SYSTEM timezone such as:

2024-04-01T22:30+01:00[SYSTEM]

The SYSTEM timezone must be stripped off using the toLocalDateTime method. For example:

var h4_rate_start_time_local = time.toZDT(obj.results[h4_rate_lowest_index].valid_from).withZoneSameInstant(time.ZoneId.systemDefault()).toLocalDateTime();