Parallel while loops

I have a great setup where I have integrated Kodi with my light arrays in the room (5 of)
They turn off when I press play and when paused a couple of arrays are turned on low.
When the kodi mediia stops, the lights go back to their setting based on the persistence of when the media started.
I want to now fade up the lights when the stop action occurs.
I am using a while loop:

var Number FadeInDimmer = 0 while(FadeInDimmer <LoungeLamp_DimmerState ){ FadeInDimmer=FadeInDimmer+1 logInfo("LoungeLampDimmer", "LoungeLamp Dimmer fade-in: " + FadeInDimmer.toString()) Lounge_Lamp_Dimmer.sendCommand(FadeInDimmer) Thread::sleep(500)

However this loop is needed for all 5 light arrays if they were all on before the media started playing.
It seems as if the while loop for the first array needs to complete before the next light while loop is processed. This results in one light fading up, then the next, then the next etc - where I was hoping for an effect of all lights fading up at the same time.

Is there a way to run these loops in parallel? Or another approach to the solution?

Just guessing here but unless you’re going to do it in an external script, I think that you could fire off 5 threads that will work in parallel. I have never done it but I think if you use 5 x createTimer commands each with the ramp of a single light, they will basically work in parallel if the timers are created for like now + 1 sec. The delay is just to get all the timers fired off before the ramping starts.
I’m sure more experienced people will chime in.

There are several approaches you can use. Some thoughts include:

  • Create a rule for each light which implements the fade up or fade down. When it is time to fade up a light, send a command to a triggering Item to activate the rule for each of the five lights. Individual events trigger and run Rules in parallel.

  • Create a Timer for each light set to go off in 10 or 50 msec with the while loop for each light. Timers run in separate threads so they will run in parallel.

  • Add additional book keeping to your one while loop so you can gradually fade up/down all five lights from within the same while loop.

Personally I would implement this as follows:

  • Create a Group to contain the five Dimmers. I’ll call it gDimmers below.
// The Key is the Dimmer's Item name and the Number is the saved/target Dimmer state to fade into
// I'm only adding this in here because I don't know how you save the "old state" you are restoring to.
// It would probably be better to just reference that old state how ever you do now inline in the while loop
// below.
val Map<String, Number> targets = createHashmap

// populate targets

var done = false
while(!done){
    // Get a list of those dimmers that are not at their target yet
    var toFade = gDimmers.members.filter[d | (d.state as DecimalType) < targets.get(d.name)]
    if(toFade.isEmpty) {
        done = true
    }
    else {
        // Increment those lights which are not yet at their targets
        gDimmers.members.forEach[d | d.sendCommand((d.getState as DecimalType) + 1)]
        Thread::sleep(500)
    }
}

Theory of operation:

Put all the targets to dim to into a Map keyed on the Item name. As the comment says, this can probably be skipped if you can figure out the target value (i.e. LoungeLamp_DimmerState) from within the while loop based on the Dimmer Item’s name or the reference to the Item itself (e.g. you pull it out with a query to persistence).

In the while loop we filter out all those Items which have not yet reached their target. If all Items have reached their target we set the flag to true and the while loop exits. Otherwise we increment those Items by one and then sleep before doing it all again.

Limitation: This loop assumes that the targets are all above the current value. You can make it generic with some added conditionals, left as an exercise for the student.

another aproach could be, to change the construction:

var Number FadeInDimmer = 0
var Number FadeMax = Lounge_Lamp_DimmerState
if (FadeMax < Lounge_Lamp_Dimmer2State)       // Set maximum level of all lamps
    FadeMax = Lounge_Lamp_Dimmer2State
while(FadeInDimmer < FadeMax) {
    FadeInDimmer=FadeInDimmer+1
//  logInfo("LoungeLampDimmer", "LoungeLamp Dimmer fade-in: " + FadeInDimmer.toString())
    if (Lounge_Lamp_Dimmer.state < Lounge_Lamp_DimmerState) 
        Lounge_Lamp_Dimmer.sendCommand(FadeInDimmer)
    if (Lounge_Lamp_Dimmer2.state < Lounge_Lamp_Dimmer2State) 
        Lounge_Lamp_Dimmer2.sendCommand(FadeInDimmer)
    Thread::sleep(500)
}

This will ensure a parallel fadein aside from the fact, that the fadein time will vary from lamp to lamp depending on the targeted level of each lamp.

I’m pretty sure this could be done in a more elegant way, but at least it should do the job. :slight_smile:

Argh… too late… :slight_smile:

Hi Thanks for the reply. Udo, this was my thoughts as well however the design is supposed to be flexible enough so that if one light was on 100% and another 50% then they fade back up to that state, so state is captured for each light.

external script might be an option with a “dummy” switch as the trigger - but what sort of delay would this put in for the rule to fire? would it still be “out” for the light set?

I like the group idea - would this still work using the script below?

Here is the full script (with my while loop commented out)

import org.openhab.core.library.types.DecimalType
import org.openhab.core.library.types.*
import org.openhab.model.script.actions.*
import java.lang.Math
import org.joda.time.*
import org.openhab.core.persistence.*

var DateTime time_play_started 

rule "Lights off when Play Starts"
when
	Item kodi_player_state changed 
then
	//only do this if after 8
	if(now.getHourOfDay > 19) {
	
		//sleep loop until player type is updated (without this there are null errors)
		while(kodi_player_type.state.toString == "") {
			Thread::sleep(500)
		}	
		//check for TV, Movies, or other such as youtube, TenPlay ABC Iview etc)
		var String KodiPlayerState = kodi_player_type.state.toString()
		if(KodiPlayerState.lowerCase == "episode" || KodiPlayerState.lowerCase == "movie" || KodiPlayerState.lowerCase == "unknown") {
			
			var String Kodistate = kodi_player_state.state.toString()
				logInfo("Kodi State:", Kodistate)
				logInfo("Kodi is playing:",kodi_is_playing.state.toString())
				switch (Kodistate.lowerCase) {
			case "play" :  {
						//check if this is the first time play pressed with a proxy switch
						if (kodi_is_playing.state == OFF) {
							//record the time to use in historicState
							time_play_started = now
							//set the proxy switch as this is the first time
							kodi_is_playing.sendCommand(ON)
							logInfo("Kodi", "kodi is playing set to on so this is the first time")
						}
						//turn off all the desired lights
						Lounge_Lamp_Dimmer.sendCommand(OFF)
						Fibaro_Lounge1_Dimmer.sendCommand(OFF)
						Fibaro_Lounge2_Dimmer.sendCommand(OFF)
						Fibaro_Kitchen1_Dimmer.sendCommand(OFF)
						Fibaro_Kitchen2_Dimmer.sendCommand(OFF)
						//leave one light on at lowest dim if it was already on
						//if (Fibaro_Kitchen1_Dimmer.state > 0 ) {
						//	Fibaro_Kitchen1_Dimmer.sendCommand(1)
						//}	
						logInfo("Kodi", "all lights off")					
					 }
			case "pause" : {
						//look into historicstate for all the light values when the first play started
						var Number LoungeLamp_DimmerState = Lounge_Lamp_Dimmer.historicState(time_play_started).state
						var Number Lounge1_DimmerState = Fibaro_Lounge1_Dimmer.historicState(time_play_started).state
						var Number Lounge2_DimmerState = Fibaro_Lounge2_Dimmer.historicState(time_play_started).state
						var Number Kitchen1_DimmerState = Fibaro_Kitchen1_Dimmer.historicState(time_play_started).state
						var Number Kitchen2_DimmerState = Fibaro_Kitchen2_Dimmer.historicState(time_play_started).state
						//turn on specific lights to a low light only if they were already on when play started
						if(LoungeLamp_DimmerState > 0) {
							Lounge_Lamp_Dimmer.sendCommand(15)
						}
						if(Lounge1_DimmerState > 0) {
							Fibaro_Lounge1_Dimmer.sendCommand(15)
						}
						//if(Lounge2_DimmerState > 0) {
						//	Fibaro_Lounge2_Dimmer.sendCommand(20)
						//}
						if(Kitchen1_DimmerState > 0) {
							Fibaro_Kitchen1_Dimmer.sendCommand(1)
						}
						logInfo("Kodi", "kodi is paused so lights should be on dim")
						//kodi_notification.postUpdate("lights should be dim")
					}
			case "stop"  : {
						//look into historicstate for all the light values when the first play started
						var Number LoungeLamp_DimmerState = Lounge_Lamp_Dimmer.historicState(time_play_started).state
						var Number Lounge1_DimmerState = Fibaro_Lounge1_Dimmer.historicState(time_play_started).state
						var Number Lounge2_DimmerState = Fibaro_Lounge2_Dimmer.historicState(time_play_started).state
						var Number Kitchen1_DimmerState = Fibaro_Kitchen1_Dimmer.historicState(time_play_started).state
						var Number Kitchen2_DimmerState = Fibaro_Kitchen2_Dimmer.historicState(time_play_started).state
						//turn on all the lights back to the value they were when play started
						if(LoungeLamp_DimmerState > 0) {
						
						
//						logInfo("Kodi", "loungelamp dimmer state" + LoungeLamp_DimmerState.toString())
//						var Number FadeInDimmer = 0
//while(FadeInDimmer<LoungeLamp_DimmerState ){
//	FadeInDimmer=FadeInDimmer+1
//	logInfo("LoungeLampDimmer", "LoungeLamp Dimmer fade-in: " + FadeInDimmer.toString())
//	Lounge_Lamp_Dimmer.sendCommand(FadeInDimmer)
//   Thread::sleep(50)
//}
						
						
						
						
							Lounge_Lamp_Dimmer.sendCommand(LoungeLamp_DimmerState)
						}
						if(Lounge1_DimmerState > 0) {
							Fibaro_Lounge1_Dimmer.sendCommand(Lounge1_DimmerState)
						}
						if(Lounge2_DimmerState > 0) {
							Fibaro_Lounge2_Dimmer.sendCommand(Lounge2_DimmerState)
						}
						if(Kitchen1_DimmerState > 0) {
							Fibaro_Kitchen1_Dimmer.sendCommand(Kitchen1_DimmerState)
						}
						if(Kitchen2_DimmerState > 0) {
							Fibaro_Kitchen2_Dimmer.sendCommand(Kitchen2_DimmerState)
						}
						//reset the proxy switch as we have stopped not paused
						kodi_is_playing.sendCommand(OFF)
						logInfo("Kodi", "kodi is stopped so lights should be on as they were")
						logInfo("Kodi", "kodi is playing:" kodi_is_playing.state.toString())
					}
			}

		}
	}
end

Yes, it would looks something like this (now that I know how you get the past state). Replace your commented while loop with the following (again assuming your Group name is gDimmers, change this if you named your Group something else):

import org.openhab.core.library.types.DecimalType
import org.openhab.core.library.types.*
import org.openhab.model.script.actions.*
import java.lang.Math
import org.joda.time.*
import org.openhab.core.persistence.*

var DateTime time_play_started 

rule "Lights off when Play Starts"
when
	Item kodi_player_state changed 
then
	//only do this if after 8
	if(now.getHourOfDay > 19) {
	
		//sleep loop until player type is updated (without this there are null errors)
		while(kodi_player_type.state.toString == "") {
			Thread::sleep(500)
		}	
		//check for TV, Movies, or other such as youtube, TenPlay ABC Iview etc)
		var String KodiPlayerState = kodi_player_type.state.toString()
		if(KodiPlayerState.lowerCase == "episode" || KodiPlayerState.lowerCase == "movie" || KodiPlayerState.lowerCase == "unknown") {
			
			var String Kodistate = kodi_player_state.state.toString()
				logInfo("Kodi State:", Kodistate)
				logInfo("Kodi is playing:",kodi_is_playing.state.toString())
				switch (Kodistate.lowerCase) {
			            case "play" :  {
						//check if this is the first time play pressed with a proxy switch
						if (kodi_is_playing.state == OFF) {
							//record the time to use in historicState
							time_play_started = now
							//set the proxy switch as this is the first time
							kodi_is_playing.sendCommand(ON)
							logInfo("Kodi", "kodi is playing set to on so this is the first time")
						}
						//turn off all the desired lights
                                                gDimmers.members.forEach[d | d.sendCommand(OFF)]

						logInfo("Kodi", "all lights off")					
					 }
			            case "pause" : {
						//turn on specific lights to a low light only if they were already on when play started
						if(Lounge_Lamp_Dimmer.historicState(time_play_started).state as Decimaltype > 0) {
							Lounge_Lamp_Dimmer.sendCommand(15)
						}
						if(Fibaro_Lounge1_Dimmer.historicState(time_play_started).state as DecimalType > 0) {
    						    Fibaro_Lounge1_Dimmer.sendCommand(15)
						}
						if(Fibaro_Kitchen1_Dimmer.historicState(time_play_started).state as DecimalType > 0) {
							Fibaro_Kitchen1_Dimmer.sendCommand(1)
						}
						logInfo("Kodi", "kodi is paused so lights should be on dim")
						//kodi_notification.postUpdate("lights should be dim")
					}
 			            case "stop"  : {
					//turn on all the lights back to the value they were when play started
                                                
                                        var done = false
                                        while(!done){
                                            var toFade = gDimmers.members.filter[d | (d.state as DecimalType) < (d.historicState(time_play_started).state as DecimalType)]
                                            if(toFade.isEmpty) {
                                                done = true
                                            }
                                            else {
                                                // Increment those lights which are not yet at their targets
                                                gDimmers.members.forEach[d | d.sendCommand((d.getState as DecimalType) + 1)]
                                                Thread::sleep(500)
                                            }
                                        }
					//reset the proxy switch as we have stopped not paused
					kodi_is_playing.sendCommand(OFF)
					logInfo("Kodi", "kodi is stopped so lights should be on as they were")
					logInfo("Kodi", "kodi is playing:" kodi_is_playing.state.toString())
				}
			}

		}
	}
end

Notes:

  • If you are OK with dimming Fibaro_Kitchen1_Dimmer to 15 as well as the other two, you can put these three lamps into their own group and replace most of the pause code with:
    gPauseDimmers.members.forEach[d | 
        if(d.historicState(time_play_started).state as Decimaltype > 0){
            d.sendCommand(15)
        }
    ]

You could still do this with groups in a couple of different ways if you need different dimmer values for each but I’m not sure it would save you much.

  • This rule is kind of long. The changes I’ve made above help but it might be worth while splitting this into separate rules or lambdas or the like if the complexity becomes a problem. I wouldn’t do that just yet but if you add a bunch more behavior I would suggest you consider that.

  • Again, I just typed the above into the forum, there are likely typos or other syntax errors. I’m pretty confident of the logic though.

For the part

if(KodiPlayerState.lowerCase == "episode" || KodiPlayerState.lowerCase == "movie" || KodiPlayerState.lowerCase == "unknown")

you could use an ArrayList:

val MyList = newArrayList("episode","movie","unknown")
 if(MyList.contains(kodi_player_type.state.toString.lowerCase)) {

the list now can grow without spaghetti code :wink:

And maybe there is a way to even make a list of dimmer values in case of pause (this one would be a little tricky, as you want a dynamic behavior dependent on the former switched on lights).

I may be wrong, but I think there is a little issue in the rule of @rlkoshak, because

else {
// Increment those lights which are not yet at their targets
    gDimmers.members.forEach[d | d.sendCommand((d.getState as DecimalType) + 1)]
    Thread::sleep(500)
}

would dimm all members of the group to maximum level of the group.
If I’m not wrong, I think it has to be

else {
    // Increment those lights which are not yet at their targets
    toFade.forEach[d | d.sendCommand((d.state as DecimalType) + 1)]
    Thread::sleep(500)
}
1 Like

You are correct. We do need to loop through toFade, not gDimmers.

The MyList.contains is also a good trick.

Thanks guys, I implimented everything youve both suggested and it works, the only issue I have now is the time it takes for the fade effect. I tried reducing the sleep down to 250, 100 and 50 but the lower I go it seems to have odd behaviour to the point that at 50 it jumps backwards and forwards possibly because the state being recorded cant keep up with whats going on. I tried incrementing by 2 instead of 1 and it starts to give a jumpy fade in effect.

Could be my LEDs which I am changing in a few weeks so they are all the same.

Thanks for advice on the code optimisation too - I love this community - not only helping with the actual problem but suggesting ways to improve and making me a better coder. I took the examples above to also do a fade out.

Seeing how this is done has now also given me that breakthrough understanding of groups and scenes which I “kinda got” in the past but never really could see a use case, or how to put rules around it which now I do!

You will have many serial commands while fading, so no wonder that you got strange effects.

You could set the interval to 5, but then you would need to take care of the last steps, and the fading would be rough.

Sorry for thumbing my nose, but: my dimmers allow to setup a fade time, so I have always smooth fadings (but I can also switch on and off at once…) :yum:

OK I will find the method that works the best with experimentation. I am using fibaro zwave dimmers - I am guessing these are not what you have?

No, I’m using knx dimmers, but it may be, that zwave dimmers have similar parameters, I don’t know, though.

Glad to hear you are getting closer.

Some thoughts for your problem. You can potentially smoothing things out a little bit by adding some logic to make each iteration through the loop be a bit more deterministic.

Take a timestamp at the top of the loop and the bottom of the loop to determine the amount of time the loop ran. Then sleep for increment time - loop runtime. So if it takes 40 msec to execute the loop this time around and your set loop time is 200 you would sleep for 160 msec. If it takes 45 msec the next time you would sleep for 155 msec.

This will even out the behavior, as long as your loop time is greater than the amount of time it takes to actually execute one time through the loop it will be stable. However, there could be another problem here.

When you send a command, at a high level, it gets processed and sent out to your devices in a separate thread (i.e. sendCommand doesn’t block until the device receives and executes the command). Consequently if you execute your loop with too short of a sleep it is possible that a new command may be received and executed by the light before the previous one is processed. This could explain the jumping backwards and forwards.

Look at habmin for those devices and see if there is a configuration parameter that seems like it is relevant to a dimmer time. If it has one and you enable it, it might work like @Udo_Hartmann’s devices and you just send the one command with the new level and the device will gradually fade in or out.

If there are no configuration parameters then you are stuck.

One final thing, if through experimentation you discover the problem/limitation is with how the rules are executing, you might consider looking into the JSR233 plugin and writing this rule in Jython. Jython rules can run several times faster than the Rules DSL.

Thanks for that - there are only soft start paramaters which dont really give the effect and only work on manual toggle on, and nothing for off!!!

I will keep playing - do you have any links to JSR233 - im an not fussed what language I write it in (as long as I can undertand it…)