Example of a Lambda Function for timing the turning on and off of Lights (or switches)

I have noticed that there are a lot of questions about how to turn a light off x seconds after a motion detector turns it on. This can be very confusing, involving Timers, getting the light to stay on when motion is re-detected, what if the sensor doesn’t send OFF? and so on. You need a global Timer for every light, you have to check to see if the Timer exists, etc.

I have written a simple lambda function that takes care of all of this. No more global timers, no more checking, setting to null whatever.

Here it is:

import org.eclipse.xtext.xbase.lib.Functions

val Map<String, Timer> LightingTimers = newHashMap  //used for motion detection timers

val Functions$Function4<GenericItem, Number, Number, Map<String, Timer>, Boolean> MakeLightTimer = [  //function (lambda) to create/reschedule/cancel timer (call with time set to 0 to cancel)
    item,
    level,
    time,
    timers |
    
    var String timerKey = item.name
    if (timers.containsKey(timerKey)) {
        var Timer timerTmp = timers.get(timerKey)
        if(time == 0) {
            timerTmp.cancel()
            timers.remove(timerKey)
            logInfo("Light Timer Function", timerKey + " Timer cancelled" )
        }
        else {
            timerTmp.reschedule(now.plusSeconds(time))
            logInfo("Light Timer Function", timerKey + " Motion detected, Timer recheduled for " + time + " more seconds" )
            }
        }
    else {
        if(time != 0) {
            if(level == 0)        sendCommand(item, OFF)
            else if(level == 100) sendCommand(item, ON)
            else                  sendCommand(item, level.intValue)
            logInfo("Light Timer Function", timerKey + " Motion detected, Light set to: " + level)
            timers.put(timerKey, createTimer(now.plusSeconds(time)) [|
                logInfo("Light Timer Function", timerKey + " Motion over, Light turning OFF" )
                sendCommand(item, OFF)
                timers.remove(timerKey)])
            }
        }
    true
    ]

You use it like this:

var int LandingDelay = 60 // 60 seconds
var int landingNightBrightness = 10

rule "Landing LA Motion Detection"
when
    Item AeonMS6MotionLA changed
then
    // Landing LA Aeon detected motion 
    logInfo("Aeon Motion", "Landing Motion Detected!" )
    logInfo("Aeon Motion", "Landing Aeon LA Motion Sensor is: " + AeonMS6MotionLA.state)
    
    if (LandingMotionEnable.state == OFF) {   // if motion disable flag is set - do nothing
        logInfo("Landing Light", "Landing Light Motion Disabled - ignoring" )
        return false
        }
    
    if (AeonMS6MotionLA.state == OPEN || AeonMS6MotionLA.state == ON)  { 
        if(now.getHourOfDay() >= 1 && now.getHourOfDay() <= 6) {    //between 1am and 7am (note hour 6 = 7am)
            MakeLightTimer.apply(landingMain, landingNightBrightness, LandingDelay, LightingTimers)
            }
        else {
            MakeLightTimer.apply(landingMain, 100, LandingDelay, LightingTimers)
            }
        }
end

Where AeonMS6MotionLA is a motion detector with an Item type of Contact or Switch, and landingMain is a light with Item type of Switch or Dimmer (if it’s a Switch, just use 0 and 100 as the levels).
To cancel an existing timer, call “MakeLightTimer.apply(landingMain, anything, 0, LightingTimers)” note time is set to 0 (level can be any number, it isn’t used if you are cancelling the timer). Nothing happens if the timer doesn’t exist, otherwise it is cancelled.

This is using OH2 syntax, but the syntax is almost identical for OH1, see https://community.openhab.org/t/reusable-functions-a-simple-lambda-example-with-copious-notes/15888 for the difference.

This is the simplest possible example:

rule "Washroom W Motion Detection"
when
    Item AeonMS6MotionW changed
then
    // Washroom W Aeon detected motion
    if (AeonMS6MotionW.state == OPEN || AeonMS6MotionW.state == ON)
        MakeLightTimer.apply(washroomLight, 100, 300, LightingTimers)    //turn washroom light on for 300 seconds (5 minutes)
end

if the device just sends an ON on motion detection (ie no OFF), the rule would be:

rule "Washroom W Motion Detection"
when
    Item AeonMS6MotionW changed to ON
then
    // Washroom W Aeon detected motion
    MakeLightTimer.apply(washroomLight, 100, 300, LightingTimers)    //turn washroom light on for 300 seconds (5 minutes)
    Thread::sleep(5000)    //may not be needed, if sensor has a lockout period
    postUpdate(AeonMS6MotionW, OFF)
end

I just wrote this, and it seems to work fine, but I will no doubt find improvements/edge issues etc. if you have an improvement, feel free to share it.

6 Likes

An alternative approach, if the timeout on your lights remains fixed, is to use the Expire bindings. That will eliminate the need to create and track any Timers at all. I’ve updated my Motion Sensor Design Pattern to show how it would work with Expire.

Your lambda handles the case where the time the light stays on can change based on time of day or some other criteria so the Expire binding might be of limited use. But if you only have one timeout value times I would probably do something like the following (NOTE: I don’t use Dimmers so don’t know if Dimmers work differently with the Expire binding, I’d love for someone to confirm it works):

Item

Dimmer landingMain { channel="...", expire="60s,command=0" } // command=OFF might work too

Rule
Note: I use Time of Day below to detect night.

val int landingNightBrightness = 10 // if the value doesn't change, make it a constant using val

rule "Landing LA Motion Detection"
when
    Item AeonS6MotionLA changed to OPEN // assumes the motion sensor changes back to CLOSED at some point, not all do in which case use received update or use Expire to turn Motion Sensor Item back off
then
    logInfo("Aeon Motion", "Landing Detected!")

    if(LandingMotionEnabled.state == ON) {
        if(TimeOfDay.state == "NIGHT") landingMain.sendCommand(landingNightBrightness)
        else landingMain.sendCommand(100)
    }
    else {
        logInfo("Landing Light", "Landing Light Motion Disabled - ignoring")
    }
end

Note: I made some minor changes to eliminate some unnecessary lines of code. For example, by using changed to OPEN there is no need to check if the motion sensor is OPEN in the rule nor a need to log out the motion sensor’s state. By swapping to check if the LandingMotionEnabled is ON instead of OFF there is no need for the return false and frankly the else is only needed if you want to keep the log statement.

The above is complete. No timers, no lambdas, no extra complexity. I highly recommend using Expire if the Timer time is fixed. Use of Expire and some minor changes to the rule described in the note has taken this from 56 lines of code (not counting blanks) to 14.

If the Timer time is not fixed, I recommend using the above lambda.

3 Likes

Thanks @rlkoshak, I wasn’t aware of the expire binding, I can certainly use that for some of my switches where I currently have a rule to turn them off (where they only send an “on”).

I admit I’m a bit lazy in some of the code, mostly because I’m running on a Proliant server, and so have boatloads of capacity. For a Pi or something, scrunching things down makes sense.

I also do tend to over log things, as the debugging in OH rules is - obscure, so every time a new release breaks all my rules, it makes it easier to find where (and why).

I have changed the lambda somewhat (posted below) to do the ON or OPEN check, and also to check for continued motion (which was an edge condition I missed). How does the expire binding cope with continuous motion? This was one of the issues that I found. For example, using the Aeon Labs MS6 motion sensor with OH2 zwave binding (2.1), you get an ON (motion is now a Switch, previously it was a Contact - which is why I check for both) when motion is detected. You only get an OFF when motion has stopped (for more than the lock out period, my sensors are set to 10 seconds). This resulted in the lights triggering on motion, but with continued motion, they would switch off after the preset time, as the motion sensor is still “ON”. I’ve since included a check to see if the Switch/Contact is still on, and reschedule for more time if that is the case.

I’m assuming that the expire binding would switch off at the allotted time, and wait for another ON event, but if it receives another ON (without an OFF) does it reset the timer? If so I could probably combine the two, and simplify things quite a lot. I think most of my time delays are tied to Items (ie each item/Light has it’s own timeout).

Does this work with OH1 bindings? I ask because my lights are all Insteon lights, and there is no OH2 binding for these (but the OH1 bindng works fine) . Will this work with any kind of switch (i have some tripped by mqtt) or does it have to be just OH2 binding type switches?

Again, thanks for pointing out this feature that I was not aware of, I can think of half a dozen uses for it right away. Anyway here is my current lambda:

val Functions$Function5<GenericItem, GenericItem, Number, Number, Map<String, Timer>, Boolean> MakeLightTimer = [  //function (lambda) to create/reschedule/cancel timer (call with time set to 0 to cancel)
    motiondetect,
    item,
    level,
    time,
    timers |
    
    var Boolean retval = false
    if(motiondetect.state == OPEN || motiondetect.state == ON) {
        var String timerKey = item.name
        if (timers.containsKey(timerKey)) {
            var Timer timerTmp = timers.get(timerKey)
            if(time == 0) {
                timerTmp.cancel()
                timers.remove(timerKey)
                logInfo("Light Timer Function", timerKey + " Timer cancelled" )
            }
            else {
                timerTmp.reschedule(now.plusSeconds(time))
                logInfo("Light Timer Function", timerKey + " Motion detected, Timer recheduled for " + time + " more seconds" )
                }
            }
        else {
            if(time != 0) {
                if(level == 0)        sendCommand(item, OFF)
                else if(level == 100) sendCommand(item, ON)
                else                  sendCommand(item, level.intValue)
                logInfo("Light Timer Function", timerKey + " Motion detected, Light set to: " + level)
                timers.put(timerKey, createTimer(now.plusSeconds(time)) [|
                    if(motiondetect.state == OPEN || motiondetect.state == ON) {
                        timers.get(timerKey).reschedule(now.plusSeconds(time))
                        logInfo("Light Timer Function", timerKey + " Motion still present, Timer recheduled for " + time + " more seconds" )
                        }
                    else {
                        logInfo("Light Timer Function", timerKey + " Motion over, Light turning OFF" )
                        sendCommand(item, OFF)
                        timers.remove(timerKey)
                        }
                    ])
                }
            }
        retval = true
        }
    retval
    ]

Landing Light example rule:

rule "Landing LA Motion Detection"
when
    Item AeonMS6MotionLA changed
then
    // Landing LA Aeon detected motion
    
    if (LandingMotionEnable.state == OFF) {   // if motion disable flag is set - do nothing
        logInfo("Landing Light", "Landing Light Motion Disabled - ignoring" )
        return false
        }
    
    if(now.getHourOfDay() >= 1 && now.getHourOfDay() <= 6)     //between 1am and 7am (note hour 6 = 7am)
        MakeLightTimer.apply(AeonMS6MotionLA, landingMain, landingNightBrightness, LandingDelay, LightingTimers)
    else 
        MakeLightTimer.apply(AeonMS6MotionLA, landingMain, 100, LandingDelay, LightingTimers)
end
1 Like

I don’t “scrunch things down” for performance reasons. Shorter code with fewer branches is easier to read, understand, and maintain. The changes in order I recommended makes zero difference in the runtime performance no matter how resource abundant or constrained the server may be that is running the code.

I like to tail my logs so I tend to be very sparse with my logging until there is a problem. If I want to forensically review what happened I’ve never failed using just the events log. I would have eliminated the logs but kept them in my rewrite so the remained functionally equivalent.

Every time the Item receives a command or update that isn’t the base state (i.e the “command=” or “state=”) it resets its internal timer for the given amount of time. It essentially works just like timer.reschedule. Put another way, it only triggers when the Item has not received a command or update for the specified amount of time. When it receives a command or update that sets it to the base state it cancels the timer.

Yes.

Yes. Expire is its own separate binding and works in combination with any other binding or channel.

Yes.

1 Like

@rlkoshak Thanks, I found the wiki for the binding, installed it and have three switches running using it that previously had reset timers. Very useful feature!

I can see that I will have to go back over my extensive rules and rethink them.

As to logging philosophies, each to their own I think. I tail my logs also, but filter on “whatever isn’t working” with separate logs for the front door, homekit, various binding, rules, alexa skills - I guess I’m just a “logging” kind of guy. Probably because I don’t use the designer (and probably because in my professional life I spend a lot of time reading logs to figure out “what went wrong”).

Thanks again for the contribution.

An alternative approach is to separate the ON from Timed-OFF functions - trigger light on with motion, but set no timer (or cancel a running one).
Start the timer when motion ceases - this is a generally a closer match to the desired effect.

I agree, and about half of my motion rules work this way, of course you then get the light on for the set duration plus whatever the timeout is for your sensor to turn off. This usually doesn’t matter for lights though.

One day I’ll have a consistent set of rules that all do things the same way. One day…

@Nicholas_Waterton
When I tries to use this script I’m getting this error:
The name '<XFeatureCallImplCustom>.containsKey(<XFeatureCallImplCustom>)' cannot be resolved to an item or type

Same issue like Ahiel has. Wonder if he managed to resolve.

running OH2

When rule is triggered I get error in log:

2017-08-15 15:39:28.842 [ERROR] [.script.engine.ScriptExecutionThread] - Rule ‘passage received command’: The name ‘.containsKey()’ cannot be resolved to an item or type.

Item is:

Dimmer	Dimmer_Passage			"Dimmer Passage"			<redled> (milight)  	{channel="milight:rgbwLed:4a387f8c:3:ledbrightness"}
val Map<String, Timer> LightingTimers = newHashMap  

val Functions$Function4<GenericItem, Number, Number, Map<String, Timer>, Boolean> MakeLightTimer = [  //function (lambda) to create/reschedule/cancel timer (call with time set to 0 to cancel)
    item,
    level,
    time,
    timers |
    
    var String timerKey = item.name
    if (timers.containsKey(timerKey)) {
        var Timer timerTmp = timers.get(timerKey)
        if(time == 0) {
            timerTmp.cancel()
            timers.remove(timerKey)
            logInfo("Light Timer Function", timerKey + " Timer cancelled" )
        }
        else {
            timerTmp.reschedule(now.plusSeconds(time.intValue))
            logInfo("Light Timer Function", timerKey + " Motion detected, Timer recheduled for " + time + " more seconds" )
            }
        }
    else {
        if(time != 0) {
            if(level == 0)        sendCommand(item, OFF)
            else if(level == 100) sendCommand(item, ON)
            else                  sendCommand(item, level.intValue)
            logInfo("Light Timer Function", timerKey + " Motion detected, Light set to: " + level)
            timers.put(timerKey, createTimer(now.plusSeconds(time.intValue)) [|
                logInfo("Light Timer Function", timerKey + " Motion over, Light turning OFF" )
                sendCommand(item, OFF)
                timers.remove(timerKey)])
            }
        }
    true
    ]
	



rule "passage received command" 
when 
     Item Switch_Passage received command 
then 
	
	MakeLightTimer.apply(Switch_Passage, 100, Delay, LightingTimers)


	 
end 

OK figured out myself.

import java.util.Map and all will work fine :slight_smile:

1 Like