Writing a toggle rule

  • Platform information:
    • Hardware: Raspberry Pi 4
    • OS: current openHABian
    • Java Runtime Environment: OpenJDK 11
    • openHAB version: 3.4.1
  • Issue of the topic: One rule that can turn something ON or OFF

I’ve got a device here (an IKEA Tradfri E1810) that doesn’t send an ON or OFF, just a “TOGGLE”. I can get a rule to run when that toggle event happens without any trouble.

However, what I want to do is to say “If the item is ON, turn it off, otherwise turn it on”

I could do this with an else in the old funky rules system, but am trying to be more current and do everything via the UI now. Is this just a thing the UI isn’t going to be good at, or have I missed something obvious?

I did think of trying to use a map to make OFF to ON and ON to OFF, but couldn’t figure out the syntax to get the map used in the rule.

It’s going to be something easy that I’m just not seeing yet, I’m sure.

Yeah, I knew it was easy. I found the “run script” in which I could embed the old DSL script language, and was able to get an “else” working trivially.

Still struggling to get some other stuff working, but I think I can figure it out.

Edited to add: Went to a text-based rule with triggers there where I could use recievedEvent and got all the buttons on my fancy remote working easily. Yay!

1 Like

There are several approaches in addition to your run script approach.

First of all, with the introduction of the shared and private cache, there’s nothing that can be done in text rules that cannot be done in UI rules.

Some additional approaches:

  1. Create two rules, both trigger when the TOGGLE command is received. But one has a condition to test that the current state of the Switch is ON in a condition and the action sends and OFF command. The other the other tests for OFF and sends ON. This would be the codeless UI approach.

  2. Use a Script Action and the language of your choice. The rule would trigger on the TOGGLE command and the Script Action would look like this in Rules DSL:

MyLight.sendCommand(if(MyLight.state == ON) OFF else ON)

In JS Scripting…

var light = items.getItem('MyLight');
light.sendCommand((light.state == 'ON') ? 'OFF' : 'ON');

or with the latest helper library (4.0)

items.MyLight.sendCommand((items.MyLight.state == 'ON') ? 'OFF' : 'ON');

You can use receivedEvent and all the rest of the implicit variables in Script Actions and Script Conditions in UI rules too.

1 Like

I’m sure there are several approcahes. I’m not sure how the caches help with that in this case, but I’ll look more closely at them.

(I trimmed a bit here.)

I tried both of those approaches!

I started with two triggers for toggle that checked for OFF and sent ON, etc. I wound up with several quirks with it that I wasn’t thrilled about, including both rules running so the lights would go off and then right back on.

With a Script Action, I could not get it to recognize Things or Items predictably, and I never got any of the implicit variables to work. I assume this was user error, and I just didn’t get quite the right magic. Or perhaps I was just misspelling them.

Using a .rules file with the triggers in the file worked exactly as I expected, including the implicit variables.

As it turns out, I was better served with a more complex rule that looked at the receivedEvent to determine what was happening, because this switch can send several messages such as “toggle”, “brightness_up_button”, and “left_arrow”, so being able to handle all that with one case statement was handy.

I used to have a whole bunch of rules to do type conversions - when data comes in Celsius, convert it to Fahrenheit and then store in another channel - and I’m trying to get UoM to do that for me this time. Between being able to write a bunch of the simpler rules via the UI I doubt I’ll have nearly as many when I’m done.

It doesn’t help with this specific case, but with the cache UI rules have reached parity with text based rules in what they can implement. They provide a replacement for global variables.

Given that you are receiving a TOGGLE command, I’m assuming you have two Items, a String Item to receive the command and a Switch Item to TOGGLE. If that’s the case, I’d have to see the rule as implemented to understand this described behavior.

If that’s not the case and you only have one Item, that’s always going to be tricky but not impossible. Again, I’d have to see the rule to understand the behavior.

Beware of differences between languages. Rules DSL has triggeringItemName but JS Scripting will have event.itemName. Always refer to the reference docs for the language you choose when something like this doesn’t work as expected.

Every implicit variable is available in Script Actions and Script Conditions.

And is should have worked in the UI too, which is why I’m keeping on this. If it didn’t because you did something wrong, this is a great learning opportunity for you and others and potentially something that could be made more clear in the docs. If it didn’t because there is a bug, that bug needs to be fixed.

This can be done in a UI rule with a script action too.

I’m game to try again. I removed my text rule (putting it someplace safe) and then re-created the UI rule that triggers on the same event.

I created the new Script Action and put a chunk of the code from the DSL rule in to it:

configuration: {}
triggers:
  - id: "1"
    configuration:
      thingUID: mqtt:topic:feefb09ab0:b17c7192c7
      channelUID: mqtt:topic:feefb09ab0:b17c7192c7:MasterBedroomDesk_switch_action
    type: core.ChannelEventTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/vnd.openhab.dsl.rule
      script: >
        
        switch(receivedEvent) {
          case 'toggle': {
            if (MasterBedroomDesk_lights_MasterBedroomDesk_switch_state.state == ON) {
              sendCommand(MasterBedroomDesk_lights_MasterBedroomDesk_switch_state, OFF);
            }
            else {
              sendCommand(MasterBedroomDesk_lights_MasterBedroomDesk_switch_state, ON);
              sendCommand(MasterBedroomDesk_lights_MasterBedroomDesk_switch_brightness, 254);
            }
          }
        }
    type: script.ScriptAction

And that worked properly. Yay!

I then tried to put the rest of the rule back in place:

configuration: {}
triggers:
  - id: "1"
    configuration:
      thingUID: mqtt:topic:feefb09ab0:b17c7192c7
      channelUID: mqtt:topic:feefb09ab0:b17c7192c7:MasterBedroomDesk_switch_action
    type: core.ChannelEventTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/vnd.openhab.dsl.rule
      script: >
        
        switch(receivedEvent) {
          case 'toggle': {
            if (MasterBedroomDesk_lights_MasterBedroomDesk_switch_state.state == ON) {
              sendCommand(MasterBedroomDesk_lights_MasterBedroomDesk_switch_state, OFF);
            }
            else {
              sendCommand(MasterBedroomDesk_lights_MasterBedroomDesk_switch_state, ON);
              sendCommand(MasterBedroomDesk_lights_MasterBedroomDesk_switch_brightness, 254);
            }
          }
          case 'brightness_down_click': {
            if (MasterBedroomDesk_lights_MasterBedroomDesk_switch_state.state == ON) {
              var currentBrightness = MasterBedroomDesk_lights_MasterBedroomDesk_switch_brightness.state as Number;
              var newBrightness = currentBrightness - 32;
              if(newBrightness < 2) {
                newBrightness = 2;
              }
              if(newBrightness != currentBrightness) {
                sendCommand(MasterBedroomDesk_lights_MasterBedroomDesk_switch_brightness, newBrightness);
              }
            }
          }
          case 'brightness_up_click': {
            if (MasterBedroomDesk_lights_MasterBedroomDesk_switch_state.state == ON) {
              var currentBrightness = MasterBedroomDesk_lights_MasterBedroomDesk_switch_brightness.state as Number;
              var newBrightness = currentBrightness + 32;
              if(newBrightness > 254) {
                newBrightness = 254;
              }
              if(newBrightness != currentBrightness) {
                sendCommand(MasterBedroomDesk_lights_MasterBedroomDesk_switch_brightness, newBrightness);
              }
            }
          }
        }
    type: script.ScriptAction

Those two new case statements work perfectly in the rules DSL. They fail here, with two errors:

 1. Type mismatch: cannot convert from int to BigDecimal; line 17, column 722, length 1
   2. Type mismatch: cannot convert from int to BigDecimal; line 29, column 1221, length 3

Those are the lines with the greater than or less than check:
if(newBrightness < 2) {
if(newBrightness > 254) {

The Internet suggested the BigDecimal.valueOf() would fix it, but that doesn’t exist, and import doesn’t seem to work here. So, I’m stuck with UI Rules, but it works in a text file.

Any suggestions on where I should look to get unstuck?

That’s really odd. There is no reason why that should work in a text rule but not work in the UI. It’s the same engine evaluating and running the code.

The error doesn’t make much sense either. It’s never had a problem converting from primitives to BigDecimal before. But column 722? That line isn’t that long. What’s that all about?

When you pasted the code from your .rules file, did you do it in the script action editor or did you paste it into the “Code” tab? If the latter there are some special formatting issues which can cause it to lose some of your newlines. That may explain these bogus column numbers.

But, because of the bogus column numbers, I don’t trust that the line numbers are correct.

Open it in the editor (not the code tab) and see if the code is formatted properly. If not, fix it so we can get “real” line numbers. If so this is some bizzaro behavior.

The first thing I might try is to force the issue. Do any of the following get it to work?

if(2 > newBrightness) { // Rules DSL uses the first argument as the type to convert the second argument to

if(newBrightness as Number < 2) {

if(newBrightness.floatValue < 2) { // force the BigDecimal to a primitive

These tricks should not be necessary but I’m at a loss for what could be the problem assuming the line numbers reported are correct.

None of those interesting solutions changed anything.

Turns out I’d misread a line, and it wasn’t the comparison that was a problem, but addition or subtraction, these lines:

var newBrightness = currentBrightness - 32;
var newBrightness = currentBrightness + 32;

This fixes it:

var newBrightness = currentBrightness.intValue - 32;
var newBrightness = currentBrightness.intValue + 32;

That conversion is not needed in the file-based DSL rules. Is this a bug?

I don’t actually understand why it can’t convert an integer to a large double, either. Ints should expand to large doubles safely, then have it do the math as large doubles. But maybe I’m too used Perl, which does that kind of thing rather casually.

I really cannot say. Rules DSL is always finicky about type. There is no reason why it should work one way in a text .rules file and not in the UI. It’s the same code interpreter.

If there is a bug here though, it’s almost certainly in the upstream library and not something we can fix here.

Rules DSL is supposed to do so casually too. It always tries to convert everything to a BigDecimal, which is why this error is weird. The result of any calculation is a BigDecimal and it tries to convert all numbers to be BigDecimal.

But you’ll have to show what you mean by converting an integer to a large double. If you mean BigDecimal it’s because in Java (and despite the fact we are in Rules DSL, all these things are driven by Java) not everything is an Object. Primitives (int, short, long, double, boolean) are what they are. They have not methods. They cannot be case to something else. They exist outside the Object hierarchy. BigDecimal is an Object. So you could never cast an int to a BigDecimal but you most definitely can create a new BigDecimal from an int. new BigDecimal(32). Rules DSL usually does this sort of thing for you though. It’s weird that it’s failing here.

I did not mean in this language; I meant that the numbers should be compatible. But going to the largest, most flexible type and doing everything there makes sense, and if that’s what it should do then this should work.

It’s almost certainly a problem with the difference between raw types and objects, then, because I’ve just got a raw number being added.

I also can’t create a BigDecimal or use “as BigDecimal” because that type isn’t defined. Should it be? Or would I have to import java.math.BigDecimal? Can you even do that in a UI rule? “import” was an unknown keyword when I tried.

Anyway, odd quirk. The other problems I was having didn’t show up, and I’m going to blame them on pilot error. I think several of them didn’t work when I tried a rules file, but I kept poking them till they did. If I’d have kept at it in the UI Rule I might have found the solution there, too.

Thank you for your never-ending help, Rich. You bring a huge amount to this community.

I thought it was already imported. However, you can always reference a Java class using it’s fully qualified name. new java.math.BigDecimal(32)

You can’t import in a UI Script Action/Condition for Rules DSL. The Rules DSL syntax requires imports to exists outside the rule and in a script action/condition there is no outside the rule where you can put these statements.

Ultimately, while I used to be a big promoter of Rules DSL, I now think the only ones who should use it are old timers with lots of legacy rules written in it. Pretty much all the other rules languages, including Blockly, is better than Rules DSL on any criteria I can think of, including ease of use.

I’m more than willing to believe you; I’ve hardly looked at my system in several years, and you’re always involved and helping people. I’ll look into the javascripty ones, or one of the other formats.

I do have some old rules, but many of them are being obsoleted by Units of Measurement, and others by changes to my infrastructure. No reason to cling madly to them if they’re going to be a struggle.

Hi,

could you send me the txt rule or paste it here. I am trying the same thing and cant get the rule with receivedEvent running.

Thnx

Better post your rule here so we can help you.
BTW - in JS there is a new command available:

sendCommandIfDifferent()

Depending on your script it might make it easier.

This is my rule:
Item is:
String MQTTIkeaDimmerE15241Action “Mqtt ikea dimmer e1524 action” {channel=“mqtt:topic:MQTTIkeaDimmerE1524_1:action”}

rule "react on Dimmerswitch"
when
    Channel "mqtt:topic:MQTTIkeaDimmerE1524_1:action" triggered
then
    var actionName = receivedEvent
    if(actionName == "toggle") {
       gLights.sendCommand(OFF)
    }
    if(actionName == "brightness_down_click") {
       gLightsdimmer.sendCommand(50)  
    }
end 

ok. do you have some more information?

  • any error messages in the log?
  • does the rule get triggered?
  • could you log out the content of receivedEvent?
  • does the rule jump into your if statement? (requires a log statement within your if clause)

No, it doesnt even jump into the then…

How do you know? There’s no log statement. If receivedEvent isn’t literally the String “toggle” or “brightness_down_click” the rule will silently exit.

The Channel is defined as an event Channel? You cannot use a state Channel to trigger a rule like this. State Channels can only be linked to Items.

Hi,

my first try was :

rule "react on Dimmerswitch"
when
    Channel "mqtt:topic:MQTTIkeaDimmerE1524_1:action" triggered
then

logInfo('MIRHOME', 'Rule triggered ')

    var actionName = receivedEvent
    if(actionName == "toggle") {
       gLights.sendCommand(OFF)
    }
    if(actionName == "brightness_down_click") {
       gLightsdimmer.sendCommand(50)  
    }
end 

and it didnt write anything in the log.

How do i make a channel an event channel? do i just link a #event at the end?

You define it as an event Channel in the first place.

I guess MQTT calls it a “trigger” Channel.