Keeping state in ECMAScript rules in OH3?

A lot of the rules I want to write have some simple state, e.g. an index into an array of scene, or the duration since the last fire, etc. In a .rules file it’s easy to keep this in a var. But I want to leverage the ability to edit and see rules in the UI. Is there a supported way to maintain that kind of state in a rule? (I guess I could create a global object outside of the UI and stuff everything there, but it’d be nicer to have access to something rule-specific.)

Suggestions?

Nit.: Could the script dialog (when triggered by Edit Script in Edit Module) be made wider and/or resizable? It’s pretty small for coding, even simple code (it’s <70 characters wide). And do syntax coloring like the bigger window view? The two view would be consistent.

This might not be what you are looking for but it sounds to me like you could use an item for such things

How do I declare a global variable in ECMAScript in the UI rule editor? I don’t think that’s an option from playing with it, variables seem to be local.

If you need the data to be persisted between OH restarts, you will need to use an Item or write the data to a file. If you only need to share the data between other rules in a script file, you can use a global variable. If you need to share the data between multiple rules created from multiple scripts and do not need it to be persisted, then you can create your own ScriptExtension. I touch on the latter here…

How do I declare a global variable in ECMAScript in the UI rule editor? I don’t think that’s an option from playing with it, variables seem to be local.

Global variables can be used in a script file. I do not believe anything has been implemented for global variables in a UI created rule. You can use a ScriptExtension in a ScriptedAction or ScriptedCondition though. I have not specifically tested this in OH3 using ES.

What is a “ScriptExtension” in this context? Googling didn’t help me.

Here’s a concrete example: I have a Hue tap switch and I want to use one of its buttons to do:

  • If any of my (living room, kitchen) lights are on, remember their state and turn them all off, or
  • If they are off, restore them to how they were beforehand

I’d love to write this in the UI and would need to store their previous state somewhere. If I can make any kind of object available to my rule I could store things there. I don’t think I could use an Item to store all the state? I could I guess use one Item per light (I have four or five of them) I guess, right?, but that’ll clutter my Items view just for some rule internal.

A previous state Item for each Item is an option. You could also store the previous state in the Item’s metadata. But this sounds like a job suited perfectly for persistence.

In regards to ScriptExtensions, the OOTB presets are outlined here…

There are facilities to create your own. Check the link I provided two posts up.

Yes

who cares? I mean I get what you are saying but it doesn’t really cost anything and is kind of an OpenHAB way of doing stuff

Which language are you using in the UI?

Click the [ ] looking icon. That will make the editor cover the full page.

It depends on how widely you want to share the variable. If you want to just preserve a variable from one run of the rule to the next you can set it in the context.

// Saving values from one run to another
var logger = Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.model.script.Rules.Examples");
logger.info(this);
logger.info("test_count = " + this.test_count);
if(this.test_count === undefined) {
  this.test_count = 0;
}
logger.info("test_count = " + this.test_count);
this.test_count += 1;
logger.info("test_count = " + this.test_count);

The context (this) gets reused from one run of the rule to the next. So anything put put into there will be preserved across one run of the rule to the next. However, be aware that if you modify or otherwise reload the rule, the context will become reset, similar to how a global variable in Rules DSL gets reset when the file is reloaded.

The check for undefined is used to keep from overwriting the old value every time the rule runs. Only do that first assignment if it’s undefined.

I’ve not yet played with ScriptExtensions but that should be available as well as a place to store stuff that can be shared across multiple rules.

If you have a rule that calls another rule you can pass data to the called rule, which might be an option that can sometimes eliminate the need to save data in a central repository.

// Run another rule
var FrameworkUtil = Java.type("org.osgi.framework.FrameworkUtil");
var _bundle = FrameworkUtil.getBundle(scriptExtension.class);
var bundle_context = _bundle.getBundleContext()
var classname = "org.openhab.core.automation.RuleManager"
var RuleManager_Ref = bundle_context.getServiceReference(classname);
var RuleManager = bundle_context.getService(RuleManager_Ref);
RuleManager.runNow("tocall");
var map = new java.util.HashMap();
map.put("test_data", "Passed data to called function!")
RuleManager.runNow("tocall", true, map); // second argument is whether to consider the conditions, third is a Map<String, Object> (way to pass data?)

NOTE: most of that Framework stuff is handled for you in the Helper Libraries.

2 Likes

I tried the example with test_count and it works fine.
Next step was to use an array instead of a variable in a similar manner. Unfortunately this doesn’t work.
Is the explained way not possible with arrays?

You’ll have to post what you tried. There is an infinite number of ways it could have gone wrong. It should be possible to save arrays from one run to the next. But often, when I see people trying to do that, there is almost always a better approach over all that doesn’t involve the array.

Here is my test code. Maybe the way I defined the array ist wrong:

this.Storage = [];
if(this.Storage[0] === undefined) {
logger.info(“Storage[0] is not defined, set it to 1.”);
this.Storage[0] = 1;
}
if(this.Storage[1] === undefined) {
logger.info(“Storage[1] is not defined, set it to 2.”);
this.Storage[1] = 2;
}
this.Storage [0] += 1;
this.Storage [1] += 2;
logger.info("Storage[0] = " + this.Storage[0] + "; Storage[1]: " + this.Storage[1]);

Sorry when my code is not placed correct in the reply. I didn’t found the way to place the code in this typical grey box. It’s my first time using this forum.

Every time the Script Action runs this.Storage = []; runs. This line creates a new array so every time the Script Action runs it wipes out your existing array and creates a new empty one.

You need to only create a new array when the array itself is undefined.

this.Storage = (this.Storage === undefined) ? [] : this.Storage;

That’s using the ternary operator and is equivalent to

if(this.Storage === undefined){
    this.Storage = [];
}
else {
    this.Storage = this.Storage
}
```
code goes here
```

Thank you very much. It works.