Toggling all lights ON/OFF with a switch and rules

I’m running OpenHAB 2.5.0M1 on Ubuntu with Zulu JVM 8.33.0.1 on an old laptop.

Here’s a way to toggle all lights on or off with a switch. You even get OpenHAB notifications for the state changes.

I assigned all lights to the gLight group and created a All_Lights_Toggle Switch item to control all lights from the sitemap, but make sure to not assign it to the gLight group (otherwise you have to check for that switch in the rules later on, which makes it needlessly more complex).

Switch All_Lights_Toggle "All lights" <light>

For my rules, I use locks and define the notification recipient address (linked to my MyOpenHAB account):

import java.util.concurrent.locks.ReentrantLock

val ReentrantLock lock  = new ReentrantLock()

val String notificationRecipient = "user@domain"

I use one rule for managing the state of the All_Lights_Toggle switch:

rule "A light changed"
when
	Member of gLight changed to ON
	or Member of gLight changed to OFF
then
	lock.lock()
	try {
		val Number numPoweredLights = gLight.members.filter[ s | (s.state == ON) && (s.name !== All_Lights_Toggle.name) ].size
		logDebug("LightChanged", "A member of gLight changed: {} set from '{}' to '{}' - {} light(s) are ON",
			triggeringItem.name, previousState, triggeringItem.state, numPoweredLights)
		if (numPoweredLights == 0) {
			All_Lights_Toggle.sendCommand(OFF)
		} else {
			All_Lights_Toggle.sendCommand(ON)
		}
	} finally {
        lock.unlock()
    }
end

And I use one rule for switching on or off the lights based on the state change of the All_Lights_Toggle switch:

rule "All Lights Toggle Switch"
when
	Item All_Lights_Toggle changed to OFF
	or Item All_Lights_Toggle changed to ON
then
	lock.lock()
	try {
		val toggleState = if ( triggeringItem.state == ON ) { OFF } else { ON }
		var lightsToToggle = gLight.members.filter[ s | s.state == toggleState ]
		val Number lightsToToggleCount = lightsToToggle.size

		logInfo("AllLightsSwitchRule", "Info: '{}' has state '{}'", triggeringItem.name, triggeringItem.state)

		if (lightsToToggleCount == 0) {
			logWarn("AllLightsSwitchRule", "All lights are already '{}'", toggleState)
			return;
		}
		// Compute the list of lights to toggle BEFORE changing their state (or you'd get 'null') !
		val listToggledLights = lightsToToggle.map[ name ].reduce[ s, name | s + ", " + name ]
		lightsToToggle.forEach [ i |
			logInfo("AllLightsSwitchRule", "Light '" + i.name + "' is '" + i.state.toString + "' - will switch '" + triggeringItem.state + "'")
			i.sendCommand(triggeringItem.state)
		]
		sendNotification(notificationRecipient, "ℹ️ Switching " + triggeringItem.state + " " + lightsToToggleCount + " light(s): " + listToggledLights)
	} finally {
        lock.unlock()
    }
end

Note: I see that I have to reload the light status frame from time to time, as I’m also using one switch in the gLight group to control 4 lights (linked to 4 channels). Apparently OpenHAB isn’t able to always post the correct status without a reload for that particular Item.

Have fun!

Hello, thank you for your contribution.
I fail to understand why you are using locks

This is a fairly complicated way to do this:

Group:Switch:OR(ON,OFF) gLights
rule "light changed"
when
    Item gLights changed
then
    if (triggeringItem.state != UNDEF) All_Lights_Toggles.postUpdate(gLights.state)
end

rule "toggle changed"
when
    Item All_Lights_Toggle changed
then
    if (triggeringItem.state != UNDEF) gLights.sendCommand(All_Lights_Toggle.state)
end

Be careful. Locks are very dangerous in Rules DSL. You cannot guarantee the finally will always execute. If there are certain types of exceptions inside the try block your Rule just dies without running the finally.

You should consider a different approach.

If you define your gLight Group as

Group:Number:SUM gLight

Then you can rewrite this Rule as

rule "A light changed"
when
    Item gLights changed
then
    logDebug("LightChanged", "A member of gLight changed")
    All_Lights_Toggle.sendCommand(if(gLights.state == 0) OFF else ON
end

No need for a lock in this case.

For that matter, there really is no need for this Rule either. This can be done mostly with Groups.

So let’s swap this around and define gLights as

Group:Switch:OR(ON, OFF) gLights

Get rid of the All_Lights_Toggle proxy Item. You don’t need it. Just put gLights on your sitemap instead. Sending a command to a Group will forward that command to all the members of that Group. With the OR aggregation function above, if any one of the members of gLights is ON then the state of gLights will be ON.

And to get the same behavior (i.e. turning on one light in gLights turns on all the rest of the lights in gLights) you need just one simple Rule, without any locks.

val Timer lightTimer = null

rule "A light changed"
when
    Item gLights changed to ON
then
    lightTimer?.cancel // in the unlikely event that gLights received an OFF command and the lights
                       // turn OFF in the quarter second after it changed to ON

    lightTimer = createTimer(now.plusMillis(250), [ |
        gLights.members.filter[ l | l.state != ON ].forEach[ l | l.sendCommand(ON) ]
        lightTimer = null 
    ]
end

Theory of operation: Instead of a proxy Item that we need to keep synchronized with the Group, let’s just use the Group. The Group is defined such that if any one member of the Group is ON the state of the Group will be ON.

The original will turn on the lights when any member of gLights turns on so to recreate this behavior we have a Rule that triggers when gLights changes to ON. This happens when any one or more of the members change to ON.

It is possible that gLights turned to ON because of a command to gLights which means that there might be some additional members of gLights that are about to turn on too. So we create a Timer to delay processing the event a bit. 250 milliseconds should be plenty.

When the timer goes off, loop through the lights that are not already ON and turn them on too.

2 Likes

Thank you for this elaborate explanation. I think I’m starting to better understand the rules DSL, its power and its constraints.

Regarding the concurrency locks, I was also wondering why there was a try clause without a catch clause…

I think I now know how I should simplify my roller shutter rules. And rid them of the concurrency locks by using groups (in case the gRollershutter group).

You don’t necessarily need the catch. It’s optional. You need either the finally, or the catch, or both.

But, like I said above, some errors can occur inside the try block that would cause the finally (or the catch) to never execute leaving your lock locked.

I recommend against using locks directly in Rules DSL unless absolutely necessary. And if it is necessary, I recommend finding another approach up to and including using JSR223 Rules instead. They just cannot be used safely in Rules DSL.

NOTE, this does not mean you shouldn’t use Java Classes that use locks internally like a ConcurrentLinkedList (see the Gatekeeper Design Pattern). In that case the lock is locked and unlocked wholly within the Java code so even if there is an exception, we know that finally will run and the lock will be unlocked.

I see. So locks aren’t reliable in Rules DSL but are in native Java.

That was an eye opener to me on various levels.

I initially assigned the (on, off) Switch, brightness Dimmer and color temperature Dimmer items to the gLight group, which resulted in funny on/off firing of all lights.

Now I assigned only the on/off switches to the gLight group, and created two other groups for the dimmer items:

Group:Switch:OR(ON, OFF)  gLight          "All Lights [%d ON]"  <light>  (Home)  ["Lighting", "Switchable"]
Group:Dimmer              gDimmer         "Dimmers"             <light>  (Home)  ["Lighting", "Switchable"]
Group:Dimmer              gWhiteSpectrum  "Color temperature"   <light>  (Home)  ["Lighting", "Switchable"]

I then slightly adapted your rule as follows:

rule "All lights group switch toggled ON or OFF"
when
    Item gLight changed to ON
	or Item gLight changed to OFF
then
    lightTimer?.cancel // in the unlikely event that gLights received an OFF command and the lights
                       // turn OFF in the quarter second after it changed to ON
	val toggleState = triggeringItem.state
	val invertedToggleState = ( if ( toggleState == OFF ) ON else OFF )
    lightTimer = createTimer(now.plusMillis(250), [ |
		gLight.members.filter[ l | l.state == invertedToggleState ].forEach[ l | l.sendCommand(toggleState) ]
        lightTimer = null 
    ] )
end

The main change is that I explicitly invert ON and OFF state of items in the gLight group depending on whether the gLight group switch received an ON or an OFF command. In combination with the elimination of the Dimmer items from the gLight group, I now get cleaner reactions when toggling the gLight group switch.

I still see some delay in one set of 4 lights that I ‘hid’ in one Item:

Switch  GF_LivingDining_Uplighters_Light_Toggle  "Uplighters"          <light>  (GF_LivingDining, gLight)   ["Lighting", "Switchable"]  {
  channel="tradfri:0100:GATEWAY_ID:65537:brightness,tradfri:0100:GATEWAY_ID:65551:brightness,tradfri:0100:GATEWAY_ID:65539:brightness,tradfri:0100:GATEWAY_ID:65540:brightness"
}
Dimmer  GF_LivingDining_Uplighters_Light_Dimmer  "Uplighters [%d %%]"  <light>  (GF_LivingDining, gDimmer)  ["Lighting", "Switchable"]  {
  channel="tradfri:0100:GATEWAY_ID:65537:brightness,tradfri:0100:GATEWAY_ID:65551:brightness,tradfri:0100:GATEWAY_ID:65539:brightness,tradfri:0100:GATEWAY_ID:65540:brightness"
}

Maybe I should rather define these as 2 Group items instead of using compound channels, in order to avoid the slight delay (about 1-2 seconds) I now see when toggling on or off all lights?

That is going to be binding specific I think and I don’t use that binding so can’t help answer.

One can get the state of a Dimmer as an OnOffType which lets you treat them as a Switch too. But I don’t know how Dimmers work with the OR(ON, OFF) so this could not matter. But assuming it does

gLights.members.filter[ l | l.getStateAs(OnOffType) != ON ]

would work with Dimmers too.

Okay, I just realised that I only need a rule for switching ON the lights as they will already nicely switch off as expected whenever I press the gLight switch OFF.

If the rule is triggered with Item gLight changed to ON then the lights will go off and will immediately turn on again. The gLight group switch works as expected if the rule is triggered with Item gLight received command ON :

// Add a rule to switch ON all lights if the gLight group switch is toggled ON.
// No rule is needed for switching OFF all lights!
rule "gLight ON"
when
    Item gLight received command ON
then
    lightTimer?.cancel // in the unlikely event that gLights received an OFF command and the lights
                       // turn OFF in the quarter second after it changed to ON
    lightTimer = createTimer(now.plusMillis(250), [ |
        // gLight.members.filter[ l | l.state != toggleState ].forEach[ l | l.sendCommand(toggleState) ]
		gLight.members.filter[ l | l.getStateAs(OnOffType) != ON ].forEach[ l | 
			logInfo("gLight ON", "gLight member '{}' of type '{}' has state '{}' - will switch 'ON'", l.name, l.getType, l.state.toString)
			l.sendCommand(ON)
		]
        lightTimer = null 
    ] )
end

I am clueless why the lights are switched on again if I use the changed to ON rule trigger.

If you only change the lights when gLights received command ON then there is no need for the Rule at all.

When using “changed to ON” combined with the way the Group was defined, the Rule will trigger when any member of gLights changes to ON independently and the Rule will turn on all the rest of the lights.

Since this Rule doesn’t actually send the OFF command to any lights, I don’t see how it can be the cause of the behavior you are seeing. The change you made is essentially disabling the Rule which does point to this Rule causing some sort of side effect that is causing the lights to turn OFF, but there is nothing actually in this Rule that can cause the lights to turn off. So you must have some other Rule that operates on these lights that send OFF commands.

Okay, I went ahead and removed the rule. It works the way I want without needing a rule, whew!

I had no other rule working with the lights, so I am puzzled as well.

But at least, I have learned a lot, and my setup works the way I wanted!

Thanks!