Rule to control ceiling fan with 4 levels (Off, 1, 2 or 3) from a smart dimmer switch

What I have:

A Sonoff iFan03, flashed with Tasmota and connected to OH via MQTT

  • Light Channel MB-Light linked to Item: MasterBedroom_Light which is a switch type
  • Fan Channel MB-Fan linked to Item: MasterBedroom_Fan which is Dimmer type. Channel is set to be a number of either 0, 1, 2 or 3.

A Zooz On/Off Z-Wave switch.

  • Switch Channel linked to Item: MasterBedroom_Light_Zooz which is switch type

A Zooz Dimmer Z-Wave dimmer switch

  • Dimmer channel linked to Item: MasterBedroom_Fan_Zooz which is a dimmer type

I have a rule that ties MasterBedroom_Light_Zooz and MasterBedroom_Light together nicely, and works perfectly.

What I need to work out how to do next is write a rule to that ties the MasterBedroom_Fan_Zooz to the MasterBedroom_Fan so that the dimmer can set the fan to one of the 4 levels mentioned above. But I really have no idea where to start. I know I have to somehow translate the dimmers 0 - 100% to the 0, 1, 2 or 3 that the iFan expects, but am not sure the best way to do that? Or would I somehow leverage scenes to do this (the Zooz Dimmer does support these)?

I have been reading the documentation and reviewing a lot of code examples, and I have a feel for the flow and syntax of the code, but still am not too sure where/how to start this.

So does anyone have a rule that more or less does this or something similar that I can dissect and change? I tend to learn to code by doing, rather than reading other code. And that actually sparks another question, is there a way to debug rule code? An output console or something like it?

Here is what I have to start with:

rule "master bedroom ceiling fan control"
when
   Item MasterBedroom_Fan changed or
   Item MasterBedroom_Fan_Zooz changed
then
   if (triggeringItem.state != MasterBedroom_Fan.state) {
     MasterBedroom_Fan.sendCommand(triggeringItem.state.toString)
   } else if  (triggeringItem.state != MasterBedroom_Fan_Zooz.state) {
     MasterBedroom_Fan_Zooz.sendCommand(triggeringItem.state.toString)
   }
end

This ties the two items together, and does work. I am just not sure how to convert the percentage from MasterBedroom_Fan_Zooz to 0, 1, 2 or 3 and pass that to MasterBedroom_Fan and vice versa.

In any other language I would do this with a case statement.

Case MasterBedroom_Fan_Zooz:
    0:          Masterbedroom_Fan.sendCommand(0) 
    1 - 33:     MasterBedroom_Fan.sendCommand(1)
    34 - 66:    MasterBedroom_Fan.sendCommand(2)
    67 - 99:    MasterBedroom_Fan.sendCommand(3)

And then vice-versa

Case MasterBedrooom_Fan:
    0:    Masterbedroom_Fan_Zooz.sendCommand(0) 
    1:    MasterBedroom_Fan_Zooz.sendCommand(25)
    2:    MasterBedroom_Fan_Zooz.sendCommand(50)
    3:    MasterBedroom_Fan_Zooz.sendCommand(75)

Not sure if that would work in this case though. Seems ineloquent and could have problems with the fade in of the dimmer.

Case statements work in Rules DSL. Hereā€™s what I use for my rule-based thermostat, and it should translate pretty easily into what you want to do. In this case, Iā€™m actually using IF statements within the case to determine if a value is between a threshold.

switch (Temperature_Google_Mode.state) {
    case "heat" :
    {
        if (CurrentTemp < TargetTemp && Maker_Fireplace.state == OFF) { Maker_Fireplace.sendCommand(ON) }
        if (CurrentTemp > TargetTemp && Maker_Fireplace.state == ON)  { Maker_Fireplace.sendCommand(OFF) }
    }
    case "cool" :
    {
        if (CurrentTemp > TargetTemp && Outlet_Air_Conditioner.state == OFF) { Outlet_Air_Conditioner.sendCommand(ON) }
        if (CurrentTemp < TargetTemp && Outlet_Air_Conditioner.state == ON)  { Outlet_Air_Conditioner.sendCommand(OFF) }
    }
    case "off" :
    {
        Temperature_Google_Mode.sendCommand("off")
    }
}

I donā€™t know how to make a case fit in a threshold (1-33), but you could just use IF statements and I doubt youā€™d see any difference in performance.

I wouldnā€™t say ā€œineloquentā€. Itā€™s just straightforward and simple. But Iā€™m not sure what your concern is with the fade-in of the dimmer, so maybe Iā€™m missing something.

No, it is far more likely that I am the one missing something. Thanks for the code example, it definitely points me in the right direction.

rule "master bedroom ceiling fan control"
when
   Item MasterBedroom_Fan changed or
   Item MasterBedroom_Fan_Zooz changed
then

if MasterBedroom_fan changed  <---- This is probably not right.
{
  switch (MasterBedroom_Fan.state) {
    case 0 :
    {
      Masterbedroom_Fan_Zooz.sendCommand(0)
    }
    case 1 :
    {
       Masterbedroom_Fan_Zooz.sendCommand(25)
    }
    case 2 :
    {
        Masterbedroom_Fan_Zooz.sendCommand(50)
    }
    case 3 :
    {
        Masterbedroom_Fan_Zooz.sendCommand(75)
    }
 }
}

end

That is my start here. But I an not sure about that first if statement. How do I separate out which item has changed and case from that?

Or can I do something like this:

when
   Item MasterBedroom changed
then
<<<<< switch block >>>>

when
    Item MasterBedroom_Fan_Zooz changed
then
<<<< switch block >>>>

Actually, there might be a better way to think of this. When you increment MasterBedroom_Fan_Zooz by tapping the switch, it probably has set values that it goes to, rather than just jumping up by a percentage. If so, you can identify those values and use those in the case statement, and thereā€™s no need to use thresholds.

It occurs to me that you should also use ReentranceLock to guard against concurrency. Otherwise, the rule will fire multiple times while youā€™re adjusting the dimmer. Thereā€™s a section on it in the Rules documentation.

1 Like

This dimmer is a hold down and let go when ready type dimmer, so there are not set increments on button push. So having a case statement with a wide margin of error is probably a good thing. Based on how I am seeing this thing work with OH, I am guessing the rule wonā€™t fire till I release the button and the switch reports back to OH

Okay, thatā€™s good (but Iā€™d definitely check by observing the behaviour in your log).

Youā€™re correct that the first IF statement doesnā€™t work. ā€œitem changedā€ only works for triggers, not in rule logic. Thinking about this more, though, you really donā€™t need the case statement. Just use a series of IF statements so that you can isolate each possible scenario. For the thresholds, it would be:

if (MasterBedroom_Fan_Zooz.state > 0 && MasterBedroom_Fan_Zooz.state <= 33) { Masterbedroom_Fan.sendCommand(1) }
etc.
if (MasterBedroom_Fan.state == 0) { Masterbedroom_Fan_Zooz.sendCommand(25) }
etc.

Now is when I belatedly tell you that I have very little training as a programmer, and someone else probably has a better solution. Iā€™m just one of the few people still awake due to living on the west coast of Canada, so youā€™re stuck with me. :wink:

No, I definitely need to know which one has changed in my main loop, otherwise the non-changed one will just cancel out the changed one. Basically I only want to change the Item that has not changed, not both, otherwise I end up in an infinite loop, or at the very least immediately undoing what I just did.

I donā€™t think I was clear. You need one rule with eight IF statements:

  • if MasterBedroom_Fan_Zooz is 0, 1-33, 34-66, or 67-100
  • if MasterBedroom_Fan is 0, 1, 2, or 3

I gave you an example of each of those types of statement, so you just have to write them all out and add in the reentrantLock.

By using reentrantLock, you will prevent the infinite loop (what I referred to earlier as a race condition).

This isnā€™t possible. If an itemā€™s state doesnā€™t change, then it canā€™t trigger a rule.

True, but this rule is triggered if one items or the other item is changed. But I did manage to work it out, the magic I was missing is:

if (triggeringItem.state != MasterBedroom_Fan_Zooz.state)

The triggeringItem.state is what I was missing.

Ah, I see what you mean now. I was previously going to suggest that you just use two separate rules for each device trigger, and I didnā€™t think that through when I revised to ā€œone rule with many IF statementsā€.

1 Like

This is what I have so far, it appears to be working more or less ok, but I welcome suggestions on better ways to do this.

import java.util.concurrent.locks.ReentrantLock

val ReentrantLock lock  = new ReentrantLock()

rule "master bedroom ceiling fan control"
when
   // MasterBedroom_Fan is the iFan03
   Item MasterBedroom_Fan changed or
   //MasterBedroom_Fan_Zooz is the Dimmer
   Item MasterBedroom_Fan_Zooz changed
then

// Prevent Concurrency from happening.
lock.lock()
try {

// If the change came form the MasterBedroom_Fan (ie the iFan03) then
// change the state of MasterBedroom_Fan (ie the Dimmer) 
if (triggeringItem.state == MasterBedroom_Fan.state) {

  switch (MasterBedroom_Fan.state) {
    case 0 :
    {
      MasterBedroom_Fan_Zooz.sendCommand(0) 
    }
    case 1 :
    {
      MasterBedroom_Fan_Zooz.sendCommand(33) 
    }
    case 2 :
    {
      MasterBedroom_Fan_Zooz.sendCommand(50) 
    }
    case 3 :
    {
      MasterBedroom_Fan_Zooz.sendCommand(67) 
    }
  }
}

// If the change came form the MasterBedroom_Fan_Zooz (ie the Dimmer) then
// change the state of MasterBedroom_Fan (ie the iFan03) 
else if (triggeringItem.state == MasterBedroom_Fan_Zooz.state) {
  
  // If the dimmer is off, then set the Fan to 0 or off
  if (MasterBedroom_Fan_Zooz.state == 0) { MasterBedroom_Fan.sendCommand(0) }
  // If the dimmer is between 1 and 33% then set it to 1 or Low
  else if (MasterBedroom_Fan_Zooz.state > 0 && MasterBedroom_Fan_Zooz.state <= 33) { MasterBedroom_Fan.sendCommand(1) }
  // If the dimmer is between 34 and 66% then set it to 2 or Medium
  else if (MasterBedroom_Fan_Zooz.state > 33 && MasterBedroom_Fan_Zooz.state <= 66) { MasterBedroom_Fan.sendCommand(2) }
  // If the dimmer is between 67 and 99% then set it to 3 or high
  else if (MasterBedroom_Fan_Zooz.state > 66 && MasterBedroom_Fan_Zooz.state <= 99) { MasterBedroom_Fan.sendCommand(3) }

}

} 

finally
 { 
  lock.unlock()
 }

end

Right, that code worksā€¦ mostly. Playing around with it I managed to get the rule to go into an infinite loop a few times (and cycle the status of both Items repeatedly, about 12 times/second), which I thought the lock was supposed to stop. Any suggestions on how to prevent this?

Would adding a 1 second pause before unlocking make a difference?

...
  createTimer(now.plusSeconds(1),[])
} 
finally
{ 
  lock.unlock() 
}

end 

or

...
} 
finally 
  { 
   createTimer(now.plusSeconds(1),[])
   lock.unlock()
  }

end

Drop the ReentrantLock (not needed) and use postUpdate instead of sendCommand for the MasterBedroom_Fan. Then change the trigger for MasterBedroom_Fan to ā€˜received commandā€™.

Wait, waitā€¦ forgot these are both devicesā€¦ is MasterBedroom_Fan ever manually adjusted at the device? If so, this may not work. But if you arenā€™t manually manipulating it, then this should work.

Not sure what you mean.

Iā€™m not familiar with a Sonoff iFan03. Are there manual controls on the deviceā€¦ like a dimmer. Or is it just a box hidden in the wall?

Itā€™s a box hidden in the fan. It has a webUI that has buttons you can click to control it, but I suspect Iā€™ll rarely if ever use those. And I actually was not even using it when I managed to drop into never ending loops, that happened when I adjusted it in the control section of PaperUI.

Coolā€¦ then using postUpdate and the received command trigger should work for you.

1 Like