Files
SmartThingsPublic/smartapps/smartthings/gentle-wake-up.src/gentle-wake-up.groovy
2015-09-01 14:35:14 -07:00

834 lines
20 KiB
Groovy

/**
* Copyright 2015 SmartThings
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
* Gentle Wake Up
*
* Author: Steve Vlaminck
* Date: 2013-03-11
*
* https://s3.amazonaws.com/smartapp-icons/HealthAndWellness/App-SleepyTime.png
* https://s3.amazonaws.com/smartapp-icons/HealthAndWellness/App-SleepyTime%402x.png
* Gentle Wake Up turns on your lights slowly, allowing you to wake up more
* naturally. Once your lights have reached full brightness, optionally turn on
* more things, or send yourself a text for a more gentle nudge into the waking
* world (you may want to set your normal alarm as a backup plan).
*
*/
definition(
name: "Gentle Wake Up",
namespace: "smartthings",
author: "SmartThings",
description: "Dim your lights up slowly, allowing you to wake up more naturally.",
category: "Health & Wellness",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/HealthAndWellness/App-SleepyTime.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/HealthAndWellness/App-SleepyTime@2x.png"
)
preferences {
page(name: "rootPage")
page(name: "schedulingPage")
page(name: "completionPage")
page(name: "numbersPage")
}
def rootPage() {
dynamicPage(name: "rootPage", title: "", install: true, uninstall: true) {
section {
input(name: "dimmers", type: "capability.switchLevel", title: "Dimmers", description: null, multiple: true, required: true, submitOnChange: true)
}
if (dimmers) {
section {
href(name: "toNumbersPage", page: "numbersPage", title: "Duration & Direction", description: numbersPageHrefDescription(), state: "complete")
}
section {
href(name: "toSchedulingPage", page: "schedulingPage", title: "Rules For Automatically Dimming Your Lights", description: schedulingHrefDescription(), state: schedulingHrefDescription() ? "complete" : "")
}
section {
href(name: "toCompletionPage", title: "Completion Actions (Optional)", page: "completionPage", state: completionHrefDescription() ? "complete" : "", description: completionHrefDescription())
}
section {
// TODO: fancy label
label(title: "Label this SmartApp", required: false, defaultValue: "")
}
}
}
}
def numbersPage() {
dynamicPage(name:"numbersPage", title:"") {
section {
paragraph(name: "pGraph", title: "These lights will dim", fancyDeviceString(dimmers))
}
section {
input(name: "duration", type: "number", title: "For this many minutes", description: "30", required: false, defaultValue: 30)
}
section {
input(name: "startLevel", type: "number", range: "0..99", title: "From this level", defaultValue: defaultStart(), description: "Current Level", required: false, multiple: false)
input(name: "endLevel", type: "number", range: "0..99", title: "To this level", defaultValue: defaultEnd(), description: "Between 0 and 99", required: true, multiple: false)
}
def colorDimmers = dimmersWithSetColorCommand()
if (colorDimmers) {
section {
input(name: "colorize", type: "bool", title: "Gradually change the color of ${fancyDeviceString(colorDimmers)}", description: null, required: false, defaultValue: "true")
}
}
}
}
def defaultStart() {
if (usesOldSettings() && direction && direction == "Down") {
return 99
}
return 0
}
def defaultEnd() {
if (usesOldSettings() && direction && direction == "Down") {
return 0
}
return 99
}
def startLevelLabel() {
if (usesOldSettings()) { // using old settings
if (direction && direction == "Down") { // 99 -> 1
return "99%"
}
return "0%"
}
return hasStartLevel() ? "${startLevel}%" : "Current Level"
}
def endLevelLabel() {
if (usesOldSettings()) {
if (direction && direction == "Down") { // 99 -> 1
return "0%"
}
return "99%"
}
return "${endLevel}%"
}
def schedulingPage() {
dynamicPage(name: "schedulingPage", title: "Rules For Automatically Dimming Your Lights") {
section {
input(name: "days", type: "enum", title: "Allow Automatic Dimming On These Days", description: "Every day", required: false, multiple: true, options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"])
}
section {
input(name: "modeStart", title: "Start when entering this mode", type: "mode", required: false, mutliple: false, submitOnChange: true)
if (modeStart) {
input(name: "modeStop", title: "Stop when leaving '${modeStart}' mode", type: "bool", required: false)
}
}
section {
input(name: "startTime", type: "time", title: "Start Dimming At This Time", description: null, required: false)
}
}
}
def completionPage() {
dynamicPage(name: "completionPage", title: "Completion Rules") {
section("Switches") {
input(name: "completionSwitches", type: "capability.switch", title: "Set these switches", description: null, required: false, multiple: true, submitOnChange: true)
if (completionSwitches || androidClient()) {
input(name: "completionSwitchesState", type: "enum", title: "To", description: null, required: false, multiple: false, options: ["on", "off"], style: "segmented", defaultValue: "on")
input(name: "completionSwitchesLevel", type: "number", title: "Optionally, Set Dimmer Levels To", description: null, required: false, multiple: false, range: "(0..99)")
}
}
section("Notifications") {
input("recipients", "contact", title: "Send notifications to") {
input(name: "completionPhoneNumber", type: "phone", title: "Text This Number", description: "Phone number", required: false)
input(name: "completionPush", type: "bool", title: "Send A Push Notification", description: "Phone number", required: false)
}
input(name: "completionMusicPlayer", type: "capability.musicPlayer", title: "Speak Using This Music Player", required: false)
input(name: "completionMessage", type: "text", title: "With This Message", description: null, required: false)
}
section("Modes and Phrases") {
input(name: "completionMode", type: "mode", title: "Change ${location.name} Mode To", description: null, required: false)
input(name: "completionPhrase", type: "enum", title: "Execute The Phrase", description: null, required: false, multiple: false, options: location.helloHome.getPhrases().label)
}
section("Delay") {
input(name: "completionDelay", type: "number", title: "Delay This Many Minutes Before Executing These Actions", description: "0", required: false)
}
}
}
// ========================================================
// Handlers
// ========================================================
def installed() {
log.debug "Installing 'Gentle Wake Up' with settings: ${settings}"
initialize()
}
def updated() {
log.debug "Updating 'Gentle Wake Up' with settings: ${settings}"
unschedule()
initialize()
}
private initialize() {
stop()
if (startTime) {
log.debug "scheduling dimming routine to run at $startTime"
schedule(startTime, "scheduledStart")
}
// TODO: make this an option
subscribe(app, appHandler)
subscribe(location, locationHandler)
}
def appHandler(evt) {
log.debug "appHandler evt: ${evt.value}"
if (evt.value == "touch") {
if (atomicState.running) {
stop()
} else {
start()
}
}
}
def locationHandler(evt) {
log.debug "locationHandler evt: ${evt.value}"
if (!modeStart) {
return
}
def isSpecifiedMode = (evt.value == modeStart)
def modeStopIsTrue = (modeStop && modeStop != "false")
if (isSpecifiedMode && canStartAutomatically()) {
start()
} else if (!isSpecifiedMode && modeStopIsTrue) {
stop()
}
}
// ========================================================
// Scheduling
// ========================================================
def scheduledStart() {
if (canStartAutomatically()) {
start()
}
}
def start() {
log.trace "START"
setLevelsInState()
atomicState.running = true
atomicState.start = new Date().getTime()
schedule("0 * * * * ?", "healthCheck")
increment()
}
def stop() {
log.trace "STOP"
atomicState.running = false
atomicState.start = 0
unschedule("healthCheck")
}
private healthCheck() {
log.trace "'Gentle Wake Up' healthCheck"
if (!atomicState.running) {
return
}
increment()
}
// ========================================================
// Setting levels
// ========================================================
private increment() {
if (!atomicState.running) {
return
}
def percentComplete = completionPercentage()
if (percentComplete > 99) {
percentComplete = 99
}
updateDimmers(percentComplete)
if (percentComplete < 99) {
def runAgain = stepDuration()
log.debug "Rescheduling to run again in ${runAgain} seconds"
runIn(runAgain, 'increment', [overwrite: true])
} else {
int completionDelay = completionDelaySeconds()
if (completionDelay) {
log.debug "Finished with steps. Scheduling completion for ${completionDelay} second(s) from now"
runIn(completionDelay, 'completion', [overwrite: true])
unschedule("healthCheck")
// don't let the health check start incrementing again while we wait for the delayed execution of completion
} else {
log.debug "Finished with steps. Execution completion"
completion()
}
}
}
def updateDimmers(percentComplete) {
dimmers.each { dimmer ->
def nextLevel = dynamicLevel(dimmer, percentComplete)
if (nextLevel == 0) {
dimmer.off()
} else {
def shouldChangeColors = (colorize && colorize != "false")
def canChangeColors = hasSetColorCommand(dimmer)
log.debug "Setting ${deviceLabel(dimmer)} to ${nextLevel}"
if (shouldChangeColors && canChangeColors) {
dimmer.setColor([hue: getHue(dimmer, nextLevel), saturation: 100, level: nextLevel])
} else {
dimmer.setLevel(nextLevel)
}
}
}
}
int dynamicLevel(dimmer, percentComplete) {
def start = atomicState.startLevels[dimmer.id]
def end = dynamicEndLevel()
if (!percentComplete) {
return start
}
def totalDiff = end - start
def actualPercentage = percentComplete / 100
def percentOfTotalDiff = totalDiff * actualPercentage
(start + percentOfTotalDiff) as int
}
// ========================================================
// Completion
// ========================================================
private completion() {
log.trace "Starting completion block"
if (!atomicState.running) {
return
}
stop()
handleCompletionSwitches()
handleCompletionMessaging()
handleCompletionModesAndPhrases()
}
private handleCompletionSwitches() {
completionSwitches.each { completionSwitch ->
def isDimmer = hasSetLevelCommand(completionSwitch)
if (completionSwitchesLevel && isDimmer) {
completionSwitch.setLevel(completionSwitchesLevel)
} else {
def command = completionSwitchesState ?: "on"
completionSwitch."${command}"()
}
}
}
private handleCompletionMessaging() {
if (completionMessage) {
if (location.contactBookEnabled) {
sendNotificationToContacts(completionMessage, recipients)
} else {
if (completionPhoneNumber) {
sendSms(completionPhoneNumber, completionMessage)
}
if (completionPush) {
sendPush(completionMessage)
}
}
if (completionMusicPlayer) {
speak(completionMessage)
}
}
}
private handleCompletionModesAndPhrases() {
if (completionMode) {
setLocationMode(completionMode)
}
if (completionPhrase) {
location.helloHome.execute(completionPhrase)
}
}
def speak(message) {
def sound = textToSpeech(message)
def soundDuration = (sound.duration as Integer) + 2
log.debug "Playing $sound.uri"
completionMusicPlayer.playTrack(sound.uri)
log.debug "Scheduled resume in $soundDuration sec"
runIn(soundDuration, resumePlaying, [overwrite: true])
}
def resumePlaying() {
log.trace "resumePlaying()"
def sonos = completionMusicPlayer
if (sonos) {
def currentTrack = sonos.currentState("trackData").jsonValue
if (currentTrack.status == "playing") {
sonos.playTrack(currentTrack)
} else {
sonos.setTrack(currentTrack)
}
}
}
// ========================================================
// Helpers
// ========================================================
def setLevelsInState() {
def startLevels = [:]
dimmers.each { dimmer ->
if (usesOldSettings()) {
startLevels[dimmer.id] = defaultStart()
} else if (hasStartLevel()) {
startLevels[dimmer.id] = startLevel
} else {
def dimmerIsOff = dimmer.currentValue("switch") == "off"
startLevels[dimmer.id] = dimmerIsOff ? 0 : dimmer.currentValue("level")
}
}
atomicState.startLevels = startLevels
}
def canStartAutomatically() {
def today = new Date().format("EEEE")
log.debug "today: ${today}, days: ${days}"
if (!days || days.contains(today)) {// if no days, assume every day
return true
}
log.trace "should not run"
return false
}
def completionPercentage() {
log.trace "checkingTime"
if (!atomicState.running) {
return
}
int now = new Date().getTime()
int diff = now - atomicState.start
int totalRunTime = totalRunTimeMillis()
int percentOfRunTime = (diff / totalRunTime) * 100
log.debug "percentOfRunTime: ${percentOfRunTime}"
percentOfRunTime
}
int totalRunTimeMillis() {
int minutes = sanitizeInt(duration, 30)
def seconds = minutes * 60
def millis = seconds * 1000
return millis as int
}
int dynamicEndLevel() {
if (usesOldSettings()) {
if (direction && direction == "Down") {
return 0
}
return 99
}
return endLevel as int
}
def getHue(dimmer, level) {
def start = atomicState.startLevels[dimmer.id] as int
def end = dynamicEndLevel()
if (start > end) {
return getDownHue(level)
} else {
return getUpHue(level)
}
}
def getUpHue(level) {
getBlueHue(level)
}
def getDownHue(level) {
getRedHue(level)
}
private getBlueHue(level) {
if (level < 5) return 72
if (level < 10) return 71
if (level < 15) return 70
if (level < 20) return 69
if (level < 25) return 68
if (level < 30) return 67
if (level < 35) return 66
if (level < 40) return 65
if (level < 45) return 64
if (level < 50) return 63
if (level < 55) return 62
if (level < 60) return 61
if (level < 65) return 60
if (level < 70) return 59
if (level < 75) return 58
if (level < 80) return 57
if (level < 85) return 56
if (level < 90) return 55
if (level < 95) return 54
if (level >= 95) return 53
}
private getRedHue(level) {
if (level < 6) return 1
if (level < 12) return 2
if (level < 18) return 3
if (level < 24) return 4
if (level < 30) return 5
if (level < 36) return 6
if (level < 42) return 7
if (level < 48) return 8
if (level < 54) return 9
if (level < 60) return 10
if (level < 66) return 11
if (level < 72) return 12
if (level < 78) return 13
if (level < 84) return 14
if (level < 90) return 15
if (level < 96) return 16
if (level >= 96) return 17
}
private hasSetLevelCommand(device) {
def isDimmer = false
device.supportedCommands.each {
if (it.name.contains("setLevel")) {
isDimmer = true
}
}
return isDimmer
}
private hasSetColorCommand(device) {
def hasColor = false
device.supportedCommands.each {
if (it.name.contains("setColor")) {
hasColor = true
}
}
return hasColor
}
private dimmersWithSetColorCommand() {
def colorDimmers = []
dimmers.each { dimmer ->
if (hasSetColorCommand(dimmer)) {
colorDimmers << dimmer
}
}
return colorDimmers
}
private int sanitizeInt(i, int defaultValue = 0) {
try {
if (!i) {
return defaultValue
} else {
return i as int
}
}
catch (Exception e) {
log.debug e
return defaultValue
}
}
private completionDelaySeconds() {
int completionDelayMinutes = sanitizeInt(completionDelay)
int completionDelaySeconds = (completionDelayMinutes * 60)
return completionDelaySeconds ?: 0
}
private stepDuration() {
int minutes = sanitizeInt(duration, 30)
int stepDuration = (minutes * 60) / 100
return stepDuration ?: 1
}
private debug(message) {
log.debug "${message}\nstate: ${state}"
}
public smartThingsDateFormat() { "yyyy-MM-dd'T'HH:mm:ss.SSSZ" }
public humanReadableStartDate() {
new Date().parse(smartThingsDateFormat(), startTime).format("h:mm a", timeZone(startTime))
}
def fancyString(listOfStrings) {
def fancify = { list ->
return list.collect {
def label = it
if (list.size() > 1 && it == list[-1]) {
label = "and ${label}"
}
label
}.join(", ")
}
return fancify(listOfStrings)
}
def fancyDeviceString(devices = []) {
fancyString(devices.collect { deviceLabel(it) })
}
def deviceLabel(device) {
return device.label ?: device.name
}
def schedulingHrefDescription() {
def descriptionParts = []
if (days) {
descriptionParts << "On ${fancyString(days)},"
}
descriptionParts << "${fancyDeviceString(dimmers)} will start dimming"
if (startTime) {
descriptionParts << "at ${humanReadableStartDate()}"
}
if (modeStart) {
if (startTime) {
descriptionParts << "or"
}
descriptionParts << "when ${location.name} enters '${modeStart}' mode"
}
if (descriptionParts.size() <= 1) {
// dimmers will be in the list no matter what. No rules are set if only dimmers are in the list
return null
}
return descriptionParts.join(" ")
}
def completionHrefDescription() {
def descriptionParts = []
def example = "Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '<message>' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to '<mode>'. The phrase '<phrase>' will be executed"
if (completionSwitches) {
def switchesList = []
def dimmersList = []
completionSwitches.each {
def isDimmer = completionSwitchesLevel ? hasSetLevelCommand(it) : false
if (isDimmer) {
dimmersList << deviceLabel(it)
}
if (!isDimmer) {
switchesList << deviceLabel(it)
}
}
if (switchesList) {
descriptionParts << "${fancyString(switchesList)} will be turned ${completionSwitchesState ?: 'on'}."
}
if (dimmersList) {
descriptionParts << "${fancyString(dimmersList)} will be dimmed to ${completionSwitchesLevel}%."
}
}
if (completionMessage && (completionPhoneNumber || completionPush || completionMusicPlayer)) {
def messageParts = []
if (completionMusicPlayer) {
messageParts << "spoken"
}
if (completionPhoneNumber) {
messageParts << "sent as a text"
}
if (completionPush) {
messageParts << "sent as a push notification"
}
descriptionParts << "The message '${completionMessage}' will be ${fancyString(messageParts)}."
}
if (completionMode) {
descriptionParts << "The mode will be changed to '${completionMode}'."
}
if (completionPhrase) {
descriptionParts << "The phrase '${completionPhrase}' will be executed."
}
return descriptionParts.join(" ")
}
def numbersPageHrefDescription() {
def title = "All dimmers will dim for ${duration ?: '30'} minutes from ${startLevelLabel()} to ${endLevelLabel()}"
if (colorize) {
def colorDimmers = dimmersWithSetColorCommand()
if (colorDimmers == dimmers) {
title += " and will gradually change color."
} else {
title += ".\n${fancyDeviceString(colorDimmers)} will gradually change color."
}
}
return title
}
def hueSatToHex(h, s) {
def convertedRGB = hslToRgb(h, s, 0.5)
return rgbToHex(convertedRGB)
}
def hslToRgb(h, s, l) {
def r, g, b;
if (s == 0) {
r = g = b = l; // achromatic
} else {
def hue2rgb = { p, q, t ->
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
}
def q = l < 0.5 ? l * (1 + s) : l + s - l * s;
def p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return [r * 255, g * 255, b * 255];
}
def rgbToHex(red, green, blue) {
def toHex = {
int n = it as int;
n = Math.max(0, Math.min(n, 255));
def hexOptions = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"]
def firstDecimal = ((n - n % 16) / 16) as int
def secondDecimal = (n % 16) as int
return "${hexOptions[firstDecimal]}${hexOptions[secondDecimal]}"
}
def rgbToHex = { r, g, b ->
return toHex(r) + toHex(g) + toHex(b)
}
return rgbToHex(red, green, blue)
}
def usesOldSettings() {
!hasEndLevel()
}
def hasStartLevel() {
return (startLevel != null && startLevel != "")
}
def hasEndLevel() {
return (endLevel != null && endLevel != "")
}