How to shorten this rule's code?

  • Platform information:
    • Hardware: Raspberry Pi 4
    • OS: Raspberry Pi OS / x64 / 11 (bullseye)
    • Java Runtime Environment: 11.0.15 (Temurin-11.0.15+10) (running in Docker)
    • openHAB version: 3.3.0 (running in Docker)
    • ConBee 2 USB stick

Hey :wave:t2:

Please see my following rule, written in openHAB’s DSL:

if (IKEASTYRBAR_Button.state==2002) {
  if (PhilipsHueSmartPlugCH1_Power.state==ON) {
    PhilipsHueSmartPlugCH1_Power.sendCommand(OFF);
  }
  else if (PhilipsHueSmartPlugCH1_Power.state==OFF) {
    PhilipsHueSmartPlugCH1_Power.sendCommand(ON);
  }
}
else if (IKEASTYRBAR_Button.state==3002) {
  if (PhilipsHueSmartPlugCH2_Power.state==ON) {
    PhilipsHueSmartPlugCH2_Power.sendCommand(OFF);
  }
  else if (PhilipsHueSmartPlugCH2_Power.state==OFF) {
    PhilipsHueSmartPlugCH2_Power.sendCommand(ON);
  }
}
else if (IKEASTYRBAR_Button.state==4002) {
  if (PhilipsHueSmartPlugDE1_Power.state==ON) {
    PhilipsHueSmartPlugDE1_Power.sendCommand(OFF);
  }
  else if (PhilipsHueSmartPlugDE1_Power.state==OFF) {
    PhilipsHueSmartPlugDE1_Power.sendCommand(ON);
  }
}

As you can see, it contains a lot of code duplication which I want to get rid off.

In C#, I’d write it like this:

var dict = new Dictionary<int, object> { 
    { 2002, PhilipsHueSmartPlugCH1_Power },
    { 3002, PhilipsHueSmartPlugCH2_Power },
    { 4002, PhilipsHueSmartPlugDE1_Power },
};

var item = dict[IKEASTYRBAR_Button.state];
item.sendCommand(!item.state);

Can I achieve the same with openHAB’s DSL? I tried the following naive approach which doesn’t work:

val myMap = #{
  2002 -> PhilipsHueSmartPlugCH1_Power,
  3002 -> PhilipsHueSmartPlugCH2_Power,
  4002 -> PhilipsHueSmartPlugDE1_Power
}

val item = myMap[IKEASTYRBAR_Button.state]
item.sendCommand(!item.state)

Sure. In Java it’s called a Map but it’s the same thing. However, the Object you get for the Items when the rule runs is specially created for that rule and will become stale and disconnected from the “actual” Item at some point (e.g. if you reload your .items file). So it’s better to use the Item’s name instead of the actual Item Object.

import java.util.Map
import org.eclipse.smarthome.model.script.ScriptServiceUtil

val Map<String, String> plugMap = newHashmap("2002" -> "PhilipsHueSmartPlugCH1_Power",
                                             "3002" -> "PhilipsHueSmartPlugCH2_Power",
                                             "4002" -> "PhilipsHueSmartPlugDE1_Power")

val plug = ScriptServiceUtil.getItemRegistry.getItem(plugMap.get(IKEASTYRBAR_Button.state.toString))
plug.sendCommand(if(plug.state == ON) OFF else ON)

There are other approaches you could use too. For examples,

  • you could embed the number into the Item name of the plug. Put the plug into a Group and you could search the members for the Item that includes the number in the name.
val plug = Plugs.members.findFirst[ i | i.name.contains(IKEASTYRBAR_Button.state.toString)]
plug.sendCommand(if(plug.state == ON) OFF else ON)
  • you could use the Map transformation to map the button number with the Item name. The nice part about that is the mapping is separate from the rule so you can add/remove/change the mapping without touching the code
import org.eclipse.smarthome.model.script.ScriptServiceUtil

val plug = ScriptServiceUtil.getItemRegistry.getItem(transform("MAP", "plugs.map", IKEASTYRBAR_Button.state.toString))
plug.sendCommand(if(plug.state == ON) OFF else ON)
  • you could tag the Items with the button number and use itemRegistry.getItemsByTag(IKEASTYRBAR_Button.state.toString)
import org.eclipse.smarthome.model.script.ScriptServiceUtil

val plug = ScriptServiceUtil.getItemRegistry.getItemsWithTag(IKEASTYRBAR_Button.state.toString).get(0)
plug.sendCommand(if(plug.state == ON) OFF else ON)
  • the other rules languages have a more complete set of programming language features than Rules DSL, using one of those might be a more natural fit.
2 Likes

Thank you so much for your detailed answer! But I was to quick in marking it as solution :smiley: because now the rule does nothing anymore - it looks like this:

When removing your code line by line, it already fails for import java.util.Map :frowning:

That might be a limitation in Rules DSL Script Actions I think. It makes sense because in Rules DSL the imports have to be the first thing in the file before any code. By the time you get to a Rules DSL Script Action, you are well past that context.

So you’ll either have to write this in a .rules file or you’ll have to use one of the other approaches that do not require imports (or use a different language like JS Scripting or jRuby which support dicts natively and even if they didn’t you can import anywhere in the code, not just at the top like in Rules DSL).

Okay, so I switched over to JavaScript which supports Map as well. But my rule fails even for this simple piece of code:

When removing const map1 = new Map();, the rule works as expected.

Does openHAB use a reduced subset of JavaScript? Or what could be the reason?

Use a JavaScript Dict: How to create a JavaScript Dictionary? | Flexiple Tutorials | JavaScript.

Which JavaScript did you switch over to? If ECMAScript 5.1 (Nashorn), keep in mind that version of JavaScript was released more than a decade ago. Map and a whole host of stuff you might expect to be there wasn’t part of the language yet for that version.

The JS Scripting add-on provides ECMAScript 2021 in a Node like environment (not completely Node like but close enough that most node libraries will work). If you want to use a modern version of JavaScript install the add-on and be sure to reference the add-on’s docs which have reference docs for the library to make interacting with OH easier from rules using pure JavaScript instead of needing to switch between Java Objects and JavaScript and keep track of what is what.

1 Like

Here’s my solution:

function getToggledPlugState(plug) {
  return getCurrentState(plug) == "ON" ? "OFF" : "ON";
}

function getCurrentState(plug) {
  return itemRegistry.getItem(plug).getState();
}

var states = {
  "2002": "PhilipsHueSmartPlugCH1_Power",
  "3002": "PhilipsHueSmartPlugCH2_Power",
  "4002": "PhilipsHueSmartPlugDE1_Power"
};

var remoteState = getCurrentState("IKEASTYRBAR_Button");
var currentPlug = states[remoteState];

if (currentPlug) {
  events.sendCommand(currentPlug, getToggledPlugState(currentPlug));
}