Design Pattern: Motion Sensor Timer

Edit: Updates for OH 4

Please see Design Pattern: What is a Design Pattern and How Do I Use Them for details on how to use DPs.

Problem Statement

Many motion detectors only report ON when they detect motion but do not send a corresponding OFF after no motion has been detected after a period of time. In addition, often one wants to control a device like a light for a certain period after the last motion is seen, not after the motion sensor goes to OFF. Therefore one needs to keep a Contact OPEN (or Switch ON) until the desired amount of time after the last motion is detected.

This also applies in any cases where you want to do something when a state is first reached, and then doing something else a certain time after.

This approach works any time one wants something to happen a certain amount of time after the last time that an Item received a command or update. It can be easily adapted to report when a device stops reporting, to avoid flapping (e.g the switch constantly switching back and forth when something is on the edge of detection.

Concept

When the motion detector sends an ON command, create a timer. If subsequent ON commands are received reschedule the timer. When the timer expires call a rule containing the code to execute when there hasn’t been motion events for long enough.

For the examples blow we will assume:

  • MotionSensor is a Switch Item linked to the motion sensor, we assume it does not change to OFF on it’s own
  • Light is a Switch Item linked to the light controlled by the motion sensor

We will turn the light on upon first motion and will turn it off five minutes after the last motion.

Expire Timer

If you have a case where Light is only controlled by the MotionSensor you can use the Expiration Timer metadata on the Light Item. Set it to send command OFF after five minutes. By default it will reschedule itself every time it’s commanded ON.

Now all you need is something to command Light to ON when motion is reported. A simple rule that triggers when MotionSensor receives an ON update and which commands the Light to ON is sufficient.

Five minutes after the last command to Light is received, expire will command the light OFF.

Rule Template

Threshold Alert and Open Reminder [4.0.0.0;4.9.9.9] provides a rule that supports any time you have to “do X when Y hasn’t changed or remains above/below a threshold for a certain amount of time.” I has additional features like do not disturb periods and hysteresis but those are not needed here.

A motion sensor timer is one of these use cases.

First create a rule commands the Light ON when it’s run (we’ll use light-on as the ruleUID) and another rule that commands the Light OFF when it’s run (we’ll use light-off as the ruleUID). Also update MotionSensor to OFF in this second rule. These rules can be written as simple UI rules or using any script language desired.

  1. In MainUI navigate to Add-on Store → Automation and under “Rule Templates” find and add “Threshold Alert and Open Reminder”.
  2. Navigate to Rules → + to create a new rule. Fill in a meaningful ID, name, and description and select “Threshold Alert” as the template to base the rule on.
  3. This will show a form of parameters to fill out.

Fill in the following for the properties. Any property not listed leave at the default.

Property Value
Triggering Group Choose anything here. We’re going to change it later.
Threshold State “ON”
Reschedule Toggled on
Alert Delay “PT5M”
Alert Rule light-off
Initial Alert Rule light-on

Now we need to open the rule that was created and change the trigger. Change the trigger type to “an item’s state is updated” and choose MotionDetector as the Item.

With these settings and changes, the light will be commanded on immediately when MotionSensor changes to ON and after five minutes the light will be commanded off and MotionSensor reset to OFF.

This approach is best when your requirements for the motion sensor may be more complicated (e.g. you want it to not turn on the light during the day) or you are doing something based on a threshold (e.g. turning on a heater based on the current temperature).

Blockly

Trigger the rule when MotionSensor receives an update.

image

If you have lots of motion sensor and light pairs, consider using Timer Manager from openHAB Rules Tools [4.1.0.0;4.9.9.9].

JS Scripting

Trigger the rule when MotionSensor receives an update.

items.Light.sendCommand('ON');
var timer = cache.private.get('timer');
if(timer === null || timer.hasTerminated()) {
  cache.private.put('timer', actions.ScriptExecution.createTimer(time.toZDT('PT5M'), () => {
    items.Light.sendCommand('OFF');
    items.MotionSensor.postUpdate('OFF');
  }
}
else {
  timer.reschedule(time.toZDT('PT5M');
}

Rules DSL

Trigger the rule when MotionSensor receives an update.

Light.sendCommand(ON)
val timer = privateCache.get('timer')
if(timer === null || timer.hasTerminated()) {
    privateCache.put('privateCache', createTimer(now.plusMinutes(5), [ |
        Light.sendCommand(OFF)
        MotionSensor.postUpdate(OFF)
    ])
}

Jython

from core.rules import rule
from core.triggers import when
from core.actions import ScriptExecution
from org.joda.time import DateTime

occupancyTimer = None

@rule("Motion sensor triggered")
@when("Item MotionSensor received update ON")
def motion(event):
    global occupancyTimer
    events.sendCommand("Light", "ON");
    if occupancyTimer is None or occupancyTimer.hasTerminated():
        occupancyTimer = ScriptExecution.createTimer(DateTime.now().plusMinutes(5), lambda: events.sendCommand("MotionSensor", "OFF"))
    else:
        occupancyTimer.reschedule(DateTime.now().plusMinutes(5))

Related Design Patterns

Design Pattern How It’s Used
Design Pattern: Sensor Aggregation The complex example of Sensor Aggregation uses this DP to implement an ant-flapping timer.
[Deprecated] Design Pattern: Debounce A similar DP that involves changes instead of updates.
23 Likes

Question -
Can we use instead

when
   Item gMotionDetectors received update ON

for a more generic rule?
(note, I believe when triggering from a Group we should look for ‘update’ rather than ‘command’
Furthermore, we should bear in mind a group may receive multiple updates for a single “real event”. But in this example, I think repeated execution will have no harmful effect.)

I haven’t tested with it that way yet. My first inclination was to write the rule that way. However, I don’t know what state comes as the update. My suspicion is that the Group’s state comes as the update. So, if for example, you have Group:AND(ON, OFF), which is perfectly reasonable, the updates will be all OFF unless all the switches are ON. And if you leave the Group with default or use OR all the updates will be ON as long as at least one Switch is ON.

Consequently there will be no way to distinguish between ON and OFF updates for a given Switch.

I’m pretty sure it could be made to work but I chose to implement it this slightly less generic way to be less error prone for inexperienced users who may try to use it.

If you get it to work triggering on the Group updates (you will have to trigger on all updates, not just ON) post it here. I will do the same if I ever get to it.

I primarily wrote this as an answer to a question so didn’t spend a whole lot of time on it.

FYI

plusMinutes(timeOutMinutes)

Should be

plusMinutes(timeoutMinutes)

Weirdly I also found the import line is required for it to work in Openhab2 else timers.get throws an exception.

import java.util.Map

Corrected the typo.

That is correct, OH 2 automatically imports all the core openHAB and ESH stuff but not other utilities like java.util.*, etc. The import is there in the second example above.

Ah ok, makes sense.

Forgot to mention you’re also missing some closing brackets I think

timers.put(sw.name, createTimer(now.plusMinutes(timeoutMinutes), [|
                        sw.sendCommand(OFF)
                        timers.put(sw.name, null)
                ]))

Indeed. That error has been corrected as well.

I’m trying the whole day to get a motion sensor timer code running with my fibaro fgms 001 motion sensor and milight bulbs, but I guess I need some help…

This is what I have so far:

.items

	//EZ_Auge
	Number EZ_Lux "Motion Sensor [%.2f Lux]"    <sun>                     (ZwaveEye1) { channel="zwave:device:5b97d7f6:node4:sensor_luminance" }
	Number EZ_Battery "Motion Sensor [%d %%]"      <battery>    (ZwaveEye1)        { channel="zwave:device:5b97d7f6:node4:battery-level" }
	Number EZ_Temp "Motion Sensor [%.1f C]"     <temperature>    (ZwaveEye1)    { channel="zwave:device:5b97d7f6:node4:sensor_temperature" }
	Switch EZ_Motion "Motion Sensor Motion [%s]"           (ZwaveEye1)     { channel="zwave:device:5b97d7f6:node4:sensor_binary" }
	Contact EZ_MotionC "Motion Sensor MotionC [%s]"           (ZwaveEye1)     { channel="zwave:device:5b97d7f6:node4:sensor_binary" }
	Switch EZ_Alarm "Motion Sensor Alarm [%s]"             <fire>   (ZwaveEye1)      { channel="zwave:device:5b97d7f6:node4:alarm_general" }
	Switch EZ_Burglar "Motion Sensor Alarm b [%s]"             <fire>   (ZwaveEye1)      { channel="zwave:device:5b97d7f6:node4:alarm_burglar" }
	Number EZ_Seismic "Motion Sensor Seismic [%f]"                (ZwaveEye1)      { channel="zwave:device:5b97d7f6:node4:sensor_seismicintensity" }

.rules

	//EZ Auge
    var Number counter = 0
    var Number lastCheck = 0

    rule "EZ Auge - Licht An"
    when   
            Item EZ_Motion changed from ON to OFF
    then   
            counter = counter + 1
            //if(EZ_Lux.state < 40) {
            sendCommand(Scene_EZ, 3)
            }
    end

    rule "EZ Auge - Licht Aus"
    when   
            Time cron "0 * * * * ?"
    then   
            if(lastCheck == counter) {
                    counter = 0
                    lastCheck = -1;
                    sendCommand(Scene_EZ, 1)
                    sendCommand(EZ_Motion, OFF)
            } else {
                    lastCheck = counter
            }
    end

	// Scenes EZ
	rule "Scene EZ"
	when
	Item Scene_EZ received command 
	then
	/*Aus*/ if (receivedCommand==1) { 
	/*Aus*/ sendCommand(Scene_ColorSelect_EZ_Deckenlampe, new HSBType(new DecimalType(0),new PercentType(0),new PercentType(0)))
	/*Aus*/ Thread::sleep(500) sendCommand(Scene_ColorSelect_EZ_Buecherregal, new HSBType(new DecimalType(0),new PercentType(0),new PercentType(0)))
	/*Aus*/ Thread::sleep(500) sendCommand(Scene_ColorSelect_EZ_Sideboard, new HSBType(new DecimalType(0),new PercentType(0),new PercentType(0)))
	}
	/*Normal*/ if (receivedCommand==2) { 
	/*Weiss*/ sendCommand(Scene_ColorSelect_EZ_Deckenlampe, new HSBType(new DecimalType(0),new PercentType(0),new PercentType(70)))
	/*Aus*/ Thread::sleep(500) sendCommand(Scene_ColorSelect_EZ_Buecherregal, new HSBType(new DecimalType(0),new PercentType(0),new PercentType(0)))
	/*Aus*/ Thread::sleep(500) sendCommand(Scene_ColorSelect_EZ_Sideboard, new HSBType(new DecimalType(0),new PercentType(0),new PercentType(0)))
	}
	/*Hell*/ if (receivedCommand==3) { 
	/*Weiss*/ sendCommand(Scene_ColorSelect_EZ_Deckenlampe, new HSBType(new DecimalType(0),new PercentType(0),new PercentType(100)))
	/*Weiss*/ Thread::sleep(500) sendCommand(Scene_ColorSelect_EZ_Buecherregal, new HSBType(new DecimalType(0),new PercentType(0),new PercentType(100)))
	/*Weiss*/ Thread::sleep(500) sendCommand(Scene_ColorSelect_EZ_Sideboard, new HSBType(new DecimalType(0),new PercentType(0),new PercentType(100)))
	}
	/*Stimmung*/ if (receivedCommand==4) { 
	/*Lila*/ sendCommand(Scene_ColorSelect_EZ_Deckenlampe, new HSBType(new DecimalType(300),new PercentType(100),new PercentType(100)))
	/*GrĂźn*/ Thread::sleep(500) sendCommand(Scene_ColorSelect_EZ_Buecherregal, new HSBType(new DecimalType(130),new PercentType(100),new PercentType(100)))
	/*TĂźrkis*/ Thread::sleep(500) sendCommand(Scene_ColorSelect_EZ_Sideboard, new HSBType(new DecimalType(190),new PercentType(100),new PercentType(100)))
	}
	/*GrĂźn*/ if (receivedCommand==5) { 
	/*GrĂźn*/ sendCommand(Scene_ColorSelect_EZ_Deckenlampe, new HSBType(new DecimalType(130),new PercentType(100),new PercentType(100)))
	/*GrĂźn*/ Thread::sleep(500) sendCommand(Scene_ColorSelect_EZ_Buecherregal, new HSBType(new DecimalType(130),new PercentType(100),new PercentType(100)))
	/*GrĂźn*/ Thread::sleep(500) sendCommand(Scene_ColorSelect_EZ_Sideboard, new HSBType(new DecimalType(130),new PercentType(100),new PercentType(100)))
	}
	end

The scene rules are working manually with some buttons.

This is probably better posted as a new topic as your implementation has almost nothing to do with the Design Pattern described above.

“EZ Auge - Licht An” has a syntax error. With the if statement commented out the } is incorrect and an error like this could prevent the rest of the rules in this file from being loaded.

Avoid using C style comments (i.e. /*) . I’ve seen some reports of people having problems when using this type of comments. That probably isn’t the case here since you said they work.

The following is what your code does as written. I’m writing this out because based on this code I have no idea what you are trying to do.

  • When EZ_Motion turns OFF one up a counter and send 3 to Scene_EZ.

  • Every second, if lastcheck == counter reset counter and lastCheck, send 1 to Scene_EZ and turn off EZ_Motion (which triggers the EZ Auge - Licht An rule.

  • When Scene_EZ receives a command 3 it sets the color of a few lights with half second sleeps between (it is a lot easier to read if you put each line on its own line).

  • When Scene_EZ receives a command 1 it turns off these lights.

Given that the flow would go something like the following.

  1. EZ Auge - Licht Aus triggers. lastCheck == counter (both start off at 0) so lastcheck is set to -1, Scene_EZ is sent 1, and EZ_Motion is set to OFF.

  2. Scene_EZ 1 runs turning off the lamps.

  3. If EZ_Motion is OFF nothing else happens. If EZ_Motion is ON counter becomes 1 and Scene_EZ 3 runs turning on the lamps.

  4. If EZ_Motion was OFF, when EZ Auge - LichtIf runs again lastCheck != counter so lastCheck gets set to 0. EZ_Motion was ON, one second later EZ Auge - Licht Aus runs again. this time lastCheck != counter so lastCheck gets set to 1 (the current value of counter).

  5. EZ_Motion turns ON and OFF again based on motion. At that point counter is set to 1 or 2 and the lamps are turned on again. Then step 1 runs again.

Honestly this code is unclear and overly complicated and exceptionally difficult to follow, even if it did work. Assuming what you are trying to do is keep a light on for a certain period of time after the last time a motion sensor went off, is there some reason you don’t want to use a Timer like the example at the top of this thread?

Sorry for that, what I trying to do is combining your timer code with the wiki-example I already posted and which can be found here: How to turn on light when motion detected and is dark?

OK thanks, I will avoid this.

I try a, I think very basic thing at home automation: “indoor lights-on at motion detection when it’s dark and lights-off after a given time” so I don’t have to switch on and off lights anymore and won’t forget lights on.

It is a very basic thing but this approach with counters is not a very easy way to do it.

Using the example at the top of the thread…

var Timer ezMotionTimer = null

rule "EZ Auge - Licht Aus"
when
    Item EZ_Motion received command
then
    Scene_EZ.sendCommand(3)

    if(ezMotionTimer != null) {
        ezMotionTimer.reschedule
    }
    else {
        ezMotionTimer = createTimer(now.plusMinutes(5), [|
            Scene_EZ.sendCommand(1)
            ezMotionTimer = null
        }
    }
end

The above will turn on the Lights (i.e. sendCommand 3 to EZ_Scene). Five minutes after the last time the EZ_Motion receives a command the Timer will go off and turn off the lights.

I’ve a few other minor comments about your rule. These are not problems but could help you avoid problems in the future.

It is better to use the sendCommand and postUpdate method on the Item rather than the actions. So you would rewrite:

sendCommand(Scene_ColorSelect_EZ_Deckenlamp, new HSBType(new DecimalType(0),new PercentType(0),new PercentType(70)))

as

Scene_ColorSelect_EZ_Deckenlamp.sendCommand(new HSBType(new DecimalType(0),new PercentType(0),new PercentType(70)))

ColorItems can receive ONOFFType and PercentType commands. So where you set the bulb to HSBType(0,0,0) you can just sendCommand(OFF).

1 Like

Thanks for the hints, I changed my code as you recommend.

But the timer code is still not working.

This is working -> items and sensor are OK

	rule "WZ Auge - Licht"
	when
    Item WZ_Burglar received update
	then
    Scene_WZ.sendCommand(2)
    end

But this does nothing

	var Timer ezMotionTimer = null

	rule "WZ Auge - Licht Timer"
	when
    Item WZ_Burglar received update
	then
    Scene_WZ.sendCommand(3)

    if(ezMotionTimer != null) {
        ezMotionTimer.reschedule
    }
    else {
    	ezMotionTimer = createTimer(now.plusMinutes(1))
    	Scene_WZ.sendCommand(1)
    	ezMotionTimer = null
    	}
    }
	end	
    else {
    	ezMotionTimer = createTimer(now.plusMinutes(1))
    	Scene_WZ.sendCommand(1)
    	ezMotionTimer = null
    	}
    }

Needs to be

    else {
    	ezMotionTimer = createTimer(now.plusMinutes(1)) [ |
    	    Scene_WZ.sendCommand(1)
    	    ezMotionTimer = null
    	]
    }

Thanks very much, but still no reaction

    var Timer ezMotionTimer = null

	rule "Licht Timer"
	when
    Item EZ_Auge_Burglar changed from OFF to ON
	then
    Scene_EZ.sendCommand(3)

    if(ezMotionTimer != null) {
        ezMotionTimer.reschedule
    }
    else {
    	ezMotionTimer = createTimer(now.plusMinutes(1)) [ |
    	    Scene_EZ.sendCommand(1)
    	    ezMotionTimer = null
    	]
    }
	end 

May I have to import some bibs or place the var at the top of the .rules or need some addons or extra services or so?

ezMotionTimer needs to be declared above any rule in that file.

Add logging statements so you know whether the rule is triggering.

That was the problem, now it works great.
Thank you very much :slight_smile:

In this topic I’ll go forward with some more terms: Motion detection → lights on - if it’s dark and within time period, with timer - Help needed

I don´t want to make the variables as global variables.

Can i make this inside the rule as well?

If i put val and var to the top of my rules-file, it works. But if i put it inside the rule, nothing happens…

working:

var Timer md1Timer = null
val int timeoutMinutes = 5

// KG Garage Licht aus nach 5 Minuten

rule "KG Garage Licht auto off"
when
    Item Licht_KG_Garage_1 changed
then
	if (Licht_KG_Garage_1.state == ON) {
			md1Timer = createTimer(now.plusMinutes(timeoutMinutes), [|
				Licht_KG_Garage_1.sendCommand(OFF)
				md1Timer = null
			]
	}
    else {
			md1Timer = null
    }
end

not working:

// KG Garage Licht aus nach 5 Minuten

rule "KG Garage Licht auto off"
when
    Item Licht_KG_Garage_1 changed
then
        var Timer md1Timer = null
        val int timeoutMinutes = 5
	if (Licht_KG_Garage_1.state == ON) {
			md1Timer = createTimer(now.plusMinutes(timeoutMinutes), [|
				Licht_KG_Garage_1.sendCommand(OFF)
				md1Timer = null
			]
	}
    else {
			md1Timer = null
    }
end

By doing this you are defining local vs. global variables. Local in this case means (as in all programming languages) the scope and the existence of this variable is limited to one execution of the code block, being your rule. The Timer variable will be deleted when reaching end and recreated when executing the rule again.

Yes, i know this. I want to have this variable “var Timer md1Timer = null” only inside my rule. The rule will not reach “end” while the timer is running.

And when the timer is “null”, i don´t need it anymore.

But this doesn´t work with “var timer…” inside the rule. No error-message in the log-file. I made some logging and the rule will not get triggered, if i put var inside it.

Do you know the reasen, why it doenst´t work?

I don’t think you understand how Timers work. They do not block the rest of the execution of your Rule. Once created the timer runs in the background while the rest of your Rule continues to execute. So in the above your rule immediately exits once the Timer is created instead of waiting around for the timeout.

This is why you must store the timer variable as a global.

See the following: