Make any light controllable by HSBType to simplify automation

As soon as you have color lights, you want to start doing fancy things with them. Two problems you run into:

  • Switched lights are in the way when you want to light the room with fancy colors
  • Switched lights stick out like a sore thumb when you want to dim the brightness

So I created rules to adjust switched lights to whatever is going on in the room. The idea was to give them a dummy color channel, so that they could receive the same commands as all the other lights, and then adjust their on/off state accordingly. I also wanted this to work vice-versa, so that their current status would be reflected in the color channel as well, so that for automation’s sake, they could be treated as color lights alltogether, no need to make different logic for different lights.

:warning: NOTE 14/11/2018: There is updated code in post #6! :warning:

Here’s a dummy color channel, as an example:

Color WandschalterSchlafzimmer_Deckenlicht_Color "Deckenlicht" (gSDeckenlicht, gSwitchToColor, gColorSZ) ["Lighting"]

As one can see, I have tagged this with “Lighting”, so that Alexa will control the light through this dummy channel and not the actual (Zigbee binding) channel. The name of the actual switch channel has to end with “_SwitchOnoff”. It also should be in the group gSwitchtoColor.

And here’s the rules, one for switching according to the color commands, and one for reflecting the switch states back to the color channel:

var onLock = false
var offLock = false
var Timer lockTimer = null

rule "Color to Switch translator"
    //Allow Switched lamps to be switched along a color command to other lights 
when
    Item SchranklichtSchlafzimmer_Color changed or 
    Item WandschalterSchlafzimmer_Deckenlicht_Color changed
then
    //construct corresponding switch item
    var itemName = triggeringItem.getName().replace("_Color", "_SwitchOnoff") 
    var lampSwitch = gSwitchToColor.members.filter[lamp|lamp.name == itemName].head

    if ( lampSwitch !== null) {
        //convert Color to switch command (low brightness or solid color switches off)
        if ((( (triggeringItem.state as HSBType).getBrightness().intValue() < 50 ) ||
            ( (triggeringItem.state as HSBType).getSaturation().intValue() > 50 ) ) && 
            (lampSwitch.state == ON ) ){
            if (!offLock) {
                onLock = true //prevent rule from bouncing on itself
                sendCommand( lampSwitch, OFF )
                logInfo( itemName, "OFF")
                lockTimer = createTimer(now.plusMillis(500) , [ | onLock = false])
            }
        }
        else if ((( (triggeringItem.state as HSBType).getBrightness().intValue() >= 50 ) &&
            ( (triggeringItem.state as HSBType).getSaturation().intValue() <= 50 ) ) && 
            (lampSwitch.state == OFF ) ){
            if (!onLock) {
                offLock = true //prevent rule from bouncing on itself
                sendCommand( lampSwitch, ON )
                logInfo( itemName, "ON")
                lockTimer = createTimer(now.plusMillis(500) , [ | offLock = false])
            }
        }
    }
    else {
        logWarn("SwitchtoColor", "The switch item to adjust color state " + itemName + " could not be found")
    }
end

rule "Switch to Color translator"
    //Reflect switched lamps state in their dummy color channel
when
    Item SchranklichtSchlafzimmer_SwitchOnoff changed or 
    Item WandschalterSchlafzimmer_Deckenlicht_SwitchOnoff changed
then
    //construct corresponding color item
    var itemName = triggeringItem.getName().replace("_SwitchOnoff", "_Color") 
    var lampColor = gSwitchToColor.members.filter[lamp|lamp.name == itemName].head

    if ( lampColor !== null) {
        //convert Color to switch command (low brightness or solid color switches off)
        logInfo(itemName,"Switch command converted to color value")
        if ((triggeringItem.state == ON) ) {
            sendCommand( lampColor, "45,40,99" )
        }
        
        else if ((triggeringItem.state == OFF) ) {
            sendCommand( lampColor, "45,40,1" )
        }
    }
    else {
        logWarn("SwitchtoColor", "The color item to adjust switch state " + itemName + " could not be found")
    }
end

Notes:

  • I think with OH 2.4 it should become possible to get a single item “triggeringItem” even when the rule trigger is defined as a group. It will then be possible that in stead of defining the individual lamp items, you can just define a single group as the trigger for a rule (and still get the correct triggeringItem later on).
  • Without reflecting back the status into the color channel (the second rule), the switching should still work, and you wouldn’t necessarily need the locks to preventing both rules to keep on triggering each other. However, the locks are also useful to prevent possible bouncing between this and other rules (example below).
  • The reflected HSB values are such because I always try to keep lights in a warm white hue, and I am not using 0 and 100 so that I can see that the values were set by this rule.
  • These rules do not yet cater for white dimmable lights, but it should be close enough for people to figure it out for themselves.

As an example of another rule that uses the locks to not to create a mess, a little bonus rule below. I have one light that is directly switched by a Ubisys S2 component (according to a conventional wall switch), and switch all other lights according to that light.

rule "Bedroom lights on according wall switch"
    //Switch lights according to ceiling light
when 
    Item WandschalterSchlafzimmer_Deckenlicht_Color changed
then
    if (!onLock && !offLock) { //make sure the change was not triggered by a color command
        if ((WandschalterSchlafzimmer_Deckenlicht_Color.state as HSBType).getBrightness().intValue() >= 99) {
            logInfo("Deckenlicht","Deckenlicht an mit Schalter")
            if (Sun_Daylight == ON) {
                gColorSZ.sendCommand("45,10,100")
            }
            else{
                gColorSZ.sendCommand("45,40,50")
            }
        }

        else if ((WandschalterSchlafzimmer_Deckenlicht_Color.state as HSBType).getBrightness().intValue() <= 1) {
            logInfo("Deckenlicht","Deckenlicht aus")
            gColorSZ.sendCommand("45,40,0")
        }
    }
end

Sun_Daylight is a dummy switch which is managed by a rule using the Astro binding to reflect whether there is daylight.

2 Likes

This has been available since 2.3. See the Member of triggers.

Thanks for that. In a few days, I might rewrite this a bit, got some ideas, and would like to test whether using postUpdate() will enable to do away with the locks. Problem is my wall switch, which does not yet reflect switch position in OH, so I need to go by the lamp state, which is however command internally in the Ubisys component, not over zigbee. So I’ll have to see how to distinguish between the wall switch and the rest. My Idea is to have the bonus rule triggered both by “changed” and “received command” and then test whether receivedCommand is populated.

Here are a few of my ideas:

Thanks I’ll have a look into that. The log was primarily to see during debugging whether the rule was causing the change, or something else. Working perfectly now, next I’ll clean it up, probably incorporate dimmers as well. And I’ll try to make the code as generic as possible, so that it can be a copy paste job and people only need to define their items appropriately.

Not really good at this Xtend stuff, I miss global functions and the type conversions give me headaches.

I have rewritten the code with the pointers given by Rich. Rougly the same length, but now it’s generic and also includes logic for dimmers. So now it allows to send color commands to all lights via Alexa, rules etc. No need anymore to differentiate between types of lights, the HSBType is now one common command that all lamps understand.

The rule is simply copy-paste. No editing needed. To configure the rule to run with your lights you need to adhere to the following conventions when defining items:

  • You have to define a dummy _Color item which does not link to a channel.
  • Items which belong to the same light need the same name, except for the ending, which needs to be either _Color or _Dimmer or _SwitchOnoff.
  • Both the dummy _Color item and the associated _Dimmer or _SwitchOnoff need to be assigned to the gVirtualColor group. This way the rule will be able to find them and react to commands.

example:

Switch SchranklichtSchlafzimmer_SwitchOnoff "Schranklicht" (gVirtualColor)                        {channel="zigbee:device:f733e1c4:84182600000dda81:84182600000DDA81_3_switch"}
Color  SchranklichtSchlafzimmer_Color       "Schranklicht" (gColorSZ, gVirtualColor) ["Lighting"]

As one can see, the switch item is only in the gVirtualColor group so the rule is made aware of it and nothing else. The (dummy) Color item is member in a group for the room which I use for automation, and also is tagged to be used by alexa. It also is part of the gVirtualColor group so the rule can find it.

The gVirtualColor group can take as many lights as you want, the rule finds out which items belong together by their names. Just stick to the naming conventions above.

code:

rule "Virtual color controller"
//This rule allows all kinds of lamps to use a color channel for control
when Member of gVirtualColor received command

then 
    if ( triggeringItem instanceof ColorItem ) {   
        //a virtual color was commanded. we need to find the corresponding item for control 
        var itemRegex = triggeringItem.name.replaceFirst("_Color$", "_(Dimmer|SwitchOnoff)")
        var lampItem = gVirtualColor.members.filter[ lamp|lamp.name.matches( itemRegex ) ].head

        if ( lampItem !== null ) {
            var lampCommand = receivedCommand.toString()
            if  ( receivedCommand instanceof HSBType ) { 
                //hsb command received, convert to a dimmer value
                lampCommand = receivedCommand.getBrightness().toString()

                if ( receivedCommand.getSaturation().intValue() > 50 ) {
                    //Solid color, so we want the light to be off
                    lampCommand = "0"
                }    
            }

            if ( (lampItem instanceof SwitchItem) && ( !( receivedCommand instanceof OnOffType ) ) ) {
                //for a switch, translate any dimmer value to on/off command
                if ( Integer::parseInt(lampCommand) < 50 ) {
                    lampCommand = "OFF"
                }
                else {
                    lampCommand = "ON"
                } 
            }

            lampItem.sendCommand( lampCommand )
        }
        else {
            logWarn("VColorControl", "A switch or dimmer to use for commands to " + triggeringItem.name + " could not be found")
        }
    }
    else if ( ( triggeringItem instanceof SwitchItem ) || ( triggeringItem instanceof DimmerItem ) ) {
        //we need to update the state of the corresponding color channel
        var itemName = triggeringItem.name.replaceFirst( "_(Dimmer|SwitchOnoff)$", "_Color" ) 
        var lampColor = gVirtualColor.members.filter[ lamp|lamp.name == itemName ].head
        var brightness = "0"

        if ( triggeringItem instanceof DimmerItem ) {
            brightness = receivedCommand.toString()
        }
        else if (receivedCommand == ON) {
            brightness = "100"
        }

        //send HSB update (keep hue and sat, brightness according switch or dimmer)
        if ( lampColor !== null ){
            lampColor.postUpdate( (lampColor.state as HSBType).getHue().toString() + "," + (lampColor.state as HSBType).getSaturation().toString() + "," + brightness )
        }
        else {
            logWarn("VColorControl", "the virtual color channel for " + triggeringItem.name + " could not be found")    
        }
    }
    else {
        logWarn("VColorControl",  "The virtual color controller rule can't handle the " + triggeringItem.name + " item")    
    }
end

Short description how the rule works: It will respond to any command for anything in the group. Normally this would at first be a color item receiving a command. It will then find the first matching switch or dimmer item by name. It then translates the command to something useful (the light goes off when high saturation is commanded, and a switched light will additionally switch off when a low brightness is commanded).

This action on the dimmer or switch item will then trigger this rule once more, and the lower part of the rule will be executed. The new state of the lamp will be translated into an appropriate HSB value for the color item. This is then updated, not commanded, to avoid an infinite loop.

6 Likes