How I have automated my lights

Another thing that came to mind is typically the light is configured to go off a certain amount of time after the last motion was detected, meaning you only really track the ON states, not the OFF states of the motion sensor. As the person in the room moves around they keep rescheduling the timer until the room goes still for the given amount of time.

OK, now that I’m at a computer…

I’m glad to see either you came up with it independently or you are using the Design Pattern: Simple State Machine (e.g. Time Of Day).

So if I were to apply some of the above suggestions to make these rules more generic they would look something like (note, I’m not going to show all the Items involved).

import java.util.Map

val Map<String, Timer> timers = createHashMap // because you will want one timer per room
val Map<String, Timer> ctrlTimers = createHashMap

rule "Motion detected"
when
    Member of LightMotionSenors received update ON // members are named Room_Motion_Sensor
then
    if(triggeringItem.state == NULL) return; // we don't care about NULL

    val room = triggeringItem.name.split("_").get(0)
    val control = LightMotionControls.findFirst[ ctrl | ctrl.name == room + "_Motion_Control" ]
    if(control.state == OFF) return; // ignore the update if control is OFF

    // Get the values and associated Items
    val timer = timers.get(triggeringItem.name)
    val level = LightMotionLevels.members.findFirst[ ll | ll.name == room + "_" + HouseMode.state + "_Level" ].state as Number
    val timoutMins = (LightMotionTimouts.members.findFirst[ to | to.name == room + "_" + HouseMode.state + "_Timeout" ].state as Number).intValue
    val virtual = MotionLightsVirtual.members.findFirst[ v | v.name == room + "_Light_Virtual" ]
    val dimLevel = LightMotionDims.members.findFirst[ dim | dim.name == room + "_Dim" ].state as Number

    // Timer is already running
    if(timer !== null) {
        virtual.sendCommand(level) // undo the dim if necessary
        timer.reschedule(timoutMins) // goes off timeoutMins minutes after the last time the motion sensor detects motion
    }
    // No Timer was running
    else {
        virtual.sendCommand(level) // turn on the light

        // Create a timer to dimm then turn off the light
        timer.put(triggeringItem.name, createTimer(now.plusMinutes(timeoutMins), [ |

            // if already dimmed then turn off
            if(virtual.state == dimLevel) {
                virtual.sendCommand(OFF)
                timers.put(triggeringItem.name, null) // clear the timer
            }

            // dim the light and reset the timer for another minute
            else {
                virtual.sendCommand(level)
                timers.get(triggeringItem.name).reschedule(1)
            }
        ])
    }
end

rule "Disable motion control if light changed by switch"
when
    Member of Lights changed
then
    val virtual = MotionLightsVirtual.members.findFirst[ v | v.name == room + "_Light_Virtual" ]
    val control = LightMotionControls.findFirst[ ctrl | ctrl.name == room + "_Motion_Control" ]

    if(triggeringItem.state != virtual.state) {
        virtual.postUpdate(triggeringItem.state) // we don't want this to be treated as a new command
        control.sendCommand(OFF) // disable the motion sensor control of the light

        val int disabledTime = if(triggeringItem.state == 0) 10 else 60
        val timer = ctrlTimers.get(triggeringItem.name)
        if(timer !== null){
            timer.reschedule(now.plusMinutes(disabledTime))
        }
        else {
            ctrlTimers.put(triggeringItem.name, createTimer(now.plusMinutes(disabledTime), [ |
                control.sendCommand(ON)
                ctrlTimers.put(triggeringItem.name, null)
            ])
        }
    }
end

rule "Motion control resumed"
when
    Member of LightMotionControls received command ON
then
    val room = triggeringItem.name.split("_").get(0)
    val motion = LightMotionSenors.members.findFirst[ m | m.name == room + "_Motion_Sensor" ]
    val light = Lights.members.findFirst[ l | l.name == room + "_Motion_Light" ]
    val dimLevel = LightMotionDims.members.findFirst[ dim | dim.name == room + "_Dim" ].state as Number
    val virtual = MotionLightsVirtual.members.findFirst[ v | v.name == room + "_Light_Virtual" ]

    if(timers.get(motion.name) !== null || light.state == 0) return; // a timer is already set to handle the light or the light is already off

    if(light.state == dimLevel) return; // Light is already at the dim level

    timers.put(motion.name, createTimer(now.plusMinutes(1), [ |
        if(triggeringItem.state == OFF) virtual.sendCommand(OFF)
        timers.put(motion.name, null)
    ]) 
end

With the above rules you just need to add your Items to the proper Group, named following the format above (see Associated Items DP) and you never have to touch your Rules to add new lights/motion sensors. Also note that there is no duplicated code. The Timer is created exactly in one place.

As I mentioned before, to get the immediate OFF at night, set the Bathroom_night_Level to 0.

One advantage of putting all of this data into Items instead of global vals is you can adjust them on your sitemap instead of having them hard coded in your rules. You will want to use persistence on these Items though.

I tried to stick to the same behavior that you already have but make the rules work for all your rooms, not just the one room. You never want to have to copy and paste code.

And for the record, I’ve posted this elsewhere before but here it is again, my lighting rules:

val logName = "lights"

// Theory of operation: Turn off the light that are members of gLights_OFF_<TOD> and
// then turn on the lights that are members of gLights_ON_<TOD>. Reset the overrides.
rule "Set lights based on Time of Day"
when
  Item vTimeOfDay changed
then
  // reset overrides
  gLights_WEATHER_OVERRIDE.postUpdate(OFF)

  val offGroupName = "gLights_OFF_"+vTimeOfDay.state.toString
  val onGroupName = "gLights_ON_"+vTimeOfDay.state.toString

  logInfo(logName, "Turning off lights for " + offGroupName)
  val GroupItem offItems = gLights_OFF.members.filter[ g | g.name == offGroupName ].head as GroupItem
  offItems.members.filter[ l | l.state != OFF ].forEach[ l | l.sendCommand(OFF) ]

  logInfo(logName, "Turning on lights for " + onGroupName)
  val GroupItem onItems = gLights_ON.members.filter[ g| g.name == onGroupName ].head as GroupItem
  onItems.members.filter[ l | l.state != ON].forEach[ l | l.sendCommand(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 and vIsCloudy isn't NULL
  if(vTimeOfDay.state != "DAY" || vIsCloudy.state == NULL) return;

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

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

  // 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)

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


// 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
  Member of gLights_ON_DAY 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
5 Likes