Control an IR device with a single item and Google Assistant

This tutorial enables you to control a device with an IR remote using a single item and Google Assistant. I’m using it to with a Rowenta pedestal fan, but it can be broadly applied to any IR-controlled device.

For the purposes of this tutorial, I’m assuming that the reader already knows:

  1. How to set up and expose openHAB devices to Google Assistant (typically through myopenhab.org).
  2. How to record and send IR commands using a controller such as a Broadlink RM4 or Logitech Harmony Hub.

Note: this solution requires that you only control the device through openHAB. If you use the original IR remote or a button on the device, openHAB won’t know about the change and will get out of sync.

Commands

My Rowenta fan’s remote has the following buttons, each with an individual IR code:

  • PowerToggle (on/off)
  • Silent
  • +/- (toggles upward through Low, Medium, and High)
  • Turbo

I needed to combine all of these commands into a single item, so that I can tell Google Assistant to “turn on the fan” or “set the fan to silent”. For this to work, openHAB has to know if the fan is currently on/off, and what setting it’s on.

Items

Here are my items in text configuration. You can do this in MainUI if you prefer.

Dimmer Rowenta_Fan "Bedroom Fan" <fan> (Group_HVAC) { ga="Fan" [speeds="0=off,10=silent:one,30=low:two,50=medium:three,70=high:four,90=turbo:five,100=on", lang="en", ordered=true, roomHint="Environment" ]}
Switch Rowenta_Fan_Switch "Power" <fan>
String Rowenta_Fan_Speed "Speed [%s]" <fan>
String Rowenta_Fan_Command "Rowenta Fan" {channel="YOUR_IR_CONTROLLER"}

The only item I interact with is Rowenta_Fan. It’s a dimmer, because that’s what Google Assistant requires for a fan. I’ve defined numerical fan speeds that represent all of the commands I want to send to the fan (off, silent, low, medium, high, turbo, on).

Note: I’ve spread my commands out from 0-100, but I could also have used 1-7. Google Assistant doesn’t care what the numbers are; it just passes them on to openHAB. If you wanted to, you could have one item that accepts 101 different commands (with 101 corresponding rules).

The Rowenta_Fan_Switch and Rowenta_Fan_Speed items are used to store the current power state and setting. I don’t show them in any UIs, as they only need to be updated by rules.

Rules

Whenever Rowenta_Fan changes, a rule carries out the following activities:

  1. checks Rowenta_Fan_Switch to see if the fan is on/off, and toggles it as necessary
  2. checks Rowenta_Fan_Speed and sends the necessary IR commands to get from the current state to the new state (e.g. from Silent to Low)
  3. updates the Rowenta_Fan_Switch and Rowenta_Fan_Speed items to the new states

I originally had one big rule with multiple IF and CASE statements to do all of this, but I’ve simplified it into individual rules. It’s easier to replicate in MainUI rules, and easier to debug.

Power off

Since my fan uses the same IR code for on and off, I have to check the current state of the fan. If you have unique IR codes for on and off, you can just send them without needing to check.

rule "Fan Control - Power Off"
when
    Item Rowenta_Fan changed to 0
then
    //If the fan isn't already off, send the IR command to toggle it
    //The IF statement is necessary due to my device using the same IR command for ON and OFF
    if (Rowenta_Fan_Switch.state != OFF)
    {
        Rowenta_Fan_Command.sendCommand("PowerToggle")
    }
    //Update power status to OFF
    Rowenta_Fan_Switch.postUpdate(OFF)
end

Power on

When my Rowenta fan powers on, it automatically returns to its previous setting. This rule accounts for that by updating the Rowenta_Fan item to match what was saved in Rowenta_Fan_Speed. If I didn’t do this, Google Assistant and my sitemap would know that the fan is on, but wouldn’t know the current setting.

As with the “Power off” rule, you don’t need to check the state if your device has a unique IR command to turn it on.

rule "Fan Control - Power On"
when
    Item Rowenta_Fan changed to 100
then
    //If the fan is off, send the IR command to toggle it and update the power status
    if (Rowenta_Fan_Switch.state != ON)
    {
        Rowenta_Fan_Command.sendCommand("PowerToggle")
        Rowenta_Fan_Switch.postUpdate(ON)
    }
    //Update Rowenta_Fan_Speed to previous level
    var level = 0
    switch (Rowenta_Fan_Speed.state)
    {
        case "Silent": level = 10
        case "Low":    level = 30
        case "Medium": level = 50
        case "High":   level = 70
        case "Turbo":  level = 90
    }
    Rowenta_Fan.postUpdate(level)
end

Complex IR sequences

Once the fan is turned on, I have to press the +/- button multiple times to toggle through the Low/Medium/High settings of my fan. By checking the current setting (stored in `Rowenta_Fan_Speed), openHAB knows the correct number of IR commands required to get to the new mode. I repeat this for the fan’s Medium and High settings, adjusting the IR sequences accordingly.

This type of rule is necessary whenever the current state of the device determines your IR sequence. You’ll need to map out and test all of your scenarios, and may need to pause between commands so that you don’t overwhelm your device.

rule "Fan Control - Low"
when
    Item Rowenta_Fan changed to 30
then
    //If the fan is off, send the IR command to toggle it and update the power status
    if (Rowenta_Fan_Switch.state != ON)
    {
        Rowenta_Fan_Command.sendCommand("PowerToggle")
        Rowenta_Fan_Switch.postUpdate(ON)
    }
    //Send the IR commands required to get from the current state to the new state
    if (Rowenta_Fan_Speed.state == "Silent" || Rowenta_Fan_Speed.state == "Turbo")
    {
        Rowenta_Fan_Command.sendCommand("+/-") //first command changes to Medium
        Rowenta_Fan_Command.sendCommand("+/-") //second command changes to High
        Rowenta_Fan_Command.sendCommand("+/-") //third command changes to Low
    }
    else if (Rowenta_Fan_Speed.state == "Medium")
    {
        Rowenta_Fan_Command.sendCommand("+/-") //first command changes to High
        Rowenta_Fan_Command.sendCommand("+/-") //second command changes to Low
    }
    else if (Rowenta_Fan_Speed.state == "High")
    {
        Rowenta_Fan_Command.sendCommand("+/-") //first command changes to Low
    }
    Rowenta_Fan_Speed.postUpdate("Low") //update item so that openHAB knows the current state
end

Unique IR commands

The Silent command on my fan has its own IR code, but if my fan is already in Silent mode, a subsequent Silent command will cancel it and revert to whatever state it was previously in. So once again, I check the state before sending the command. You should test all of your IR commands for behaviours like this.

rule "Fan Control - Silent"
when
    Item Rowenta_Fan changed to 10
then
    //If the fan is off, send the IR command to toggle it and update the power status
    if (Rowenta_Fan_Switch.state != ON)
    {
        Rowenta_Fan_Command.sendCommand("PowerToggle")
        Rowenta_Fan_Switch.postUpdate(ON)
    }
    //If the fan isn't already in Silent mode, send the IR command
    if (Rowenta_Fan_Speed.state != "Silent")
    {
        Rowenta_Fan_Command.sendCommand("SilentNight")
    }
    //Update Rowenta_Fan_Speed to new setting
    Rowenta_Fan_Speed.postUpdate("Silent")
end

That’s a lot of repetitive code

You may have noticed that we’re constantly trying to turn the fan on, which means that this section of code shows up A LOT.

    //If the fan is off, send the IR command to toggle it and update the power status
    if (Rowenta_Fan_Switch.state != ON)
    {
        Rowenta_Fan_Command.sendCommand("PowerToggle")
        Rowenta_Fan_Switch.postUpdate(ON)
    }

It’s not really hurting anything, because these are pretty simple rules. But if you’re bothered by the inefficiency of it all, you can use a lambda. What’s a lambda?

Seriously, that’s an explanation that even I can understand.

If you’re using UI rules, the equivalent would be…umm…well…let’s just ask Rich again.

And with that, I’ve run out of useful information to steal quote from Rich.

1 Like

Nice tutorial, thanks for posting!

Just a couple of minor code ideas which might make the code even easier to read.

  • In “Fan Control - Power On” using a switch instead of the if/else the code will look a little cleaner. I also like to centralize the lines of code that do things to one place.
    var level = 0
    switch(Rowenta_Fan_Speed.state) {
        case "Silent": level = 10
        case "Low":    level = 30
        case "Medium": level = 50
        case "High":   level = 70
        case "Turbo":  level = 90
    }
    if(level > 0) Rowenta_Fan.postUpdate(level)

You could even get clever her and use a map lookup or the like but I think this hits the spot for readability. If you are certain Rowenta_Fan_Speed will always have one of those states, you can eliminate the if(level > 0).

  • In all the rules, when the fan isn’t on you toggle it on and then immediate adjust it. Be wary of the timing here, particularly since this is driven by IR. It might still be processing the “PowerToggle” when the subsequent commands come in which not all devices/technologies handle well. Some sleeps might be needed if commands get lost. The Gatekeeper DP could be employed in that case which will queue up the commands and make sure enough time passes between them to no cause trouble.

  • The “Fan Control - Low” could probably be adjusted to handle all the levels using a for loop. However, I don’t understand how three “+/-” commands gets you from “Silent” or “Turbo” to “Low”. But you should be able to get the delta between the the current level and the new level to determine how many “+/-” you need to send and use a for loop to send that many.

  • The “Fan Control - Silent” rule could probably be merged with the “Turbo” and avoid the duplicated code to toggle the fan on.

  • It might be worth while centralizing that code that toggles the fan on to a global lambda and have all your rules call that to avoid the duplicated code.

  • Ultimately this is a state machine so there might be some additional ideas in A State Machine Primer with HABlladin, the openHAB Genie

1 Like

Thank you very much for taking the time to write this tutorial :pray: :pray: :pray:

I removed all of the switch/case statements in my original rule to make this easier for non-coders to grasp, and so that it would directly translate into Blockly. But you’re totally right about that. It’s such a simple switch/case that it’s worth doing that way in DSL and then just explaining how to make the conversion if someone asks about it.

Yep, it will always have one of the operational states defined by the user, so no need for the if(level > 0) to account for others.

Right, I forgot to point out that the user will need to lay out and test their IR sequences for each state change. My Rowenta fan doesn’t need any sleeps between commands, but others may need to do so.

Those three +/- commands are a specific oddity of my fan. Whenever I switch it out of Silent or Turbo then it defaults to Medium, so I have to press the button three times to go Medium → High → Low.

I left that code as it is in my system to show that the users needs to map out the specific sequences for their device, but I think I’ll simplify it to just say “INSERT IR COMMANDS” to avoid confusion.

In my original rule it only runs once before going into a case statement to handle the specific level. When I broke it into separate rules, it seemed best to keep the power-on code in each rule to ensure that the IR commands are processed in the correct order. Would that be a concern with a lambda?

No. A lambda is just a way to replace a bunch of repeated lines of code with a function call. So if you put at the top of your .rules file:

val toggleOn = [ |
  if(Rowenta_Fan_Switch.state != ON) {
    Rowenta_Fan_Command.sendCommand("PowerToggle")
    Rowenta_Fan_Switch.postUpdate(ON)
]

And then at the top of your rules replace that code with:

    toggleOn.apply()

Well then, I’ve just learned something new. :brain:

Is there an equivalent in Blockly? I’m admittedly overthinking the “make it easy to convert to Blockly” bit, because I’m setting up to eventually go in that direction.

Maybe.

The “store value” block accesses the cache. If you choose “shared” for which cache to store it than other rules can access it too. So if you can assign a function to a variable and store that variable in the shared cache than other rules should be able to access it and call it. It’s that last part I’m not positive will work. I don’t know if Blockly will let you pass a function to the “store value” block.

However, it might be easier to put that function into a Script (Settings → Scripts) and use “runRule” to call that script. You could event just make that be a separate action on the UI rule. UI rules can have multiple actions.

Once you are in the UI, not everything needs to be coded out. A UI rule can have multiple actions which get executed in sequence. So the first action can be to call the Script to toggle on the fan if necessary and the second action would just need to implement the unique stuff for that rule.