|
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 |