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 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", description: "Current Level", required: false, multiple: false)
152 input(name: "endLevel", type: "number", range: "0..99", title: "To this level", , 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])
572 log.debug "Setting ${deviceLabel(dimmer)} level to ${nextLevel}"
573 dimmer.setLevel(nextLevel)
578 sendTimeRemainingEvent(percentComplete)
581 int dynamicLevel(dimmer, percentComplete) {
582 def start = atomicState.startLevels[dimmer.id]
583 def end = dynamicEndLevel()
585 if (!percentComplete) {
589 def totalDiff = end - start
590 def actualPercentage = percentComplete / 100
591 def percentOfTotalDiff = totalDiff * actualPercentage
593 (start + percentOfTotalDiff) as int
596 // ========================================================
598 // ========================================================
600 private completion() {
601 log.trace "Starting completion block"
603 if (!atomicState.running) {
609 handleCompletionSwitches()
611 handleCompletionMessaging()
613 handleCompletionModesAndPhrases()
616 private handleCompletionSwitches() {
617 completionSwitches.each { completionSwitch ->
619 def isDimmer = hasSetLevelCommand(completionSwitch)
621 if (completionSwitchesLevel && isDimmer) {
622 completionSwitch.setLevel(completionSwitchesLevel)
624 def command = completionSwitchesState ?: "on"
625 completionSwitch."${command}"()
630 private handleCompletionMessaging() {
631 if (completionMessage) {
632 if (location.contactBookEnabled) {
633 sendNotificationToContacts(completionMessage, recipients)
635 if (completionPhoneNumber) {
636 sendSms(completionPhoneNumber, completionMessage)
638 if (completionPush) {
639 sendPush(completionMessage)
642 if (completionMusicPlayer) {
643 speak(completionMessage)
648 private handleCompletionModesAndPhrases() {
650 if (completionMode) {
651 setLocationMode(completionMode)
654 if (completionPhrase) {
655 location.helloHome.execute(completionPhrase)
661 def sound = textToSpeech(message)
662 def soundDuration = (sound.duration as Integer) + 2
663 log.debug "Playing $sound.uri"
664 completionMusicPlayer.playTrack(sound.uri)
665 log.debug "Scheduled resume in $soundDuration sec"
666 runIn(soundDuration, resumePlaying, [overwrite: true])
669 def resumePlaying() {
670 log.trace "resumePlaying()"
671 def sonos = completionMusicPlayer
673 def currentTrack = sonos.currentState("trackData").jsonValue
674 if (currentTrack.status == "playing") {
675 sonos.playTrack(currentTrack)
677 sonos.setTrack(currentTrack)
682 // ========================================================
684 // ========================================================
686 def setLevelsInState() {
687 def startLevels = [:]
688 dimmers.each { dimmer ->
689 if (usesOldSettings()) {
690 startLevels[dimmer.id] = defaultStart()
691 } else if (hasStartLevel()) {
692 startLevels[dimmer.id] = startLevel
694 def dimmerIsOff = dimmer.currentValue("switch") == "off"
695 startLevels[dimmer.id] = dimmerIsOff ? 0 : dimmer.currentValue("level")
699 atomicState.startLevels = startLevels
702 def canStartAutomatically() {
704 def today = new Date().format("EEEE")
705 log.debug "today: ${today}, days: ${days}"
707 if (!days || days.contains(today)) {// if no days, assume every day
711 log.trace "should not run"
715 def completionPercentage() {
716 log.trace "checkingTime"
718 if (!atomicState.running) {
722 def now = new Date().getTime()
723 def timeElapsed = now - atomicState.start
724 def totalRunTime = totalRunTimeMillis() ?: 1
725 def percentComplete = timeElapsed / totalRunTime * 100
726 log.debug "percentComplete: ${percentComplete}"
728 return percentComplete
731 int totalRunTimeMillis() {
732 int minutes = sanitizeInt(duration, 30)
733 convertToMillis(minutes)
736 int convertToMillis(minutes) {
737 def seconds = minutes * 60
738 def millis = seconds * 1000
742 def timeRemaining(percentComplete) {
743 def normalizedPercentComplete = percentComplete / 100
744 def duration = sanitizeInt(duration, 30)
745 def timeElapsed = duration * normalizedPercentComplete
746 def timeRemaining = duration - timeElapsed
750 int millisToEnd(percentComplete) {
751 convertToMillis(timeRemaining(percentComplete))
754 String displayableTime(timeRemaining) {
755 def timeString = "${timeRemaining}"
756 def parts = timeString.split(/\./)
760 def minutes = parts[0]
761 if (parts.size() == 1) {
762 return "${minutes}:00"
764 def fraction = "0.${parts[1]}" as double
765 def seconds = "${60 * fraction as int}".padLeft(2, "0")
766 return "${minutes}:${seconds}"
769 def jumpTo(percentComplete) {
770 def millisToEnd = millisToEnd(percentComplete)
771 def endTime = new Date().getTime() + millisToEnd
772 def duration = sanitizeInt(duration, 30)
773 def durationMillis = convertToMillis(duration)
774 def shiftedStart = endTime - durationMillis
775 atomicState.start = shiftedStart
776 updateDimmers(percentComplete)
777 sendTimeRemainingEvent(percentComplete)
781 int dynamicEndLevel() {
782 if (usesOldSettings()) {
783 if (direction && direction == "Down") {
788 return endLevel as int
791 def getHue(dimmer, level) {
792 def start = atomicState.startLevels[dimmer.id] as int
793 def end = dynamicEndLevel()
795 return getDownHue(level)
797 return getUpHue(level)
801 def getUpHue(level) {
805 def getDownHue(level) {
809 private getBlueHue(level) {
810 if (level < 5) return 72
811 if (level < 10) return 71
812 if (level < 15) return 70
813 if (level < 20) return 69
814 if (level < 25) return 68
815 if (level < 30) return 67
816 if (level < 35) return 66
817 if (level < 40) return 65
818 if (level < 45) return 64
819 if (level < 50) return 63
820 if (level < 55) return 62
821 if (level < 60) return 61
822 if (level < 65) return 60
823 if (level < 70) return 59
824 if (level < 75) return 58
825 if (level < 80) return 57
826 if (level < 85) return 56
827 if (level < 90) return 55
828 if (level < 95) return 54
829 if (level >= 95) return 53
832 private getRedHue(level) {
833 if (level < 6) return 1
834 if (level < 12) return 2
835 if (level < 18) return 3
836 if (level < 24) return 4
837 if (level < 30) return 5
838 if (level < 36) return 6
839 if (level < 42) return 7
840 if (level < 48) return 8
841 if (level < 54) return 9
842 if (level < 60) return 10
843 if (level < 66) return 11
844 if (level < 72) return 12
845 if (level < 78) return 13
846 if (level < 84) return 14
847 if (level < 90) return 15
848 if (level < 96) return 16
849 if (level >= 96) return 17
852 private dimmersContainUnsupportedDevices() {
853 def found = dimmers.find { hasSetLevelCommand(it) == false }
857 private hasSetLevelCommand(device) {
858 return hasCommand(device, "setLevel")
861 private hasSetColorCommand(device) {
862 return hasCommand(device, "setColor")
865 private hasCommand(device, String command) {
866 return (device.supportedCommands.find { it.name == command } != null)
869 private dimmersWithSetColorCommand() {
870 def colorDimmers = []
871 dimmers.each { dimmer ->
872 //if (hasSetColorCommand(dimmer)) {
873 colorDimmers << dimmer
879 private int sanitizeInt(i, int defaultValue = 0) {
887 catch (Exception e) {
893 private completionDelaySeconds() {
894 int completionDelayMinutes = sanitizeInt(completionDelay)
895 int completionDelaySeconds = (completionDelayMinutes * 60)
896 return completionDelaySeconds ?: 0
899 private stepDuration() {
900 int minutes = sanitizeInt(duration, 30)
901 int stepDuration = (minutes * 60) / 100
902 return stepDuration ?: 1
905 private debug(message) {
906 log.debug "${message}\nstate: ${state}"
909 public smartThingsDateFormat() { "yyyy-MM-dd'T'HH:mm:ss.SSSZ" }
911 public humanReadableStartDate() {
912 new Date().parse(smartThingsDateFormat(), startTime).format("h:mm a", timeZone(startTime))
915 def fancyString(listOfStrings) {
917 def fancify = { list ->
918 return list.collect {
920 if (list.size() > 1 && it == list[-1]) {
921 label = "and ${label}"
927 return fancify(listOfStrings)
930 def fancyDeviceString(devices = []) {
931 fancyString(devices.collect { deviceLabel(it) })
934 def deviceLabel(device) {
935 return device.label ?: device.name
938 def schedulingHrefDescription() {
940 def descriptionParts = []
942 if (days == weekdays()) {
943 descriptionParts << "On weekdays,"
944 } else if (days == weekends()) {
945 descriptionParts << "On weekends,"
947 descriptionParts << "On ${fancyString(days)},"
951 descriptionParts << "${fancyDeviceString(dimmers)} will start dimming"
954 descriptionParts << "at ${humanReadableStartDate()}"
959 descriptionParts << "or"
961 descriptionParts << "when ${location.name} enters '${modeStart}' mode"
964 if (descriptionParts.size() <= 1) {
965 // dimmers will be in the list no matter what. No rules are set if only dimmers are in the list
969 return descriptionParts.join(" ")
972 def completionHrefDescription() {
974 def descriptionParts = []
975 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"
977 if (completionSwitches) {
978 def switchesList = []
982 completionSwitches.each {
983 def isDimmer = completionSwitchesLevel ? hasSetLevelCommand(it) : false
986 dimmersList << deviceLabel(it)
990 switchesList << deviceLabel(it)
996 descriptionParts << "${fancyString(switchesList)} will be turned ${completionSwitchesState ?: 'on'}."
1000 descriptionParts << "${fancyString(dimmersList)} will be dimmed to ${completionSwitchesLevel}%."
1005 if (completionMessage && (completionPhoneNumber || completionPush || completionMusicPlayer)) {
1006 def messageParts = []
1008 if (completionMusicPlayer) {
1009 messageParts << "spoken"
1011 if (completionPhoneNumber) {
1012 messageParts << "sent as a text"
1014 if (completionPush) {
1015 messageParts << "sent as a push notification"
1018 descriptionParts << "The message '${completionMessage}' will be ${fancyString(messageParts)}."
1021 if (completionMode) {
1022 descriptionParts << "The mode will be changed to '${completionMode}'."
1025 if (completionPhrase) {
1026 descriptionParts << "The phrase '${completionPhrase}' will be executed."
1029 return descriptionParts.join(" ")
1032 def numbersPageHrefDescription() {
1033 def title = "All dimmers will dim for ${duration ?: '30'} minutes from ${startLevelLabel()} to ${endLevelLabel()}"
1035 def colorDimmers = dimmersWithSetColorCommand()
1036 if (colorDimmers == dimmers) {
1037 title += " and will gradually change color."
1039 title += ".\n${fancyDeviceString(colorDimmers)} will gradually change color."
1045 def hueSatToHex(h, s) {
1046 def convertedRGB = hslToRgb(h, s, 0.5)
1047 return rgbToHex(convertedRGB)
1050 def hslToRgb(h, s, l) {
1054 r = g = b = l; // achromatic
1056 def hue2rgb = { p, q, t ->
1059 if (t < 1 / 6) return p + (q - p) * 6 * t;
1060 if (t < 1 / 2) return q;
1061 if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
1065 def q = l < 0.5 ? l * (1 + s) : l + s - l * s;
1068 r = hue2rgb(p, q, h + 1 / 3);
1069 g = hue2rgb(p, q, h);
1070 b = hue2rgb(p, q, h - 1 / 3);
1073 return [r * 255, g * 255, b * 255];
1076 def rgbToHex(red, green, blue) {
1079 n = Math.max(0, Math.min(n, 255));
1080 def hexOptions = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"]
1082 def firstDecimal = ((n - n % 16) / 16) as int
1083 def secondDecimal = (n % 16) as int
1085 return "${hexOptions[firstDecimal]}${hexOptions[secondDecimal]}"
1088 def rgbToHex = { r, g, b ->
1089 return toHex(r) + toHex(g) + toHex(b)
1092 return rgbToHex(red, green, blue)
1095 def usesOldSettings() {
1099 def hasStartLevel() {
1100 return (startLevel != null && startLevel != "")
1104 return (endLevel != null && endLevel != "")