Problem Statement
You have an item you want to send a command to after a delay, but if there’s another rule or user interaction with that item in the meantime, cancel the automated action.
For example, open a blind at the front of the house 15 minutes after dawn could be achieved with:
rule "Open blind after dawn"
when
Item DayNight changed from NIGHT to DAY
then
createTimer(now.plusMinutes(15), [|
LoungeBlind.sendCommand(UP)
])
end
However, if the “close blinds when playing a game” activity has triggered (becaused someone got up to play a game), you don’t want the blind to open.
Concept
Have a proxy item used to create the timer, and a generic rule that manages a map of timers. Both the proxy item and the target item are part of a group: when a member of the group is changed, the rule can cancel the timer.
Solution
In this example, the group gDeferredAction
has been defined.
import org.openhab.model.script.actions.Timer
import org.eclipse.smarthome.core.library.items.StringItem
import org.eclipse.smarthome.model.script.ScriptServiceUtil
import java.util.LinkedHashMap
import java.util.Map
import java.util.regex.Pattern
import java.util.regex.Matcher
import org.joda.time.Period
/**
* Store the timers.
*/
val Map<String, Timer> timers = new LinkedHashMap()
/**
* Offset time.
*/
val Pattern timeOffset = Pattern.compile("^(?i)\\+((?:\\d+[hms])+)->(.*)")
/**
* Specific time.
*/
val Pattern timeSpecific = Pattern.compile("^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2})->(.*)")
/**
* Set a timer. Assumes the triggering item will be named
* `TARGET_Timer`. The format is `TIME->COMMAND`. `TIME`
* can be relative ("+1h5m30s") or absolute in ISO8601 format.
*/
rule "Set timer"
when
Member of gDeferredAction changed
then
if (triggeringItem instanceof StringItem && triggeringItem.state != "") {
val String target = triggeringItem.name.replaceAll("(?i)_Timer$", "")
/* Find the target item, checking it exists. There's no need for specific
* error handling: if corresponding item isn't found as the exception is
* handled well by OpenHAB.
*/
val GenericItem targetItem = ScriptServiceUtil.getItemRegistry?.getItem(target) as GenericItem
// -- Parse the time...
//
var DateTime triggerTime
var Matcher matcher
matcher = timeOffset.matcher(triggeringItem.state.toString())
if (matcher.matches()) {
triggerTime = now.plus(Period.parse("PT" + matcher.group(1).toUpperCase()))
} else {
matcher = timeSpecific.matcher(triggeringItem.state.toString())
if (matcher.matches()) {
triggerTime = DateTime.parse(matcher.group(1))
} else {
throw new RuntimeException("Invalid timer format [" + triggeringItem.state + "] from " + triggeringItem.name + ", try `+5m->OFF`")
}
}
val String command = matcher.group(matcher.groupCount())
// -- Check target time is in the past...
//
if (triggerTime.isBefore(now))
throw new RuntimeException("Target time " + triggerTime + " from " + triggeringItem.name + " is in the past. Ignoring")
// -- Check for an existing timer, and cancel it...
//
var Timer timer = timers.get(target)
if (timer !== null) {
logInfo("DeferredAction", "Cancelling existing timer [{}]", timer)
timer.cancel()
}
// -- Create timer, and store it...
//
timer = createTimer(triggerTime, [|
logInfo("DeferredAction", "Executing deferred action {} against {}", command, targetItem)
timers.remove(target)
targetItem.sendCommand(command)
])
timers.put(target, timer)
logInfo("DeferredAction", "Send {} to {} at {} through {}", command, targetItem, triggerTime, timer)
triggeringItem.postUpdate("")
}
end
/**
* Remove a timer, if present.
*/
rule "Remove timer"
when
Member of gDeferredAction changed
then
val timer = timers.get(triggeringItem.name)
if (timer !== null) {
logInfo("DeferredAction", "Removing timer for {} [{}]", triggeringItem.name, timer);
timer.cancel()
timers.remove(triggeringItem.name)
}
end
Simple Example
A new string item, LoungeBlind_Timer
, is created and the initial example is now changed to:
rule "Open blind after dawn"
when
Item DayNight changed from NIGHT to DAY
then
LoungeBlind_Timer.sendCommand("+15m->UP")
end