[SOLVED] Switch multiple timer in succession

I have made the following rule but here I get the accompanying error message
I do not expect the rule to be collapsed.
can someone help me?

error message in log: tail,
22:47:34.304 [ERROR] [untime.internal.engine.RuleEngineImpl] - Rule ‘poort loop licht’: An error occurred during the script execution: Couldn’t invoke ‘assignValueTo’ for feature JvmVoid:

The intention is for the outputs to switch on after one another with a certain amount of time.

rule "poort loop licht"
		when
			Item PO_open_lamp_uit received command ON
		then
			LV_timer5_tuinv = createTimer(now.plusSeconds(5)) 
     	   [|        
			sendCommand(Output11_3, ON)  //Spot 1    to
			LV_timer5_tuinv = null
			]
			LV_timer6_tuinv = createTimer(now.plusSeconds(2)) 
     	   [|        
			sendCommand(Output11_4, ON)  //Spot 2
			LV_timer6_tuinv = null
			]
			LV_timer7_tuinv = createTimer(now.plusSeconds(2)) 
     	   [|        
			sendCommand(Output11_2, ON)  // Lamp ornament
			LV_timer7_tuinv = null
			]
			LV_timer8_tuinv = createTimer(now.plusSeconds(2)) 
     	   [|        
			sendCommand(Output12_1, ON)  //Spot 3&4
			LV_timer8_tuinv = null
			]
			LV_timer11_tuinv = createTimer(now.plusSeconds(2)) 
     	   [|        
			sendCommand(Output12_2, ON)  //Spot 5&6
			LV_timer11_tuinv = null
			]
			LV_timer12_tuinv = createTimer(now.plusSeconds(2)) 
     	   [|        
			sendCommand(Output17_3, ON)  //Spot afdak
			sendCommand(PO_open_lamp_uit, OFF)
			LV_timer12_tuinv = null
			]
			LV_timer13_tuinv = createTimer(now.plusSeconds(3)) 
     	   [|        
			sendCommand(Output11_2, OFF)  // Lamp ornament
			LV_timer13_tuinv = null
			]
		end

I think the timers are automatically set to null when they expire. Also, are all of the timer variables globals declared at the top of the rules file?

Try nesting the createTimer() calls within the previous timer’s lambda so the next timer in the chain is started at the end of the previous timer’s lambda:

var Timer LV_timer5_tuinv = null
var Timer LV_timer6_tuinv = null
var Timer LV_timer7_tuinv = null
var Timer LV_timer8_tuinv = null
var Timer LV_timer11_tuinv = null
var Timer LV_timer12_tuinv = null
var Timer LV_timer13_tuinv = null

rule "poort loop licht"
		when
			Item PO_open_lamp_uit received command ON
		then
			if (LV_timer5_tuinv == null &&
			    LV_timer6_tuinv == null &&
			    LV_timer7_tuinv == null &&
			    LV_timer8_tuinv == null &&
			    LV_timer11_tuinv == null &&
			    LV_timer12_tuinv == null &&
			    LV_timer13_tuinv == null) {
				LV_timer5_tuinv = createTimer(now.plusSeconds(5)) [|
					sendCommand(Output11_3, ON)  //Spot 1    to
					LV_timer6_tuinv = createTimer(now.plusSeconds(2)) [|
						sendCommand(Output11_4, ON)  //Spot 2
						LV_timer7_tuinv = createTimer(now.plusSeconds(2)) [|
							sendCommand(Output11_2, ON)  // Lamp ornament
...
											]
										]
									]
								]
							]
						]
					]
				}
	end

Use a simple state machine:

var Timer tLight = null
var Integer iStep = 0

rule "poort loop licht"
when
    Item PO_open_lamp_uit received command ON
then
    tLight?.cancel
    iStep = 0
    tLight = createTimer(now.plusSeconds(5), [
        iStep ++ // or iStep = iStep + 1
        switch iStep {
            case 1: {
                Output11_3.sendCommand(ON)
                tLight.reschedule(now.plusSeconds(2))
            }
            case 2: {
                Output11_4.sendCommand(ON)
                tLight.reschedule(now.plusSeconds(2))
            }
            case 3: {
                Output11_2.sendCommand(ON)
                tLight.reschedule(now.plusSeconds(2))
            }
            case 4: {
                Output12_1.sendCommand(ON)
                tLight.reschedule(now.plusSeconds(2))
            }
            case 5: {
                Output12_2.sendCommand(ON)
                tLight.reschedule(now.plusSeconds(2))
            }
            case 6: {
                Output17_3.sendCommand(ON)
                PO_open_lamp_uit.sendCommand(OFF)
                tLight.reschedule(now.plusSeconds(3))
            }
            default: {
                Output11_2.sendCommand(OFF)
                tLight = null
            }
        }
    ]) 
end
5 Likes

Yup, that’s a nice way to do it. I use a pair of similar rules in my system to implement my state machine:

var Timer bedtimeTimer = null

rule "Handle Bedtime Event (ON | OFF)"
when
    Item Bedtime_Event received command
then
    val logID = TODR + ".bedtime-event-rcvd-cmd"

    if (receivedCommand == ON) {
        // Start Bedtime_Event received command ON, start Bedtime sequence
        logInfo(logID, "Bedtime_Event sequence started")
        logInfo(logID,
                "Turned off {} Upstairs Bedroom switch(es)/dimmer(s)",
                gUBLts.sendCommand(OFF))
        Bedtime_State.postUpdate(0)
    } else if (receivedCommand == OFF) {
        // Initial bedtime timeout has finished, set Bedtime_State to 1
        logInfo(logID, "Bedtime_Event ON expired")
        Bedtime_State.postUpdate(1)
    }
end


rule "Handle Bedtime State Updates"
when
    Item Bedtime_State received update
then
    val logID = TODR + ".bedtime-state-updated"
    logInfo(logID, "Bedtime_State received update: {}", Bedtime_State.state)

    if (bedtimeTimer !== null) {
        bedtimeTimer.cancel
        bedtimeTimer = null
    }

    if (Bedtime_State.state as DecimalType == 1) {
        logInfo(logID,
                "Turned off {} Office switch(es)/dimmer(s)",
                gOFLts.sendCommand(OFF))
        bedtimeTimer = createTimer(now.plusSeconds(5)) [|
            Bedtime_State.postUpdate(2)
        ]
    } else if (Bedtime_State.state as DecimalType == 2) {
        logInfo(logID,
                "Turned off {} Stairway switch(es)/dimmer(s)",
                gSWDimmer.sendCommand(OFF))
        bedtimeTimer = createTimer(now.plusSeconds(10)) [|
            Bedtime_State.postUpdate(3)
        ]
    } else if (Bedtime_State.state as DecimalType == 3) {
        logInfo(logID,
                "Turned off {} Kitchen switch(es)/dimmer(s)",
                gKTLts.sendCommand(OFF))
        bedtimeTimer = createTimer(now.plusSeconds(5)) [|
            Bedtime_State.postUpdate(4)
        ]
    } else if (Bedtime_State.state as DecimalType == 4) {
        logInfo(logID,
                "Turned off {} Living Room switch(es)/dimmer(s)",
                gLRLts.sendCommand(OFF))
        Bedtime_State.postUpdate(0)
    }
end

Thank you guys, I’m going to get on with it

Hello Udo,

I have several error messages,
Something is going wrong?

00:11:57.962 [WARN ] [del.core.internal.ModelRepositoryImpl] - Configuration model ‘Tuinverlichting.rules’ has errors, therefore ignoring it: [255,13]: mismatched input ‘case’ expecting ‘}’
[255,18]: mismatched input ‘1’ expecting ‘{’
[255,19]: mismatched input ‘:’ expecting ‘]’
[255,21]: missing ‘)’ at ‘{’
[259,13]: mismatched input ‘case’ expecting ‘end’

If I remember I may steal this example due the cascading timer DP. it’s a bit simpler than the example I have there now. Great post!

The colon after switch iStep should be removed, it should be switch iStep {

You should use VS Code to edit your files, it will highlight syntax errors such as these:

Thank you for the solution, it works now !! :slight_smile:

Sure :slight_smile:

Sorry for the typo. Should have reread my posting… :slight_smile:

Hi Udo

thank you for this great example but I dont get it all the way through.

I want to cascade my stair light and tried work with your rule, but ist doesn´t work as ist should.
What I want to achive is: when presence is detected downstairs turn on the light of the first stair, wait x seconds (should be variable with a number item) turn on the next and so on
The same when precence is detectet upstairs
Turn on light of stair 8 then 7 and so on then turn them off one after the other.

I have two precence detectors

precence_upstairs
precence_downstairs

8 Lights

light_stair1
light_stair2
light_stair3
light_stair4
light_stair5
light_stair6
light_stair7
light_stair8

all of them are in the group glight_stair

Whould the scenario as described be possible with your state machine?

Maybe you can give me a hint

BR
Daniel

Sure.
I guess the time to wait should be the same for each step? You will need a less complex rule :slight_smile:

You will need a Number Item duration, which is the wait time in Seconds. If the Item is not set, the duration will be 1 Second as a default value.

var Timer tLight = null
var Integer iStep = 0
var Boolean bDir = true                                                     // true == up, false == down

rule "steps ON"
when
    Item precence_upstairs received command ON or
    Item precence_downstairs received command ON
then
    tLight?.cancel
    bDir = if(triggeringItem.name.contains("up")) true else false
    iStep = if(triggeringItem.name.contains("up")) 0 else 9
    tLight = createTimer(now.plusMillis(10), [
        var Integer nDur = 1
        if(duration.state instanceof Number)
            nDur = duration.state as Number
        if(bDir)
            iStep ++                                                        // or iStep = iStep + 1
        else
            iStep --                                                        // or iStep = iStep - 1
        glight_stair.members.filter[i |
            i.name.contains(iStep.toString)].head.sendCommand(ON)
        if(iStep > 1 && iStep < 8) 
            tLight.reschedule(now.plusSeconds(nDur))
    ])
end

Hi Udo

Thank you for your fast reply.
I must admit, that I have no clue what I am reading when I read through your code.
At which place, do the different stairs get their on command, and when do they turn off?
I don´t just want to copy and past, I want to understand, at least a little bit, what I am doing.

BR
Daniel

The chunk of code between the createTimer’s square brackets [ ] runs at some point in the future.
When it does run, it can reschedule itself to run again.
By counting each time it runs, it can know how many times it has run and do something different each on each pass.

Sorry I don´t get it and it doesnt work.
I set up these items:

Switch Shelly1_1 “Stufe 1” (glight_stair)
Switch Shelly1_2 “Stufe 2” (glight_stair)
Switch Shelly1_3 “Stufe 3” (glight_stair)
Switch Shelly1_4 “Stufe 4” (glight_stair)
//Switch Shelly1_input “Bewegung unten”
Switch precence_upstairs “Bewegung oben”
Switch precence_downstairs “bewegung unten”

This Rule:

var Timer tLight = null
var Integer iStep = 0
var Boolean bDir = true // true == up, false == down
rule “steps ON”
when
Item precence_upstairs received command ON or
Item precence_downstairs received command ON
then
tLight?.cancel
bDir = if(triggeringItem.name.contains(“up”)) true else false
iStep = if(triggeringItem.name.contains(“up”)) 0 else 5
tLight = createTimer(now.plusMillis(10), [
var Integer nDur = 1
if(duration.state instanceof Number)
nDur = duration.state as Number
if(bDir)
iStep ++ // or iStep = iStep + 1
else
iStep – // or iStep = iStep - 1
glight_stair.members.filter[i | i.name.contains(iStep.toString)].head.sendCommand(ON)
if(iStep > 1 && iStep < 5)
tLight.reschedule(now.plusSeconds(nDur))
])
end

this is what I get when triggering upstairs:

2020-03-16 22:28:07.869 [vent.ItemStateChangedEvent] - precence_upstairs changed from OFF to ON

==> /var/log/openhab2/openhab.log <==

2020-03-16 22:28:07.889 [ERROR] [org.quartz.core.JobRunShell         ] - Job DEFAULT.Timer 105 2020-03-16T22:28:07.888+01:00: Proxy for org.eclipse.xtext.xbase.lib.Procedures$Procedure0: [ {

  var nDur

  org.eclipse.xtext.xbase.impl.XIfExpressionImpl@17138be (conditionalExpression: false)

  org.eclipse.xtext.xbase.impl.XIfExpressionImpl@1e2affb (conditionalExpression: false)

  <XMemberFeatureCallImplCustom>.sendCommand(<XFeatureCallImplCustom>)

  org.eclipse.xtext.xbase.impl.XIfExpressionImpl@948387 (conditionalExpression: false)

} ] threw an unhandled Exception: 

java.lang.reflect.UndeclaredThrowableException: null

	at com.sun.proxy.$Proxy730.apply(Unknown Source) ~[?:?]

	at org.eclipse.smarthome.model.script.internal.actions.TimerExecutionJob.execute(TimerExecutionJob.java:48) ~[?:?]

	at org.quartz.core.JobRunShell.run(JobRunShell.java:202) [bundleFile:?]

	at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573) [bundleFile:?]

Caused by: org.eclipse.smarthome.model.script.engine.ScriptExecutionException: 'state' is not a member of 'int'; line 52, column 12, length 14

	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.invokeFeature(ScriptInterpreter.java:133) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:861) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:231) ~[?:?]

	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:226) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:215) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:907) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:257) ~[?:?]

	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:226) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:215) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:469) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:255) ~[?:?]

	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:226) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:215) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:458) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:239) ~[?:?]

	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:226) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:215) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.evaluate(XbaseInterpreter.java:201) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.ClosureInvocationHandler.doInvoke(ClosureInvocationHandler.java:46) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.AbstractClosureInvocationHandler.invoke(AbstractClosureInvocationHandler.java:29) ~[?:?]

	... 4 more

2020-03-16 22:28:07.919 [ERROR] [org.quartz.core.ErrorLogger         ] - Job (DEFAULT.Timer 105 2020-03-16T22:28:07.888+01:00: Proxy for org.eclipse.xtext.xbase.lib.Procedures$Procedure0: [ {

  var nDur

  org.eclipse.xtext.xbase.impl.XIfExpressionImpl@17138be (conditionalExpression: false)

  org.eclipse.xtext.xbase.impl.XIfExpressionImpl@1e2affb (conditionalExpression: false)

  <XMemberFeatureCallImplCustom>.sendCommand(<XFeatureCallImplCustom>)

  org.eclipse.xtext.xbase.impl.XIfExpressionImpl@948387 (conditionalExpression: false)

} ] threw an exception.

org.quartz.SchedulerException: Job threw an unhandled exception.

	at org.quartz.core.JobRunShell.run(JobRunShell.java:213) [bundleFile:?]

	at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573) [bundleFile:?]

Caused by: java.lang.reflect.UndeclaredThrowableException

	at com.sun.proxy.$Proxy730.apply(Unknown Source) ~[?:?]

	at org.eclipse.smarthome.model.script.internal.actions.TimerExecutionJob.execute(TimerExecutionJob.java:48) ~[?:?]

	at org.quartz.core.JobRunShell.run(JobRunShell.java:202) ~[?:?]

	... 1 more

Caused by: org.eclipse.smarthome.model.script.engine.ScriptExecutionException: 'state' is not a member of 'int'; line 52, column 12, length 14

	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.invokeFeature(ScriptInterpreter.java:133) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:861) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:231) ~[?:?]

	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:226) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:215) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:907) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:257) ~[?:?]

	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:226) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:215) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:469) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:255) ~[?:?]

	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:226) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:215) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:458) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:239) ~[?:?]

	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:226) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:215) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.evaluate(XbaseInterpreter.java:201) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.ClosureInvocationHandler.doInvoke(ClosureInvocationHandler.java:46) ~[?:?]

	at org.eclipse.xtext.xbase.interpreter.impl.AbstractClosureInvocationHandler.invoke(AbstractClosureInvocationHandler.java:29) ~[?:?]

	at com.sun.proxy.$Proxy730.apply(Unknown Source) ~[?:?]

	at org.eclipse.smarthome.model.script.internal.actions.TimerExecutionJob.execute(TimerExecutionJob.java:48) ~[?:?]

	at org.quartz.core.JobRunShell.run(JobRunShell.java:202) ~[?:?]

	... 1 more

I am clueless - sorry

BR

Hmm… I think you did not create an Item duration (this is the item which will allow you to choose the x seconds.
Another thing… I just forgot to complete the code to switch off… :wink:

So the question here is: When shall the lights turn off?
Especially, should all lights turned on before the first light turns off?

There was another bug in the code (missing - ). I’ve commented the lines:

// global vars have to be defined at top of file
var Timer tLight = null
var Integer iStep = 0
var Boolean bDir = true // true == up, false == down

rule "steps ON"
when
    Item precence_upstairs received command ON or                                             // detected movement upstairs
    Item precence_downstairs received command ON                                              // detected movement downstairs
then
    tLight?.cancel                                                                            // cancel existing timer
    bDir = if(triggeringItem.name.contains("up")) true else false                             // which direction?
    iStep = if(triggeringItem.name.contains("up")) 0 else 5                                   // count up or down?
    tLight = createTimer(now.plusMillis(10), [                                                // create the timer
        var Integer nDur = 1                                                                  // setup default duration
        if(duration.state instanceof Number)                                                  // if duration is set
            nDur = duration.state as Number                                                   // get duration from item
        if(bDir)                                                                              // if direction up
            iStep ++ // or iStep = iStep + 1                                                  // count up
        else                                                                                  // if direction down
            iStep –- // or iStep = iStep - 1                                                  // count down
        glight_stair.members.filter[i | i.name.contains(iStep.toString)].head.sendCommand(ON) // get the actual step and send command ON
        if(iStep > 1 && iStep < 4)                                                            // if not last step
            tLight.reschedule(now.plusSeconds(nDur))                                          // schedule the next step
    ])
end

And after thinking hard :wink: I changed the code a little bit plus created another timer for switching off the lights.

// create global vars and vals always on top of file
var Timer   tLightOn = null                                               // object for ON timer
var Timer   tLightOff = null                                              // object for OFF timer
var Integer iStepOn = 0                                                   // counter for ON
var Integer iStepOff = 0                                                  // counter for OFF
var Boolean bDir = true                                                   // true == up, false == down
var Integer nStep = 1                                                     // setup default stepTime
var Integer nDuration = 30                                                // setup default time between ON and OFF 
var Integer nMaxStep = 0                                                  // number of steps (automatic)

rule "steps ON and OFF"
when
    Item precence_upstairs received command ON or                         // detected movement upstairs
    Item precence_downstairs received command ON                          // detected movement downstairs
then
    if(tLightOn !== null || tLightOff !== null )
        return;
    bDir = if(triggeringItem.name.contains("up")) true else false         // which direction?
    iStepOn = if(triggeringItem.name.contains("up")) 0 else nMaxStep + 1  // count up or down?
    iStepOff = if(triggeringItem.name.contains("up")) 0 else nMaxStep + 1 // count up or down?
    if(stepTime.state instanceof Number)                                  // if stepTime is set (item must exist!)
        nStep = stepTime.state as Number                                  // get stepTime from item
    if(stepOnDuration.state instanceof Number)                            // if stepOnDuration is set (item must exist!)
        nDuration = stepOnDuration.state as Number                        // get stepOnDuration from item
    nMaxStep = glight_stair.members.size
    tLightOn = createTimer(now.plusMillis(10), [                          // create the timer for ON
        iStepOn = iStepOn + if(bDir) 1 else -1                            // count up or down depending on direction
        glight_stair.members.filter[i | 
            i.name.contains(iStepOn.toString)].head.sendCommand(ON)       // get the actual step and send command ON
        if(iStepOn > 1 && iStepOn < nMaxStep)                             // if not last step
            tLightOn.reschedule(now.plusSeconds(nStep))                   // schedule the next step
        else
            tLightOn = null
    ])
    tLightOff = createTimer(now.plusSeconds(nDuration), [                 // create the timer for OFF
        iStepOff = iStepOff + if(bDir) 1 else -1                          // count up or down depending on direction
        glight_stair.members.filter[i | 
            i.name.contains(iStepOff.toString)].head.sendCommand(OFF)     // get the actual step and send command OFF
        if(iStepOff > 1 && iStepOff < nMaxStep)                           // if not last step
            tLightOff.reschedule(now.plusSeconds(nStep))                  // schedule the next step
        else
            tLightOff = null
    ])
end

I created a second timer for switching OFF. Of course we need another counter for that timer.
I changed the name nDur to nStep and changed the var to be global, as this var has to be the same for both timers and the time should not be changed while running timers.
I changed the behavior of the rule, since most likely when using the steps, you will trigger both motion detectors. So the second trigger (from the other motion detector) has to be ignored, as long as the timers do their job.
I created a var for maximum steps to make more clear the function of some values. This could be a val, as it’s very unlikely that the amount of steps will ever change :wink: but as it’s a nice feature, the rule determines the number of items in the group. This becomes handy when testing the rule with less steps (just don’t put them into the group - but be aware that the item names must contain a counter which starts with 1 up to the number of steps. But I have an idea to get rid of this problem, too :wink: )
Furthermore I created another var nDuration for the switch off time. This is: 30 seconds after a step is switched ON, it is switched OFF again. This value is configurable via an Item stepOnDuration, which must exist.
I changed count up and down, as the ternary operator is much more efficient here.

If you want, give it a try. :slight_smile: