Rules and rule templates YAML integration

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.)

1 Like

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.

:person_shrugging: 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:

  1. 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.
  2. Implement a runRule action 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.
  3. 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.