Skip to content

Instantly share code, notes, and snippets.

@asciipip
Created December 14, 2015 02:25
Show Gist options
  • Save asciipip/ea07028b5c8ef955d56a to your computer and use it in GitHub Desktop.
Save asciipip/ea07028b5c8ef955d56a to your computer and use it in GitHub Desktop.
OpenHAB Alarm Definition

This defines a rule that allows for a "sunrise" style alarm. Basically, you trigger it by setting the Alarm_Trigger item to a value that defines parameters for the alarm:

  • duration gives the length of the alarm in minutes
  • light gives the minimum and maximum brightness values over the course of the alarm, e.g. light=0,75.
  • temp gives the range of color temperatures over the course of the alarm, e.g. temp=2700,5000

Add dimmers to the Alarm_Dimmers group. Add Number items for color temperature to the Alarm_Temps group. Any switches in the Alarm_Switches group will be turned on at the end of the alarm.

The Alarm_Running item serves as a UI element to halt a running alarm. Just turn it OFF while the alarm is running,

Group Alarms (All)
Group:Switch:OR(ON,OFF) Alarm_Switches (Alarms)
Group:Dimmer:MIN() Alarm_Dimmers (Alarms)
Group:Number:MIN() Alarm_Temps (Alarm)
String Alarm_Trigger (Alarms)
Switch Alarm_Running (Alarms)
import java.util.concurrent.locks.ReentrantLock
import org.joda.time.*
import org.openhab.core.library.types.*
// The Alarm_Running item is our UI trigger for stopping an alarm run early.
// There are certain possible race conditions between it being changed and the
// alarm rule noticing the change. The following variable is the authoritative
// reference on whether there's currently an alarm running. Access to it is
// controlled by the mutex that follows it.
var boolean alarm_really_running = false
val ReentrantLock alarm_lock = new ReentrantLock()
// The Alarm_Running switch exists to allow people to terminate an alarm run
// before it finishes its full cycle. The only really valid UI change, then,
// is changing the switch's value from ON to OFF. The following rule catches
// all transitions in the opposite direction (OFF to ON) and resets them if they
// don't match the actual alarm state.
rule "Ignore UI Alarm_Running Activation"
when
Item Alarm_Running changed from OFF to ON
then
alarm_lock.lock()
try {
if (!alarm_really_running) {
logInfo('alarm', 'Alarm_Running was turned on without an alarm actually running. Turning it back off...')
Alarm_Running.postUpdate(OFF)
}
} finally {
alarm_lock.unlock()
}
end
rule "Alarm"
when
Item Alarm_Trigger received update
then
// Ignore updates that simply clear the parameters.
if (Alarm_Trigger.state == Uninitialized) { return true }
// Check to see if there's already an alarm running. If so, ignore this
// request.
alarm_lock.lock()
try {
if (alarm_really_running) {
logWarn('alarm', 'Alarm already running. Ignoring new invocation: {}', Alarm_Trigger.state)
Alarm_Trigger.postUpdate(Uninitialized)
return false
} else {
alarm_really_running = true
Alarm_Running.postUpdate(ON)
logInfo('alarm', 'Alarm triggered. params: {}', Alarm_Trigger.state)
}
} finally {
alarm_lock.unlock()
}
val time_start = DateTimeUtils::currentTimeMillis()
// defaults
var duration = 0
var light_start = 0
var light_end = 99
var temp_start = 0
var temp_end = 0
// Pull the parameters out of Alarm_Trigger and then clear it for future use.
val params = Alarm_Trigger.state.toString.split(';')
Alarm_Trigger.postUpdate(Uninitialized)
// We can't exit this rule from within the forEach statement, because we're
// really passing the statement lambda, and `return` just exits that lambda.
// The `error` variable allows us to flag errors and exit properly once the
// lambda returns, if that's what we need to do. It also lets us
// consolidate some resource cleanup code.
var error = false
params.forEach[ p |
logDebug('alarm', 'param: ' + p)
val kv = p.split('=')
if (kv.size() != 2) {
error = true
logError('alarm', 'alarm parameter is not in k=v format: {}', p.toString)
return false
}
switch kv.get(0) {
case 'duration': duration = Integer::parseInt(kv.get(1))
case 'light' : {
val ls = kv.get(1).split(',')
if (ls.size() != 2) {
error = true
logError('alarm', '"light" parameter is not a pair of values: {}', p.toString)
return false
}
light_start = Math::max( 0, new Integer(ls.get(0)))
light_end = Math::min(99, new Integer(ls.get(1)))
val ts = kv.get(1).split(',')
if (ts.size() != 2) {
error = true
logError('alarm', '"temp" parameter is not a pair of values: {}', p.toString)
return false
}
temp_start = new Integer(ts.get(0))
temp_end = new Integer(ts.get(1))
}
}
]
if (!error && duration <= 0) {
logError('alarm', 'Duration must be greater than zero: {}', duration.toString)
error = true
}
if (error) {
alarm_lock.lock()
try {
logDebug('alarm', 'Disabling Alarm_Running because of error.')
Alarm_Running.postUpdate(OFF)
alarm_really_running = false
} finally {
alarm_lock.unlock()
}
return false
}
// time_start and time_end are in milliseconds. duration is in minutes.
val time_end = time_start + duration * 60 * 1000
logDebug('alarm', 'time parameters: start: {}; end: {}; now: {}', time_start, time_end, DateTimeUtils::currentTimeMillis())
// Standard linear equation is `y = mx + b`. `x` is time. `y` is
// brightness, color, etc. `m` is slope. `b` is the y intercept.
val light_slope = new Double(light_end - light_start) / (time_end - time_start)
val light_intercept = light_start - time_start * (new Double(light_end - light_start) / (time_end - time_start))
logDebug('alarm', 'light parameters: start: {}; end: {}; slope: {}; intercept: {}', light_start, light_end, light_slope, light_intercept)
val temp_slope = new Double(temp_end - temp_start) / (time_end - time_start)
val temp_intercept = temp_start - time_start * (new Double(temp_end - temp_start) / (time_end - time_start))
logDebug('alarm', 'temp parameters: start: {}; end: {}; slope: {}; intercept: {}', temp_start, temp_end, temp_slope, temp_intercept)
// Here's the main loop of the alarm.
do {
val light_target = Math::min(light_end, (light_slope * DateTimeUtils::currentTimeMillis() + light_intercept).longValue())
logDebug('alarm', 'Setting dimmers to {}', light_target)
Alarm_Dimmers.sendCommand(light_target)
if (temp_start < temp_end) {
val temp_target = Math::min(temp_end, (temp_slope * DateTimeUtils::currentTimeMillis() + temp_intercept).longValue())
logDebug('alarm', 'Setting color temperatures to {}', temp_target)
Alarm_Temps.sendCommand(temp_target)
}
// See what time it will be when we next need to increment the brightness level. x = (y - b) / m
val light_next = Math::min(light_target + 1, light_end)
val increment_time = ((light_next - light_intercept) / light_slope).longValue()
val sleep_interval = increment_time - DateTimeUtils::currentTimeMillis()
logDebug('alarm', 'Need to sleep {}ms for brightness {}', sleep_interval, light_next)
if (sleep_interval > 0) {
Thread::sleep(sleep_interval)
}
} while (DateTimeUtils::currentTimeMillis() < time_end && Alarm_Running.state == ON) {
// As long as we weren't interrupted, make sure everything hit its max
// value.
if (Alarm_Running.state == ON) {
logDebug('alarm', 'finishing dimmers')
Alarm_Dimmers.sendCommand(light_end)
if (temp_start < temp_end) {
logDebug('alarm', 'finishing temps')
Alarm_Temps.sendCommand(temp_end)
}
logDebug('alarm', 'finishing switches')
Alarm_Switches.sendCommand(ON)
logDebug('alarm', 'finished')
}
// We're done!
alarm_lock.lock()
try {
if (Alarm_Running.state == ON) {
logDebug('alarm', 'Disabling Alarm_Running because execution has finished.')
} else {
logInfo('alarm', 'Terminating alarm because Alarm_Running was toggled off.')
}
Alarm_Running.postUpdate(OFF)
alarm_really_running = false
} finally {
alarm_lock.unlock()
}
return true
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment