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: "numbersPage")
39 page(name: "schedulingPage")
40 page(name: "completionPage")
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", 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")
66 input(name: "manualOverride", type: "enum", options: ["Cancel dimming","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")
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 paragraph "If you think there is a mistake here, please contact support."
94 paragraph "You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)"
100 def controllerExplanationPage() {
101 dynamicPage(name: "controllerExplanationPage", title: "How To Control Gentle Wake Up") {
103 section("With other SmartApps", hideable: true, hidden: false) {
104 paragraph "When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!"
105 paragraph "The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!"
106 paragraph "Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up."
109 section("More about the controller", hideable: true, hidden: true) {
110 paragraph "You can find the controller with your other 'Things'. It will look like this."
111 image "http://f.cl.ly/items/2O0v0h41301U14042z3i/GentleWakeUpController-tile-stopped.png"
112 paragraph "You can start and stop Gentle Wake up by tapping the control on the right."
113 image "http://f.cl.ly/items/3W323J3M1b3K0k0V3X3a/GentleWakeUpController-tile-running.png"
114 paragraph "If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls."
115 image "http://f.cl.ly/items/291s3z2I2Q0r2q0x171H/GentleWakeUpController-richTile-stopped.png"
116 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."
117 image "http://f.cl.ly/items/0F0N2G0S3v1q0L0R3J3Y/GentleWakeUpController-richTile-running.png"
118 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."
119 paragraph "Of course, you may also tap the middle to start or stop the dimming cycle at any time."
122 section("Starting and stopping the SmartApp itself", hideable: true, hidden: true) {
123 paragraph "Tap the 'play' button on the SmartApp to start or stop dimming."
124 image "http://f.cl.ly/items/0R2u1Z2H30393z2I2V3S/GentleWakeUp-appTouch2.png"
127 section("Turning off devices while dimming", hideable: true, hidden: true) {
128 paragraph "It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option."
129 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."
130 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."
131 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. :)"
137 dynamicPage(name:"numbersPage", title:"") {
140 paragraph(name: "pGraph", title: "These lights will dim", fancyDeviceString(dimmers))
144 input(name: "duration", type: "number", title: "For this many minutes", description: "30", required: false, defaultValue: 30)
148 input(name: "startLevel", type: "number", range: "0..99", title: "From this level", description: "Current Level", required: false, multiple: false)
149 input(name: "endLevel", type: "number", range: "0..99", title: "To this level", , description: "Between 0 and 99", required: true, multiple: false)
152 def colorDimmers = dimmersWithSetColorCommand()
155 input(name: "colorize", type: "bool", title: "Gradually change the color of ${fancyDeviceString(colorDimmers)}", description: null, required: false, defaultValue: "true")
162 if (usesOldSettings() && direction && direction == "Down") {
169 if (usesOldSettings() && direction && direction == "Down") {
175 def startLevelLabel() {
176 if (usesOldSettings()) { // using old settings
177 if (direction && direction == "Down") { // 99 -> 1
182 return hasStartLevel() ? "${startLevel}%" : "Current Level"
185 def endLevelLabel() {
186 if (usesOldSettings()) {
187 if (direction && direction == "Down") { // 99 -> 1
192 return "${endLevel}%"
196 ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
200 ["Saturday", "Sunday"]
203 def schedulingPage() {
204 dynamicPage(name: "schedulingPage", title: "Rules For Automatically Dimming Your Lights") {
206 section("Use Other SmartApps!") {
207 href(title: "Learn how to control Gentle Wake Up", page: "controllerExplanationPage", description: null)
210 section("Allow Automatic Dimming") {
211 input(name: "days", type: "enum", title: "On These Days", description: "Every day", required: false, multiple: true, options: weekdays() + weekends())
214 section("Start Dimming...") {
215 input(name: "startTime", type: "time", title: "At This Time", description: null, required: false)
216 input(name: "modeStart", title: "When Entering This Mode", type: "mode", required: false, mutliple: false, submitOnChange: true, description: null)
218 input(name: "modeStop", title: "Stop when leaving '${modeStart}' mode", type: "bool", required: false)
225 def completionPage() {
226 dynamicPage(name: "completionPage", title: "Completion Rules") {
228 section("Switches") {
229 input(name: "completionSwitches", type: "capability.switch", title: "Set these switches", description: null, required: false, multiple: true, submitOnChange: true)
230 if (completionSwitches) {
231 input(name: "completionSwitchesState", type: "enum", title: "To", description: null, required: false, multiple: false, options: ["on", "off"], defaultValue: "on")
232 input(name: "completionSwitchesLevel", type: "number", title: "Optionally, Set Dimmer Levels To", description: null, required: false, multiple: false, range: "(0..99)")
236 section("Notifications") {
237 input("recipients", "contact", title: "Send notifications to", required: false) {
238 input(name: "completionPhoneNumber", type: "phone", title: "Text This Number", description: "Phone number", required: false)
239 input(name: "completionPush", type: "bool", title: "Send A Push Notification", description: "Phone number", required: false)
241 input(name: "completionMusicPlayer", type: "capability.musicPlayer", title: "Speak Using This Music Player", required: false)
242 input(name: "completionMessage", type: "text", title: "With This Message", description: null, required: false)
245 section("Modes and Phrases") {
246 input(name: "completionMode", type: "mode", title: "Change ${location.name} Mode To", description: null, required: false)
247 input(name: "completionPhrase", type: "enum", title: "Execute The Phrase", description: null, required: false, multiple: false, options: location.helloHome.getPhrases().label)
251 input(name: "completionDelay", type: "number", title: "Delay This Many Minutes Before Executing These Actions", description: "0", required: false)
256 // ========================================================
258 // ========================================================
261 log.debug "Installing 'Gentle Wake Up' with settings: ${settings}"
267 log.debug "Updating 'Gentle Wake Up' with settings: ${settings}"
270 def controller = getController()
272 controller.label = app.label
278 private initialize() {
279 startLevel = 0//Chagne start level to 0 to make it possible for the light to be off!
280 stop("settingsChange")
283 log.debug "scheduling dimming routine to run at $startTime"
284 schedule(startTime, "scheduledStart")
287 // TODO: make this an option
288 subscribe(app, appHandler)
290 subscribe(location, locationHandler)
292 if (manualOverride) {
293 subscribe(dimmers, "switch.off", stopDimmersHandler)
296 /*if (!getAllChildDevices()) {
297 // create controller device and set name to the label used here
298 def dni = "${new Date().getTime()}"
299 log.debug "app.label: ${app.label}"
300 addChildDevice("smartthings", "Gentle Wake Up Controller", dni, null, ["label": app.label])
301 state.controllerDni = dni
305 def appHandler(evt) {
306 log.debug "appHandler evt: ${evt.value}"
307 if (evt.value == "touch") {
308 if (atomicState.running) {
316 def locationHandler(evt) {
317 log.debug "locationHandler evt: ${evt.value}"
323 def isSpecifiedMode = (evt.value == modeStart)
324 def modeStopIsTrue = (modeStop && modeStop != "false")
326 if (isSpecifiedMode && canStartAutomatically()) {
328 } else if (!isSpecifiedMode && modeStopIsTrue) {
334 def stopDimmersHandler(evt) {
335 log.trace "stopDimmersHandler evt: ${evt.value}"
336 def percentComplete = completionPercentage()
337 // 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
338 if (percentComplete > 2 && percentComplete < 98) {
339 if (manualOverride == "cancel") {
340 log.debug "STOPPING in stopDimmersHandler"
341 stop("manualOverride")
342 } else if (manualOverride == "jumpTo") {
343 def end = dynamicEndLevel()
344 log.debug "Jumping to 99% complete in stopDimmersHandler"
349 log.debug "not stopping in stopDimmersHandler"
353 // ========================================================
355 // ========================================================
357 def scheduledStart() {
358 if (canStartAutomatically()) {
363 public def start(source) {
366 sendStartEvent(source)
370 atomicState.running = true
372 atomicState.start = new Date().getTime()
374 schedule("0 * * * * ?", "healthCheck")
378 public def stop(source) {
381 sendStopEvent(source)
383 atomicState.running = false
384 atomicState.start = 0
386 unschedule("healthCheck")
389 private healthCheck() {
390 log.trace "'Gentle Wake Up' healthCheck"
392 if (!atomicState.running) {
399 // ========================================================
401 // ========================================================
403 def sendStartEvent(source) {
404 log.trace "sendStartEvent(${source})"
406 name: "sessionStatus",
408 descriptionText: "${app.label} has started dimming",
413 if (source == "modeChange") {
414 eventData.descriptionText += " because of a mode change"
415 } else if (source == "schedule") {
416 eventData.descriptionText += " as scheduled"
417 } else if (source == "appTouch") {
418 eventData.descriptionText += " because you pressed play on the app"
419 } else if (source == "controller") {
420 eventData.descriptionText += " because you pressed play on the controller"
423 sendControllerEvent(eventData)
426 def sendStopEvent(source) {
427 log.trace "sendStopEvent(${source})"
429 name: "sessionStatus",
431 descriptionText: "${app.label} has stopped dimming",
436 if (source == "modeChange") {
437 eventData.descriptionText += " because of a mode change"
438 eventData.value += "cancelled"
439 } else if (source == "schedule") {
440 eventData.descriptionText = "${app.label} has finished dimming"
441 } else if (source == "appTouch") {
442 eventData.descriptionText += " because you pressed play on the app"
443 eventData.value += "cancelled"
444 } else if (source == "controller") {
445 eventData.descriptionText += " because you pressed stop on the controller"
446 eventData.value += "cancelled"
447 } else if (source == "settingsChange") {
448 eventData.descriptionText += " because the settings have changed"
449 eventData.value += "cancelled"
450 } else if (source == "manualOverride") {
451 eventData.descriptionText += " because the dimmer was manually turned off"
452 eventData.value += "cancelled"
455 // send 100% completion event
456 sendTimeRemainingEvent(100)
458 // send a non-displayed 0% completion to reset tiles
459 sendTimeRemainingEvent(0, false)
461 // send sessionStatus event last so the event feed is ordered properly
462 sendControllerEvent(eventData)
465 def sendTimeRemainingEvent(percentComplete, displayed = true) {
466 log.trace "sendTimeRemainingEvent(${percentComplete})"
468 def percentCompleteEventData = [
469 name: "percentComplete",
470 value: percentComplete as int,
471 displayed: displayed,
474 sendControllerEvent(percentCompleteEventData)
476 def duration = sanitizeInt(duration, 30)
477 def timeRemaining = duration - (duration * (percentComplete / 100))
478 def timeRemainingEventData = [
479 name: "timeRemaining",
480 value: displayableTime(timeRemaining),
481 displayed: displayed,
484 sendControllerEvent(timeRemainingEventData)
487 def sendControllerEvent(eventData) {
488 def controller = getController()
490 controller.controllerEvent(eventData)
494 def getController() {
495 def dni = state.controllerDni
497 log.warn "no controller dni"
500 def controller = getChildDevice(dni)
502 log.warn "no controller"
505 log.debug "controller: ${controller}"
509 // ========================================================
511 // ========================================================
514 private increment() {
516 if (!atomicState.running) {
520 def percentComplete = completionPercentage()
522 if (percentComplete > 99) {
526 updateDimmers(percentComplete)
528 if (percentComplete < 99) {
530 def runAgain = stepDuration()
531 log.debug "Rescheduling to run again in ${runAgain} seconds"
533 //runIn(runAgain, 'increment', [overwrite: true])
537 int completionDelay = completionDelaySeconds()
538 if (completionDelay) {
539 log.debug "Finished with steps. Scheduling completion for ${completionDelay} second(s) from now"
540 runIn(completionDelay, 'completion', [overwrite: true])
541 unschedule("healthCheck")
542 // don't let the health check start incrementing again while we wait for the delayed execution of completion
544 log.debug "Finished with steps. Execution completion"
552 def updateDimmers(percentComplete) {
553 dimmers.each { dimmer ->
555 def nextLevel = dynamicLevel(dimmer, percentComplete)
557 if (nextLevel == 0) {
563 def shouldChangeColors = (colorize != "false")
564 if (colorize == "false")
569 if (shouldChangeColors/*&& hasSetColorCommand(dimmer)*/) {
570 def hue = getHue(dimmer, nextLevel)
571 log.debug "Setting ${deviceLabel(dimmer)} level to ${nextLevel} and hue to ${hue}"
572 dimmer.setColor([hue: hue, saturation: 100, level: nextLevel])
574 log.debug "Setting ${deviceLabel(dimmer)} level to ${nextLevel}"
575 dimmer.setLevel(nextLevel)
580 sendTimeRemainingEvent(percentComplete)
583 int dynamicLevel(dimmer, percentComplete) {
584 def start = atomicState.startLevels[dimmer.id]
585 def end = dynamicEndLevel()
587 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
732 // We do not have the notion of time for model-checking
736 int totalRunTimeMillis() {
737 int minutes = sanitizeInt(duration, 30)
738 convertToMillis(minutes)
741 int convertToMillis(minutes) {
742 def seconds = minutes * 60
743 def millis = seconds * 1000
747 def timeRemaining(percentComplete) {
748 def normalizedPercentComplete = percentComplete / 100
749 def duration = sanitizeInt(duration, 30)
750 def timeElapsed = duration * normalizedPercentComplete
751 def timeRemaining = duration - timeElapsed
755 int millisToEnd(percentComplete) {
756 convertToMillis(timeRemaining(percentComplete))
759 String displayableTime(timeRemaining) {
760 def timeString = "${timeRemaining}"
761 def parts = timeString.split(/\./)
765 def minutes = parts[0]
766 if (parts.size() == 1) {
767 return "${minutes}:00"
769 def fraction = "0.${parts[1]}" as double
770 def seconds = "${60 * fraction as int}".padLeft(2, "0")
771 return "${minutes}:${seconds}"
774 def jumpTo(percentComplete) {
775 def millisToEnd = millisToEnd(percentComplete)
776 def endTime = new Date().getTime() + millisToEnd
777 def duration = sanitizeInt(duration, 30)
778 def durationMillis = convertToMillis(duration)
779 def shiftedStart = endTime - durationMillis
780 atomicState.start = shiftedStart
781 updateDimmers(percentComplete)
782 sendTimeRemainingEvent(percentComplete)
786 int dynamicEndLevel() {
787 if (usesOldSettings()) {
788 if (direction && direction == "Down") {
793 return endLevel as int
796 def getHue(dimmer, level) {
797 def start = atomicState.startLevels[dimmer.id] as int
798 def end = dynamicEndLevel()
800 return getDownHue(level)
802 return getUpHue(level)
806 def getUpHue(level) {
810 def getDownHue(level) {
814 private getBlueHue(level) {
815 if (level < 5) return 72
816 if (level < 10) return 71
817 if (level < 15) return 70
818 if (level < 20) return 69
819 if (level < 25) return 68
820 if (level < 30) return 67
821 if (level < 35) return 66
822 if (level < 40) return 65
823 if (level < 45) return 64
824 if (level < 50) return 63
825 if (level < 55) return 62
826 if (level < 60) return 61
827 if (level < 65) return 60
828 if (level < 70) return 59
829 if (level < 75) return 58
830 if (level < 80) return 57
831 if (level < 85) return 56
832 if (level < 90) return 55
833 if (level < 95) return 54
834 if (level >= 95) return 53
837 private getRedHue(level) {
838 if (level < 6) return 1
839 if (level < 12) return 2
840 if (level < 18) return 3
841 if (level < 24) return 4
842 if (level < 30) return 5
843 if (level < 36) return 6
844 if (level < 42) return 7
845 if (level < 48) return 8
846 if (level < 54) return 9
847 if (level < 60) return 10
848 if (level < 66) return 11
849 if (level < 72) return 12
850 if (level < 78) return 13
851 if (level < 84) return 14
852 if (level < 90) return 15
853 if (level < 96) return 16
854 if (level >= 96) return 17
857 private dimmersContainUnsupportedDevices() {
858 def found = dimmers.find { hasSetLevelCommand(it) == false }
862 private hasSetLevelCommand(device) {
863 return hasCommand(device, "setLevel")
866 private hasSetColorCommand(device) {
867 return hasCommand(device, "setColor")
870 private hasCommand(device, String command) {
871 return (device.supportedCommands.find { it.name == command } != null)
874 private dimmersWithSetColorCommand() {
875 def colorDimmers = []
876 dimmers.each { dimmer ->
877 //if (hasSetColorCommand(dimmer)) {
878 colorDimmers << dimmer
884 private int sanitizeInt(i, int defaultValue = 0) {
892 catch (Exception e) {
898 private completionDelaySeconds() {
899 int completionDelayMinutes = sanitizeInt(completionDelay)
900 int completionDelaySeconds = (completionDelayMinutes * 60)
901 return completionDelaySeconds ?: 0
904 private stepDuration() {
905 int minutes = sanitizeInt(duration, 30)
906 int stepDuration = (minutes * 60) / 100
907 return stepDuration ?: 1
910 private debug(message) {
911 log.debug "${message}\nstate: ${state}"
914 public smartThingsDateFormat() { "yyyy-MM-dd'T'HH:mm:ss.SSSZ" }
916 public humanReadableStartDate() {
917 new Date().parse(smartThingsDateFormat(), startTime).format("h:mm a", timeZone(startTime))
920 def fancyString(listOfStrings) {
922 def fancify = { list ->
923 return list.collect {
925 if (list.size() > 1 && it == list[-1]) {
926 label = "and ${label}"
932 return fancify(listOfStrings)
935 def fancyDeviceString(devices = []) {
936 fancyString(devices.collect { deviceLabel(it) })
939 def deviceLabel(device) {
940 return device.label ?: device.name
943 def schedulingHrefDescription() {
945 def descriptionParts = []
947 if (days == weekdays()) {
948 descriptionParts << "On weekdays,"
949 } else if (days == weekends()) {
950 descriptionParts << "On weekends,"
952 descriptionParts << "On ${fancyString(days)},"
956 descriptionParts << "${fancyDeviceString(dimmers)} will start dimming"
959 descriptionParts << "at ${humanReadableStartDate()}"
964 descriptionParts << "or"
966 descriptionParts << "when ${location.name} enters '${modeStart}' mode"
969 if (descriptionParts.size() <= 1) {
970 // dimmers will be in the list no matter what. No rules are set if only dimmers are in the list
974 return descriptionParts.join(" ")
977 def completionHrefDescription() {
979 def descriptionParts = []
980 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"
982 if (completionSwitches) {
983 def switchesList = []
987 completionSwitches.each {
988 def isDimmer = completionSwitchesLevel ? hasSetLevelCommand(it) : false
991 dimmersList << deviceLabel(it)
995 switchesList << deviceLabel(it)
1001 descriptionParts << "${fancyString(switchesList)} will be turned ${completionSwitchesState ?: 'on'}."
1005 descriptionParts << "${fancyString(dimmersList)} will be dimmed to ${completionSwitchesLevel}%."
1010 if (completionMessage && (completionPhoneNumber || completionPush || completionMusicPlayer)) {
1011 def messageParts = []
1013 if (completionMusicPlayer) {
1014 messageParts << "spoken"
1016 if (completionPhoneNumber) {
1017 messageParts << "sent as a text"
1019 if (completionPush) {
1020 messageParts << "sent as a push notification"
1023 descriptionParts << "The message '${completionMessage}' will be ${fancyString(messageParts)}."
1026 if (completionMode) {
1027 descriptionParts << "The mode will be changed to '${completionMode}'."
1030 if (completionPhrase) {
1031 descriptionParts << "The phrase '${completionPhrase}' will be executed."
1034 return descriptionParts.join(" ")
1037 def numbersPageHrefDescription() {
1038 def title = "All dimmers will dim for ${duration ?: '30'} minutes from ${startLevelLabel()} to ${endLevelLabel()}"
1040 def colorDimmers = dimmersWithSetColorCommand()
1041 if (colorDimmers == dimmers) {
1042 title += " and will gradually change color."
1044 title += ".\n${fancyDeviceString(colorDimmers)} will gradually change color."
1050 def hueSatToHex(h, s) {
1051 def convertedRGB = hslToRgb(h, s, 0.5)
1052 return rgbToHex(convertedRGB)
1055 def hslToRgb(h, s, l) {
1059 r = g = b = l; // achromatic
1061 def hue2rgb = { p, q, t ->
1064 if (t < 1 / 6) return p + (q - p) * 6 * t;
1065 if (t < 1 / 2) return q;
1066 if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
1070 def q = l < 0.5 ? l * (1 + s) : l + s - l * s;
1073 r = hue2rgb(p, q, h + 1 / 3);
1074 g = hue2rgb(p, q, h);
1075 b = hue2rgb(p, q, h - 1 / 3);
1078 return [r * 255, g * 255, b * 255];
1081 def rgbToHex(red, green, blue) {
1084 n = Math.max(0, Math.min(n, 255));
1085 def hexOptions = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"]
1087 def firstDecimal = ((n - n % 16) / 16) as int
1088 def secondDecimal = (n % 16) as int
1090 return "${hexOptions[firstDecimal]}${hexOptions[secondDecimal]}"
1093 def rgbToHex = { r, g, b ->
1094 return toHex(r) + toHex(g) + toHex(b)
1097 return rgbToHex(red, green, blue)
1100 def usesOldSettings() {
1104 def hasStartLevel() {
1105 return (startLevel != null && startLevel != "")
1109 return (endLevel != null && endLevel != "")