Can i pass items as arguments to rule?

Hello,

I’ve been using openhab as a simple UI for my smart house without automations.
Now i want to integrate the heating control into openhab using the rules/scripts.
I created a rule to control the contacts of the zone valve and the gas heater.

Is there a way to pass the items to the rule as arguments? I need this rule multiple times for each room, and it would be much easier.
Here’s my rule code:

configuration: {}
triggers:
  - id: "2"
    configuration:
      itemName: HvacLiving_LivingTemperature
    type: core.ItemStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "1"
    configuration:
      type: application/vnd.openhab.dsl.rule
      script: >
        val temperature = HvacLiving_LivingTemperature.state as Number

        val hvacMode = HvacLiving_LivingHVACmode.state.toString

        val setpointComfort = HvacLiving_LivingComfortTemperature.state as
        Number

        val setpointStandby = HvacLiving_LivingStandbyTemperature.state as
        Number

        val heatingValve = KNX_Device_heating_valve_living

        val heatingContact = KNX_Device_cv_heat

        val hysteresis = 0.3


        // logInfo("TemperatureScript", "Current temperature is: " +
        temperature.toString())


        if (hvacMode == "COMFORT") {
          if (temperature < setpointComfort){
            heatingValve.sendCommand(ON)
            heatingContact.sendCommand(ON)
            logInfo("HVACModeScript", "Temperature is below the comfort level. Heating valve turned ON.")
          }
          else if (temperature > (setpointComfort + hysteresis)) {
            heatingValve.sendCommand(OFF)
            heatingContact.sendCommand(OFF)
            logInfo("HVACModeScript", "Temperature is above the comfort range. Heating valve turned OFF.")
          }
        }

        else if (hvacMode == "STANDBY") {
          if (temperature < setpointStandby){
            heatingValve.sendCommand(ON)
            heatingContact.sendCommand(ON)
            logInfo("HVACModeScript", "Temperature is below the comfort level. Heating valve turned ON.")
          }
          else if (temperature > (setpointStandby + hysteresis)) {
            heatingValve.sendCommand(OFF)
            heatingContact.sendCommand(OFF)
            logInfo("HVACModeScript", "Temperature is above the comfort range. Heating valve turned OFF.")
          }
        }

        else {}
    type: script.ScriptAction

This seems to work:

var parameter = { 'test_data': 'Passed data to called rule..WOW' };

// rules.runRule('testrule', {'test_data': 'Passed data to called rule' });
rules.runRule('testrule', parameter);

And the rule called testrule looks like this:

console.log("Ran rule: " + ctx.ruleUID)
console.log(test_data)

When I look on the console log it looks like this:

2025-03-21 17:23:50.519 [INFO ] [penhab.automation.script.ui.testrule] - Ran rule: testrule
2025-03-21 17:23:50.520 [INFO ] [penhab.automation.script.ui.testrule] - Passed data to called rule..WOW

All the above is done in the UI and ECMAScript 262

As above, I would recommend Javascript for the rule.

A common approach for me would be to use a minimal inline script in the rule to call a script using rules.runRule(), saved under the script menu.

You can provide multiple context key/val pairs, and they can be any data type I think. I even use multidimensional arrays.

In your main script you can then access the values with ctx[‘key’]

E.g. in your inline script:

rules.runRule('rule_id', {'key1': val1, 'key2': val2});

In your script:

variable = ctx['key1'];

It’s very easy to do it via groups. The only thing to do is to get items and groups named in a well organized way.
For example, I have multiple room controllers for heating which can be set to different operation modes. the state is received as a Byte where every bit has a meaning (bit 0 is comfort, bit 1 is standby, bit 2 is night, bit 3 is frost protection, bits 4 to 7 are for heating/cooling…)
But the control of the operation mode is done as 1, 2, 3 or 4. So I came up with a rule to translate from one to the other.

Here are two Items from one of my controllers:

Number NEGTechnik_OpMode "Betriebsart ist"  <heating> (gNEGTechnik,gHeat_Mode) ["Status"]  ...
Number NEGTechnik_OpSet  "Betriebsart soll" <heating> (gNEGTechnik,gHeat_Set)  ["Control"] ...

As you can see, the first part of the Item name is the same for both Items, so I can “select” the correct Item from the group.

And this is the rule:

rule "Betriebsart RTR"
 when
    Member of gHeat_Mode received update
 then
    var Integer newMode = 2
    val mode = (triggeringItem.state as DecimalType).toBigDecimal.toBigInteger
    val iName = triggeringItem.name.split("_").get(0).toString
    logDebug("rtr","Name is: {}, Mode is: {}",iName,mode)
    switch(mode) {
        case mode.testBit(0) : newMode = 1
        case mode.testBit(2) : newMode = 3
        case mode.testBit(3) : newMode = 4
    }
    var myItem = gHeat_Set.members.filter[ f | f.name.startsWith(iName) ].head
    var Integer oldMode = 0
    if(myItem.state instanceof Number) oldMode = (myItem.state as Number).intValue
    logDebug("rtr","Name is: {}, oldMode is: {}, newMode is: {}",myItem.name,oldMode,newMode)
    myItem.postUpdate(newMode)
end

This rule is the only one for all controllers. I did one group Item per “controller Item”, so one for the received mode, one for the mode set, one for set temperature and so on, but there are other ways to organize it as well.
The main issue is to get the controller to be controlled :slight_smile: and get the Items in question from the groups.

1 Like

To elaborate a little on the other answers here and provice more detail.

@ubeaut’s reply is one approach but it won’t work with Rules DSL. There is no way to access the passed in data from Rules DSL. I’m not sure that’s the best approach here though anyway, though having a rule call another rule would work.

@Udo_Hartmann describes the better approach. When a rule is called it gets injected with a bunch of “implicit variables” which includes the name of the Item that triggered the rule.

Udo shows how to construct the names of the Items needed given the name of the Item that triggered the rule and pull the Item from a Group. You can also pull the name from the ItemRegistry. See Design Pattern: Associated Items for more details and examples. All the rules languages have access to the ItemRegistry by default excep for Rules DSL.

But there is another approach not yet mentioned which is to use the Semantic Model.

Given the Item that triggered the rule you can get the Location. From the Location you can get all the Items with the “Control, Temperature” tags (for example) to find the Items you need to adjust as the measured or setpoint temperatures change. That does not require a consistent naming scheme like Associated Items but it does require a consistent semantic model.

2 Likes

I wonder if there is a simple way to get semantic tags in DSL rules?

Semantic tags are are just tags like any other so you interact with them in all the usual ways.

val location = getLocation(triggeringItem)
val tempControlItems = location.allMembers.filter(i | i.tags.contains("Control") && i.tags.contains("Temperature"))

But there are actions to getPointType(Item) which will return a class you can test. But that seems overly complex to me.

val location = getLocation(triggeringItem)
val tempControlItems = location.allMembers.filter(i | getPontType(i) instanceOf Control getPropertyType(i) instanceOf Property)

I’m not at all certain this last bit of code is correct. May need to test against Class<Control extends Point> and Class<Property extends Property>. Or maybe even something different. My knolwedge of Java introspection has atrophied. But no matter what is correct, clearly just looking for the tags as Strings is easierr and more straight forward.

Edit: @Goossensbas I’ve moved this to a more appropriate topic. Tutorials and Solutions is a place to post a solution, not a place to ask for one.

Another reason not to use DSL. :slightly_smiling_face:
I stopped using it when I migrated from OH2.5 to OH3.

Thank you for all the ideas. This is very helpful.
I tried the example code of @ubeaut but the contact is not switching.

When i change the temperature, the log shows that the temperature changed,
the rule shows the dict, but none of the logging from the thermostat script shows.
Am i doing something wrong?

I also feel like this solution is bulkier than copy/pasting my initial code for each room :sweat_smile:

My “thermostat_living” rule is defined like this:

configuration: {}
triggers:
  - id: "2"
    configuration:
      itemName: HvacLiving_LivingTemperature
    type: core.ItemStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "1"
    configuration:
      type: application/javascript
      script: >
        var temp = parseFloat(items["HvacLiving_LivingTemperature"].state); 
        var mode = items["HvacLiving_LivingHVACmode"].state.toString();   
        var standbytemp = parseFloat(items["HvacLiving_LivingStandbyTemperature"].state);   
        var comforttemp = parseFloat(items["HvacLiving_LivingComfortTemperature"].state);   
        var valve = items["KNX_Device_heating_valve_living"];   
        var contact = items["KNX_Device_cv_heat"];
        
        var parameter = {
          'temperature': temp,
          'hvacMode': mode,
          'setpointComfort': comforttemp,
          'setpointStandby': standbytemp,
          'heatingValve': valve,
          'heatingContact': contact,
          'hysteresis': 0.3
        };
        
        rules.runRule('thermostat', parameter);
        console.log(parameter);
    type: script.ScriptAction

My “thermostat” script is defined like this:

var temperature = Number(ctx['temperature']);
var hvacMode = String(ctx['hvacmode']);
var setpointComfort = Number(ctx['comforttemperature']);
var setpointStandby = Number(ctx['standbytemperature']);
var heatingValve = ctx['valve'];
var heatingContact = ctx['heatingcontact'];
var hysteresis = Number(ctx['hysteresis']);


if (hvacMode == "COMFORT") {
  if (temperature < setpointComfort){
    events.sendCommand("heatingValve", "ON");
    events.sendCommand("heatingContact", "ON");
    logInfo("HVACModeScript", "Temperature is below the comfort level. Heating valve turned ON.");
  }
  else if (temperature > (setpointComfort + hysteresis)) {
    events.sendCommand("heatingValve","OFF");
    events.sendCommand("heatingContact", "OFF");
    logInfo("HVACModeScript", "Temperature is above the comfort range. Heating valve turned OFF.");
  }
}

else if (hvacMode == "STANDBY") {
  if (temperature < setpointStandby){
    events.sendCommand("heatingValve", "ON");
    events.sendCommand("heatingContact", "ON");
    logInfo("HVACModeScript", "Temperature is below the comfort level. Heating valve turned ON.");
  }
  else if (temperature > (setpointStandby + hysteresis)) {
    events.sendCommand("heatingValve","OFF");
    events.sendCommand("heatingContact", "OFF");
    logInfo("HVACModeScript", "Temperature is above the comfort range. Heating valve turned OFF.");
  }
}

else {}

Is your thermostat rule actually called thermostat?
It is the ruleID
Below is my rule called wled_rule
Screenshot from 2025-03-22 18-29-36

The thermostat rule looks like DSL script as you are using loginfo.
Javascript uses console.log
Not sure what ctx is?

You could build up the thermostat a bit at a time and use console.log to see if the parameters are being passed. That is how I do it. A bit at a time and use console.log a lot.

eg:

console.log("Temperature: " + temperature);

You can remark the console.log out by putting // in front of the statements. That way you can remove the // if you want to test more later.

//console.log("Temperature: " + temperature);

You don’t have to declare or initialise the variables in the thermostat rule as they are passed from the originating rule.

I just changed a lot of rules I use for the wled today to call another script and pass parameters. It is working fine.

Good luck. Don’t give up. :slightly_smiling_face:

Variable temperature is immediately available without declaring it if you pass in variables by runRule. Delete all declarations and use temperature instead of ctx['temperature']

It looks like a DSL rule so the parameters won’t be there anyway.

The script I am referring to is a javascript

Yes that is the originating rule the thermostat rule is the one I am referring to.

Ok, that seems to do the trick. The script is called now. :grinning_face_with_big_eyes:

I still get an error when sending the commands ON and OFF to the actuators.
What is the correct syntax for sending an on/off command to an item?

Script execution of rule with UID 'thermostat' failed: org.graalvm.polyglot.PolyglotException: ReferenceError: "events" is not defined

Try out:

items.getItem('heatingContact').sendCommand('ON');

This is working and not working :slight_smile:
it is the correct syntax, but it searches for item ‘heatingContact’ in my items,
while i pass the item with the other script.
Also tried:

items.heatingValve.sendCommand('OFF');

but no luck.
Maybe i’m passing the item the wrong way?

The error message “events is not defined” is resulting if you run a rule manually, but the rule is referring to the event object like event.itemName or event.itemState.
Please double check, probably your script was simply stopping because of this error.

items.heatingContact.sendCommand("ON");

is the better syntax for sending a command.

Are you running the originating rule manually?
It should work.
You can’t run the thermostat rule manually because the variables are not passed.

The way I did mine was have 2 windows opened and I changed the rule that was receiving the parameters and then selected the other window and ran the originating rule.

I had console logs to see what was happening.

Also in the editor if you press ctrl-space the syntax for the command come up.
The line below you could nearly write by using ctrl-space

items.getItem('heatingContact').sendCommand('ON');

Sorry I didn’t realise heatingcontact was a variable in this case rather than just the ID of an item. If so, just pass the variable rather than a string:

items.getItem(heatingContact).sendCommand('ON');

The syntax I’ve been giving always works for me. There may be other ways of calling the functions.