[jsscripting] Automatically convert event data in UI-based scripts from Java to JS types (#19260)

* [jsscripting] Automatically convert event data in UI-based scripts from Java to JS types

Signed-off-by: Florian Hotze <dev@florianhotze.com>
This commit is contained in:
Florian Hotze
2025-08-30 10:06:55 +02:00
committed by GitHub
parent 2966d33fc1
commit 8efd3990f8
5 changed files with 99 additions and 57 deletions
@@ -71,16 +71,57 @@ console.log("Thing status",thingStatusInfo.getStatus());
See [openhab-js](https://openhab.github.io/openhab-js) for a complete list of functionality.
### UI Event Object
### Event Object
**NOTE**: Note that `event` object is different in UI based rules and file based rules! This section is only valid for UI based rules. If you use file based rules, refer to [file based rules event object documentation](#event-object).
Note that `event` object is only available when the UI based rule was triggered by an event and is not called from another rule!
Trying to access `event` in this case does not work and will lead to an error. Use `this.event` instead (will be `undefined` when it does not exist).
When a rule is triggered, the script is provided the event instance that triggered it.
The specific data depends on the event type.
The `event` object provides some information about that trigger.
When you use "Item event" as trigger (i.e. "[item] received a command", "[item] was updated", "[item] changed"), there is additional context available for the action in a variable called `event`.
When a rule is triggered, there is additional context available for the action in a variable called `event`.
This table gives an overview over the `event` object:
This table gives an overview over the `event` object for most common trigger types:
| Property Name | Trigger Types | Description | Rules DSL Equivalent |
|-------------------|-----------------------------------------------------|--------------------------------------------------------------------------------------------------------|------------------------|
| `oldState` | `ItemStateChangeTrigger`, `GroupStateChangeTrigger` | Previous state of Item or Group that triggered event | `previousState` |
| `newState` | `ItemStateChangeTrigger`, `GroupStateChangeTrigger` | New state of Item or Group that triggered event | N/A |
| `receivedState` | `ItemStateUpdateTrigger`, `GroupStateUpdateTrigger` | State of Item that triggered event | `triggeringItem.state` |
| `receivedCommand` | `ItemCommandTrigger`, `GroupCommandTrigger` | Command that triggered event | `receivedCommand` |
| `itemName` | `Item****Trigger`, `Group****Trigger` | Name of Item that triggered event | `triggeringItem.name` |
| `groupName` | `Group****Trigger` | Name of the group whose member triggered event | N/A |
| `receivedEvent` | `ChannelEventTrigger` | Channel event that triggered event | N/A |
| `channelUID` | `ChannelEventTrigger` | UID of channel that triggered event | N/A |
| `oldStatus` | `ThingStatusChangeTrigger` | Previous state of Thing that triggered event | N/A |
| `newStatus` | `ThingStatusChangeTrigger` | New state of Thing that triggered event | N/A |
| `status` | `ThingStatusUpdateTrigger` | State of Thing that triggered event | N/A |
| `thingUID` | `Thing****Trigger` | UID of Thing that triggered event | N/A |
| `cronExpression` | `GenericCronTrigger` | Cron expression of the trigger | N/A |
| `time` | `TimeOfDayTrigger` | Time of day value of the trigger | N/A |
| `timeOnly` | `DateTimeTrigger` | Whether the trigger only considers the time part of the DateTime Item | N/A |
| `offset` | `DateTimeTrigger` | Offset in seconds added to the time of the DateTime Item | N/A |
| `eventType` | all except `PWMTrigger`, `PIDTrigger` | Type of event that triggered event (change, command, triggered, update, time) | N/A |
| `triggerType` | all except `PWMTrigger`, `PIDTrigger` | Type of trigger that triggered event | N/A |
| `eventName` | all | simple Java class name of the triggering event, e.g. `ExecutionEvent` | N/A |
| `eventClass` | all | full Java class name of the triggering event, e.g. `org.openhab.core.automation.events.ExecutionEvent` | N/A |
| `module` | all | (user-defined or auto-generated) name of trigger | N/A |
| `raw` | all | Original contents of the event including data passed from a calling rule | N/A |
All properties are typeof `string` except for properties contained by `raw` which are unmodified from the original types.
Please note that when using `GenericEventTrigger`, the available properties depend on the chosen event types.
It is not possible for the openhab-js library to provide type conversions for all properties of all openHAB events, as those are too many.
In case the event object does not provide type-conversed properties for your chosen event type, use the `payload` property to gain access to the event's (Java data type) payload.
**NOTE:**
`Group****Trigger`s use the equivalent `Item****Trigger` as trigger for each member.
See [openhab-js : EventObject](https://openhab.github.io/openhab-js/rules.html#.EventObject) for full API documentation.
When disabling the option _Convert Event from Java to JavaScript type in UI-based scripts_, you will receive a raw Java event object instead of the `event` object described above.
See the expandable section below for more details.
<details>
<summary>Raw UI Event Object</summary>
This table gives an overview over the raw Java `event` object for UI-based scripts for most common trigger types:
| Property Name | Type | Trigger Types | Description | Rules DSL Equivalent |
|----------------|----------------------------------------------------------------------------------------------------------------------|----------------------------------------|---------------------------------------------------------------------------------------------------------------|------------------------|
@@ -110,6 +151,8 @@ console.log(event.itemState == "test") // WRONG. Will always log "false"
console.log(event.itemState.toString() == "test") // OK
```
</details>
## Scripting Basics
The openHAB JavaScript Scripting runtime attempts to provide a familiar environment to JavaScript developers.
@@ -1192,6 +1235,9 @@ Local variable state is not persisted among reloads, see using the [cache](#cach
File based rules can be created in 2 different ways: using [JSRule](#jsrule) or the [Rule Builder](#rule-builder).
When a rule is triggered, the script is provided information about the event that triggered the rule in the `event` object.
Please refer to [Event Object](#event-object) for documentation.
See [openhab-js : rules](https://openhab.github.io/openhab-js/rules.html) for full API documentation.
### JSRule
@@ -1412,55 +1458,6 @@ rules.when(true).item('HallLight').receivedCommand().then().sendIt().toItem('Kit
rules.when(true).item('HallLight').receivedUpdate().then().copyState().fromItem('BedroomLight1').toItem('BedroomLight2').build();
```
### Event Object
**NOTE**: The `event` object is different in UI Based Rules and File Based Rules!
This section is only valid for File Based Rules.
If you use UI Based Rules, refer to [UI based rules event object documentation](#ui-event-object).
When a rule is triggered, the script is provided the event instance that triggered it.
The specific data depends on the event type.
The `event` object provides some information about that trigger.
This table gives an overview over the `event` object:
| Property Name | Trigger Types | Description | Rules DSL Equivalent |
|-------------------|-----------------------------------------------------|--------------------------------------------------------------------------------------------------------|------------------------|
| `oldState` | `ItemStateChangeTrigger`, `GroupStateChangeTrigger` | Previous state of Item or Group that triggered event | `previousState` |
| `newState` | `ItemStateChangeTrigger`, `GroupStateChangeTrigger` | New state of Item or Group that triggered event | N/A |
| `receivedState` | `ItemStateUpdateTrigger`, `GroupStateUpdateTrigger` | State of Item that triggered event | `triggeringItem.state` |
| `receivedCommand` | `ItemCommandTrigger`, `GroupCommandTrigger` | Command that triggered event | `receivedCommand` |
| `itemName` | `Item****Trigger`, `Group****Trigger` | Name of Item that triggered event | `triggeringItem.name` |
| `groupName` | `Group****Trigger` | Name of the group whose member triggered event | N/A |
| `receivedEvent` | `ChannelEventTrigger` | Channel event that triggered event | N/A |
| `channelUID` | `ChannelEventTrigger` | UID of channel that triggered event | N/A |
| `oldStatus` | `ThingStatusChangeTrigger` | Previous state of Thing that triggered event | N/A |
| `newStatus` | `ThingStatusChangeTrigger` | New state of Thing that triggered event | N/A |
| `status` | `ThingStatusUpdateTrigger` | State of Thing that triggered event | N/A |
| `thingUID` | `Thing****Trigger` | UID of Thing that triggered event | N/A |
| `cronExpression` | `GenericCronTrigger` | Cron expression of the trigger | N/A |
| `time` | `TimeOfDayTrigger` | Time of day value of the trigger | N/A |
| `timeOnly` | `DateTimeTrigger` | Whether the trigger only considers the time part of the DateTime Item | N/A |
| `offset` | `DateTimeTrigger` | Offset in seconds added to the time of the DateTime Item | N/A |
| `eventType` | all except `PWMTrigger`, `PIDTrigger` | Type of event that triggered event (change, command, triggered, update, time) | N/A |
| `triggerType` | all except `PWMTrigger`, `PIDTrigger` | Type of trigger that triggered event | N/A |
| `eventName` | all | simple Java class name of the triggering event, e.g. `ExecutionEvent` | N/A |
| `eventClass` | all | full Java class name of the triggering event, e.g. `org.openhab.core.automation.events.ExecutionEvent` | N/A |
| `module` | all | (user-defined or auto-generated) name of trigger | N/A |
| `raw` | all | Original contents of the event including data passed from a calling rule | N/A |
All properties are typeof `string` except for properties contained by `raw` which are unmodified from the original types.
Please note that when using `GenericEventTrigger`, the available properties depend on the chosen event types.
It is not possible for the openhab-js library to provide type conversions for all properties of all openHAB events, as those are too many.
In case the event object does not provide type-conversed properties for your chosen event type, use the `payload` property to gain access to the event's (Java data type) payload.
**NOTE:**
`Group****Trigger`s use the equivalent `Item****Trigger` as trigger for each member.
Time triggers do not provide any event instance, therefore no property is populated.
See [openhab-js : EventObject](https://openhab.github.io/openhab-js/rules.html#.EventObject) for full API documentation.
## Advanced Scripting
### Libraries
@@ -31,6 +31,7 @@ public class GraalJSScriptEngineConfiguration {
private static final String CFG_INJECTION_ENABLED = "injectionEnabledV2";
private static final String CFG_INJECTION_CACHING_ENABLED = "injectionCachingEnabled";
private static final String CFG_WRAPPER_ENABLED = "wrapperEnabled";
private static final String CFG_EVENT_CONVERSION_ENABLED = "eventConversionEnabled";
private static final String CFG_DEPENDENCY_TRACKING_ENABLED = "dependencyTrackingEnabled";
private static final int INJECTION_ENABLED_FOR_UI_BASED_SCRIPTS_ONLY = 1;
@@ -40,6 +41,7 @@ public class GraalJSScriptEngineConfiguration {
private int injectionEnabled = INJECTION_ENABLED_FOR_ALL_SCRIPTS;
private boolean injectionCachingEnabled = true;
private boolean wrapperEnabled = true;
private boolean eventConversionEnabled = true;
private boolean dependencyTrackingEnabled = true;
/**
@@ -57,11 +59,18 @@ public class GraalJSScriptEngineConfiguration {
* @param config configuration parameters to apply to JavaScript
*/
void modified(Map<String, ?> config) {
boolean oldInjectionEnabledForUiBasedScript = isInjectionEnabledForUiBasedScript();
boolean oldDependencyTrackingEnabled = dependencyTrackingEnabled;
boolean oldWrapperEnabled = wrapperEnabled;
boolean oldEventConversionEnabled = eventConversionEnabled;
this.update(config);
if (oldInjectionEnabledForUiBasedScript != isInjectionEnabledForUiBasedScript()
&& !isInjectionEnabledForUiBasedScript() && isEventConversionEnabled()) {
logger.warn(
"Injection disabled for UI-based scripts, but event conversion is enabled. Event conversion will not work.");
}
if (oldDependencyTrackingEnabled != dependencyTrackingEnabled) {
logger.info(
"{} dependency tracking for JavaScript Scripting. Please resave your scripts to apply this change.",
@@ -72,6 +81,16 @@ public class GraalJSScriptEngineConfiguration {
"{} wrapper for JavaScript Scripting. Please resave your UI-based scripts to apply this change.",
wrapperEnabled ? "Enabled" : "Disabled");
}
if (oldEventConversionEnabled != eventConversionEnabled) {
if (eventConversionEnabled && (!isInjectionEnabledForUiBasedScript() || !wrapperEnabled)) {
logger.warn(
"Enabled event conversion for UI-based scripts, but auto-injection or wrapper is disabled. Event conversion will not work.");
}
if (!eventConversionEnabled) {
logger.info(
"Disabled event conversion for JavaScript Scripting. Please resave your scripts to apply this change.");
}
}
}
/**
@@ -87,6 +106,8 @@ public class GraalJSScriptEngineConfiguration {
injectionCachingEnabled = ConfigParser.valueAsOrElse(config.get(CFG_INJECTION_CACHING_ENABLED), Boolean.class,
true);
wrapperEnabled = ConfigParser.valueAsOrElse(config.get(CFG_WRAPPER_ENABLED), Boolean.class, true);
eventConversionEnabled = ConfigParser.valueAsOrElse(config.get(CFG_EVENT_CONVERSION_ENABLED), Boolean.class,
true);
dependencyTrackingEnabled = ConfigParser.valueAsOrElse(config.get(CFG_DEPENDENCY_TRACKING_ENABLED),
Boolean.class, true);
}
@@ -111,6 +132,10 @@ public class GraalJSScriptEngineConfiguration {
return wrapperEnabled;
}
public boolean isEventConversionEnabled() {
return eventConversionEnabled;
}
public boolean isDependencyTrackingEnabled() {
return dependencyTrackingEnabled;
}
@@ -100,6 +100,7 @@ public class OpenhabGraalJSScriptEngine
}
}
private static final String OPENHAB_JS_INJECTION_CODE = "Object.assign(this, require('openhab'));";
private static final String EVENT_CONVERSION_CODE = "const event = (typeof this.rules?._getTriggeredData === 'function') ? rules._getTriggeredData(ctx, true) : this.event";
private static final String REQUIRE_WRAPPER_NAME = "__wraprequire__";
/** Shared Polyglot {@link Engine} across all instances of {@link OpenhabGraalJSScriptEngine} */
@@ -329,7 +330,14 @@ public class OpenhabGraalJSScriptEngine
protected String onScript(String script) {
if (isUiBasedScript() && configuration.isWrapperEnabled()) {
logger.debug("Wrapping script for engine '{}' ...", engineIdentifier);
return "(function() {" + System.lineSeparator() + script + System.lineSeparator() + "})()";
String eventConversionScript = "";
if (configuration.isEventConversionEnabled()) {
eventConversionScript = EVENT_CONVERSION_CODE + System.lineSeparator();
}
return "(function() {" + System.lineSeparator() + eventConversionScript + script + System.lineSeparator()
+ "})()";
}
return super.onScript(script);
}
@@ -39,6 +39,16 @@
<default>true</default>
<advanced>true</advanced>
</parameter>
<parameter name="eventConversionEnabled" type="boolean" required="true" groupName="environment">
<label>Convert Event from Java to JavaScript type in UI-based scripts</label>
<description><![CDATA[
Converting the event data from Java to JavaScript types in UI-based scripts allows working with event data in a native JS way without special handling for Java types.<br>
With this option enabled, the event data available in UI-based scripts is all JS types and the same as in file-based scripts.<br>
Please note that this option <strong>requires both auto-injection & wrapper enabled</strong> and only applies to UI-based scripts and does not affect file-based scripts.
]]></description>
<default>true</default>
<advanced>true</advanced>
</parameter>
<parameter name="injectionCachingEnabled" type="boolean" required="true" groupName="system">
<label>Cache openHAB JavaScript Library Injection</label>
@@ -7,6 +7,8 @@ addon.jsscripting.description = This adds a JS (ECMAScript-2024) script engine.
automation.config.jsscripting.dependencyTrackingEnabled.label = Enable Dependency Tracking
automation.config.jsscripting.dependencyTrackingEnabled.description = Dependency tracking allows your scripts to automatically reload when one of its dependencies is updated. You may want to disable dependency tracking if you plan on editing or updating a shared library, but don't want all your scripts to reload until you can test it. Please note that changing this setting only applies to scripts loaded after the change.
automation.config.jsscripting.eventConversionEnabled.label = Convert Event from Java to JavaScript type in UI-based scripts
automation.config.jsscripting.eventConversionEnabled.description = Converting the event data from Java to JavaScript types in UI-based scripts allows working with event data in a native JS way without special handling for Java types.<br> With this option enabled, the event data available in UI-based scripts is all JS types and the same as in file-based scripts.<br> Please note that this option <strong>requires both auto-injection & wrapper enabled</strong> and only applies to UI-based scripts and does not affect file-based scripts.
automation.config.jsscripting.group.environment.label = JavaScript Environment
automation.config.jsscripting.group.environment.description = This group defines JavaScript's environment.
automation.config.jsscripting.group.system.label = System Behaviour