A More Clever System for "Locking" Lights?

I’ve got different ways of triggering the same sets of lights:

  • Motion sensors
  • Voice command (Amazon Echo)
  • OpenHAB UI, etc.

I’ve also got a system in place for “locking” lights if they’ve been triggered by the user (i.e., through voice command or other means), because I don’t want the motion sensors’ OFF updates to turn the lights off if the user is still in the room but not moving around very much. The system works like this:

  • If the user initiates the lights turning on, I update a virtual switch (a “light lock”) to ON, then turn on the lights
  • If the user initiates the lights turning off, I update the same virtual switch to OFF, then turn off the lights
  • Before a motion detector sends an OFF command to a group of lights, I first check the light lock’s state. If the lock is ON, then I don’t send the OFF command; otherwise I do.

So I’m wondering if there’s a more concise way of accomplishing this. I currently maintain a “light lock” for each group of lights, and then check against this lock on every motion-sensor initiated OFF update.

I also maintain different groups of Items for each group of lights: there’s an ALEXA_LTS_GF_OFFC group (Alexa Lights Ground Floor Office), and an LTS_GF_OFFC group. When Alexa initiates a light action, she’s tied to the ALEXA groups, and I check against updates on them in the Rules; the motion sensors check against updates on the LTS_GF_OFFC group.

The system works as it should, but I can’t shake the feeling that there’s a much better way of accomplishing the same thing. I’m a couple weeks old at OpenHAB, so I’m still very much learning.

Any ideas?

Here are examples of the relevant Items & Rules:

ITEMS

// The same group of lights, controlled by two different entities
Dimmer LTS_GF_OFFC "Office Lights" <dimmer> (GF_Office, Lights) { channel="hue:0100:01178820e953:7:brightness,hue:0100:01178820e953:14:brightness" }
Dimmer ALEXA_LTS_GF_OFFC "Office Lights" <dimmer> (GF_Office, Lights) ["Lighting"] { channel="hue:0100:01178820e953:7:brightness,hue:0100:01178820e953:14:brightness" }

// The "light lock" associated with this group
LOCK_LTS_GF_OFFC "Office Light Lock" <lock> (GF_Office, SoftwareLocks)

RULES

// Here, Alexa sends a command to the group of lights associated with her current room
rule "ALEXA_LTS_GF_OFFC"
when
    Item ALEXA_LTS_GF_OFFC received command
then
    if (receivedCommand == OFF) {
        sendCommand(LOCK_LTS_GF_OFFC, OFF)
    else {
        sendCommand(LOCK_LTS_GF_OFFC, ON)
    }
end

// Here, the motion sensor in the same room receives an update
rule "MS_GF_OFFC"
when
    Item MS_GF_OFFC changed
then
    if (MS_GF_HALL.state == ON) {
        sendCommand(LTS_GF_HALL, ON)
    } else {
        if (LOCK_LTS_GF_OFFC.state !== ON) {
            sendCommand(LTS_GF_OFFC, OFF)
        }
    }
end

You are doing the right thing.
It is king of a mini “wasp-in-the-box” algorithm for presence.
If you came up with it yourself, well done.

1 Like

Thanks, @vzorglub!

If you got this working after only a couple of weeks with OH then you are doing great! This is an alternative way of implementing Design Pattern: Manual Trigger Detection using proxy Items that I haven’t written up yet.

From looking at your rules I’v ea couple of recommendations mostly having to do with style:

    then
        LOCK_LTS_GF_OFFC.sendCommand(receivedCommand)
    end

OK, with that out of the way I suspect you have these rules repeated over and over again, once for each room with a motion sensor. You might be able to make these Rule generic using triggeringItem, Design Pattern: Associated Items, and Design Pattern: Working with Groups in Rules.

For example, here is how I detect a manual command to a light and set an override flag (I use timestamps instead of a proxy Item).

// Theory of operation: any change in the relevant lights that occur more than five seconds after
// the change to DAY or after a change caused by cloudy is an override
rule "Watch for overrides"
when
  Item aFrontLamp changed or
        Item aFamilyLamp changed
then
  // wait a minute before reacting after vTimeOfDay changes, ignore all other times of day
  if(vTimeOfDay.state != "DAY" || vTimeOfDay.lastUpdate("mapdb").isAfter(now.minusMinutes(1).millis)) return;

  // Assume any change to a light that occurs more than n seconds after time of day or cloudy is a manual override
  val n = 5
  val causedByClouds = vIsCloudy.lastUpdate("mapdb").isAfter(now.minusSeconds(n).millis)
  val causedByTime = vTimeOfDay.lastUpdate("mapdb").isAfter(now.minusSeconds(n).millis)

  if(!causedByClouds && !causedByTime) {
    logInfo(logName, "Manual light trigger detected, overriding cloudy control for " + triggeringItem.name)
    postUpdate(triggeringItem.name+"_Override", "ON")
  }
end

// Thoery of operation: If it is day time, turn on/off the weather lights when cloudy conditions
// change. Trigger the rule when it first becomes day so we can apply cloudy to lights then as well.
rule "Turn on lights when it is cloudy"
when
  Item vIsCloudy changed or
  Item vTimeOfDay changed
then
  // We only care about daytime
  if(vTimeOfDay.state != "DAY") return;

  // give the time of day time to complete
  if(triggeringItem.name == "vTimeOfDay") Thread::sleep(1000)

  logInfo(logName, "It is DAY and cloudy changed: " + vIsCloudy.state.toString)

  // Apply the cloudy state to all the lights in the weather group
  gLights_ON_WEATHER.members.forEach[ l |

          val overrideName = l.name+"_Override"
          val override = gLights_WEATHER_OVERRIDE.members.findFirst[ o | o.name == overrideName ] as SwitchItem

          if(override.state != ON && l.state != vIsCloudy.state) l.sendCommand(vIsCloudy.state as OnOffType)

          logInfo(logName, l.name + if(override.state == ON) " is overridden" else " is not overridden")
  ]
end

So for you the rule could look something like the following:

rule "Manual turn on light"
when
    Item ALEXA_LTS_GF_OFFC received command or
    Item ALEXA_blah_blah_blah received command or
    ...
then
    sendCommand(triggeringItem.name.replace("ALEXA", "LOCK"), receivedCommand)
end

Wow that’s satisfying. A one liner that handles ALL of your locking of the lights and all you have to do is add triggers (and keep up this naming convention). Note this is one of the few cases where using the sendCommand Action is appropriate.

You could do the same for the motion sensor rule.

rule "Motion Sensors"
when
    Item MS_GF_OFFC changed or
    Item MS_blah_blah changed or
    ...
then
    val lock = LightLocks.members.findFirst[ l | l.name == triggeringItem.name.replace("MS", "LOCK_LTS") ]

    // only send a command if the lock isn't ON
    if(lock.state != ON){
        sendCommand(triggeringItem.name.replace("MS", "LTS"), triggeringItem.state)
    }
end

Note that I’m not certain this will work for you as there are some different Items that you are using in your example Rule that do not follow the naming pattern. You would also need to put all your locks into a Group so you can get access to them using the findFirst trick.

I leave it to you to determine if this approach will work for you.

1 Like

Thanks, @rlkoshak. This is exactly what I was looking for, and exactly why I was looking for it. I’ll have an increasing number of groups of lights as I keep expanding my system, and duplicating code for each group in several places would be tedious, inflexible, and possibly brittle. I’ll study this more carefully when I get home tonight and add the ideas to my knowledge base.

Coming from an OOP background and trying to wrap my brain around an event-driven system has been interesting!

edit: Followed the link to your design pattern for this, and there’s enough to keep me occupied for a week. Great stuff!

If you find yourself struggling too much consider looking at the JSR223 binding. It will let you write Rules in Jython, JavaScript, or Groovy which might feel more natural for you. If you want to stick with the Rules DSL, be sure to look at all the Design Patterns. Most of them were written when I or one of the other authors encountered a problem and came up with a fairly generic way to address that problem. And often that problem is the need to apply DRY.

But a few rules of thumb I apply to avoid falling into the traps the Rules DSL has if you try to treat it like a “regular” language:

  • Use Groups and List operators instead of data structures where possible (DP Working with Groups in Rules). Lists support filtering, finding, map/reduce, and iteration and you can stream them together (e.g. val mostRecent = MyGroup.members.filter[ i | i.lastUpdate !== null ].sortBy[ lastUpdate ].last
  • Take advantage of triggeringItem and (in OH 2.3) Member of to figure out what Item triggered a Rule. From there you can do a couple of lines of code to grab DP Assocaited Items to send commands to.
  • Use DP Separation of Behaviors to centralize common calculations (e.g. DP Time of Day) or actions (e.g. alerting)
  • Structure your rules to maximize DRY. First calculate what needs to be done and then do it. A line that generates side effects (e.g sendCommand, postUpdate, etc) should only appear once in the file. This makes it easier in the long run as you add more cases or checks or the like before causing side effects.

Here is a link to all the DPs written up so far.

1 Like

Thanks for the pointers and resources! You’ve given me loads to look into.

@rlkoshak Had to tweak the rule slightly to account for integers (e.g., “Alexa, dim office lights to 50%”), but it otherwise compressed about 60 lines of code down to 9:

rule "ALEXA_LTS"
when
    Item ALEXA_LTS_GF_HALL received command or
    Item ALEXA_LTS_GF_OFFC received command or
    Item ALEXA_LTS_FF_LVRM received command or
    Item ALEXA_LTS_FF_HALL received command
then
    sendCommand(triggeringItem.name.replace("ALEXA", "LOCK"), if (receivedCommand == OFF) "OFF" else "ON")
end

Extremely nice!

1 Like

Since you send the same state as the receivexCommand you can replace that inline if with receivedCommand.toString.

When you move to OH 2.3 you can put talk the ALEXA items/groups into a new group (let’s call it ALEXA_LIGHTS) and replace all the triggers with Member of ALEXA_LIGHTS and should you ever need to added it remove a control like this you just need to add or remove it to/from the group.