OH 3 Examples: Writing and using JavaScript Libraries in MainUI created Rules

That’s what I was afraid of. I need to fix the library and I need to do a better review next time. A library should save it’s internal logger to this.

Ah, I see. Do you fix it by “wrapping the function” as you described above in the timeUtils.js?

(function(context) {

})(this)

Yes, and then only save things to context that you want available in the rule that loaded the library. Internal stuff used only by the library should be just vars.

Thank you for your help and explanation!

Hi,

I have started migrating from 2.5 to 3. Mi idea is to write all my rules in javascript.

I have created a rule that uses a timer that when it finish creates another timer in order to execute it self again and again.

Something like this:

function loop() {
if(some condition) stop();

this.timer = ScriptExecution.createTimer( next time, loop);
}

function stop() {
this.timer.cancel();
}

The problem comes when after modifying the code and saving, the handler of the timer is lost and it continues executing without the posibility to cancel it.

I don’t know how to manage with this because the previous context is lost and the handler with it

Any idea how to deal with this?

thanks in advance

The simple answer is, you can’t. As you already found out, the script context gets destroyed when the script is saved. Because there is currently no other way to store a timer instance, that was created on your behalf, it is lost. You can therefore neither reschedule or cancel this timer and it will fire eventually when it is due.

There is one exception to that. If you are writing the rules in .js files, assuming they work the same as Python .py files, you can define a scriptUnloaded function that will get called before the rule is destroyed. You can cancel the running timers there.

Note I don’t really know the mechanism behind it. It may be something unique to Python, or something implemented by the Helper Libraries and not something generally available.

I do know that it is true that we cannot do this clean up with UI created rules and that is where I’ve been focusing right now so I’ve a lot of unanswered questions about the mechanism.

The scriptLoaded and scriptUnloaded calls are done by the Script Engine Container on the OH side, so are language agnostic. That said, I never got anything working in scriptUnloaded in JS.

1 Like

Thanks for your response.
I think that this class has the same problem that I have said. If the loop function has not a condition to stop itself and the timer only stops when the calling script cancels it, if the script is modified and saved while timer is executing it will not be posible to stop it any more.

That’s going to always be a problem no matter what. If you use Python rules in .py files you can use scriptUnloaded to cancel the timers when the file is unloaded. Theoretically it should work with JavaScript .js files too but maybe not based on Michael’s experience.

Thanks

I’m going to explore then scriptUnloaded function proposed by @rlkoshak. If it does not work I will try to think in other ways to achive it

After setting log level:

org.openhab.core.automation.module.script.internal │ TRACE
org.openhab.core.automation.module.script.internal.ScriptEngineManagerImpl │ TRACE

I found out that at least for the scripts stored in files, the scriptUnloaded is called:

function scriptUnloaded() {
   logger.info("Script unloaded!!");
}

2021-01-31 11:23:38.689 [TRACE] [ipt.internal.ScriptEngineManagerImpl] - scriptLoaded() is not defined in the script: file:/openhab/conf/automation/jsr223/javascript/personal/PID.js
2021-01-31 11:24:15.381 [INFO ] [org.openhab.rule.automation.PID ] - Script unloaded!!
2021-01-31 11:24:15.394 [INFO ] [ab.core.service.AbstractWatchService] - Loading script ‘/openhab/conf/automation/jsr223/javascript/personal/PID.js’

When function is not defined, a trace message is written as it can be seen for scripLoaded()

Knowing this, using timer manager that stores timers by name like in the library by @rlkoshak is all that is needed to avoid looping timers running indefinitely. When the script that creates and uses the timer is loaded again, it will retrieve the timer by name. And when the timer library is unloaded, you can cancel all the running timers that otherwise the handlers would be lost.

On the other hand I will try to figure out how to get running the scriptLoaded function for UI rules too

2 Likes

I think the reason for scriptUnloaded not being executed in rules is because rules don have their own ScriptEngine. Rules are excuted by scriptEngine.eval(script) and I thing that sctiptEngine is singleton and when the rule changes is not getting unloaded as is done when a file based script changes. Therfore is not possible to define unloadedSctipt function that will get called as the rule changes

1 Like

This might not be the right place to ask, but I was looking through the timeUtils.js for hints. I’m trying to work with a DateTimeType (passed in as a string) in a transformation JS script. The string that gets passed is not valid ISO8601, as the timezone doesn’t contain a colon (ex -0800, not -08:00).

What would be the best way of handling that?

Thanks for doing the research and experimentation to figure this out and find the edge cases! It’s kind of sad that there is no way to clean up a rule (for now, and issue is probably worth creating to add something like this for UI rules too). But I’m really glad you found how to make it workin for .js files!

EDIT:

For this I would probably insert the colon into the String so it becomes ISO8601. That would probably be the easiest. Something like:

var iso8601 = i.slice(0, -2) + ":" + i.slice((i.length - 2), i.length);

Note, I just typed that in, the indices may be wrong. See JavaScript String Methods for the full details on slice.

2 Likes

For anyone that comes across this later, for my case, the DateTime string containing milliseconds was also problematic. Removing those helped:

ISO8601 = ISO8601.replace(ISO8601.match(/\.([0-9])+/g), '');

Hello @rlkoshak - I have a problem to cancel a running LoopTimer.
The timer is running as expected and is terminated as well if the condition in the function is true.

However, when I try to cancel the running timer I see an error message in the log file.

EDIT: I solved the error message - something was wrong in the function.
However, timer still doesn’t terminate when I try to cancel it.

  iLaufzeit = 0;
  
  var countdown = function (){
    
    iLaufzeit += 1;
    
    if(iLaufzeit <= iDauer) {
      events.sendCommand("Bewaesserung_Rasen_LaufzeitAktuell", iLaufzeit );
      return "1m"; }    
    else {      
      events.sendCommand("Bewaesserung_Rasen_Schalter", OFF );      
    }
      
  }

  this.lp.loop(countdown,"1m");

I use below to cancel the timer

   this.lp.cancel();

I added some debugging to the code to see why cancelling doesn’t work.

  /**
   * Cancels the running timer.
   */
  LoopingTimer.prototype.cancel = function() {
    this.log.debug("Looping timer - Canceling");
    this.log.debug("Looping Timer state: " + this.timer );
    this.log.debug("Looping Timer hasTerminated() " + this.timer.hasTerminated() );

      if (this.timer !== undefined && !(this.hasTerminated()) ){
          this.timer.cancel();
          this.log.debug("Looping timer - Canceled");
      }
  }

As below, the timer seems to be undefined, when checking this.timer.
When checking hasTerminated() the script raised a ERROR accordingly.


==> /log/openhab.log <==
2021-03-27 17:35:04.753 [DEBUG] [nhab.model.script.Rules.LoopingTimer] - Looping timer - loop called
2021-03-27 17:35:04.759 [DEBUG] [penhab.model.script.Rules.time_utils] - Converting duration 1m
2021-03-27 17:35:04.768 [DEBUG] [penhab.model.script.Rules.time_utils] - Match = 1m
2021-03-27 17:35:04.776 [DEBUG] [penhab.model.script.Rules.time_utils] - Days = 0 hours = 0 minutes = 1 seconds = 0 msec = 0
==> /log/events.log <==
2021-03-27 17:35:06.948 [INFO ] [openhab.event.ItemCommandEvent      ] - Item 'Bewaesserung_Rasen_Schalter' received command OFF
2021-03-27 17:35:06.948 [INFO ] [openhab.event.ItemStateChangedEvent ] - Item 'Bewaesserung_Rasen_Schalter' changed from ON to OFF
==> /log/openhab.log <==
2021-03-27 17:35:06.950 [INFO ] [enhab.core.model.script.Bewaesserung] -  Bewaesserung AUS - OFF
2021-03-27 17:35:06.951 [DEBUG] [nhab.model.script.Rules.LoopingTimer] - Looping timer - Canceling
2021-03-27 17:35:06.952 [DEBUG] [nhab.model.script.Rules.LoopingTimer] - Looping Timer state: undefined
2021-03-27 17:35:06.952 [ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID '077fd9674d' failed: TypeError: Cannot read property "hasTerminated" from undefined in /openhab/conf/automation/lib/javascript/community/loopingTimer.js at line number 54

Show the full code. I don’t see where you’ve imported any libraries.

I don’t see the code where this.lp is defined.

I don’t see where this.timer is defined.

Hello Rich,

below the full code I use - however I think I found the problem.

The error message only occurs if the function LoopingTimer.prototype.loop for the first iteration of the loop is executed, as this timer is not assigned to this.timer and therefore cannot be canceled, as far as I see.

Would that assumption make sense?

this.OPENHAB_CONF = java.lang.System.getenv("OPENHAB_CONF");
this.logger = (this.logger === undefined) ? Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.model.script.Rules.LoopingTimer") : this.logger; 
load(OPENHAB_CONF+'/automation/lib/javascript/community/loopingTimer.js');
var Log = Java.type("org.openhab.core.model.script.actions.Log");

this.lp = (this.lp === undefined) ? new LoopingTimer() : this.lp;


var dtStartZeit
var iDauer 
var iLaufzeit 


if (event.itemName === "Bewaesserung_Rasen_Schalter" && items["Bewaesserung_Rasen_Schalter"] === ON){
  
  // Dauer der Bewässerung bei Direktbetrieb ----- (Muss noch für Timerbetrieb angepasst werden)
  iDauer = parseInt(items["Bewaesserung_Rasen_Ausschalt_Timer"]);
  Log.logInfo("Bewaesserung" , "Dauer der Bewässerung " + iDauer + "min" );
  
  // Speichert Startzeit der letzten Bewässerung
  dtStartZeit = Date.now();
    
    
  // Init - Aktuelle Laufzeit
  events.sendCommand('Bewaesserung_Rasen_LaufzeitAktuell', 0);
  
  // Bewaesserung_Rasen_LaufzeitGesamt = Bewaesserung_Rasen_Ausschalt_Timer
  events.sendCommand('Bewaesserung_Rasen_LaufzeitGesamt', items["Bewaesserung_Rasen_Ausschalt_Timer"]);
  
  iLaufzeit = 0;
  
  var countdown = function (){
    
    iLaufzeit += 1;
    events.sendCommand("Bewaesserung_Rasen_LaufzeitAktuell", iLaufzeit );
    Log.logInfo("Bewaesserung" , "Dauer der Bewässerung " + iLaufzeit + "min / " + iDauer + "min" );
    
    if(iLaufzeit < iDauer) {
      
      return "1m"; } 
    
    else {      
      events.sendCommand("Bewaesserung_Rasen_Schalter", OFF );      
    }
      
  }

  this.lp.loop(countdown,"1m");
  
}