Configurable Sequencer with timed steps (e.g. for holiday light effects)

Problem Statement

For my holiday lights, I’m using LED strips with over 30 different effects. Not all of these effects fit the theme I’m looking for, so I would like to be able to select which effects are shown and which will be skipped. Further, I wanted to have the effects “roll” from one to the next after a predetermined amount of time, so I don’t have to manually switch them. While I had rules in place last year that did this for me, I didn’t like that they weren’t easily configurable without rewriting. So, for this year, I decided to create something more dynamically configurable, and the idea of a sequencer popped into my head. Sequencers are powerful automation devices, allow a variable number of steps, and great configurability. I didn’t see anything that fit the bill in the forums, so I decided to make my own.

Solution

My implementation of a sequencer for openHAB is shown below:

The sequencer allows:
-Starting by pressing the “Start” PB. The Current Step selected will be the step from which the sequencer begins
-Stopping/Resetting the sequencer by pressing the “Stop” PB. The Current Step will be reset to 0 (and starts from step 1 on next restart or then-selected Current Step)
-Changing the Current Step (either prior to sequencer start, or while the sequencer is running)
-Changing the Step Length
-Selecting an action/value for each step. Selecting a value of “End” determines the last step before the sequence is restarted from step 1 again.

openHAB Items, Rules and Sitemap entries

Below are the sample items, rules and sitemap files which implement the sequencer. I’ve included comments that should be self-explanatory to allow the user to adapt the sample to their own configuration, step values, etc…

Note: This is pretty much version “1.0”, also known in the software industry as “hey it worked, publish it!” I wanted to get it out in case anyone else can use them for this holiday season, but there are several “known issues” that need to be corrected for stability improvements:

1. Not selecting an “End” value for at least one step will cause the sequencer to “run away” and error out when it gets past step 10.
2. Better error/null checking should be implemented in general, so NULL steps and rule errors can be avoided
3. There’s probably a more efficient way of implementing the rules.
4. Need to handle canceling any running timers when “Stop/Reset” is pressed (so the current step isn’t incremented after the “Reset” rule sets it to 0, and the next sequencer operation doesn’t start with step 2 instead of 1)

That said, this definitely works for me and I have been successfully using it to rotate through a set of effects for my holiday light LED strips…but, I have some other ideas for possible improvement:

  1. Add “Shuffle” switch, which will make the sequencer pick a random step from the available ones (NULL or END steps would be automatically skipped until a good step is found). This would be cool for a “Halloween” style effect, with random lights defined in each step, and the output rule turning them on and off for the duration of the step (e.g. step duration of 1 second)
  2. Add selectable “interlocks” to each step, so the sequencer doesn’t increment until the interlock is latched (think, beer brewing, where each step has a process variable, like temperature of the mash, fill level, etc…)

sequencer.items

Group Sequencer_Step "Sequencer Steps"

Switch Sequencer_Start "Sequencer Start"
Number Sequencer_CurrentStep "Current Step [%d]"
Switch Sequencer_Reset "Sequencer Stop/Reset"
Number Sequencer_Step_Time "Sequencer Step Time"

Number Sequencer_Step1_Value "Sequencer Step 1" (Sequencer_Step)
Number Sequencer_Step2_Value "Sequencer Step 2" (Sequencer_Step)
Number Sequencer_Step3_Value "Sequencer Step 3" (Sequencer_Step)
Number Sequencer_Step4_Value "Sequencer Step 4" (Sequencer_Step)
Number Sequencer_Step5_Value "Sequencer Step 5" (Sequencer_Step)
Number Sequencer_Step6_Value "Sequencer Step 6" (Sequencer_Step)
Number Sequencer_Step7_Value "Sequencer Step 7" (Sequencer_Step)
Number Sequencer_Step8_Value "Sequencer Step 8" (Sequencer_Step)
Number Sequencer_Step9_Value "Sequencer Step 9" (Sequencer_Step)
Number Sequencer_Step10_Value "Sequencer Step 10" (Sequencer_Step)
// Add more step items if needed

Number Sequencer_Output "Sequencer Output [%d]" // This is the action item (which receives each step value as command)


sequencer.rules

import org.eclipse.smarthome.model.script.ScriptServiceUtil

var Integer seqCurrentStep = 0
var Integer seqSequencerActive = 0
var Integer seqSequencerMaxStep = 10   // Number of Steps defined in the sitemap

/* Handle Sequencer Start */
rule "Sequencer Start Rule"
when 
    Item Sequencer_Start received command ON
then
    seqCurrentStep = (Sequencer_CurrentStep.state as DecimalType).intValue
    seqSequencerActive = 1
    logInfo("Sequencer", "Starting Sequencer Timer after Start button with CurrentStep @ " + Sequencer_CurrentStep.state.toString)
    createTimer(now.plusMinutes((Sequencer_Step_Time.state as DecimalType).intValue))[|
                logInfo("Sequencer", "Sequencer Timer elapsed after " + Sequencer_Step_Time.state.toString + " minutes. Incrementing" )
                sendCommand(Sequencer_CurrentStep, (Sequencer_CurrentStep.state as DecimalType).intValue + 1)
            ]
    Sequencer_Start.sendCommand(OFF)
end

rule "Update command when Current Step changes"
when
    Item Sequencer_CurrentStep received update
then
    logInfo("Sequencer", "Received Current Step Update")
    if (seqSequencerActive == 1) //sequencer active, process command
    {
        logInfo("Sequencer", "Sequencer Active, sending command")
        val n = ScriptServiceUtil.getItemRegistry.getItem("Sequencer_Step" + Sequencer_CurrentStep.state.toString + "_Value")
        logInfo("Sequencer", n.name + " Item state is " + n.state.toString)
        if((n.state == 0) || (n.state == NULL) || (seqCurrentStep > seqSequencerMaxStep)) // If we see an "End Step" value, a NULL or we're at the last step, reset currentStep back to the top
        {
            Sequencer_CurrentStep.sendCommand(1)
        }
        else
        {
            /* Send command value stored in current Step Selection box */
            sendCommand( Sequencer_Output, n.state as Number )  // Replace Sequencer_Test_Item with actual action item
            /* Start timer for next step */
            createTimer(now.plusMinutes((Sequencer_Step_Time.state as DecimalType).intValue))[|
                logInfo("Sequencer", "Sequencer Timer elapsed after " + Sequencer_Step_Time.state.toString + " minutes. Incrementing" )
                sendCommand(Sequencer_CurrentStep, (Sequencer_CurrentStep.state as DecimalType).intValue + 1)
            ]

        }
    }
    else // Sequencer is not active, skip out
    {
        //NOP
    }
end

rule "Sequencer Stop/Reset Rule"
when
    Item Sequencer_Reset received command ON
then
    seqCurrentStep = 0
    Sequencer_CurrentStep.sendCommand(0)
    logInfo("Sequencer", "Resetting Sequencer timer to step " + seqCurrentStep.toString)
    seqSequencerActive = 0
    Sequencer_Reset.sendCommand(OFF)
end

rule "Handle Sequencer Output" //This is the ONLY rule the end user should have to change (use the value stored in Sequencer_Output to do proper tasks in your system)
when
    Item Sequencer_Output received update
then
    //LEDStrip_Group1_FX.sendCommand(Sequencer_Output.state.toString)
    //LEDStrip_Kitchen_Cabinet_FX.sendCommand(Sequencer_Output.state.toString)
    //... Handle actual devices based on the output value currently stored in the Sequencer_Output
end

sequencer.sitemap

		Text label="Sequencer"
		{
			Frame{
				Switch item=Sequencer_Start mappings=[ON="ON"]
				Selection item=Sequencer_CurrentStep mappings=[0="Start",1="1",2="2",3="3",4="4",5="5",6="6",7="7",8="8",9="9",10="10"] //Add more values if more steps needed
				Switch item=Sequencer_Reset mappings=[ON="ON"]
				Selection item=Sequencer_Step_Time mappings=[1="1 min",15="15 mins",30="30 mins",45="45 mins",60="60 mins"] //Modify timer values as needed
			}
			Frame{
				Selection item=Sequencer_Step1_Value mappings=[0="End",1="FX 1",2="FX 2",3="FX 3",4="FX 4"] //Modify Step values as needed (this is what will be sent to the action item)
				Selection item=Sequencer_Step2_Value mappings=[0="End",1="FX 1",2="FX 2",3="FX 3",4="FX 4"]			
				Selection item=Sequencer_Step3_Value mappings=[0="End",1="FX 1",2="FX 2",3="FX 3",4="FX 4"]
				Selection item=Sequencer_Step4_Value mappings=[0="End",1="FX 1",2="FX 2",3="FX 3",4="FX 4"]
				Selection item=Sequencer_Step5_Value mappings=[0="End",1="FX 1",2="FX 2",3="FX 3",4="FX 4"]
				Selection item=Sequencer_Step6_Value mappings=[0="End",1="FX 1",2="FX 2",3="FX 3",4="FX 4"]
				Selection item=Sequencer_Step7_Value mappings=[0="End",1="FX 1",2="FX 2",3="FX 3",4="FX 4"]			
				Selection item=Sequencer_Step8_Value mappings=[0="End",1="FX 1",2="FX 2",3="FX 3",4="FX 4"]			
				Selection item=Sequencer_Step9_Value mappings=[0="End",1="FX 1",2="FX 2",3="FX 3",4="FX 4"]			
				Selection item=Sequencer_Step10_Value mappings=[0="End",1="FX 1",2="FX 2",3="FX 3",4="FX 4"]			
			}
			Frame{
				Selection item=Sequencer_Output mappings=[0="End",1="FX 1",2="FX 2",3="FX 3",4="FX 4"]			
			}
		}

If you find a use for this, let me know in a post below!

8 Likes

Bookmarked!!
That will come in useful. Thanks

I don’t have time to dive deep into the Rules but they look good to me. Nothing pops out as a potential problem.

This does look a little like the Design Pattern: Cascading Timers.

Thanks for posting! I’m certain it will be heavily used by users of the forum right now. :slight_smile:

Original post updated with small rule changes, to better handle some of the “runaway” conditions (NULL step values and no “END” step (sequence will restart from the top when a NULL step is reached or the current step is greater than 10).

Thanks for the check, Rich! My rule writing is definitely “brute force” trial & error, based mostly on examples I find from people like you within the forum posts. Type conversion (and knowing when to use vals/vars/ItemTypes and conversion between them) is by far the thing that causes me most headaches in my rule writing.

Yes it does! I didn’t find that post when searching for something like this, but it’s very similar, only being different in visual representation to the end user. I think the TimelinePicker post I saw this morning also generally handles the functionality (minus the ability to select an arbitrary number of values to send with each step). Good to have different options for specific user needs!

1 Like

Another minor update to the OP - fixed some type conversion errors in the rules, and created dedicated “output rule” that fires when the Sequencer_Output (previously known as Sequencer_Test_Item) value is updated. This allows the sequencer code to be kept separate from the “process logic” (i.e. the user only has to modify that last rule and won’t affect any of the sequencer logic doing so).

This has now been tested over the last few days running my holiday LEDs, and it’s working perfectly!

What hardware are you using?

I’m using NodeMCUs falshed with some MQTT controlled effects, outputting to WS2812 LED pixels. I have a full write-up for that project - NodeMCU MQTT LED Strip Controller Build & Config How-To Videos

:+1:thanks