ECMAScript-2021 - variable error on the second time the rule is run?

Hi there,

I am trying to make a start with the new javascript rules and have run into a challenge.

When the rule runs the first time it works just fine and gives the expected output, at the moment just a log entry but down the track hopefully a bunch of interesting stuff.

But when the same rule runs for the second time with the same data, it gives an error,

2022-01-17 19:50:02.304 [ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID 'GPS-GW' failed: org.graalvm.polyglot.PolyglotException: SyntaxError: Variable "CircularGeofenceRegion" has already been declared

Could someone point me in the right direction.
Any opinions on improvements would also be welcome, just learning a bit of JS.

var logger = Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.model.script.Rules.Experiments");
var ScriptExecution = Java.type("org.openhab.core.model.script.actions.ScriptExecution");
var NotificationAction = org.openhab.io.openhabcloud.NotificationAction;

var gwLocation = items.getItem("GWsiPhoneiPhone13_Location").state;    

var fields = gwLocation.split(',');
var gwLat = Number(fields[0]);
var gwLon = Number(fields[1]);

logger.info(`GW latitude ${gwLat}`);
logger.info(`GW latitude ${gwLon}`);

/////////////////////////////////////////////////////////////////////////////////////

    class CircularGeofenceRegion {
        constructor(opts) {
          Object.assign(this, opts)
        }
      
        inside(lat2, lon2) {
          const lat1 = this.latitude
          const lon1 = this.longitude
              const R = 63710; // Earth's radius in m
      
          return Math.acos(Math.sin(lat1)*Math.sin(lat2) + 
                           Math.cos(lat1)*Math.cos(lat2) *
                           Math.cos(lon2-lon1)) * R < this.radius;
        }
      }
        
      const fenceA = new CircularGeofenceRegion({
        name: 'home',
        latitude: -xx.303915,
        longitude: xxx.933684,
        radius: 75 // meters
      });
    
      const fenceB = new CircularGeofenceRegion({
        name: 'work',
        latitude: -xx.407669,
        longitude: xxx.886176,
        radius: 50 // meters
      });
    
      const fenceC = new CircularGeofenceRegion({
        name: 'Chair',
        latitude: -xx.305422,
        longitude: xxx.937532,
        radius: 20 // meters
      });
        
      const fences = [fenceA, fenceB, fenceC]           // add others [fenceA, fenceB]
      
        for (const fence of fences) {
          if (fence.inside(gwLat, gwLon) && fence === fences[0]) {
            logger.info(`You are at ${fenceA.name}.`);        
          } else if (fence.inside(gwLat, gwLon) && fence === fences[1]) {
            logger.info(`You are at ${fenceB.name}.`);
          } else if (fence.inside(gwLat, gwLon) && fence === fences[2]) {
            logger.info(`You are at ${fenceC.name}.`);
        }
        }

I made the same experience, although I do not know if this is regarded as a feature by design or a bug. The problem here is that I could not find any hints on the lifecyle of a scripts variables. The problem arises, if you use the javascript statements let or const for declaring variables. Their lifecyle seems to persist between script invocations. On the second invocation of a script these variables are already defined and so give you the error in the log.

In your case the error log indicates that the variable CircularGeofenceRegion is already defined. Since this is a class, I guess that the class definition is not the problem here but the constant definitions inside them.

Hi,
Rich described this in migration-tutorial under “disadvantages” : Migrate from Nashorn to JSScripting for UI Rules

Thanks Frank & Markus

When I change the const variables to var the same error happens on the second run of the rule. Interestingly when I disable and re enable the rule it again runs fine for the first execution then gives the error on the second run.

Maybe the class declaration is an issue as well.

Are there any potential workarounds for this at the moment?

Cheers,

I am myself just starting to migrate my old files/rules-DSL stuff to JSScripting so I can unfortunately not be of much help here.
Perhaps putting the class into an external file and loading this makes a difference here ?
See how @rlkoshak does this in his bag of rules here : https://community.openhab.org/t/some-js-scripting-ui-rules-examples/131305

Multiple execution of the same rule / script will share the same context.
Only if you update/save a rule or restart OH it’s considered to run in a new context. Therefore it’s working the first time, but the 2nd time you run the rule, your rule will do something that can only be done once.
Maybe the script is failing because you try to define a class that’s already existing in the 2nd run

I also started converting my rules to ECMAScript 2021 rules recently. To work around the “Variable already declared” error I went old school: I wrapped the rule in an anonymous function that is run immediately. So, something like

(function() {
  //rule code added here...
})();

I don’t know if it’s the best way of doing things but my rules run without a problem.

And, if I need to persist a value between rule-runs I use the cache key-value store.

Sooo, does that make a new anonymous function every run, and slowly gobble up memory …

Thanks, that is an interesting workaround, I will give it a try.

I would like to run this type of rule every 5 mins or so for a few people’s GPS coords so I hope it doesn’t gobble up the memory. Only one way to find out.

Cheers

1 Like

Running a rule every X minutes should be avoided, because OH is event based.

You could store lat & lon of each person in an item and only run the rule once this is changing.

Also if a rectangular geo-fence region will also work for your usecase instead of a circular than you can simple compare lat & lon with < & > operations in a very simple rule.

Thanks everyone for your thoughts, Rob your suggestion looks the be working fine, thanks!

It might not be the best way but wrapping the all the JS scrip in a function works.

gwGeofence()

function gwGeofence(){

let gwLocation = items.getItem("GWsiPhoneiPhone13_Location").state;    
let fields = gwLocation.split(',');
let gwLat = Number(fields[0]);
let gwLon = Number(fields[1]);

console.log(`GW latitude ${gwLat}`);
console.log(`GW latitude ${gwLon}`);
  
  
    class CircularGeofenceRegion {
        constructor(opts) {
          Object.assign(this, opts)
        }
      
        inside(lat2, lon2) {
          const lat1 = this.latitude
          const lon1 = this.longitude
              const R = 63710; // Earth's radius in m
      
          return Math.acos(Math.sin(lat1)*Math.sin(lat2) + 
                           Math.cos(lat1)*Math.cos(lat2) *
                           Math.cos(lon2-lon1)) * R < this.radius;
        }
      }
        
      const fenceA = new CircularGeofenceRegion({
        name: 'home',
        latitude: -xx.303915,
        longitude: xxx.933684,
        radius: 75 // meters
      });
    
      const fenceB = new CircularGeofenceRegion({
        name: 'work',
        latitude: -xx.407669,
        longitude: xxx.886176,
        radius: 50 // meters
      });
    
      const fenceC = new CircularGeofenceRegion({
        name: 'Chair',
        latitude: -xx.305422,
        longitude: xxx.937532,
        radius: 20 // meters
      });
        
      const fences = [fenceA, fenceB, fenceC]          
      
        for (const fence of fences) {
          if (fence.inside(gwLat, gwLon) && fence === fences[0]) {
            console.log(`You are at ${fenceA.name}.`);        
          } else if (fence.inside(gwLat, gwLon) && fence === fences[1]) {
            console.log(`You are at ${fenceB.name}.`);
          } else if (fence.inside(gwLat, gwLon) && fence === fences[2]) {
            console.log(`You are at ${fenceC.name}.`);
        }
        }
}

Hi Matthias

A rectangular geo-fence might work ok, do you happen to have an example of JS code for that?

I have the GPS coords coming into items via the iCloud binding for a few family members and was thinking a simple time based trigger would be suitable. The location data seems to be coming in every 5-10 minutes. Perhaps that it is better trigger.

Cheers

E.g. if you draw a rectangle geofence around liberty island in New York, I would do the following:

lat_min: 40.692397
lat_max: 40.687548
lon_min: -74.048557
lon_max: -74.041647

what would result into the following rectangular:


(source)

You can simple check if the provided lat & lon values are within the range (=someone is on or near the island) or not.
The only thing you need are < & > operators and the lat / lon definition of your rectangular geofence
If you want to check on multiple geofences than I would put the min & max values into a map and loop through the map:

  • Fence 1
    • lon_min:
    • lon_max:
    • lat_min:
    • lat_max:
  • Fence 2
    • lon_min:
    • lon_max:
    • lat_min:
    • lat_max:

I don’t think so. Since it’s anonymous and not assigned to any variables when the script exits there won’t be any more references to the function and it becomes garbage collected eventually.

That is my assumption too: garbage collection should clean this up. I’m planning on doing some experimentation shortly to make sure. I’ll post my findings when I have more insights.

I ran a test rule every second for about 30 minutes and saw no increase of the memory usage of my docker container that runs openhab. The rule consisted of getting a few items, settings some variables, caching an item state and printing to the console.

Running the experiment in combination with the handful of other ecmascript-2021 rules that run continuously for me without issues, makes me conclude that the garbage collector is doing its job.