Control HmIP rollershutter slats / blades using groups in rules

Hi everyone,

I want to control Homematic IP shutter slats using groups in rules. I’m a beginner with rules (and not a programmer) but already tried different options.

Beneath you see my first approach which worked fine, but unfortunately is “evil” (see for example Why have my Rules stopped running? Why Thread::sleep is a bad idea or Implement delay in x.members.forEach, exec binding+rcswitch) and therefore skipped. Please don’t copy&paste. :upside_down_face:
Still, it worked for me and I will post it here for you to get an idea what I’m aiming for and where I’m coming from:

rule "Control slat positions"
when 
	Item Lamellen received update
then 
	logInfo( "rollershutter.rules", "Rule 'Control slats positions' triggered" )
	if(Lamellen.state == 0) {
		Wintergarten_Jalousie_Tuer_Level2.sendCommand(0)
		Wintergarten_Jalousie_Tuer_Stop.sendCommand(ON)
		Thread::sleep(3500)
		Wintergarten_Jalousie_links_Level2.sendCommand(0)
		Wintergarten_Jalousie_links_Stop.sendCommand(ON)
		Thread::sleep(3500)
		Wintergarten_Jalousie_rechts_Level2.sendCommand(0)
		Wintergarten_Jalousie_rechts_Stop.sendCommand(ON)
	}
	else if (Lamellen.state == 25) {
		Wintergarten_Jalousie_Tuer_Level2.sendCommand(25)
		Wintergarten_Jalousie_Tuer_Stop.sendCommand(ON)
		Thread::sleep(3500)
		Wintergarten_Jalousie_links_Level2.sendCommand(25)
		Wintergarten_Jalousie_links_Stop.sendCommand(ON)
		Thread::sleep(3500)
		Wintergarten_Jalousie_rechts_Level2.sendCommand(25)
		Wintergarten_Jalousie_rechts_Stop.sendCommand(ON)	
	}
	else if (Lamellen.state == 50) {
		Wintergarten_Jalousie_Tuer_Level2.sendCommand(50)
		Wintergarten_Jalousie_Tuer_Stop.sendCommand(ON)
		Thread::sleep(3500)
		Wintergarten_Jalousie_links_Level2.sendCommand(50)
		Wintergarten_Jalousie_links_Stop.sendCommand(ON)
		Thread::sleep(3500)
		Wintergarten_Jalousie_rechts_Level2.sendCommand(50)
		Wintergarten_Jalousie_rechts_Stop.sendCommand(ON)		
	}
	else if (Lamellen.state == 75) {
		Wintergarten_Jalousie_Tuer_Level2.sendCommand(75)
		Wintergarten_Jalousie_Tuer_Stop.sendCommand(ON)
		Thread::sleep(3500)
		Wintergarten_Jalousie_links_Level2.sendCommand(75)
		Wintergarten_Jalousie_links_Stop.sendCommand(ON)
		Thread::sleep(3500)
		Wintergarten_Jalousie_rechts_Level2.sendCommand(75)
		Wintergarten_Jalousie_rechts_Stop.sendCommand(ON)			
	}
	else if (Lamellen.state == 100) {
		Wintergarten_Jalousie_Tuer_Level2.sendCommand(99)
		Wintergarten_Jalousie_Tuer_Stop.sendCommand(ON)
		Thread::sleep(3500)
		Wintergarten_Jalousie_links_Level2.sendCommand(99)
		Wintergarten_Jalousie_links_Stop.sendCommand(ON)
		Thread::sleep(3500)
		Wintergarten_Jalousie_rechts_Level2.sendCommand(99)
		Wintergarten_Jalousie_rechts_Stop.sendCommand(ON)				
	}
end

The relevant part of my sitemap looks like this:

	Frame label="Gruppensteuerung" {
		Switch 			item=		gRollos									label="Rollladen [%d %%]"	
		Switch 			item=		gJalousien								label="Jalousien [%d %%]"		
		Switch 			item=		Lamellen 								label="Lamellen"						mappings=[0="0%", 25="25%",50="50%", 75="75%", 100="100%"]
	}

I want to be able to change the 5 slats (=“Lamellen”) conditions 0%, 25%, 50%, 75% and 100%. And I want them to change short after each other, that’s why there is a delay of 3.5 seconds which I personally liked most. As you can see, there are 3 shutters in total.

I’m sending 2 commands for each shutter, since - for whatever reason - the first command itself doesn’t work (known problem, see Rollershutter with Homematic IP (HmIP) HmIPW-DRBL4), Homematic Raffstore/blinds control) or here (external link, german only), the 2nd command is needed.
Looks and feels ugly, but I couldn’t find any better option:

	Wintergarten_Jalousie_Tuer_Level2.sendCommand(25)
	Wintergarten_Jalousie_Tuer_Stop.sendCommand(ON)

My second approach from this morning looked like this:

rule "Control slat positions"
when 
	Item Lamellen received update
then
	if(Lamellen.state == 0) {
		tSlats?.cancel
		iStep = 0
		tSlats = createTimer(now.plusSeconds(0.5), [
			iStep ++ 														// short for "iStep = iStep + 1"
			switch iStep {
				case 1: {
					Wintergarten_Jalousie_Tuer_Level2.sendCommand(0)
					Wintergarten_Jalousie_Tuer_Stop.sendCommand(ON)
					tSlats.reschedule(now.plusSeconds(3))
				}
				case 2: {
					Wintergarten_Jalousie_links_Level2.sendCommand(0)
					Wintergarten_Jalousie_links_Stop.sendCommand(ON)
					tSlats.reschedule(now.plusSeconds(3))					
				}
				case 3: {
					Wintergarten_Jalousie_rechts_Level2.sendCommand(0)
					Wintergarten_Jalousie_rechts_Stop.sendCommand(ON)
					tSlats.reschedule(now.plusSeconds(3))					
				}
			}
		])
	}
	else if(Lamellen.state == 25) {
		tSlats?.cancel
		iStep = 0
		tSlats = createTimer(now.plusSeconds(0.5), [
			iStep ++
			switch iStep {
				case 1: {
					Wintergarten_Jalousie_Tuer_Level2.sendCommand(25)
					Wintergarten_Jalousie_Tuer_Stop.sendCommand(ON)
					tSlats.reschedule(now.plusSeconds(3))
				}
				case 2: {
					Wintergarten_Jalousie_links_Level2.sendCommand(25)
					Wintergarten_Jalousie_links_Stop.sendCommand(ON)
					tSlats.reschedule(now.plusSeconds(3))					
				}
				case 3: {
					Wintergarten_Jalousie_rechts_Level2.sendCommand(25)
					Wintergarten_Jalousie_rechts_Stop.sendCommand(ON)
					tSlats.reschedule(now.plusSeconds(3))					
				}
			}
		])
	}
	else if(Lamellen.state == 50) {
		tSlats?.cancel
		iStep = 0
		tSlats = createTimer(now.plusSeconds(0.5), [
			iStep ++
			switch iStep {
				case 1: {
					Wintergarten_Jalousie_Tuer_Level2.sendCommand(50)
					Wintergarten_Jalousie_Tuer_Stop.sendCommand(ON)
					tSlats.reschedule(now.plusSeconds(3))
				}
				case 2: {
					Wintergarten_Jalousie_links_Level2.sendCommand(50)
					Wintergarten_Jalousie_links_Stop.sendCommand(ON)
					tSlats.reschedule(now.plusSeconds(3))					
				}
				case 3: {
					Wintergarten_Jalousie_rechts_Level2.sendCommand(50)
					Wintergarten_Jalousie_rechts_Stop.sendCommand(ON)
					tSlats.reschedule(now.plusSeconds(3))					
				}
			}
		])
	}
	else if(Lamellen.state == 100) {
		tSlats?.cancel
		iStep = 0
		tSlats = createTimer(now.plusSeconds(0.5), [
			iStep ++
			switch iStep {
				case 1: {
					Wintergarten_Jalousie_Tuer_Level2.sendCommand(99)
					Wintergarten_Jalousie_Tuer_Stop.sendCommand(ON)
					tSlats.reschedule(now.plusSeconds(3))
				}
				case 2: {
					Wintergarten_Jalousie_links_Level2.sendCommand(99)
					Wintergarten_Jalousie_links_Stop.sendCommand(ON)
					tSlats.reschedule(now.plusSeconds(3))					
				}
				case 3: {
					Wintergarten_Jalousie_rechts_Level2.sendCommand(99)
					Wintergarten_Jalousie_rechts_Stop.sendCommand(ON)
					tSlats.reschedule(now.plusSeconds(3))					
				}
			}
		])
	}
end

I got the state machine idea from [SOLVED] Switch multiple timer in succession and it basically worked for me + it avoids sleep + it looks a bit nicer than v1, but I was aiming for something more clever with less code.
This is where I came across the Rollershutter group & rule thread, but I couldn’t geht that version to run (c&p version):

rule "Rollläden schließen 1h nach Sonnenuntergang"

when
	Channel "astro:sun:home:set#event" triggered START
then 	if (zeit_rolladen.state == ON) {
		var Timer rolladen_nacht = null
		rolladen_nacht = createTimer(now.plusMinutes(60), [| 
			val openShutters = rollershutter.members.filter[ rs | rs.state != 100 ].forEach[ rs, i | createTimer(now.plusMillis(500*i), [ | rs.sendCommand(100) ])	
			sendTelegram("marco" , "Rollläden werden automatisch geschlossen")	])
			}
end

And this is where I struggled alot and couldn’t get it adapted although it’s already a shutter example and quite near to my use case. :confused:
At this point, may I ask for some assistance? Could this be a better solution for me than the state machine approach?

Thanks,
Linus

PS: This is my first own topic and first “serious” post. It’s probably a bit too long at all and I’m not sure if these long code quotes are okay, but I guess you will let me know. :slight_smile:

PPS: Sorry for that deleted topic before. I accidentally and very early hit the “Create topic” button and didn’t want to let the topic be unfinished for such a long time (until I finished this post).

I think I might look to exploit the difference between openHAB commands and Item state.

When you poke the ‘Lamellen’ switch on your sitemap, it sends a command to your Item.
By default, the autoupdate feature will shortly update the Item state.
But if you disable autoupdate on that Item, it will uncouple command from state.

The idea would be to trigger your rule from command. Remember the command in a global variable to use as a “target” for your timed state machine.

Each time your state machine makes a step, compare current Lamellen state with target …
If different, issue the commands for a 25% move. Update the Lamellan state to new expected %. Reschedule timer for next step…
If target equals Lamellan state, the job is finished, make no more steps.

The same code should be able to run at each step.

EDIT oh wait, that’s tosh -I thought your purpose was to move in 25% steps over time. Ignore all that.

A more apropriate rule (second approach):

// define global vars on top of file
var int iStep = 0
var Timer tSlats = null
var Number nLevel = 0

rule "Control slate positions"
when 
    Item Lamellen received update                                      // maybe better use "received command" or "changed"
then
    if(tSlats !== null) return;                                        // timer already scheduled, so stop rule
    if(!(Lamellen.state instanceof Number)) return;                    // illegal state, so stop rule

    nLevel = Lamellen.state as Number                                  // save position
    if(nLevel == 100) nLevel = 99
    iStep = 0

    tSlats = createTimer(now.plusMillis(10), [                         // parameter is integer! 
        iStep ++                                                       // short for "iStep = iStep + 1"
        switch iStep {
            case 1: {
                Wintergarten_Jalousie_Tuer_Level2.sendCommand(nLevel)
                Wintergarten_Jalousie_Tuer_Stop.sendCommand(ON)
                tSlats.reschedule(now.plusMillis(3500))                // reschedule next step
            }
            case 2: {
                Wintergarten_Jalousie_links_Level2.sendCommand(nLevel)
                Wintergarten_Jalousie_links_Stop.sendCommand(ON)
                tSlats.reschedule(now.plusMillis(3500))                // reschedule next step
            }
            default: {
                Wintergarten_Jalousie_rechts_Level2.sendCommand(nLevel)
                Wintergarten_Jalousie_rechts_Stop.sendCommand(ON)
                tSlats = null                                          // delete pointer to timer
            }
        }
    ]
end

In question of the second rule: There are some errors in your rule.
First point is, never define a Timer var inside a rule, as it’s immediately killed when the rule ends.
Second, don’t assign a members.forEach to a val (you don’t use it anyway)
Third, when using a filter and a forEach, use different vals (i.e. the text before |).
Fourth, you can’t use the timer in such a way. the problem here is, that rs is invalid inside the second timer. As it’s a one-run per day rule and it’s a short time, either use Thread::sleep():

// define global vars on top to file!
var Timer rolladen_nacht = null

rule "Rollläden schließen 1h nach Sonnenuntergang"
when
    Channel "astro:sun:home:set#event" triggered START
then
    if(zeit_rolladen.state != ON) return;             // cancel rule if automatic off

    rolladen_nacht = createTimer(now.plusMinutes(60), [| 
        gShutters.members.filter[ m | m.state != 100 ].forEach[ rs |
            rs.sendCommand(100)
            Thread::sleep(500)
        ]
        sendTelegram("marco" , "Rollläden werden automatisch geschlossen")
    ])
end

Another option:

// define global vars on top to file!
var Timer rolladen_nacht = null
var int iCount = 0

rule "Rollläden schließen 1h nach Sonnenuntergang"
when
    Channel "astro:sun:home:set#event" triggered START
then
    if(zeit_rolladen.state != ON) return;             // cancel rule if automatic off
    iCount = 0
    rolladen_nacht = createTimer(now.plusMinutes(60), [| 
        if(iCount == 0)
            sendTelegram("marco" , "Rollläden werden automatisch geschlossen")
        gShutters.members.sortBy["Name"].forEach[ rs, i|
            if(i == iCount && rs.state != 100)
                rs.sendCommand(100)
        ]
        iCount ++
        if(iCount < gShutters.members.size) 
            rolladen_nacht.reschedule(now.plusMillis(500))
    ])
end

Please be aware that the the shutters are sorted by name. If there are shutters that are already closed, there will be a bigger time gap. As the list pointer is zero-based, we have to count up after sending command.
If the rollershutter Items aren’t set to autoupdate=“false”, it’s very likely that it’s even easier:

// define global vars on top to file!
var Timer rolladen_nacht = null
var Boolean bMessage = false

rule "Rollläden schließen 1h nach Sonnenuntergang"
when
    Channel "astro:sun:home:set#event" triggered START
then
    if(zeit_rolladen.state != ON) return;             // cancel rule if automatic off
    bMessage = true
    rolladen_nacht = createTimer(now.plusMinutes(60), [| 
        if(bMessage) {
            sendTelegram("marco" , "Rollläden werden automatisch geschlossen")
            bMessage = false
        }
        val itemlist = gShutters.members.filter[m|m.state != 100]
        if(itemlist.size > 0) {
            itemlist.head.sendCommand(100)
            rolladen_nacht.reschedule(now.plusMillis(500))
        }
    ])
end

If setting an offset to the set#event channel, we don’t need the initial 60 minutes timer and therefor can avoid the bMessage workaround, as the message is created outside the timer lambda.

1 Like

I suppose your post is just the perfect answer. It helped me a lot and ofc it fixed my problem.

I decided for your pimped state machine approach which looks smart and most important: I understand what it does (your comments helped there). Even if I suppose it is very basic, I wouldn’t have figured out lines like if(nLevel == 100) nLevel = 99 by myself.

After some minor adjustments, this is the final result:


var int iStep = 0
var Timer tSlats = null
var Number nLevel = 0

rule "Control slat positions"

when 
    Item Lamellen changed												// better use "received command" or "changed" than "received update"
then
    if(tSlats !== null) return;											// timer already scheduled, so stop rule
    if(!(Lamellen.state instanceof Number)) return;						// illegal state, so stop rule

    nLevel = Lamellen.state as Number									// save position
    if(nLevel == 100) nLevel = 99
    iStep = 0

    tSlats = createTimer(now.plusMillis(10)) [							// parameter is integer! 
        iStep ++														// short for "iStep = iStep + 1"
        switch iStep {
            case 1: {
                Wintergarten_Jalousie_Tuer_Level2.sendCommand(nLevel)
                Wintergarten_Jalousie_Tuer_Stop.sendCommand(ON)
                tSlats.reschedule(now.plusMillis(3500))					// reschedule next step
            }
            case 2: {
                Wintergarten_Jalousie_links_Level2.sendCommand(nLevel)
                Wintergarten_Jalousie_links_Stop.sendCommand(ON)
                tSlats.reschedule(now.plusMillis(3500))					// reschedule next step
            }
            default: {
                Wintergarten_Jalousie_rechts_Level2.sendCommand(nLevel)
                Wintergarten_Jalousie_rechts_Stop.sendCommand(ON)
                tSlats = null											// delete pointer to timer
            }
        }
    ]
end

For the second part of your post I now found an even better suiting astro channel in the docs to trigger my rule, which is NauticDusk_Start. Means no need for an additional timer (or setting an offset) anymore.
The zeit_rolladen control is currently “under construction” which is why I skipped this part for the moment. Will surely later come back to your advises and extend my current rule, which is quite handy:

rule "Close all living area shutters at NauticDusk_Start"
when
    Channel "astro:sun:local:nauticDusk#event" triggered START	// can be tested with a cron trigger á la Time cron "0 45 14 ? * * *"
then
	gRolladen_Gesamt.members.filter[ r | r.state < 100 ].forEach[ r, i | createTimer(now.plusMillis(100*i), [ | r.sendCommand(0) ])]
	logInfo("shutter", "Rule 'Close all living area shutters at NauticDusk_Start' successfully executed.")
	sendTelegram("linus" , "Beginn der nautischen Abenddämmerung, alle Rollläden wurden geschlossen.")
end

Thank you again for your time and suggestions! :raised_hands:t2: :+1:t2:
Marked your post as solution.