edit: I’ve updated the code to show the final form of what I have in my rules file, having to work around limitations of the language caused it to diverge from my original idea, but it works well and makes the rest of my rules far more maintainable and readable.
I have been annoyed with how setting up timers for turning off lights is done in OH. Mainly for two reasons;
having two different triggers in different .rules files (front porch lights on door open and also when a new Presence is detected), the timers can step on each other and if you ever edit a file, the timers that are saved as global variables in the .rules files are lost and the logs are filled with errors once they expire.
I’m experimenting with a new timeout pattern and thought I would share it for discussion. The approach is this:
Create new items to match each of your Switch and Dimmer items, prepending Timer_ to the name.
Group gLightTimers
Number Timer_SwitchFrontFloods_Switch (gLightTimers) // matches SwitchFrontFloods_Switch
Number Timer_SwitchDownstairsHall_Dimmer (gLightTimers) // matches SwitchDownstairsHall_Dimmer
...
create a new .rules file and add a big old ugly “function” at the top:
import java.util.HashMap
var HashMap<String, Pair<DateTime, Timer>> SwitchTimers = newHashMap()
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
val Functions$Function2<GenericItem, HashMap<String, Pair<DateTime, Timer>>, DateTime> doTimer=[GenericItem lightTimeoutItem, HashMap<String, Pair<DateTime, Timer>> SwitchTimers |
val DateTime lightTimeout = new DateTime((lightTimeoutItem.state as DateTimeType).calendar.timeInMillis)
val String switchName = lightTimeoutItem.name.substring(6)
val Pair<DateTime, Timer> existingEntry = SwitchTimers.get(switchName)
var Timer currentTimer = null
var DateTime currtimeoutTime
// if there is an existing timer item, load its values if it is still valid
if(existingEntry != null)
{
currtimeoutTime = existingEntry.key
if(currtimeoutTime.isAfter(now))
{
currentTimer = existingEntry.value
}
}
// if the timer item is for the future
if(lightTimeout.isAfter(now))
{
val lightSwitch = gLights.allMembers.findFirst[name.equals(switchName)]
// make sure that it is valid to turn the light on and set a new timer (if there isn't a valid current timer or this timer is later than the previous one, or the light is off)
if(currentTimer == null || currentTimer.hasTerminated || lightTimeout.isAfter(currtimeoutTime) || lightSwitch.state == OFF || lightSwitch.state == 0)
{
// cheat here, pull the dimmer value out of the timer millisecond values
var dimVal = lightTimeout.getMillisOfSecond()
// cancel any current timer
if(currentTimer != null && !currentTimer.hasTerminated)
{
currentTimer.cancel()
}
// turn on the light if the dimVal is within an acceptable range
if(switchName.contains("_Dimmer"))
{
if(dimVal > 0 && dimVal < 100)
{
logInfo("Illumination", "Turning on " + switchName + " to " + dimVal + "%")
lightSwitch.sendCommand(dimVal)
}
}
else
{
// if dimVal is 0, don't actually turn on the light
if(dimVal > 0)
{
logInfo("Illumination", "Turning on " + switchName)
lightSwitch.sendCommand(ON)
}
}
// create a new timer and load it into the hashmap
// have to replace the millisOfSecond with the actual value, as creating two timers at the exact same time actually causes a problem with the framework
SwitchTimers.put(switchName, new Pair(lightTimeout, createTimer(lightTimeout.withMillisOfSecond(now.getMillisOfSecond())) [|
logInfo("Illumination", "Timer Turning off " + switchName)
lightSwitch.sendCommand(OFF)
]))
}
}
else if(existingEntry != null && currentTimer != null && !currentTimer.hasTerminated)
{
// if the value is in the past, just kill the current timer
currentTimer.cancel()
}
return lightTimeout
]
then, unfortunately, create a rule for every Timer_ item:
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
rule "Light Timeout Timer_SwitchFrontFloods_Switch"
when
Item Timer_SwitchFrontFloods_Switch changed
then
doTimer.apply(Timer_SwitchFrontFloods_Switch, SwitchTimers)
end
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
rule "Light Timeout Timer_SwitchDownstairsHall_Dimmer"
when
Item Timer_SwitchDownstairsHall_Dimmer changed
then
doTimer.apply(Timer_SwitchDownstairsHall_Dimmer, SwitchTimers)
end
//etc, etc ( I have 35 light switches, so its a lot of copy/paste)
now, wherever you used to check if a timer exists, then cancel it, then send a command to an item, then create a timer with a closure to turn it off after a set time, you can replace with this:
Timer_SwitchFrontFloods_Switch.postUpdate(new DateTimeType(now.plusMinutes(15)).toString)
or for dimmers (this is slightly messy as I want to send the level instead of just assuming OFF/ON, so I use the millisOfSecond value to store the dimmer level so the actual timeout will be +- 1second). You could use a second item for this as well.
Timer_SwitchDownstairsHall_Dimmer.postUpdate(new DateTimeType((now.plusMinutes(15).withMillisOfSecond(50)).toString)) // sets the level to 50
you can also cancel an existing timer (useful for motion sensor rules) by updating the timer to a value in the past
Timer_SwitchFrontFloods_Switch.postUpdate(new DateTimeType(now.minusMinutes(1)).toString)
Finally, I’ve found in my motion rules that being able to update the timer without turning the light on is useful so if the level is 0 for either a dimmer or switch item, the timer will be created but the light won’t be turned on
Timer_SwitchDownstairsHall_Dimmer.postUpdate(new DateTimeType((now.plusMinutes(15).withMillisOfSecond(0)).toString))
This code will track the timeouts in a central place so there is no more conflict between timers. It also ensures that if multiple commands come in for a single light switch, the latest timeout will be used. If you never touch this .rules file, then you can edit the rest of your rules without breaking all the running timers.
It would be ideal if a method that encompasses all this could be integrated into the framework, something like:
sendCommandWithTimeout(, state, timeout, timeoutstate)