Bathroom fan timer rule

Trying to set a rule that will turn off a bathroom fan after two hours. This is working, kinda, but I notice in the logs that it’s sending the OFF command every two hours. I’m sure it’s the “received update” keeps the timer firing every time it sends the OFF command. I need the rule to set the timer when the fan gets turned on, and reset it if the fan is turned off and then back on inside that two hours. I’ve tried it with just the “changed to ON” part of the when but that doesn’t work if it’s already on. I think I’m explaining that right, been a long day. Anyone see where I can improve this rule?

var Timer master_fan_timer = null val Number Timeout = 7200

rule “Master Bath fan”
when
Item zwave_device_2eebb52b_node37_switch_binary changed to ON or
Item zwave_device_2eebb53b_node37_switch_binary received update
then
if(master_fan_timer === null)
{
master_fan_timer = createTimer(now.plusSeconds(Timeout))[|
sendCommand(zwave_device_2eebb52b_node37_switch_binary, OFF)
master_fan_timer = null
]
}
else
{
master_fan_timer.reschedule(now.plusSeconds(Timeout))
}
end

It doesn’t make sense to trigger a Rule by both a changed and an update on the same Item. For example, in this case the Rule will be triggered twice when it changes to ON, once for the change and once for the update, since a change is an update.

To get the behavior you are after I think all you need to do is remove the received update trigger. The Rule will trigger when ever the Item changes to ON and create the Timer. If you turn off the Switch then turn it on while the Timer exists, it will be rescheduled.

In the future, please How to use code fences for posts containing configs, rules, or logs.

My bathroom fan timer looks like this:

Switch Light_GF_Bath           // Light switched
Switch Motor_GF_Bath           // Fan Switch
Number Motor_GF_Bath_afterrun  // How long should the Fan work after switching light off

var long StartTime_Bath = 0
var long runtime_Bath = 0
var long StopTime_Bath = 0 
var Timer VentiTimeOn = null
var Timer VentiTimeOff = null

rule "Ventilator Bad"
when
	Item Light_GF_Bath received command
then
	if(receivedCommand==ON) {
		if (Motor_GF_Bath.state==OFF) {
			// first ON command, so create a timer to turn the light off again
			StopTime_Bath = 0
			StartTime_Bath = now.millis
			logDebug("Nachlauf","Light ON @ {}", StartTime_Bath)
			if (VentiTimeOn!=null) {
				VentiTimeOn.cancel
				VentiTimeOn = null
			}
			VentiTimeOn = createTimer(now.plusSeconds(10), [|
				sendCommand(Motor_GF_Bath, ON)
			])
		}
	} 
	else if(receivedCommand==OFF) {
		// remove any previously scheduled timer
		if(VentiTimeOn!=null) {
			logDebug("Nachlauf","Light OFF @ {}", now.millis)
			VentiTimeOn.cancel
			VentiTimeOn = null
		}	
		logDebug("Nachlauf","Motor_GF_Bath = {}", Motor_GF_Bath.state)
		if(Motor_GF_Bath.state==ON) {
			runtime_Bath = (now.millis - StartTime_Bath)
			logDebug("Nachlauf","runtime_Bath = {} msec", runtime_Bath)
			logDebug("Nachlauf","StartTime_Bath = {} ; runtime_Bath = {} msec", StartTime_Bath, runtime_Bath)
			if (runtime_Bath > 900000)
				Motor_GF_Bath_afterrun.postUpdate(900)
			else if (runtime_Bath > 300000)
				Motor_GF_Bath_afterrun.postUpdate(300)
			else if (runtime_Bath > 120000)
				Motor_GF_Bath_afterrun.postUpdate(120)
			else 
				Motor_GF_Bath_afterrun.postUpdate(0)
		}
		StartTime_Bath = 0
	}
end

rule "Nachlauf"
when
	Item Motor_GF_Bath_afterrun received update 
then 
	logDebug("Nachlauf","Nachlaufzeit = " + Motor_GF_Bath_afterrun.state.toString + " sec")
	sendCommand(Motor_GF_Bath, ON)
	var myStop = now.millis + 1000 * (Motor_GF_Bath_afterrun.state as DecimalType).intValue 
	if (myStop > StopTime_Bath) {
		logDebug("Nachlauf","neuer Nachlauf länger als alter Nachlauf")
		logDebug("Nachlauf","Stoptime_Bath = {} ; myStop = {}", StopTime_Bath, myStop)
		if (VentiTimeOff!=null) {
			VentiTimeOff.cancel
			VentiTimeOff = null
		}
		StopTime_Bath = myStop
		VentiTimeOff = createTimer(now.plusSeconds((Motor_GF_Bath_afterrun.state as DecimalType).intValue), [|
				if (Light_GF_Bath.state==OFF)
					sendCommand(Motor_GF_Bath, OFF)
				StopTime_Bath = 0
		])
	}
	else { 
		logDebug("Nachlauf","alter Nachlauf länger als neuer Nachlauf")
		logDebug("Nachlauf","Stoptime_Bath = {} ; myStop = {}", StopTime_Bath, myStop)
	}
end

I built this rule when starting with openHAB1.0, so I’m pretty sure I could do it better today :wink: but the rule works as intended.
If light is switched on longer than two minutes, the fan will stay on two minutes after lights off,
if light was switched on longer than five Minutes, the fan will stay on five Minutes after lights of.
If light was switched on for longer than fifteen Minutes, the fan will stay on fifteen minutes after lights off.
If someone switches the lights on while the fan is running, the scheduled timer is cancelled and the new stop time is calculated in consideration of the old stop time.
Let’s say I was showering for 15 Minutes, leaving the bath and someone enters to wash hands (very likely to last less than two minutes), the fan would stop immediately, but the rule knows of the old timer and schedules the old stop time.