At runtime the environment can define elements and ON and CLOSED are added at runtime - they are not available, as the input is parsed. Likewise rule, then and end could also be added just for the execution, while during parsing these can be undefined: this is a warning, not an error. (This is my understanding.)
Does anybody have any suggestions about how to handle the two above challenges regarding âScriptâ and âSceneâ tagged rules?
No, the âscriptâ folder. âscriptsâ is definitely part of the distro. The âdemo.scriptsâ file is there too, but I never looked in it. If itâs empty as it appears it should be removed. If it is supposed to have content, that probably should be in a README.md file or the extension fixed. And there should be a README.md file in there anyway explaining the restrictions of .script files.
It is better to make it clear that the files need to be â.scriptâ than to raise the question âwhy can I only put one script in a â.scriptsâ fileâ?
A .rules file can have 1-N rules defined in it. A .script file can only have exactly one.
With a whole bunch of caveats. You canât pass anything to them like you can with âregularâ rules (technically you can pass data, but there is no way to access them from Rules DSL, rules or scripts). But they can be invoked using the runRule action (which doesnât exist in Rules DSL).
Thatâs what Iâve been trying to say from the beginning. The whole concept of âScriptsâ in the UI is something that only really exists in the UI. As far as OH core is concerned there really is no difference between a Rule and a Script.
If by âtheyâ you mean Rules DSL Scripts, those have always been different. Somewhat recently they were made so that OH core registers them as rules so they can be accessible from the UI.
But note, Rules DSL has no way to call another rule. It still needs to use callScript which can only invoke Rules DSL scripts in .script files. The change was to make Rules DSL scripts available to the other rules languages.
There is not and never has been an action that allows Rules DSL to call other rules and there is no way I and others were able to figure out to gain access to the rule registry in Rules DSL to do it ourselves.
Maybe something changed over the years, but access to the RulesEngine and the MetadataRegistry is the main reason I moved to Jython and then Nashron and finally JS Scripting. It jsut wasnât possible to get access to those from Rules DSL. The whole OSGI stuff never worked. And these were never added to the rule context.
Thereâs an issue or maybe even a PR open on Github.
It would be unfortunate if users couldnât create Scenes in text files. But maybe thatâs something to tackle later.
A template author is by definition a more sophisticated and experienced OH user. Documentation in the to be written âhow to write a templateâ should be sufficient I think to identify keywords/reserved words like that.
For Scenes punt and deal with them later in a separate PR. Scenes really are kind of different and not related to Rules DSL so they donât need to be addressed at the same time.
For Scripts, I guess it depends on the outcome of the PR/Issue to allow a Rules DSL rule without triggers. But thereâs also the problem that Rules DSL also needs a way to call other rules. So I can see these distinct paths:
- Refuse to export Rules DSL scripts (i.e. rules with a single Rules DSL Script Action whether or not it has a Script tag). Trying to do so is an error.
- Implement a
runRuleaction for Rules DSL so Rules DSL can call other rules, not just Rules DSL scripts, and export the Rules DSL scritps as a rule without triggers. - Export all Rules DSL scripts as a .script file. These can only be exported one at a time though given the one script per .script file restriction.
YAML already allows a rule without triggers so if exporting to YAML just leave it as a rule without triggers.
You might not be familiar with the âdemo appâ, but OH has such a thing. I donât know if itâs related to the online âdemoâ or not, for some reason, the âdemo appâ is what we run when developing from Eclipse (and it can also be run without Eclipse). It runs without Karaf, which is necessary for Eclipse integration. These files belong to this âdemo appâ, everything you find under openhab-distro/launch/app at main ¡ openhab/openhab-distro ¡ GitHub is a part of this âdemo appâ. In there, there are demo.rules, demo.items etc., so that when you start it, itâs not entirely empty. These âdemo filesâ can also be handy as a very basic reference for how things are done.
demo.scripts is a part of this, and it should have been named demo.script, obviously. My point was that Iâm not the only one that find this confusing when even the âofficial demo appâ has gotten this wrong. As to why the file is empty, I donât know, but I can only assume that nothing was added because the author couldnât get it to work. In my opinion, it should be renamed, and some very basic script added, so that it provides a âdemo scriptâ for the installation.
None of these demo files are part of the âdownloadable distributionâ, because that content is generated from another part of the file structure in openhab-distro: openhab-distro/distributions/openhab at main ¡ openhab/openhab-distro ¡ GitHub
The README files doesnât exist in the âdemo appâ, since they are part of the âother treeâ: openhab-distro/distributions/openhab/src/main/resources/conf/scripts at main ¡ openhab/openhab-distro ¡ GitHub
Itâs not worth the time making a big discussion out of this, I think it would be better if both worked because I think itâs ample opportunity to get it wrong. For those that would want more than one script in a .scripts file, I would ask: How do you plan to indicate where one script stops and another one starts? But, forget this point, I wonât do anything about this.
Yeah, the caveats are on the âDSL sideâ because it canât access the âinputsâ. I find this a bit strange, but havenât looked into it, but it should be possible to make this available, because the triggers use these âinputsâ when they trigger. So, they are âaccessibleâ to DSL at some level, but not directly accessible from the code.
It doesnât seem like you can pass anything to callScript either, so arenât they both âequally badâ in that sense? Overall, it seems to me like âDSL scriptsâ must have very limited utilization.
Yes, and Iâm still trying to understand exactly what they are useful for, and in particular why the distinction is useful. I assume that they are used for doing some âstandardâ thing that you want to trigger from different rules, but without parameters to pass, the possibilities will be very limited. Also, since the ârun in rule threadâ has never been merged, running them is full of threading issues.
As I said above, you donât actually need the rule registry to do this, you need the rule engine/rule manager (for some reason this has two names). But, without access to the instance, the problem is still a fact. Iâve never had the time to dig into how these instances are provided to other scripting languages, but I find it hard to believe that the same couldnât be done for DSL.
I doubt that something has changed, Iâm just saying that there are no âobviousâ reason why these canât be made available. But, perhaps there are reasons that become apparent when you try to actually do it, like startup timing/cyclic redundancy. I donât know why it was concluded that it was impossible, but it should be possible to access any such OSGi instance in the same âhacky wayâ as is done for ScriptExecution, which is necessary for callScript() to work. So, to me, itâs hard to understand what exactly makes these cases different, but maybe it is in fact âimpossibleâ regardless of how strange it seems to me.
Yes, both:
This is just one of several challenges with these âhacksâ. Itâs clear to me at least, that other fields are âabusedâ because there are no place where this information really belong. But convincing everybody that adding a âproper placeâ for this type of information might be more work than just living with the complications.
Yeah, it seems like a whole project in itself to deal with âthe illlusionâ that is Scenes. To avoid confusion, I should probably just block them for export to YAML.
Yes, I definitely need to wait for some decisions to be made. As for you options, only 1) is âeasily achievableâ. I donât know how difficult 2) would be, but it would represent a âside trackâ from what Iâm really trying to do now in any case, and should be its own PR. So, it would also act as a delay. 3) might be doable, but impractical, until you mentioned that you can only export one at a time. That is true, and is an even bigger challenge, because it breaks with the whole âconversion logicâ. I donât know how I would have to handle messaging back to the user why it failed if itâs because there are more than one script. This isnât a very appealing option all in all.
Yes.
Indeed.
if the script action is written in any other language except Rules DSL, you can pass data to the called script. Even Blockly can retrieve passed in arguments.
Itâs only Rules DSL that canât.
Thatâs only a concern for JS Scripting. The other languages do not have this restriction.
JS Scripting uses the OSGI interface to get an instance of RuleManager. The RuleRegistry it gets from the context. From there it accesses everything else it needs. OSGI does not work from Rules DSL.
If it could be made to work that would solve a bunch of other problems too (no access to the ThingRegistry, ItemMetadataRegistry, etc.).
No. JS scripting is the only one that refuse to accept it because itâs not thread-safe, which is why you âexperienceâ this as more of a problem there. But, the problem is really the same for any language, you must either do full manual locking, or gamble that you wonât be bitten by the randomness of concurrent execution without âprotectionâ.
What I need to find is how this is done, and then look at why the same canât be done for DSL. Do you have any idea of where this âlivesâ? Is a provided as part of the âpresetsâ? Exactly how do you get hold of what you call the âOSGi interfaceâ? The place where you can retrieve instances.
I did a quick test and added runRule() to DSL, and it seems like the problem is in the startup sequence as I suspected. After doing that, it wonât progress past startlevel 30:
20:01:28.139 [DEBUG] (OH-startlevel-1 ) [enhab.core.service.StartLevelService] - Missing marker automation=scriptEngineFactories for start level 30
Trying to figure out these startup issues isnât among my favorite things to do, but itâs somewhat hard to believe that it shouldnât be possible to find a way around that, if that is the only thing âblockingâ this.
Probably this is something akin to: RuleManager depends on Scripting something to start, and now Scripting something depends on RuleManager. Ergo, none of them will ever start.
Here is the OSGI stuff: openhab-js/src/osgi.js at main ¡ openhab/openhab-js ¡ GitHub
Here is the rules stuff: openhab-js/src/rules/rules.js at main ¡ openhab/openhab-js ¡ GitHub
getService() is implemented in the osgi.js file.
It uses FrameworkUtil to look up instances during runtime. Thatâs one way to âwork aroundâ the startup issues, but it means that you must handle the fact that the instance might potentially not be found. When you ârequireâ them, the component wonât start until they are available, and you donât have to take into account that the instance might be null.
I canât quite understand why DSL canât use FrameworkUtil in the same way.
Itâs not an add-on and isntead a part of core so it gets loaded much earlier in the startup process would be my guess.
Disable / enable a thing from DSL or? - #21 by dpa-openhab shows how to use FrameworkUtil in DSL Rules:
This is how to enable/disable a thing from a DSL Rule without HTTP requests:
import org.openhab.core.thing.ThingManager
rule "Framework util"
when
System reached start level 100
then
val thingManagerBundleContext = org.osgi.framework.FrameworkUtil.getBundle(ThingManager).bundleContext
val thingManagerServiceReference = thingManagerBundleContext.getServiceReference(ThingManager)
val thingManager = thingManagerBundleContext.getService(thingManagerServiceReference)
// ThingManager interface is described at https://www.openhab.org/javadoc/latest/org/openhab/core/thing/thingmanager
thingManager.setEnabled(new org.openhab.core.thing.ThingUID("mqtt:topic:dht22_1"), false) // disables the thing; true enables it
end
Yes, I understand that you can do it like that, but itâs very cumbersome/manual. I was thinking of providing some method that made it easier to acquire the instance.
Itâs certainly not impossible to execute other rules from DSL rules.
My test case was two DSL rules, the shared-test:
var globalVariable = "Global variable"
rule "Test rule"
when
Item MySwitch received command
then
logInfo('test', "Global variable is " + globalVariable);
end
The other one (run-other) looks like this (must use YAML since it lacks triggers):
version: 1
rules:
run-other:
label: RunRule
actions:
- id: "1"
config:
type: DSL
script: runRule("shared-test-1")
type: Script
When I run run-other in the UI, this is logged:
00:07:07.764 [DEBUG] (qtp1543591256-179 ) [.handler.AbstractScriptModuleHandler] - Executing script of rule with UID 'run-other'
00:07:07.871 [DEBUG] (qtp1543591256-179 ) [.handler.AbstractScriptModuleHandler] - Executing script of rule with UID 'shared-test-1'
00:07:07.871 [DEBUG] (qtp1543591256-179 ) [time.internal.engine.DSLScriptEngine] - Script uses context 'shared-test-1'.
00:07:07.909 [INFO ] (qtp1543591256-179 ) [org.openhab.core.model.script.test ] - Global variable is Global variable
00:07:07.909 [DEBUG] (qtp1543591256-179 ) [e.automation.internal.RuleEngineImpl] - The rule 'shared-test-1' is executed.
00:07:07.910 [DEBUG] (qtp1543591256-179 ) [e.automation.internal.RuleEngineImpl] - The rule 'run-other' is executed.
Making this work didnât take very much. This has only been done as a test, so the implementation isnât âsolidâ, no error handling, no attention to names, commented code and general comments, but here it is:
.../core/model/script/actions/RuleExecution.java | 85 ++++++++++++++++++++++
1 file changed, 85 insertions(+)
diff --git a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/actions/RuleExecution.java b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/actions/RuleExecution.java
new file mode 100644
index 0000000000..d2a297e219
--- /dev/null
+++ b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/actions/RuleExecution.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2010-2026 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.model.script.actions;
+
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.automation.RuleManager;
+import org.openhab.core.model.script.engine.action.ActionDoc;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceReference;
+
+/**
+ * The {@link RuleExecution} is a wrapper for the ScriptExecution actions
+ *
+ * @author Ravi Nadahar - Initial contribution
+ */
+@NonNullByDefault
+public class RuleExecution { // TODO: (Nad) JavaDocs
+
+ @ActionDoc(text = "run a rule")
+ public static Map<String, Object> runRule(String ruleUID) {
+ Bundle bundle = FrameworkUtil.getBundle(RuleManager.class);
+// RuleManager ruleManager = ScriptServiceUtil.getRuleManager();
+ if (bundle != null) {
+ BundleContext bc = bundle.getBundleContext();
+ if (bc != null) {
+ ServiceReference<RuleManager> ref = bc.getServiceReference(RuleManager.class);
+ if (ref != null) {
+ RuleManager ruleManager = bc.getService(ref);
+ if (ruleManager != null) {
+ return ruleManager.runNow(ruleUID);
+ }
+ }
+ }
+ }
+ return Map.of();
+ }
+
+ /**
+ * Calls a script which must be located in the configurations/scripts folder.
+ *
+ * @param scriptName the name of the script (if the name does not end with
+ * the .script file extension it is added)
+ *
+ * @return the return value of the script
+ * @throws ScriptExecutionException if an error occurs during the execution
+ */
+// @ActionDoc(text = "call a script file")
+// public static Object callScript(String scriptName) throws ScriptExecutionException {
+// ModelRepository repo = ScriptServiceUtil.getModelRepository();
+// if (repo != null) {
+// String scriptNameWithExt = scriptName;
+// if (!scriptName.endsWith(Script.SCRIPT_FILEEXT)) {
+// scriptNameWithExt = scriptName + "." + Script.SCRIPT_FILEEXT;
+// }
+// XExpression expr = (XExpression) repo.getModel(scriptNameWithExt);
+// if (expr != null) {
+// ScriptEngine scriptEngine = ScriptServiceUtil.getScriptEngine();
+// if (scriptEngine != null) {
+// Script script = scriptEngine.newScriptFromXExpression(expr);
+// return script.execute();
+// } else {
+// throw new ScriptExecutionException("Script engine is not available.");
+// }
+// } else {
+// throw new ScriptExecutionException("Script '" + scriptName + "' cannot be found.");
+// }
+// } else {
+// throw new ScriptExecutionException("Model repository is not available.");
+// }
+// }
+}
.../core/model/script/runtime/internal/engine/DSLScriptEngine.java | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/bundles/org.openhab.core.model.script.runtime/src/org/openhab/core/model/script/runtime/internal/engine/DSLScriptEngine.java b/bundles/org.openhab.core.model.script.runtime/src/org/openhab/core/model/script/runtime/internal/engine/DSLScriptEngine.java
index 7ce8cbf056..6ce187a8aa 100644
--- a/bundles/org.openhab.core.model.script.runtime/src/org/openhab/core/model/script/runtime/internal/engine/DSLScriptEngine.java
+++ b/bundles/org.openhab.core.model.script.runtime/src/org/openhab/core/model/script/runtime/internal/engine/DSLScriptEngine.java
@@ -143,7 +143,9 @@ public class DSLScriptEngine implements javax.script.ScriptEngine {
} catch (ScriptExecutionException | ScriptParsingException e) {
// in case of error, drop the cached script to make sure, it is re-resolved.
parsedScript = null;
- throw new ScriptException(e.getMessage(), modelName, -1);
+ ScriptException se = new ScriptException(e.getMessage(), modelName, -1);
+ se.initCause(e);
+ throw se;
}
}
bundles/org.openhab.core.model.script/bnd.bnd | 1 +
1 file changed, 1 insertion(+)
diff --git a/bundles/org.openhab.core.model.script/bnd.bnd b/bundles/org.openhab.core.model.script/bnd.bnd
index a03f2dda38..4d49f171a1 100644
--- a/bundles/org.openhab.core.model.script/bnd.bnd
+++ b/bundles/org.openhab.core.model.script/bnd.bnd
@@ -19,6 +19,7 @@ Export-Package: org.openhab.core.model.script,\
org.openhab.core.model.script.validation
Import-Package: \
org.openhab.core.audio,\
+ org.openhab.core.automation,\
org.openhab.core.automation.module.script.action,\
org.openhab.core.automation.module.script.rulesupport.shared,\
org.openhab.core.common.registry,\
bundles/org.openhab.core.model.script/pom.xml | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/bundles/org.openhab.core.model.script/pom.xml b/bundles/org.openhab.core.model.script/pom.xml
index b666248ae3..c63df47a5c 100644
--- a/bundles/org.openhab.core.model.script/pom.xml
+++ b/bundles/org.openhab.core.model.script/pom.xml
@@ -50,6 +50,11 @@
<artifactId>org.openhab.core.io.net</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.openhab.core.bundles</groupId>
+ <artifactId>org.openhab.core.automation</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.automation.module.script</artifactId>
.../core/model/script/scoping/ScriptImplicitlyImportedTypes.java | 3 +++
1 file changed, 3 insertions(+)
diff --git a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/scoping/ScriptImplicitlyImportedTypes.java b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/scoping/ScriptImplicitlyImportedTypes.java
index d8c68693d2..e711d4d88a 100644
--- a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/scoping/ScriptImplicitlyImportedTypes.java
+++ b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/scoping/ScriptImplicitlyImportedTypes.java
@@ -35,6 +35,7 @@ import org.openhab.core.model.script.actions.HTTP;
import org.openhab.core.model.script.actions.Log;
import org.openhab.core.model.script.actions.Ping;
import org.openhab.core.model.script.actions.ScriptExecution;
+import org.openhab.core.model.script.actions.RuleExecution;
import org.openhab.core.model.script.actions.Transformation;
import org.openhab.core.model.script.engine.IActionServiceProvider;
import org.openhab.core.model.script.engine.IThingActionsProvider;
@@ -76,6 +77,7 @@ public class ScriptImplicitlyImportedTypes extends ImplicitlyImportedFeatures {
result.add(Ping.class);
result.add(Transformation.class);
result.add(ScriptExecution.class);
+ result.add(RuleExecution.class);
result.add(URLEncoder.class);
result.addAll(getActionClasses());
@@ -92,6 +94,7 @@ public class ScriptImplicitlyImportedTypes extends ImplicitlyImportedFeatures {
result.add(Ping.class);
result.add(Transformation.class);
result.add(ScriptExecution.class);
+ result.add(RuleExecution.class);
result.add(URLEncoder.class);
result.add(CoreUtil.class);
It turns out that the startlevel problems I had earlier was caused by the Groovy add-on (somebody has probably updated some dependencies that cause trouble). So, it works just fine to get the references to the RuleManager and MetadataRegistry before starting the component.
Iâve thus added methods to get the ThingRegistry and MetadataRegistry to DSL as well, and updated my rule:
version: 1
rules:
run-other:
label: RunRule
actions:
- id: "1"
config:
type: DSL
script: |-
runRule("shared-test-1")
logInfo("test", "ThingRegistry: " + getThingRegistry())
logInfo("test", "MetadataRegistry: " + getMetadataRegistry())
type: Script
This is the result:
02:42:22.976 [DEBUG] (qtp531674724-174 ) [.handler.AbstractScriptModuleHandler] - Executing script of rule with UID 'run-other'
02:42:23.004 [DEBUG] (qtp531674724-174 ) [.handler.AbstractScriptModuleHandler] - Executing script of rule with UID 'shared-test-1'
02:42:23.005 [DEBUG] (qtp531674724-174 ) [time.internal.engine.DSLScriptEngine] - Script uses context 'shared-test-1'.
02:42:23.050 [INFO ] (qtp531674724-174 ) [org.openhab.core.model.script.test ] - Global variable is Global variable
02:42:23.050 [DEBUG] (qtp531674724-174 ) [e.automation.internal.RuleEngineImpl] - The rule 'shared-test-1' is executed.
02:42:23.051 [INFO ] (qtp531674724-174 ) [org.openhab.core.model.script.test ] - ThingRegistry: org.openhab.core.thing.internal.ThingRegistryImpl@5ad3835e
02:42:23.052 [INFO ] (qtp531674724-174 ) [org.openhab.core.model.script.test ] - MetadataRegistry: org.openhab.core.internal.items.MetadataRegistryImpl@36bf988
02:42:23.052 [DEBUG] (qtp531674724-174 ) [e.automation.internal.RuleEngineImpl] - The rule 'run-other' is executed.
Somewhat related, I just created this PR which makes the stack trace when a DSL script fails to execute, actually contain the reason why:
This made troubleshooting my âextra DSL commandsâ magnitudes easier.
Upstream demo.scripts was renamed to demo.script.