Timeconversion: Astro Binding (DateTime) to Date() javascript object

Hello everyone,

I am a bit lost with the time calculations right now when using javascript. I already checked out this nice summary DateTime Conversion (openHAB 3.x) but I assume that it only works for DSL Rules?

I am using the Date object of javascript to save some times and want to compare them to the beginning of the sunset which I receive from the Astro Binding.

  • The SunSet Item stores a DateTime object and looks like this when I run itemRegistry.getItem("LokaleSonnendaten_Set_Start").getState();" → “2021-09-23 07:13”.
  • The code var now = new Date(); shows “Thu Sep 23 2021 20:10:08 GMT+0200 (CEST)”

Now I want to compare “now” to the sunset time. From what I found out, the javascript Date() object should be comparable to Epoch and the DateTime from Astro binding is a String in Format in ISO 8601 standard.
So I tried all paths to convert to Epoch without success. Is that the right route to try further, or am I completely wrong here?

No. Every time you are interacting with OH in rules, no matter what the rules language, you are using Java Objects. Items, Item states, Actions like createTimer, and everything else are all Java Objects. That linked tutorial shows how to use the Java ZonedDateTime and openHAB DateTimeType classes to do date time operations. Since it’s Java, no matter what language you are using will be largely the same.

You could convert a ZonedDateTime (or more likely a LocalDateTime) to a JavaScript DateTime Object, but then you’d have to convert it back to Java to use it in any sort of interaction with OH. Since the whole point of rules is to interact with OH, why bother? It’s mostly just creating more work and complexity for you. Just use the Java stuff.

No, it stores a DateTimeType Object which is a Java class.

So use ZonedDateTime like demonstrated in the link.

ZonedDateTime.now().isBefore(items["LokaleSonnendaten_Set_Start"].getZonedDateTime())

Unfortunately though you’ll have to “import” ZonedDateTime to do this.

var ZonedDateTime = Java.type("java.time.ZonedDateTime");

Or you can create a new DateTimeType which will be initialized to now.

new DateTimeType.getZonedDateTime().isBefore(items["LokaleSonnendaten_Set_Start"].getZonedDateTime())

Notice how I don’t mess with the itemRegistry. There is no point in doing that here either. There is a dict of all the Item’s state injected into the rule for your use.

So do I need to write this stuff into the DSL Rule editor?

I like the new possibility to trigger a javascript script because it is clean and has a good readability. Usually I create the rules like that: "If X triggers/updates/receivesCommand → Run Script → Script checks the details of the trigger/update/command.

Since I am doing more calculations inside that rule from above I’d like to stay within javascript for the time check. I guess that the reason why all attempts failed so far is that the examples are Java and I am using the javascript rule editor. So I need to somehow find a way to transform ISO 8601 into a javascript new Date() object.

Nice, didnt know that and tried it directly with the .getState() method which runs into an error (entry not found in the enum. Is this not available for all interactions? Or may this again be linked to the javascript text editor?

Seems to work with
var sonnenuntergang = new Date(itemRegistry.getItem("LokaleSonnendaten_Set_Start").getState())

No, as I said, except for minor differences (e.g. like the need and how to import ZonedDateTime) the lines of code will be the same no matter what rules language you are using.

The code I typed above is the code you’d type into a JavaScript Script Action.

The editor is irrelevant. It’s irrelevant whether you are coding in text rules or in the UI. Those are the lines of code to use.

Rules DSL to compare the state of an Item to now

if(now.isAfter(MyDateItem.state.getZonedDateTime){
    // do something
}

Python

from java.time import ZonedDateTime

if ZonedDateTime.now().isAfter(items["MyDateItem"].getZonedDateTime():
    # do something

JavaScript

var ZonedDateTime = Java.type("java.time.ZonedDateTime");

if(ZonedDateTime.now().isAfter(items["MyDateItem"].getZonedDateTime())) {
    // do something
}

Notice, except for minor differences based on the specific syntax used, they are basically almost identical lines of code. And it’s possible to easily understand what each one does regardless of your preferred langauge.

Look at my examples and description again. It’s a dict of the Item’s states. The key is the Item’s name. The value is the Item’s state, not the Item. You don’t call getState() on an Item’s state. It’s already the Item’s state. If you want the state of Item “Foo”

`items["Foo"]`

That’s it. Nothing more.

I’m really not a proponent of trying to be “pure” in rules. When you go out of your way to use pure JavaScript stuff when there is an openHAB native way to do something (e.g. working with date times):

  • You end up needing to do extra conversion back and forth from the native OH Objects to the pure language version (e.g from OH’s DateTimeType to JavaScript Date and back again) any time you need to use that date time to interact with OH (e.g. createTimer, minimumSince, etc.).

  • You end up needing to learn two ways of working with DateTimes because there will be cases where you have to use the Java classes at some point because you can’t for example, schedule a Timer using a JavaScript Date Object. Consistency is a really useful way to reduce the complexity and make code easier to understand.

  • It’s harder to look at a Rules DSL or Python (or some other supported language) example and easily convert it to JavaScript and if you share any code it’s harder for others to look at and understand your rules if they are not JavaScript. As a result all these “pure to the language” implementations basically become siloed into a language specific communities who end up unable to mutually understand each other.

2 Likes

Ooh I may have completely overlooked that inside the documentation. I was on the wrong understanding based on my interpretation of the scripting engine selection (screenshot).
So in my own words: The ruleengine doesn’t matter, they all understand the same Java content from your post but I can use the engine’s coding language to work with that content which you posted (such as use it in if clauses). Is that right?

The content you posted is the same as it was handled inside the old OH2 rules files?

grafik

Right. The over all syntax of the code will follow the language chosen. And there are other differences of course (e.g. how to import things, working with lists, etc) which will be different for the different languages. But as soon as you touch something that is specific to openHAB you are working with Java and it works the same no matter what the langauge.

The code fragments are the same no matter where you put it, be it in a Script Action in a UI rule or in a .rules file or .py file or .js file. The code is the code no matter where you type it in.

1 Like

Thanks for the clarifications so far! I somehow got the impression when reading some ressources that the old way was or will be depricated so I somehow assumed that there is a downwards compatibility right now which will be deprecated soon and the “new possibilities” are really something new and different. But it is clear now! Thanks again.

Outch, okay, makes sense!

For the last part of your previous post I will go through it in detail tomorrow with a fresh mind.

That’s acually a really good point. I have not yet migrated Timers so I will run into that issue next when rewriting my rules inside OH3. So I will get into detail with that OH3 way of solving issues.

As a beginning I wanted to re-read the Rules documentation (Rules | openHAB) again for a better understanding but to be honest, the content you explained to me was far better than the documentation. It is completely misleading in my eyes. For example it tells me - as an ACTION - to start the DSL editor. Below that text, every example and explanation is java code. Hence my assumtion was that Java comes into Rule DSL. There is no word of mixing around and being able to put in the generic OH stuff into javascript / python etc. But I know it now based on your really good explanations.

Next issue, the syntax for the rule (which is entered as an ACTION (see above)) has a rule and a when block, which should not be required because both of them are given inside the UI part of the rule editor. I would assume that only the then block is required here but the documentation tells me something different. So my assumtion was that the documentation might be wrong here and that this part is only valid for text based rules in the old way.

rule "<RULE_NAME>"
when
    <TRIGGER_CONDITION> [or <TRIGGER_CONDITION2> [or ...]]
then
    <SCRIPT_BLOCK>
end

And as a last example, I cannot find your hint with the items[“itemname”] inside the rules and inside the items section of the documentation

Could you tell me if there is an other resource where I get read the details of rules and item handling to set up my rules in a way which fits to all your hints?

Just to head off a problem you will encounter when you do migrate your Timers. There is no such thing as a “global” variable in OH. There is such a thing as a variable global to a specific file, but you don’t have a file in UI rules. Each Script Action and Script Condition lives in isolation. So how can you manage Timers after they are created?

There is a trick in JavaScript and a similar one for Python (not in Rules DSL which, despite what you read is in no danger of becoming deprecated) that lets us take advantage of the fact that the Script Action is itself an Object and the same Object gets reused every time it’s called. Therefore we can initialize a variable the first time it’s called and each subsequent time use the old value instead. In JavaScript that looks like:

this.myTimer = (this.myTimer === undefined) ? null : this.myTimer;

That line will initialize myTimer to null if it’s not defined. Otherwise it keeps the value it already has. That will let you preserve the handle on a Timer from one run of a Script to the next.

You cannot yet share variables between Scripts though so if you’ve had multiple rules to work with one Timer that will have to be reworked so it’s all in the one Script.

No, it’s Rules DSL code which uses Java Objects as explained above. While there is an in-work add-on to write rules in Java, it’s not yet fully implemented and the docs you reference do not talk anything about Java. They only talk about Rules DSL. However, all rules no matter how they are written and no matter in what language are interacting with OH and unless it’s an external rules engine like HABApp or NodeRed, it’s doing so through the same Java Objects.

Therefore most if not all the examples you see, no matter what language they are written in, should be translatable fairly easily since most of the important bits are interacting with OH which is going to be the same almost character for character.

The Rules Documentation is for how to write rules in Rules DSL in .rules files. That’s still and always will be supported. It’s not wrong.

But as you surmise, indeed the rule proforma nor the triggers. Those are handled by UI form. You also have a but only if section of the rule in the UI where you can create a Script Condition which is a separate block of code that can be run to determine if the rule should run or not.

A common assumption that many run into is that UI rules have replaced the old ways of doing rules. They have not. The old ways using text files are still valid and will be supported into the foreseeable future.

1 Like

Is this the solution to stop a timer runs twice as well?
Usecase: a fictional powersocket of a coffee machine should switch to off 4hrs after it was turned on. So after it is turned on the rule starts a timer of 4hrs to turn the socket off again.
Now I immediately turn it off by myself and turn it on 3hrs and 55 minutes later again. Will the old timer still run so it may turn off the coffee mashine 5 minutes after it was turned on?

In one of my old rules I had a (different) usecase handled like that:

rule "Licht und SONOS - Leaving Home"
when
    Item Dummy_Anwesenheit changed
then
    timer_Abwesenheitspuffer?.cancel() 
    timer_Abwesenheitspuffer = null
    if (Dummy_Anwesenheit.state == "Abwesend") {
        timer_Abwesenheitspuffer = createTimer(now.plusMinutes(1), [| 
            if (Dummy_Anwesenheit.state == "Abwesend") { 
                GP_Gesamtes_Licht_Wohnung.sendCommand("OFF")
                GP_Sonos.sendCommand("OFF") 
            }
        ])
    }
end

The “old” timer -if existing- gets cancelled and a new one will be created.

To take this up again, I solved the issue from above in javascript but as you stated it may be better to use the proper OH way. So I tried to switch the working javascript rule over to the OH way (at least as far as I understood it).

The rule in javascript (this may look amateurish but it really easy to understand for me - even in 6 month I can follow the idea of the script - so a high maintainability for a non professional coder is key):

var logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + ctx.ruleUID);

var jetzt = new Date();

var sonnenuntergang = new Date(items["LokaleSonnendaten_Set_Start"]);

var fruehestens = sonnenuntergang
var offset_hr = 0
var offset_min = 0
var offset_sec = 0
fruehestens.setTime(fruehestens.getTime() + (offset_hr * 60 * 60 * 1000)); // Convert hours to milliseconds
fruehestens.setTime(fruehestens.getTime() + (offset_min * 60 * 1000)); // Convert minutes to milliseconds
fruehestens.setTime(fruehestens.getTime() + (offset_sec *  1000)); // 

var spaetestens = new Date();
spaetestens.setHours(23,30,0);

if(jetzt >= fruehestens && jetzt < spaetestens){
  logger.info("Coming Home getriggert! Jetzt: " + jetzt + ", frühestens: " + fruehestens + ", spätestens: " +spaetestens);
  events.sendCommand("WohnzimmerLampen", "ON");
} 
else {
  logger.info("Coming Home NICHT getriggert! Jetzt: " + jetzt + ", frühestens: " + fruehestens + ", spätestens: " +spaetestens);
}

Now I try to move it over to the other way, after checking out ZonedDateTime (Java SE 11 & JDK 11 ) and Set the time-of-day on a ZonedDateTime in java.time? - Stack Overflow (again I try to have it as clear as possible and minimize queued commands. I’d rather have more easy understandable lines than less but squeezed code):

var logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + ctx.ruleUID);
var ZonedDateTime = Java.type("java.time.ZonedDateTime");

var jetzt = ZonedDateTime.now();

var sonnenuntergang = items["LokaleSonnendaten_Set_Start"].getZonedDateTime();

var fruehestens = sonnenuntergang;
var offset_sec = 0;
var offset_min = 0;
var offset_hr = 0;
fruehestens.plusSeconds(offset_sec);
fruehestens.plusMinutes(offset_min);
fruehestens.plusHours(offset_hr);

//This is where I am lost now... the following approach does not work
var spaetestens_hr = 23;
var spaetestens_min = 30;
var spaetestenns = ZonedDateTime.now();
spaetestens.with(LocalTime.of(spaetestens_hr , spaetestens_min));
logger.info(spaetestens);

// this is not transformed yet
if(jetzt >= fruehestens && jetzt < spaetestens){
  logger.info("Coming Home getriggert! Jetzt: " + jetzt + ", frühestens: " + fruehestens + ", spätestens: " +spaetestens);
  //events.sendCommand("WohnzimmerLampen", "ON");
} 
else {
  logger.info("Coming Home NICHT getriggert! Jetzt: " + jetzt + ", frühestens: " + fruehestens + ", spätestens: " +spaetestens);
}

From my understanding I would need to create a ZonedDateTime Object for spaetestens now which can then be compared to the other two times. But I am really confused of how to create this part right now.

That is the absolutely most important thing to achieve in your code. Take pity on future Rey and make your code easy to understand. :smiley:

Why not do it the same way you did with fruehestens and use withHour and withMinute? Then you don’t need to import and use LocalTime (that’s probably one of your errors, you’re missing Java.type("java.time.LocalTime");)

Personally, I find it easier to follow when the code almost reads like prose and calling withX does that better than using with.

However, you have an error in fruegestens. When you call plusX or even with that doesn’t change the ZonedDateTime, it returns a new ZonedDateTime with the change applied. So:

var offset_sec = 0;
var offset_min = 0;
var offset_hr = 0;
var fruehestens = sonnenuntergang.plusSeconds(offest_sec);
fruehestens = fruehestens.plusMinutes(offset_min);
fruehestens = fruehestens.plusHours(offset_hr);

That’s why you often see them all combined on the same line.

var fruehestens = sonnenuntergang.plusSeconds(offest_sec)
                                 .plusMinutes(offset_min)
                                 .plusHours(offest_hr);

It avoids repeated assignments to the same variable. Also note that just because it’s one line of code doesn’t mean it has to be all on one line of the text. Personally, I find it easier to follow if the offset variables are removed too. It’s pretty self explanatory to just put the numbers in the function calls instead of creating a single use variable.

So doing it the same way with spaetestens

var spaetestens_hr = 23;
var spaetestens_min = 30;
var spaetestens = ZonedDateTime.now(); // NOTE: you have a type here, two "nn" but only one "n" everywhere else
spaetestens = spaetestens.withHour(spaetestens_hr);
spaetestens = spaetestens.withMinute(spaetestens_min);

Or more compressed:

var spaetestens_hr = 23;
var spaetestens_min = 30;
var spaetestenns = ZonedDateTime.now()
                                .withHour(spaetestens_hr)
                                .withMinute(spaetestens_min);

and without the variables

var spaetestenns = ZonedDateTime.now()
                                .withHour(23)
                                .withMinute(30);

One other note. You have a syntax error in your code above. You defined spaetestenns but then try to use spaetestens. That will generate an error in the logs. When working on a rule, always look in the logs. When asking for help, always post the relevant logs. The logs are vital to understand what is going on when your code runs. You should have seen an error when running the above along the lines of “undefined doesn’t have a function with”.

1 Like

That’s the way :slight_smile:

Yep, that was exactly what I was looking for but I somehow overlooked that - thanks for the hint! This is way better.

Correct as well - after placing some log-outputs I saw that the time didn’t change and had the next questionmark.
Here is the updated version thanks to your input:

var logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + ctx.ruleUID);
var ZonedDateTime = Java.type("java.time.ZonedDateTime");

var jetzt = ZonedDateTime.now();

var sonnenuntergang = items["LokaleSonnendaten_Set_Start"].getZonedDateTime();

var offset_sec = 0;
var offset_min = 0;
var offset_hr = 0;
var fruehestens = sonnenuntergang.plusSeconds(offset_sec).plusMinutes(offset_min).plusHours(offset_hr);

var spaetestens_hr = 23;
var spaetestens_min = 30;
var spaetestens = ZonedDateTime.now().withHour(spaetestens_hr).withMinute(spaetestens_min);

if(jetzt >= fruehestens && jetzt < spaetestens){
  logger.info("Coming Home getriggert! Jetzt: " + jetzt + ", frühestens: " + fruehestens + ", spätestens: " +spaetestens);
  events.sendCommand("WohnzimmerLampen", "ON");
} 
else {
  logger.info("Coming Home NICHT getriggert! Jetzt: " + jetzt + ", frühestens: " + fruehestens + ", spätestens: " +spaetestens);
}

That code seems to work as well and has the same readability as the javascript version with the benefit of being able to reuse the values for OH and Timers.

True, especially with the methods plusHours etc.

This gives new objects as well, doesn’t it? At least the time didn’t change when running it like that. The linked version works though.

Is there a trick to align the “.” when indenting? Or do I need to use Tab and Space?

Yep, I found that typo already. But thank you for the hint!
I have the logs open all the time and I use a lot of logging lines which I removed in my code quotes here to keep it shorter.

EDIT: I recently postet 2 replys after each other - could you kindly have a look on this post here as well? It addresses the Timer topic, which will need to be migrated next: Timeconversion: Astro Binding (DateTime) to Date() javascript object - #11 by Rey

Yes, both withHour and plusHours will give a new ZonedDateTime. Date time classes in Java are like Strings. They are immutable. You cannot change their value after they’ve been created. You can only create a new one with changes.

That’s weird because both versions are functionally equivalent.

I always use spaces. I configure my editors to convert tabs to spaces. You can’t even use tabs when editing UI rules because the browser treats that as a way to advance to the next field. Just add spaces until they line up.

Code editors have some stuff built in to make it work with just a key combo but the web based editor for UI rules does not.

So after reading a bit in Use of Timers within OH 3 - #22 by rlkoshak, I created the Rule with the timer now inside OH3.

The old OH2 rule

var Timer timer_Abwesenheitspuffer = null

rule "Licht und SONOS - Leaving Home"
when
    Item Dummy_Anwesenheit changed
then
    timer_Abwesenheitspuffer?.cancel()
    timer_Abwesenheitspuffer = null

    if (Dummy_Anwesenheit.state == "Abwesend") {
        timer_Abwesenheitspuffer = createTimer(now.plusMinutes(1), [| 
            if (Dummy_Anwesenheit.state == "Abwesend") {
                GP_Gesamtes_Licht_Wohnung.sendCommand("OFF")
                GP_Sonos.sendCommand("OFF") 
            }
        ])
    }
end

So the timer gets “resetted” when the rule triggers again.

My OH3 rule looks like that now (only the “then” part):

var logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + ctx.ruleUID);
var ScriptExecution = Java.type("org.openhab.core.model.script.actions.ScriptExecution");
var ZonedDateTime   = Java.type("java.time.ZonedDateTime");

if (this.pufferTimer) {
  this.pufferTimer.cancel();
}
this.pufferTimer = null;
//this.pufferTimer = (this.pufferTimer === undefined) ? null : this.pufferTimer;

if (items["Dummy_Anwesenheit"] == OFF) {
  pufferTimer = ScriptExecution.createTimer(ZonedDateTime.now().plusMinutes(1), function(){
    events.sendCommand("GP_Gesamtes_Licht_Wohnung", OFF);
  });
}

I am not really sure what the line (coming from your hint) this.pufferTimer = (this.pufferTimer === undefined) ? null : this.pufferTimer; should do for me here. In my own words I think that it will check if a Timer is already running, if yes it will be kept and if no (because it already ran) it will be “nulled”. But what I need is a Timer reset.

So what I tried to do is to check if a Timer is already running. If yes it is cancelled and if no it just goes on and creates a timer.
It is basically a simple “leaving home” rule with a short buffer.

Did I do the right thing here?

EDIT: It seems to work…

If a timer already exists and you don’t want two running, you need to cancel it just like always. How you interact with the timer doesn’t change at all. The only thing that’s different is that instead of defining the timer as a global (to the file) variable so it’s not overwritten when the rule runs again, you use the above line of code to define the timer variable. After that point you treat it exactly the same as you use timers now.

You don’t have to do that. You could just reschedule the timer.

In JavaScript…

this.timer_Abwesenheitspuffer = (this.timer_Abwesenheitspuffer === undefined) ? null : this.timer_Abwesenheitspuffer;
if(this.timer_Abwesenheitspuffer !== null) {
  this.timer_Abwesenheitspuffer.reschedule(...
}
else{
  create timer(...

Okay so that is really strange because after my tests I assumed that the javascript rule from above seems to work properly - without the line this.pufferTimer = (this.pufferTimer === undefined) ? null : this.pufferTimer; . But maybe my tests were wrong or too limited.

I forgot to explain what the rule does. So basically once the Dummy_Anwesenheit (Switch that represents presence) receives an update (doesn’t matter if ON or OFF) the rule triggers. I want to handle the difference of ON/OFF inside the rule itself.

Then the rule cancels the timers. That is important for the case that the presence switches to ON. Especially when the rule triggered and the timer is running but I come back during the timer duration. To avoid that I am available again but the timer keeps running it would turn off al devices. I did not wrap the cancel statements into an if clause checking if the Switch is ON to avoid any situation where the switch might be undefined or so.

If the presence switch updated to OFF, the timer is set to turn off some lights and more stuff.

Based on this usecase I think that the if (...) {...reschedule...} else {...create...} approach does not fit. Do I see that right?

It might work. My point was that there isn’t anything really special about Timers in JS except for the fact that you have to work around the fact that there is no such thing as a global. As written though that is probably happening. undefined is falsey so if(this.pufferTimer) will evaluate to false if this.pufferTimer is undefined.

You don’t actually manage the Timers here. You just blindly cancel it if it already exists and create a new one if it’s needed. So you can take this sort of shortcut without problem. However, if you need to actually interact with the Timer, such as rescheduling it, you need to preserve the value assigned.

If you ever hav questions or doubts about what is going on, add lots of logging.

Not necessarily. You can implement this with a reschedule too. As with many things, there are multiple ways to solve any given problem.