2 * Copyright 2016 SmartThings
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at:
7 * http://www.apache.org/licenses/LICENSE-2.0
9 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
10 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
11 * for the specific language governing permissions and limitations under the License.
15 * Author: Steve Vlaminck
18 * https://s3.amazonaws.com/smartapp-icons/HealthAndWellness/App-SleepyTime.png
19 * https://s3.amazonaws.com/smartapp-icons/HealthAndWellness/App-SleepyTime%402x.png
20 * Gentle Wake Up turns on your lights slowly, allowing you to wake up more
21 * naturally. Once your lights have reached full brightness, optionally turn on
22 * more things, or send yourself a text for a more gentle nudge into the waking
23 * world (you may want to set your normal alarm as a backup plan).
27 name: "Gentle Wake Up",
28 namespace: "smartthings",
29 author: "SmartThings",
30 description: "Dim your lights up slowly, allowing you to wake up more naturally.",
31 category: "Health & Wellness",
32 iconUrl: "https://s3.amazonaws.com/smartapp-icons/HealthAndWellness/App-SleepyTime.png",
33 iconX2Url: "https://s3.amazonaws.com/smartapp-icons/HealthAndWellness/App-SleepyTime@2x.png"
37 page(name: "rootPage")
38 page(name: "schedulingPage")
39 page(name: "completionPage")
40 page(name: "numbersPage")
41 page(name: "controllerExplanationPage")
42 page(name: "unsupportedDevicesPage")
46 dynamicPage(name: "rootPage", title: "", install: true, uninstall: true) {
48 section("What to dim") {
49 input(name: "dimmers", type: "capability.switchLevel", title: "Dimmers", description: null, multiple: true, required: true, submitOnChange: true)
51 if (dimmersContainUnsupportedDevices()) {
52 href(name: "toUnsupportedDevicesPage", page: "unsupportedDevicesPage", title: "Some of your selected dimmers don't seem to be supported", description: "Tap here to fix it", required: true)
54 href(name: "toNumbersPage", page: "numbersPage", title: "Duration & Direction", description: numbersPageHrefDescription(), state: "complete")
60 section("Gentle Wake Up Has A Controller") {
61 href(title: "Learn how to control Gentle Wake Up", page: "controllerExplanationPage", description: null)
64 section("Rules For Dimming") {
65 href(name: "toSchedulingPage", page: "schedulingPage", title: "Automation", description: schedulingHrefDescription() ?: "Set rules for when to start", state: schedulingHrefDescription() ? "complete" : "")
66 input(name: "manualOverride", type: "enum", options: ["cancel": "Cancel dimming", "jumpTo": "Jump to the end"], title: "When one of the dimmers is manually turned off…", description: "dimming will continue", required: false, multiple: false)
67 href(name: "toCompletionPage", title: "Completion Actions", page: "completionPage", state: completionHrefDescription() ? "complete" : "", description: completionHrefDescription() ?: "Set rules for what to do when dimming completes")
72 label(title: "Label This SmartApp", required: false, defaultValue: "", description: "Highly recommended", submitOnChange: true)
78 def unsupportedDevicesPage() {
80 def unsupportedDimmers = dimmers.findAll { !hasSetLevelCommand(it) }
82 dynamicPage(name: "unsupportedDevicesPage") {
83 if (unsupportedDimmers) {
84 section("These devices do not support the setLevel command") {
85 unsupportedDimmers.each {
86 paragraph deviceLabel(it)
90 input(name: "dimmers", type: "capability.sensor", title: "Please remove the above devices from this list.", submitOnChange: true, multiple: true)
93 paragraph "If you think there is a mistake here, please contact support."
97 paragraph "You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)"
103 def controllerExplanationPage() {
104 dynamicPage(name: "controllerExplanationPage", title: "How To Control Gentle Wake Up") {
106 section("With other SmartApps", hideable: true, hidden: false) {
107 paragraph "When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!"
108 paragraph "The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!"
109 paragraph "Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up."
112 section("More about the controller", hideable: true, hidden: true) {
113 paragraph "You can find the controller with your other 'Things'. It will look like this."
114 image "http://f.cl.ly/items/2O0v0h41301U14042z3i/GentleWakeUpController-tile-stopped.png"
115 paragraph "You can start and stop Gentle Wake up by tapping the control on the right."
116 image "http://f.cl.ly/items/3W323J3M1b3K0k0V3X3a/GentleWakeUpController-tile-running.png"
117 paragraph "If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls."
118 image "http://f.cl.ly/items/291s3z2I2Q0r2q0x171H/GentleWakeUpController-richTile-stopped.png"
119 paragraph "The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep."
120 image "http://f.cl.ly/items/0F0N2G0S3v1q0L0R3J3Y/GentleWakeUpController-richTile-running.png"
121 paragraph "In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle."
122 paragraph "Of course, you may also tap the middle to start or stop the dimming cycle at any time."
125 section("Starting and stopping the SmartApp itself", hideable: true, hidden: true) {
126 paragraph "Tap the 'play' button on the SmartApp to start or stop dimming."
127 image "http://f.cl.ly/items/0R2u1Z2H30393z2I2V3S/GentleWakeUp-appTouch2.png"
130 section("Turning off devices while dimming", hideable: true, hidden: true) {
131 paragraph "It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option."
132 paragraph "If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings."
133 paragraph "Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop."
134 paragraph "That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)"
140 dynamicPage(name:"numbersPage", title:"") {
143 paragraph(name: "pGraph", title: "These lights will dim", fancyDeviceString(dimmers))
147 input(name: "duration", type: "number", title: "For this many minutes", description: "30", required: false, defaultValue: 30)
151 input(name: "startLevel", type: "number", range: "0..99", title: "From this level", defaultValue: defaultStart(), description: "Current Level", required: false, multiple: false)
152 input(name: "endLevel", type: "number", range: "0..99", title: "To this level", defaultValue: defaultEnd(), description: "Between 0 and 99", required: true, multiple: false)
155 def colorDimmers = dimmersWithSetColorCommand()
158 input(name: "colorize", type: "bool", title: "Gradually change the color of ${fancyDeviceString(colorDimmers)}", description: null, required: false, defaultValue: "true")
165 if (usesOldSettings() && direction && direction == "Down") {
172 if (usesOldSettings() && direction && direction == "Down") {
178 def startLevelLabel() {
179 if (usesOldSettings()) { // using old settings
180 if (direction && direction == "Down") { // 99 -> 1
185 return hasStartLevel() ? "${startLevel}%" : "Current Level"
188 def endLevelLabel() {
189 if (usesOldSettings()) {
190 if (direction && direction == "Down") { // 99 -> 1
195 return "${endLevel}%"
199 ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
203 ["Saturday", "Sunday"]
206 def schedulingPage() {
207 dynamicPage(name: "schedulingPage", title: "Rules For Automatically Dimming Your Lights") {
209 section("Use Other SmartApps!") {
210 href(title: "Learn how to control Gentle Wake Up", page: "controllerExplanationPage", description: null)
213 section("Allow Automatic Dimming") {
214 input(name: "days", type: "enum", title: "On These Days", description: "Every day", required: false, multiple: true, options: weekdays() + weekends())
217 section("Start Dimming...") {
218 input(name: "startTime", type: "time", title: "At This Time", description: null, required: false)
219 input(name: "modeStart", title: "When Entering This Mode", type: "mode", required: false, mutliple: false, submitOnChange: true, description: null)
221 input(name: "modeStop", title: "Stop when leaving '${modeStart}' mode", type: "bool", required: false)
228 def completionPage() {
229 dynamicPage(name: "completionPage", title: "Completion Rules") {
231 section("Switches") {
232 input(name: "completionSwitches", type: "capability.switch", title: "Set these switches", description: null, required: false, multiple: true, submitOnChange: true)
233 if (completionSwitches) {
234 input(name: "completionSwitchesState", type: "enum", title: "To", description: null, required: false, multiple: false, options: ["on", "off"], defaultValue: "on")
235 input(name: "completionSwitchesLevel", type: "number", title: "Optionally, Set Dimmer Levels To", description: null, required: false, multiple: false, range: "(0..99)")
239 section("Notifications") {
240 input("recipients", "contact", title: "Send notifications to", required: false) {
241 input(name: "completionPhoneNumber", type: "phone", title: "Text This Number", description: "Phone number", required: false)
242 input(name: "completionPush", type: "bool", title: "Send A Push Notification", description: "Phone number", required: false)
244 input(name: "completionMusicPlayer", type: "capability.musicPlayer", title: "Speak Using This Music Player", required: false)
245 input(name: "completionMessage", type: "text", title: "With This Message", description: null, required: false)
248 section("Modes and Phrases") {
249 input(name: "completionMode", type: "mode", title: "Change ${location.name} Mode To", description: null, required: false)
250 input(name: "completionPhrase", type: "enum", title: "Execute The Phrase", description: null, required: false, multiple: false, options: location.helloHome.getPhrases().label)
254 input(name: "completionDelay", type: "number", title: "Delay This Many Minutes Before Executing These Actions", description: "0", required: false)
259 // ========================================================
261 // ========================================================
264 log.debug "Installing 'Gentle Wake Up' with settings: ${settings}"
270 log.debug "Updating 'Gentle Wake Up' with settings: ${settings}"
273 def controller = getController()
275 controller.label = app.label
281 private initialize() {
282 stop("settingsChange")
285 log.debug "scheduling dimming routine to run at $startTime"
286 schedule(startTime, "scheduledStart")
289 // TODO: make this an option
290 subscribe(app, appHandler)
292 subscribe(location, locationHandler)
294 if (manualOverride) {
295 subscribe(dimmers, "switch.off", stopDimmersHandler)
298 if (!getAllChildDevices()) {
299 // create controller device and set name to the label used here
300 def dni = "${new Date().getTime()}"
301 log.debug "app.label: ${app.label}"
302 addChildDevice("smartthings", "Gentle Wake Up Controller", dni, null, ["label": app.label])
303 state.controllerDni = dni
307 def appHandler(evt) {
308 log.debug "appHandler evt: ${evt.value}"
309 if (evt.value == "touch") {
310 if (atomicState.running) {
318 def locationHandler(evt) {
319 log.debug "locationHandler evt: ${evt.value}"
325 def isSpecifiedMode = (evt.value == modeStart)
326 def modeStopIsTrue = (modeStop && modeStop != "false")
328 if (isSpecifiedMode && canStartAutomatically()) {
330 } else if (!isSpecifiedMode && modeStopIsTrue) {
336 def stopDimmersHandler(evt) {
337 log.trace "stopDimmersHandler evt: ${evt.value}"
338 def percentComplete = completionPercentage()
339 // Often times, the first thing we do is turn lights on or off so make sure we don't stop as soon as we start
340 if (percentComplete > 2 && percentComplete < 98) {
341 if (manualOverride == "cancel") {
342 log.debug "STOPPING in stopDimmersHandler"
343 stop("manualOverride")
344 } else if (manualOverride == "jumpTo") {
345 def end = dynamicEndLevel()
346 log.debug "Jumping to 99% complete in stopDimmersHandler"
351 log.debug "not stopping in stopDimmersHandler"
355 // ========================================================
357 // ========================================================
359 def scheduledStart() {
360 if (canStartAutomatically()) {
365 public def start(source) {
368 sendStartEvent(source)
372 atomicState.running = true
374 atomicState.start = new Date().getTime()
376 schedule("0 * * * * ?", "healthCheck")
380 public def stop(source) {
383 sendStopEvent(source)
385 atomicState.running = false
386 atomicState.start = 0
388 unschedule("healthCheck")
391 private healthCheck() {
392 log.trace "'Gentle Wake Up' healthCheck"
394 if (!atomicState.running) {
401 // ========================================================
403 // ========================================================
405 def sendStartEvent(source) {
406 log.trace "sendStartEvent(${source})"
408 name: "sessionStatus",
410 descriptionText: "${app.label} has started dimming",
415 if (source == "modeChange") {
416 eventData.descriptionText += " because of a mode change"
417 } else if (source == "schedule") {
418 eventData.descriptionText += " as scheduled"
419 } else if (source == "appTouch") {
420 eventData.descriptionText += " because you pressed play on the app"
421 } else if (source == "controller") {
422 eventData.descriptionText += " because you pressed play on the controller"
425 sendControllerEvent(eventData)
428 def sendStopEvent(source) {
429 log.trace "sendStopEvent(${source})"
431 name: "sessionStatus",
433 descriptionText: "${app.label} has stopped dimming",
438 if (source == "modeChange") {
439 eventData.descriptionText += " because of a mode change"
440 eventData.value += "cancelled"
441 } else if (source == "schedule") {
442 eventData.descriptionText = "${app.label} has finished dimming"
443 } else if (source == "appTouch") {
444 eventData.descriptionText += " because you pressed play on the app"
445 eventData.value += "cancelled"
446 } else if (source == "controller") {
447 eventData.descriptionText += " because you pressed stop on the controller"
448 eventData.value += "cancelled"
449 } else if (source == "settingsChange") {
450 eventData.descriptionText += " because the settings have changed"
451 eventData.value += "cancelled"
452 } else if (source == "manualOverride") {
453 eventData.descriptionText += " because the dimmer was manually turned off"
454 eventData.value += "cancelled"
457 // send 100% completion event
458 sendTimeRemainingEvent(100)
460 // send a non-displayed 0% completion to reset tiles
461 sendTimeRemainingEvent(0, false)
463 // send sessionStatus event last so the event feed is ordered properly
464 sendControllerEvent(eventData)
467 def sendTimeRemainingEvent(percentComplete, displayed = true) {
468 log.trace "sendTimeRemainingEvent(${percentComplete})"
470 def percentCompleteEventData = [
471 name: "percentComplete",
472 value: percentComplete as int,
473 displayed: displayed,
476 sendControllerEvent(percentCompleteEventData)
478 def duration = sanitizeInt(duration, 30)
479 def timeRemaining = duration - (duration * (percentComplete / 100))
480 def timeRemainingEventData = [
481 name: "timeRemaining",
482 value: displayableTime(timeRemaining),
483 displayed: displayed,
486 sendControllerEvent(timeRemainingEventData)
489 def sendControllerEvent(eventData) {
490 def controller = getController()
492 controller.controllerEvent(eventData)
496 def getController() {
497 def dni = state.controllerDni
499 log.warn "no controller dni"
502 def controller = getChildDevice(dni)
504 log.warn "no controller"
507 log.debug "controller: ${controller}"
511 // ========================================================
513 // ========================================================
516 private increment() {
518 if (!atomicState.running) {
522 def percentComplete = completionPercentage()
524 if (percentComplete > 99) {
528 updateDimmers(percentComplete)
530 if (percentComplete < 99) {
532 def runAgain = stepDuration()
533 log.debug "Rescheduling to run again in ${runAgain} seconds"
535 runIn(runAgain, 'increment', [overwrite: true])
539 int completionDelay = completionDelaySeconds()
540 if (completionDelay) {
541 log.debug "Finished with steps. Scheduling completion for ${completionDelay} second(s) from now"
542 runIn(completionDelay, 'completion', [overwrite: true])
543 unschedule("healthCheck")
544 // don't let the health check start incrementing again while we wait for the delayed execution of completion
546 log.debug "Finished with steps. Execution completion"
554 def updateDimmers(percentComplete) {
555 dimmers.each { dimmer ->
557 def nextLevel = dynamicLevel(dimmer, percentComplete)
559 if (nextLevel == 0) {
565 def shouldChangeColors = (colorize && colorize != "false")
567 if (shouldChangeColors && hasSetColorCommand(dimmer)) {
568 def hue = getHue(dimmer, nextLevel)
569 log.debug "Setting ${deviceLabel(dimmer)} level to ${nextLevel} and hue to ${hue}"
570 dimmer.setColor([hue: hue, saturation: 100, level: nextLevel])
571 } else if (hasSetLevelCommand(dimmer)) {
572 log.debug "Setting ${deviceLabel(dimmer)} level to ${nextLevel}"
573 dimmer.setLevel(nextLevel)
575 log.warn "${deviceLabel(dimmer)} does not have setColor or setLevel commands."
581 sendTimeRemainingEvent(percentComplete)
584 int dynamicLevel(dimmer, percentComplete) {
585 def start = atomicState.startLevels[dimmer.id]
586 def end = dynamicEndLevel()
588 if (!percentComplete) {
592 def totalDiff = end - start
593 def actualPercentage = percentComplete / 100
594 def percentOfTotalDiff = totalDiff * actualPercentage
596 (start + percentOfTotalDiff) as int
599 // ========================================================
601 // ========================================================
603 private completion() {
604 log.trace "Starting completion block"
606 if (!atomicState.running) {
612 handleCompletionSwitches()
614 handleCompletionMessaging()
616 handleCompletionModesAndPhrases()
619 private handleCompletionSwitches() {
620 completionSwitches.each { completionSwitch ->
622 def isDimmer = hasSetLevelCommand(completionSwitch)
624 if (completionSwitchesLevel && isDimmer) {
625 completionSwitch.setLevel(completionSwitchesLevel)
627 def command = completionSwitchesState ?: "on"
628 completionSwitch."${command}"()
633 private handleCompletionMessaging() {
634 if (completionMessage) {
635 if (location.contactBookEnabled) {
636 sendNotificationToContacts(completionMessage, recipients)
638 if (completionPhoneNumber) {
639 sendSms(completionPhoneNumber, completionMessage)
641 if (completionPush) {
642 sendPush(completionMessage)
645 if (completionMusicPlayer) {
646 speak(completionMessage)
651 private handleCompletionModesAndPhrases() {
653 if (completionMode) {
654 setLocationMode(completionMode)
657 if (completionPhrase) {
658 location.helloHome.execute(completionPhrase)
664 def sound = textToSpeech(message)
665 def soundDuration = (sound.duration as Integer) + 2
666 log.debug "Playing $sound.uri"
667 completionMusicPlayer.playTrack(sound.uri)
668 log.debug "Scheduled resume in $soundDuration sec"
669 runIn(soundDuration, resumePlaying, [overwrite: true])
672 def resumePlaying() {
673 log.trace "resumePlaying()"
674 def sonos = completionMusicPlayer
676 def currentTrack = sonos.currentState("trackData").jsonValue
677 if (currentTrack.status == "playing") {
678 sonos.playTrack(currentTrack)
680 sonos.setTrack(currentTrack)
685 // ========================================================
687 // ========================================================
689 def setLevelsInState() {
690 def startLevels = [:]
691 dimmers.each { dimmer ->
692 if (usesOldSettings()) {
693 startLevels[dimmer.id] = defaultStart()
694 } else if (hasStartLevel()) {
695 startLevels[dimmer.id] = startLevel
697 def dimmerIsOff = dimmer.currentValue("switch") == "off"
698 startLevels[dimmer.id] = dimmerIsOff ? 0 : dimmer.currentValue("level")
702 atomicState.startLevels = startLevels
705 def canStartAutomatically() {
707 def today = new Date().format("EEEE")
708 log.debug "today: ${today}, days: ${days}"
710 if (!days || days.contains(today)) {// if no days, assume every day
714 log.trace "should not run"
718 def completionPercentage() {
719 log.trace "checkingTime"
721 if (!atomicState.running) {
725 def now = new Date().getTime()
726 def timeElapsed = now - atomicState.start
727 def totalRunTime = totalRunTimeMillis() ?: 1
728 def percentComplete = timeElapsed / totalRunTime * 100
729 log.debug "percentComplete: ${percentComplete}"
731 return percentComplete
734 int totalRunTimeMillis() {
735 int minutes = sanitizeInt(duration, 30)
736 convertToMillis(minutes)
739 int convertToMillis(minutes) {
740 def seconds = minutes * 60
741 def millis = seconds * 1000
745 def timeRemaining(percentComplete) {
746 def normalizedPercentComplete = percentComplete / 100
747 def duration = sanitizeInt(duration, 30)
748 def timeElapsed = duration * normalizedPercentComplete
749 def timeRemaining = duration - timeElapsed
753 int millisToEnd(percentComplete) {
754 convertToMillis(timeRemaining(percentComplete))
757 String displayableTime(timeRemaining) {
758 def timeString = "${timeRemaining}"
759 def parts = timeString.split(/\./)
763 def minutes = parts[0]
764 if (parts.size() == 1) {
765 return "${minutes}:00"
767 def fraction = "0.${parts[1]}" as double
768 def seconds = "${60 * fraction as int}".padLeft(2, "0")
769 return "${minutes}:${seconds}"
772 def jumpTo(percentComplete) {
773 def millisToEnd = millisToEnd(percentComplete)
774 def endTime = new Date().getTime() + millisToEnd
775 def duration = sanitizeInt(duration, 30)
776 def durationMillis = convertToMillis(duration)
777 def shiftedStart = endTime - durationMillis
778 atomicState.start = shiftedStart
779 updateDimmers(percentComplete)
780 sendTimeRemainingEvent(percentComplete)
784 int dynamicEndLevel() {
785 if (usesOldSettings()) {
786 if (direction && direction == "Down") {
791 return endLevel as int
794 def getHue(dimmer, level) {
795 def start = atomicState.startLevels[dimmer.id] as int
796 def end = dynamicEndLevel()
798 return getDownHue(level)
800 return getUpHue(level)
804 def getUpHue(level) {
808 def getDownHue(level) {
812 private getBlueHue(level) {
813 if (level < 5) return 72
814 if (level < 10) return 71
815 if (level < 15) return 70
816 if (level < 20) return 69
817 if (level < 25) return 68
818 if (level < 30) return 67
819 if (level < 35) return 66
820 if (level < 40) return 65
821 if (level < 45) return 64
822 if (level < 50) return 63
823 if (level < 55) return 62
824 if (level < 60) return 61
825 if (level < 65) return 60
826 if (level < 70) return 59
827 if (level < 75) return 58
828 if (level < 80) return 57
829 if (level < 85) return 56
830 if (level < 90) return 55
831 if (level < 95) return 54
832 if (level >= 95) return 53
835 private getRedHue(level) {
836 if (level < 6) return 1
837 if (level < 12) return 2
838 if (level < 18) return 3
839 if (level < 24) return 4
840 if (level < 30) return 5
841 if (level < 36) return 6
842 if (level < 42) return 7
843 if (level < 48) return 8
844 if (level < 54) return 9
845 if (level < 60) return 10
846 if (level < 66) return 11
847 if (level < 72) return 12
848 if (level < 78) return 13
849 if (level < 84) return 14
850 if (level < 90) return 15
851 if (level < 96) return 16
852 if (level >= 96) return 17
855 private dimmersContainUnsupportedDevices() {
856 def found = dimmers.find { hasSetLevelCommand(it) == false }
860 private hasSetLevelCommand(device) {
861 return hasCommand(device, "setLevel")
864 private hasSetColorCommand(device) {
865 return hasCommand(device, "setColor")
868 private hasCommand(device, String command) {
869 return (device.supportedCommands.find { it.name == command } != null)
872 private dimmersWithSetColorCommand() {
873 def colorDimmers = []
874 dimmers.each { dimmer ->
875 if (hasSetColorCommand(dimmer)) {
876 colorDimmers << dimmer
882 private int sanitizeInt(i, int defaultValue = 0) {
890 catch (Exception e) {
896 private completionDelaySeconds() {
897 int completionDelayMinutes = sanitizeInt(completionDelay)
898 int completionDelaySeconds = (completionDelayMinutes * 60)
899 return completionDelaySeconds ?: 0
902 private stepDuration() {
903 int minutes = sanitizeInt(duration, 30)
904 int stepDuration = (minutes * 60) / 100
905 return stepDuration ?: 1
908 private debug(message) {
909 log.debug "${message}\nstate: ${state}"
912 public smartThingsDateFormat() { "yyyy-MM-dd'T'HH:mm:ss.SSSZ" }
914 public humanReadableStartDate() {
915 new Date().parse(smartThingsDateFormat(), startTime).format("h:mm a", timeZone(startTime))
918 def fancyString(listOfStrings) {
920 def fancify = { list ->
921 return list.collect {
923 if (list.size() > 1 && it == list[-1]) {
924 label = "and ${label}"
930 return fancify(listOfStrings)
933 def fancyDeviceString(devices = []) {
934 fancyString(devices.collect { deviceLabel(it) })
937 def deviceLabel(device) {
938 return device.label ?: device.name
941 def schedulingHrefDescription() {
943 def descriptionParts = []
945 if (days == weekdays()) {
946 descriptionParts << "On weekdays,"
947 } else if (days == weekends()) {
948 descriptionParts << "On weekends,"
950 descriptionParts << "On ${fancyString(days)},"
954 descriptionParts << "${fancyDeviceString(dimmers)} will start dimming"
957 descriptionParts << "at ${humanReadableStartDate()}"
962 descriptionParts << "or"
964 descriptionParts << "when ${location.name} enters '${modeStart}' mode"
967 if (descriptionParts.size() <= 1) {
968 // dimmers will be in the list no matter what. No rules are set if only dimmers are in the list
972 return descriptionParts.join(" ")
975 def completionHrefDescription() {
977 def descriptionParts = []
978 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"
980 if (completionSwitches) {
981 def switchesList = []
985 completionSwitches.each {
986 def isDimmer = completionSwitchesLevel ? hasSetLevelCommand(it) : false
989 dimmersList << deviceLabel(it)
993 switchesList << deviceLabel(it)
999 descriptionParts << "${fancyString(switchesList)} will be turned ${completionSwitchesState ?: 'on'}."
1003 descriptionParts << "${fancyString(dimmersList)} will be dimmed to ${completionSwitchesLevel}%."
1008 if (completionMessage && (completionPhoneNumber || completionPush || completionMusicPlayer)) {
1009 def messageParts = []
1011 if (completionMusicPlayer) {
1012 messageParts << "spoken"
1014 if (completionPhoneNumber) {
1015 messageParts << "sent as a text"
1017 if (completionPush) {
1018 messageParts << "sent as a push notification"
1021 descriptionParts << "The message '${completionMessage}' will be ${fancyString(messageParts)}."
1024 if (completionMode) {
1025 descriptionParts << "The mode will be changed to '${completionMode}'."
1028 if (completionPhrase) {
1029 descriptionParts << "The phrase '${completionPhrase}' will be executed."
1032 return descriptionParts.join(" ")
1035 def numbersPageHrefDescription() {
1036 def title = "All dimmers will dim for ${duration ?: '30'} minutes from ${startLevelLabel()} to ${endLevelLabel()}"
1038 def colorDimmers = dimmersWithSetColorCommand()
1039 if (colorDimmers == dimmers) {
1040 title += " and will gradually change color."
1042 title += ".\n${fancyDeviceString(colorDimmers)} will gradually change color."
1048 def hueSatToHex(h, s) {
1049 def convertedRGB = hslToRgb(h, s, 0.5)
1050 return rgbToHex(convertedRGB)
1053 def hslToRgb(h, s, l) {
1057 r = g = b = l; // achromatic
1059 def hue2rgb = { p, q, t ->
1062 if (t < 1 / 6) return p + (q - p) * 6 * t;
1063 if (t < 1 / 2) return q;
1064 if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
1068 def q = l < 0.5 ? l * (1 + s) : l + s - l * s;
1071 r = hue2rgb(p, q, h + 1 / 3);
1072 g = hue2rgb(p, q, h);
1073 b = hue2rgb(p, q, h - 1 / 3);
1076 return [r * 255, g * 255, b * 255];
1079 def rgbToHex(red, green, blue) {
1082 n = Math.max(0, Math.min(n, 255));
1083 def hexOptions = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"]
1085 def firstDecimal = ((n - n % 16) / 16) as int
1086 def secondDecimal = (n % 16) as int
1088 return "${hexOptions[firstDecimal]}${hexOptions[secondDecimal]}"
1091 def rgbToHex = { r, g, b ->
1092 return toHex(r) + toHex(g) + toHex(b)
1095 return rgbToHex(red, green, blue)
1098 def usesOldSettings() {
1102 def hasStartLevel() {
1103 return (startLevel != null && startLevel != "")
1107 return (endLevel != null && endLevel != "")