How to reuse code between UI rules?

Hello,

I want to share/reuse code between multiple UI rules in openHAB. For example, I have several rooms where I want to control lights and shutters using the same logic but with different parameters.

I’ve searched the forum and documentation but couldn’t find a clear answer on how to properly reuse code between rules.

What I want to achieve:

  • Create reusable functions/scripts for common tasks (light control, shutter control etc.)
  • Call these functions from different UI rules with room-specific parameters
  • Maintain the code in one place instead of copy/pasting it into each rule

Example scenario:

  • Room A: Turn on light at sunset, close shutters
  • Room B: Same logic but different times/parameters
  • Room C: Same logic again but other parameters

What’s the best practice for code reuse between UI rules in openHAB?

Best regards

The answer depends on which rules language you are using. If your talking DSL, I highly doubt that you can accomplish this at all.

With jsscripting you have two perfectly good options. The more UI-centric one is to use rule.runRule which lets you trigger the actions of any other rule you’ve defined and even lets you inject variable into the context of that rule so it can act like a function (although there is no real way to get a return value from this method).

https://openhab.github.io/openhab-js/rules.html#.runRule

The other option is create your own custom js module and include that module in your js scripts. You can see an example of this with openhab_rules_tools:

(or just search these forums for ‘openhab_rules_tools’)

Wiring DSL scripts and using callScript might be sufficient.

There are significant limitations with DSL scripts but it might be enough to work here.

Other options not yet mentioned include:

  • creating a block library for use in Blockly
  • creating a rule template, though it’ll have to be published to the marketplace
  • figuring out how to do it all from just one rule

Thank you both for your answers!

This may be the solution to a problem that I will post here in a few days. :slight_smile:

// controlItem.js
/**
 * Öffnet den Rolladen nur wenn er nicht bereits offen ist
 * @param {Item} item - Das openHAB Rolladen-Item
 */
const openShutter = (item) => {
    if (item.state === 'UNDEF' || 
        item.state === 'NULL' || 
        item.state !== 0) {
        
        item.sendCommand(0);
    }
}

/**
 * Schließt den Rolladen nur wenn er nicht bereits geschlossen ist
 * @param {Item} item - Das openHAB Rolladen-Item
 */
const closeShutter = (item) => {
    if (item.state === 'UNDEF' || 
        item.state === 'NULL' || 
        item.state !== 100) {
        
        item.sendCommand(100);
    }
}

/**
 * Schaltet das Licht ein wenn es nicht bereits an ist
 * @param {Item} item - Das openHAB Licht-Item
 */
const turnOn = (item) => {
    if (item.state === 'UNDEF' || 
        item.state === 'NULL' || 
        item.state !== 'ON') {
        
        item.sendCommand('ON');
    }
}

/**
 * Schaltet das Licht aus wenn es nicht bereits aus ist
 * @param {Item} item - Das openHAB Licht-Item
 */
const turnOff = (item) => {
    if (item.state === 'UNDEF' || 
        item.state === 'NULL' || 
        item.state !== 'OFF') {
        
        item.sendCommand('OFF');
    }
}

module.exports = {
    openShutter,
    closeShutter,
    turnOn,
    turnOff
}
const controlItem = require('/etc/openhab/automation/js/controlItem');
controlItem.openShutter(items.Rolladen_Wohnzimmer);

This works for me! :smile:

Blockly is not an option for me :grin:

openhab_rules_tools is certainly worth the effort, but not what I am doing. :smiley:

The problem mentioned above relates precisely to this, because I don’t think it’s possible to represent my ideas with just one rule.

I’m not sure how this is supposed to work, but since the JavaScript version works, I’ll save myself the trouble. The language is not that important to me. :slight_smile:

Thanks again for your help and best regards!

You didn’t say what you are using :person_shrugging:

OHRT has some rule templates but It isn’t rule templates. It’s a node module (i.e. a library).

I’m recommending you write your own. You could use OHRT’s as examples but I wouldn’t expect OHRT to be any more involved than that.

It’s always possible but not always available. The rule can become too long or complicated to run.

At a high level you write a but of Rules DSL code in a file with the .script extension in the scripts folder and you call it from another rule using callScriot(file name). However, you can’t use imports and you can’t get a return value.

That’s true, sorry! :sweat_smile:

To be honest, I always have a hard time with the wording. What I did up there with the code, what do you call it?

Also true again, it can be assumed that the rule will be too complicated for what I have in mind. Now I have already created the thread that concerns this topic :smile:

Sounds to to me like JavaScript is the better option, doesn’t it?

That’s a library, it node module.

A rule templates you will find at Rule Templates - openHAB Community. These can be installed like an add-on from the add-on store and used to instantiate new rules with different configurations.

OHRT is something you would install using npm and use in your JS rules, importing it using require.

Certainly, but other people come along and read these threads so it’s important that answers are as clear and complete as possible. If you didn’t know, others will not know either and they may prefer to stock with DSL.

1 Like

With the update to OH 4 I started from scratch and rewrote all my rules because I had the same problem: too many individual rules for basically the same functionality. I now went for the one rule fits all approach in most cases. It is very rewarding when you have a rule that just works also for other rooms when you add more equipment to your smart home.

So based on your requirements I can give you a rough idea how this might work:

  • Room A: Turn on light at sunset, close shutters
  • Room B: Same logic but different times/parameters
  • Room C: Same logic again but other parameters
  1. By applying consistent use of the semantic model in your setup it is relatively easy to find all lights/shutters/whatever in a location. So if you make sure that items are all tagged the same if they represent the same kind of control (e.g. a light switch) you don’t have to deal with concrete items/item names at all.
  2. item metadata: add custom metadata to your items for individualisation. For example I dim the lights during the night time but have set individual brightness levels for the lamps applied via metadata. You can also use metadata to exclude certain items from this logic (e.g. don’t dim the light on the terrace because if its on at night I am probably sitting there and want the light to be exactly like it is).
  3. Triggers: add all items that should trigger rules to groups and trigger the rule when anything in this group happens (updates/changes). Examine the trigger to find out which location is affected and proceed with finding relevant items and metadata as described in 1. and 2. Since you are talking about time based triggers you may also decouple this: you could add an item per location to determine whether it should be in day or night mode in this location and update this item when the corresponding time has come. Then use this item as part of a trigger group to proceed as described above.

You may or may not take this approach but I’m very happy to find all the logic in one place now and rules are still readable when it comes to length (use functions wherever possible to increase readability). All in all I think in a lot of cases this does not necessarily lead to overly complicated rules.

2 Likes

Only tangential to your question: This

is equivalent to

    if (item.state !== 'OFF') {
        
        item.sendCommand('OFF');
    }

because ‘UNDEF’ and ‘NULL’ are not equal to ‘OFF’.

In JavaScript scripting there is sendCommandIfDifferent(value), see https://www.openhab.org/addons/automation/jsscripting/#items
With this:

        item.sendCommandIfDifferent('OFF');

You may have other examples, but at least this logic is not worth from my point of view to be shared in a libary.

Here’s a bit of code I have that uses this approach to find the status Item for an equipment that has gone offline as an example of what @DrRSatzteil describes.

var equipment = actions.Semantics.getEquipment(items[alertItem]);
var statusItem = items[equipment.name+'_Status'];
if(equipment === null || statusItem === null) {
  console.warn(alertItem + ' does not belong to an equipment or equipment doesn\'t have a Status Item!');
  false;
}
else {
  var statusItem = items[equipment.name+'_Status'];
  console.debug('Sensor status reporting called for ' + alertItem + ', equipment ' + equipment.label + ', is alerting ' + isAlerting + ', and is initial alert ' + isInitialAlert 
               + ', current equipment state is ' + statusItem.state);
  // Sensor is offline                         Sensor back online
  (isAlerting && statusItem.state != 'OFF') || (!isAlerting && statusItem.state != 'ON');
}

This is a script condition for a rule that gets called by Threshold Alert and Open Reminder [4.0.0.0;4.9.9.9] when a sensor stops reporting for too long. That’s where alertItem and isAlerting is comming from.

I use the Item name but could just as easily used a tag.

I can also add that in JS you can test for both “NULL” and “UNDEF” using item.isUninitialized.

Recently I created a rule that recommends ventilation for rooms (note that we usually don’t have problems with low humidity so I ignored this in my rule). It’s kind of big and takes quite a few things into account. Even though it is rather long, when you start reading the main function at the bottom you can understand quite easily what happens there even when you don’t read all the other funtions first. I even thought about moving some of the basic functions to my own library as I may be able to reuse them in other rules.

var INDOOR_LOCATION_NAME = "loc_house"
var OUTSIDE_DEWPOINT = items.thermostat_carport_dewpoint.quantityState;
var OUTSIDE_TEMPERATURE = items.thermostat_carport_temperature.quantityState;
var DEWPOINT_TRIGGER_DIFF = Quantity("4.5 °C");
var DEWPOINT_MIN_DIFF = Quantity("2.5 °C");
var MAX_HUMIDITY = Quantity("50 %");
var MAX_CO2 = Quantity("1000 ppm");

function getLocationForItemName(itemName) {
  if (items.existsItem(itemName)){
    var item = items[itemName];
    if(item.semantics.location != null) {
      return item.semantics.location;
    } else {
      if (item.semantics.equipment){
        return getLocationForItemName(item.semantics.equipment.name);
      }
    }
  }
}

function isOutdoors(locationItem) {
  if(locationItem.name === INDOOR_LOCATION_NAME) {
    return false;
  }
  if(locationItem.semantics.location != null) {
    return isOutdoors(locationItem.semantics.location);
  }
  return true;
}

function getSubLocations(locationName, locations) {
  var location = items[locationName];
  var locationMembers = location.members;
  for (member of locationMembers) {
    if (member.semantics.isLocation) {
      locations.push(member);
      getSubLocations(member.name, locations);
    }
  }
  return locations;
}

function getLocationsByTriggerItemName(triggerItemName) {
  var triggerLocation = getLocationForItemName(triggerItemName);
  if(isOutdoors(triggerLocation)) {
    var locations = new Array();
    return getSubLocations(INDOOR_LOCATION_NAME, locations);
  } else {
    return [triggerLocation];
  }
}

function needsRecommendation(location) {
  return items.existsItem(location.name.replace("loc_", "") + "_vent");
}

function updateRecommendation(location, venting) {
  var ventItemName = location.name.replace("loc_", "") + "_vent";
  if(items.existsItem(ventItemName)) {
    items[ventItemName].sendCommandIfDifferent(venting ? "ON" : "OFF");
  }
}

function getProperty(propertyType, modelRoot) {
  if (modelRoot.semantics.isPoint){
    if (item.semantics.propertyType === propertyType){
      return item.quantityState;
    }
  } else if(modelRoot.semantics.isLocation || modelRoot.semantics.isEquipment) {
    for(item of modelRoot.members) {
      if(item.semantics.isEquipment || item.semantics.isPoint) {
        var prop = getProperty(propertyType, item);
        if (prop) {
          return prop;
        }
      }
    }
  }
}

function getDewpoint(modelRoot) {
  if (modelRoot.semantics.isPoint){
    if (item.name.includes("dewpoint")){
      return item.quantityState;
    }
  } else if(modelRoot.semantics.isLocation || modelRoot.semantics.isEquipment) {
    for(item of modelRoot.members) {
      if(item.semantics.isEquipment || item.semantics.isPoint) {
        var dewpoint = getDewpoint(item);
        if (dewpoint) {
          return dewpoint;
        }
      }
    }
  }
}

function getTargetTemperatureForLocation(locationItem){
  var setpointItemName = null;
  switch (locationItem.name) {
    case 'loc_diningroom':
    case 'loc_kitchen':
    case 'loc_livingroom':
      setpointItemName = "heating_livingroom_setpoint";
      break;
    default:
      setpointItemName = locationItem.name.replace("loc_", "heating_") + "_setpoint";
  }
  if (items.existsItem(setpointItemName)) {
    return items[setpointItemName].quantityState;
  } else {
    console.debug("Cannot determine target temperature for location", locationItem.name);
  }
}

function humidityVentilationRequired(humidity, dewpoint) {
  if (humidity.greaterThanOrEqual(MAX_HUMIDITY)) {
    var dewpointDiff = dewpoint.subtract(OUTSIDE_DEWPOINT);
    if (dewpointDiff.greaterThan(DEWPOINT_TRIGGER_DIFF)) {
      return true;
    } else if (dewpointDiff.lessThanOrEqual(DEWPOINT_MIN_DIFF)) {
      return false;
    }
  }
  return false;
}

function co2VentilationRequired(co2) {
  return co2.greaterThanOrEqual(MAX_CO2);
}

function main(itemName) {
  
  console.debug("Venting conditions changed", itemName);
  
  var allLocations = getLocationsByTriggerItemName(itemName);

  for (location of allLocations) {

    if(needsRecommendation(location)) {
      var venting = false;

      // Check CO2
      var co2 = getProperty("CO2", location);
      if (co2) {
        venting = co2VentilationRequired(co2);
      }

      // Check Humidity if CO2 was false
      if (!venting) {
        var dewpoint = getDewpoint(location);
        if (dewpoint) {
          var humidity = getProperty("Humidity", location);
          venting = humidityVentilationRequired(humidity, dewpoint);
        }
      }

      // Check Temperature
      if(venting) {
        var setpoint = getTargetTemperatureForLocation(location);
        console.debug("Setpoint is", setpoint, location.label);
        if (setpoint) {
          var temp = getProperty("Temperature", location);
          console.debug("Temp is", temp, location.label);
          if(temp.lessThan(setpoint) && OUTSIDE_TEMPERATURE.lessThan(temp)) {
            venting = false;
          }
        }
      }
      
      updateRecommendation(location, venting);
    }
  }
}

main(this.event.itemName);

The only recommendations I have is since all your variables are in functions, you can use const and let instead of var. Outside of openHAB managed rules, it’s a best practice since it limits the scope of the variables (var is globally scopped whereas const and let are limited to the context where they are defined).

Inside OH managed rules, it’s not that big of a deal as the “global” context is limited to just the one script action/condition. There’s no risk that your var is going to pop up in another rule or something like that. But outside of OH managed rules, you very much run that risk. So it’s a good habbit to get into.

Note, given the way script actions and script conditions are loaded for managed rules, you cannot use const and let in the global context of the script (i.e. outside of a function or outside an if statement or the like). There you have to use var or you’ll get an error after the first run of the rule saying that the variable already exists.

1 Like

You’re absolutely right, this makes my library redundant, but at least now I know how to create one in case I have something worthwhile. :smiley:

@DrRSatzteil First of all, thank you for your reply.

These are both things I didn’t know and they sound very good.

However, I don’t like the idea of putting all the items that can trigger a rule into one group

I have created non-semantic groups for all devices.
2024-12-10_21-42

I wonder if this can be applied to them somehow.
Or is it better to do it all via the semantic model?

Can we perhaps move this conversation to the other thread, because this is actually off-topic here

Why?

You can have a separate functional hierarchy of Groups and Items from the semantic model. The semantic model is strictly a model of the physical layout of your home automation. No Item can be in more than one location at the same time.

Just thinking about it makes my inner Monk start crying :see_no_evil:

Is there an intended or preferred way or are both ways equally good?

But there is nothing wrong with it. You can use groups for various reasons, I also use groups just to make it easier to find similar items or to control how items are persisted.

They solve two different problems in two different ways. It would be unusual if you didn’t use both, particularly for controling the home automation like you descrbie. The semantic model helps you access and managed the stuff based on the room they are in. The functional model helps you access and manage the stuff based on what the Items do or represent, though the semantic model can help you with that too.

For example:

Turn on all the light switches from the room where the motion sensor triggered motion (rule triggered by any member of the Group of all motion sensors changing, code is JS):

var location = actions.Semantics.getLocation(items[event.itemName]);
var lights = location.members.descendents.filter(i => i.tags.includes("light") && i.tags.includes("switch"));
lights.forEach(light => light.sendCommandIfDifferent("ON"));

This one rule consisting of one trigger and three lines of code handles all rooms that have a motion sensor and if you add or removed lights, rooms, or motion sensors you do not need to change the rule. It doesn’t care how many or what the Items are named. I just knows that if a motion sensor detects motion, it should turn on all the lights in that location and the Grouping and tagging handles the rest.

But you have to have both a functional Group (i.e. the Group of all motion sensors) and the semantic model (to get the location and the lights in that location) for the rule to work. It’s not either/or.

2 Likes

Brilliant, I love how simple this rule is. I will definitely check where I can refactor my own code to make it better :heart_eyes:

I’m using this as an example to get a light in a room location. I don’t have any subgroups in my location, so I didn’t use descendents (testing that caused an error - possibly because there are no subgroups?). The location and location members are printed, but the filter fails with the log message below. What am I doing wrong?

var location = actions.Semantics.getLocation(items[event.itemName]);
console.log("location of " + event.itemName + " is: " + location);
console.log("location.members of " + event.itemName + " is: " + location.members);
var lights = location.members.filter(i => i.tags.includes("Lightbulb"));
2024-12-24 17:09:01.115 [INFO ] [tion.script.ui.AutomaticRoomLighting] - location of CabinBathLaundryMotionSensor is: gLaundryBath (Type=GroupItem, Members=4, State=NULL, Label=Laundry Bath, Category=, Tags=[Location], Groups=[gIndoor])
2024-12-24 17:09:01.116 [INFO ] [tion.script.ui.AutomaticRoomLighting] - location.members of CabinBathLaundryMotionSensor is: [CabinBathLaundrySinkLight (Type=SwitchItem, State=OFF, Label=Cabin Bath Laundry Sink Light, Category=light, Tags=[Lightbulb], Groups=[gLaundryBath]), CabinBathLaundryMotionSensor (Type=SwitchItem, State=OFF, Label=Cabin Bath Laundry Motion Sensor, Category=Motion, Tags=[Point], Groups=[gLaundryBath]), CabinBathLaundryMotionSensorBatteryLevel (Type=NumberItem, State=86 %, Label=Cabin Bath Laundry Motion Sensor Battery Level, Category=Battery, Tags=[Level, Measurement], Groups=[gLaundryBath, gBatteryLevel]), CabinLaundryBathDoorSensor (Type=ContactItem, State=OPEN, Label=Cabin Laundry Bath Door Sensor, Category=door, Tags=[Door], Groups=[gLaundryBath])]
2024-12-24 17:09:01.118 [ERROR] [.handler.AbstractScriptModuleHandler] - Script execution of rule with UID 'AutomaticRoomLighting' failed: org.graalvm.polyglot.PolyglotException: TypeError: invokeMember (filter) on java.util.Collections$UnmodifiableSet@eab1d1f failed due to: Unknown identifier: filter

FYI, when I tried with the descendents, it says it is ‘undefined’:

console.log("location.members.descendents of " + event.itemName + " is: " + location.members.descendents);
2024-12-24 17:05:07.759 [INFO ] [tion.script.ui.AutomaticRoomLighting] - location.members.descendents of CabinBathLaundryMotionSensor is: undefined