RGB Colour rule help

Hi Guys

I have a rule which cycles through colours on my Yeelight RGB Strip which while working could be allot simpler. I was hoping someone might be able to point me in the right direction to make the rule simpler/more compact. I’m not a good coder but once I have a working example I can work through it so any help would be greatly appreciated.

rule "Kitchen LED Party Mode"
when
	Item Kitchen_Party_Mode changed from OFF to ON
then
	if (KitchenYeelightLEDStripTest_Power.state == ON) {
		while (KitchenYeelightLEDStripTest_Power.state == ON && Kitchen_Party_Mode.state == ON) {
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"0,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"10,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"20,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"30,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"40,100,100")
		Thread::sleep(1500)
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"50,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"60,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"70,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"80,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"90,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"100,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"110,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"120,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"130,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"140,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"150,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"160,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"170,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"180,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"190,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"200,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"210,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"220,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"230,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"240,100,100")
		Thread::sleep(1500)
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"250,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"260,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"270,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"280,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"290,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"300,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"310,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"320,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"330,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"340,100,100")
		Thread::sleep(1500)
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"350,100,100")
		Thread::sleep(1500) 
		sendCommand(KitchenYeelightLEDStripTest_RGBColor,"359,100,100")
		Thread::sleep(1500) 
		}
	sendCommand(KitchenYeelightLEDStripTest_RGBColor,"0,0,100")
}
end

Not sure if I can help, but I will try. Just started a couple of weeks ago…

My Sonoff Tasmota light supports a “Color Cycle Mode” and a “Jump trough Colors mode”.
So I can define a switch to user “Party mode” and “Disco mode”

Doesn’t the Ledstrip support that kind of command?

Hi onacvooe

The Yeelight strip allows you to set a “flow” of up to 4 colours only from what I can see in the original app. What I am trying to achieve is for it to cycle through as many colours as possible, randomly might also work well.

Is this any use to you?

Alternatively…

You could totally cheat…

Setup a Chase using Jan’s DMX binding, then use a rule to copy a colorpicker mapped to the same DMX RGB channels and push the HSL value into your LED fixture?

(I tried this 2 days ago with a simple 2 Node NodeRed flow and it works like a charm, no reason why a simple DSL rule can’t do the same)

Ah yes, I just found “Flow” thing in a online PDF Document, you can find it here.

It should be possible more easy, reading from the linked PDF document. You can put it in a infinte loop. Lots of fun stuff. Start reading from page 14. It can do a lot of work for you. And make your rule a lot more easy.

Oh, I’m glad you asked for help. This code is really bad in an OH context. There are a few things that the Rules DSL is not so friendly for helping new users and this is one of those cases.

The naive approach for most new coders is to work through the steps in their mind and then code each step in the Rule. Most of the time that’s not a problem. It may be a little ugly but it doesn’t cause any real harm to OH. But in this case you have a Rule that consumes a run time thread potentially indefinitely which is a really bad idea and at a minimum over 40 seconds!

The other thing I notice is you are using the sendCommand Action instead of the function. See this section of the docs for details.

Finally I see that the only difference between all those lines of code is the first value of the HSB being sent to the light. Thus this is a clear violation of DRY (Don’t Repeat Yourself) which is probably why you asked the question in the first place.

I notice some other undesirable side effects of this approach as well. For example, if the lights are turned OFF, the LED strip could potentially take 40+ seconds before the light actually turns OFF.

So let’s address these problems. First, let’s get rid of the duplicate code. We will tighten the loop to run every 1.5 seconds and recalculate the state on each time through the loop.

then
    if(KitchenYeelightLEDStripTest_Power.state == ON){
        val increment = 10
        var currColor = 0
        while(KitchenYeelightLEDStripTest_Power.state == ON && Kitchen_PartyMode.state == ON){
            KitchenYeelightLEDStripTest.sendCommand(currColor+",100,100")
            currColor = (currColor + increment) % 360 // remainder operation, when it gets over 360 currColor goes back to 0
            Thread::sleep(1500)
        }
        KitchenYeelightLEDStripTest.sendCommand("0,100,100")
    }
end

This is already WAY better. 10 lines of code compared to around 74 lines of code in the original. We also use the method on the Item instead of Action.

The real trick is the use of the modulo operator %. This is sometimes called the remainder operation. Essentially we divide (currColor + increment) by 360 and set currColor to the remainder of that operation. The remainder of 150 / 360 = 150. The remainder of 360/360 = 0. The remainder of 370/360 = 10. So the value will never get above 360 and it will continue to loop through the values until the loop is canceled.

Also, note that we are looping and therefore checking the state of the two Switches every 1.5 seconds, the light will remain on no more than 1.5 seconds after you turn one of them OFF. That’s better than over 40 seconds, but not yet perfect.

Now let’s deal with that sleep. We can avoid consuming a runtime thread for long period of time doing nothing by using Design Pattern: Looping Timers.

var Timer partyTimer = null

rule "Kitchen LED Party Mode"
when
    Item Kitchen_Party_Mode changed from OFF to ON
then

    partyTimer?.cancel // cancel the timer if one exists for some reason

    val increment = 10
    var currColor = 0

    partyTimer = createTimer(now, [ |
        if(KitchenYeelinghtLEDStripTest_Power.state == ON && Kitchen_PartyMode.state == ON) {
            KitchenYeelightLEDStripTest.sendCommand(currColor+",100,100")
            currColor = (currColor + increment) % 360
            partyTimer.reschedule(now.plusMillis(1500))
        }
        else {
            KitchenYeelightLEDStripTest.sendCommand("0,100,100")
            partyTimer = null
        }
    ])

In the above we create a looping Timer to execute immediately. If the two switches are ON, we set the Light to the calculated color. Then we reschedule the Timer to go off again in a 1.5 seconds. If not, we set the color to 0 and exit the Timer. Despite the warnings in the logs, this will work. The Timer will get it’s own copy of increment and currColor so the incrementing will work.

The big difference between this and using Thread::sleep is that the Timer doesn’t consume a thread doing nothing during that 1.5 second period waiting to run the next step. Instead it is sitting in the background not consuming a runtime thread, freeing that thread up for other Rules to use.

But, like the previous one, this version also will keep the light ON for up to 1.5 seconds after one of the Switches is turned OFF. To deal with that problem we need a separate Rule.

And because the canceling of the Timer is handled elsewhere we can move all the end of Timer loop stuff to that other Rule.

var Timer partyTimer = null

rule "Kitchen LED Party Mode"
when
    Item Kitchen_Party_Mode changed from OFF to ON
then

    partyTimer?.cancel // cancel the timer if one exists for some reason

    val increment = 10
    var currColor = 0

    partyTimer = createTimer(now, [ |
        KitchenYeelightLEDStripTest.sendCommand(currColor+",100,100")
        currColor = (currColor + increment) % 360
        partyTimer.reschedule(now.plusMillis(1500))
    ])
end

rule "Party mode turned off"
when
    Item Kitchen_Party_Mode changed from ON to OFF or
    Item KitchenYeellightLEDStripTest_Power.state changed from ON to OFF
then
    partyTimer?.cancel
    KitchenYeelightLEDStripTest.sendCommand("0,100,100")
    partyTimer = null
end

NOTE: The partyTimer?.cancel is the same as if(partyTimer !== null) partyTimer.cancel.

9 Likes

Hi Rich

Thank you for looking at my problem. I have implemented your script & understand how & why it works so thank you for being so thorough! I only have 1 issue with it in that while it is running the light seems to get stuck on a colour every now & again & then updates to a colour much further in the cycle. I can see the log updating but the colour does not change. This was not happening when running my original script & was wondering if there is anything else I need to do to get the consistency of changing colours back like my original script?

It should work. You are seeing the Item recurving the command for each color but the light tan changing? Unfortunately, that means the problem is likely in the binding.

I’m not sure why it would reveal itself when using a timer instead of sleep.

Look at the timestamps in events.log and verify that they are 1.5 seconds apart.

great example!

partyTimer?.cancel

is this the elusive elvis operator?
we are testing for null

It’s a typo. That should be !==.

1 Like

I found my problem with other rules for the UniFi Binding transforming numbers into readable time stamps. Every time these rules ran the RGB cycle would stop & then play catch up. Changing the following in runtime.cfg has fixed it for now. Thanks again for the help!

org.eclipse.smarthome.threadpool:RuleEngine=15

That points to other serious problems with your rules. Increasing the number of threads may only be a band aide. Over time you should try to reduce how long each of your rules run individually.

I believe the Unifi binding guys are working on including a channel for uptime that is in a readable format so my rules will no longer be required. Once this is implemented I will reduce the number of threads back to defaults.

Thanks again for all you do Rich!

I have just had a play with this code and love the simplicity. There is an error though in the partyTime?.cancel line of “Party mode turned off”. It should be partyTimer?.cancel.

Thanks, I’ve corrected the posting.

1 Like