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 stop("settingsChange")
282 log.debug "scheduling dimming routine to run at $startTime"
283 schedule(startTime, "scheduledStart")
286 // TODO: make this an option
287 subscribe(app, appHandler)
289 subscribe(location, locationHandler)
291 if (manualOverride) {
292 subscribe(dimmers, "switch.off", stopDimmersHandler)
295 /*if (!getAllChildDevices()) {
296 // create controller device and set name to the label used here
297 def dni = "${new Date().getTime()}"
298 log.debug "app.label: ${app.label}"
299 addChildDevice("smartthings", "Gentle Wake Up Controller", dni, null, ["label": app.label])
300 state.controllerDni = dni
304 def appHandler(evt) {
305 log.debug "appHandler evt: ${evt.value}"
306 if (evt.value == "touch") {
307 if (atomicState.running) {
315 def locationHandler(evt) {
316 log.debug "locationHandler evt: ${evt.value}"
322 def isSpecifiedMode = (evt.value == modeStart)
323 def modeStopIsTrue = (modeStop && modeStop != "false")
325 if (isSpecifiedMode && canStartAutomatically()) {
327 } else if (!isSpecifiedMode && modeStopIsTrue) {
333 def stopDimmersHandler(evt) {
334 log.trace "stopDimmersHandler evt: ${evt.value}"
335 def percentComplete = completionPercentage()
336 // 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
337 if (percentComplete > 2 && percentComplete < 98) {
338 if (manualOverride == "cancel") {
339 log.debug "STOPPING in stopDimmersHandler"
340 stop("manualOverride")
341 } else if (manualOverride == "jumpTo") {
342 def end = dynamicEndLevel()
343 log.debug "Jumping to 99% complete in stopDimmersHandler"
348 log.debug "not stopping in stopDimmersHandler"
352 // ========================================================
354 // ========================================================
356 def scheduledStart() {
357 if (canStartAutomatically()) {
362 public def start(source) {
365 sendStartEvent(source)
369 atomicState.running = true
371 atomicState.start = new Date().getTime()
373 schedule("0 * * * * ?", "healthCheck")
377 public def stop(source) {
380 sendStopEvent(source)
382 atomicState.running = false
383 atomicState.start = 0
385 unschedule("healthCheck")
388 private healthCheck() {
389 log.trace "'Gentle Wake Up' healthCheck"
391 if (!atomicState.running) {
398 // ========================================================
400 // ========================================================
402 def sendStartEvent(source) {
403 log.trace "sendStartEvent(${source})"
405 name: "sessionStatus",
407 descriptionText: "${app.label} has started dimming",
412 if (source == "modeChange") {
413 eventData.descriptionText += " because of a mode change"
414 } else if (source == "schedule") {
415 eventData.descriptionText += " as scheduled"
416 } else if (source == "appTouch") {
417 eventData.descriptionText += " because you pressed play on the app"
418 } else if (source == "controller") {
419 eventData.descriptionText += " because you pressed play on the controller"
422 sendControllerEvent(eventData)
425 def sendStopEvent(source) {
426 log.trace "sendStopEvent(${source})"
428 name: "sessionStatus",
430 descriptionText: "${app.label} has stopped dimming",
435 if (source == "modeChange") {
436 eventData.descriptionText += " because of a mode change"
437 eventData.value += "cancelled"
438 } else if (source == "schedule") {
439 eventData.descriptionText = "${app.label} has finished dimming"
440 } else if (source == "appTouch") {
441 eventData.descriptionText += " because you pressed play on the app"
442 eventData.value += "cancelled"
443 } else if (source == "controller") {
444 eventData.descriptionText += " because you pressed stop on the controller"
445 eventData.value += "cancelled"
446 } else if (source == "settingsChange") {
447 eventData.descriptionText += " because the settings have changed"
448 eventData.value += "cancelled"
449 } else if (source == "manualOverride") {
450 eventData.descriptionText += " because the dimmer was manually turned off"
451 eventData.value += "cancelled"
454 // send 100% completion event
455 sendTimeRemainingEvent(100)
457 // send a non-displayed 0% completion to reset tiles
458 sendTimeRemainingEvent(0, false)
460 // send sessionStatus event last so the event feed is ordered properly
461 sendControllerEvent(eventData)
464 def sendTimeRemainingEvent(percentComplete, displayed = true) {
465 log.trace "sendTimeRemainingEvent(${percentComplete})"
467 def percentCompleteEventData = [
468 name: "percentComplete",
469 value: percentComplete as int,
470 displayed: displayed,
473 sendControllerEvent(percentCompleteEventData)
475 def duration = sanitizeInt(duration, 30)
476 def timeRemaining = duration - (duration * (percentComplete / 100))
477 def timeRemainingEventData = [
478 name: "timeRemaining",
479 value: displayableTime(timeRemaining),
480 displayed: displayed,
483 sendControllerEvent(timeRemainingEventData)
486 def sendControllerEvent(eventData) {
487 def controller = getController()
489 controller.controllerEvent(eventData)
493 def getController() {
494 def dni = state.controllerDni
496 log.warn "no controller dni"
499 def controller = getChildDevice(dni)
501 log.warn "no controller"
504 log.debug "controller: ${controller}"
508 // ========================================================
510 // ========================================================
513 private increment() {
515 if (!atomicState.running) {
519 def percentComplete = completionPercentage()
521 if (percentComplete > 99) {
525 updateDimmers(percentComplete)
527 if (percentComplete < 99) {
529 def runAgain = stepDuration()
530 log.debug "Rescheduling to run again in ${runAgain} seconds"
532 //runIn(runAgain, 'increment', [overwrite: true])
536 int completionDelay = completionDelaySeconds()
537 if (completionDelay) {
538 log.debug "Finished with steps. Scheduling completion for ${completionDelay} second(s) from now"
539 runIn(completionDelay, 'completion', [overwrite: true])
540 unschedule("healthCheck")
541 // don't let the health check start incrementing again while we wait for the delayed execution of completion
543 log.debug "Finished with steps. Execution completion"
551 def updateDimmers(percentComplete) {
552 dimmers.each { dimmer ->
554 def nextLevel = dynamicLevel(dimmer, percentComplete)
556 if (nextLevel == 0) {
562 def shouldChangeColors = (colorize && colorize != "false")
564 if (shouldChangeColors/*&& hasSetColorCommand(dimmer)*/) {
565 def hue = getHue(dimmer, nextLevel)
566 log.debug "Setting ${deviceLabel(dimmer)} level to ${nextLevel} and hue to ${hue}"
567 dimmer.setColor([hue: hue, saturation: 100, level: nextLevel])
569 log.debug "Setting ${deviceLabel(dimmer)} level to ${nextLevel}"
570 dimmer.setLevel(nextLevel)
575 sendTimeRemainingEvent(percentComplete)
578 int dynamicLevel(dimmer, percentComplete) {
579 def start = atomicState.startLevels[dimmer.id]
580 def end = dynamicEndLevel()
582 if (!percentComplete) {
586 def totalDiff = end - start
587 def actualPercentage = percentComplete / 100
588 def percentOfTotalDiff = totalDiff * actualPercentage
590 (start + percentOfTotalDiff) as int
593 // ========================================================
595 // ========================================================
597 private completion() {
598 log.trace "Starting completion block"
600 if (!atomicState.running) {
606 handleCompletionSwitches()
608 handleCompletionMessaging()
610 handleCompletionModesAndPhrases()
613 private handleCompletionSwitches() {
614 completionSwitches.each { completionSwitch ->
616 def isDimmer = hasSetLevelCommand(completionSwitch)
618 if (completionSwitchesLevel && isDimmer) {
619 completionSwitch.setLevel(completionSwitchesLevel)
621 def command = completionSwitchesState ?: "on"
622 completionSwitch."${command}"()
627 private handleCompletionMessaging() {
628 if (completionMessage) {
629 if (location.contactBookEnabled) {
630 sendNotificationToContacts(completionMessage, recipients)
632 if (completionPhoneNumber) {
633 sendSms(completionPhoneNumber, completionMessage)
635 if (completionPush) {
636 sendPush(completionMessage)
639 if (completionMusicPlayer) {
640 speak(completionMessage)
645 private handleCompletionModesAndPhrases() {
647 if (completionMode) {
648 setLocationMode(completionMode)
651 if (completionPhrase) {
652 location.helloHome.execute(completionPhrase)
658 def sound = textToSpeech(message)
659 def soundDuration = (sound.duration as Integer) + 2
660 log.debug "Playing $sound.uri"
661 completionMusicPlayer.playTrack(sound.uri)
662 log.debug "Scheduled resume in $soundDuration sec"
663 runIn(soundDuration, resumePlaying, [overwrite: true])
666 def resumePlaying() {
667 log.trace "resumePlaying()"
668 def sonos = completionMusicPlayer
670 def currentTrack = sonos.currentState("trackData").jsonValue
671 if (currentTrack.status == "playing") {
672 sonos.playTrack(currentTrack)
674 sonos.setTrack(currentTrack)
679 // ========================================================
681 // ========================================================
683 def setLevelsInState() {
684 def startLevels = [:]
685 dimmers.each { dimmer ->
686 if (usesOldSettings()) {
687 startLevels[dimmer.id] = defaultStart()
688 } else if (hasStartLevel()) {
689 startLevels[dimmer.id] = startLevel
691 def dimmerIsOff = dimmer.currentValue("switch") == "off"
692 startLevels[dimmer.id] = dimmerIsOff ? 0 : dimmer.currentValue("level")
696 atomicState.startLevels = startLevels
699 def canStartAutomatically() {
701 def today = new Date().format("EEEE")
702 log.debug "today: ${today}, days: ${days}"
704 //if (!days || days.contains(today)) {// if no days, assume every day
708 log.trace "should not run"
712 def completionPercentage() {
713 log.trace "checkingTime"
715 if (!atomicState.running) {
719 def now = new Date().getTime()
720 def timeElapsed = now - atomicState.start
721 def totalRunTime = totalRunTimeMillis() ?: 1
722 def percentComplete = timeElapsed / totalRunTime * 100
723 log.debug "percentComplete: ${percentComplete}"
725 return percentComplete
728 int totalRunTimeMillis() {
729 int minutes = sanitizeInt(duration, 30)
730 convertToMillis(minutes)
733 int convertToMillis(minutes) {
734 def seconds = minutes * 60
735 def millis = seconds * 1000
739 def timeRemaining(percentComplete) {
740 def normalizedPercentComplete = percentComplete / 100
741 def duration = sanitizeInt(duration, 30)
742 def timeElapsed = duration * normalizedPercentComplete
743 def timeRemaining = duration - timeElapsed
747 int millisToEnd(percentComplete) {
748 convertToMillis(timeRemaining(percentComplete))
751 String displayableTime(timeRemaining) {
752 def timeString = "${timeRemaining}"
753 def parts = timeString.split(/\./)
757 def minutes = parts[0]
758 if (parts.size() == 1) {
759 return "${minutes}:00"
761 def fraction = "0.${parts[1]}" as double
762 def seconds = "${60 * fraction as int}".padLeft(2, "0")
763 return "${minutes}:${seconds}"
766 def jumpTo(percentComplete) {
767 def millisToEnd = millisToEnd(percentComplete)
768 def endTime = new Date().getTime() + millisToEnd
769 def duration = sanitizeInt(duration, 30)
770 def durationMillis = convertToMillis(duration)
771 def shiftedStart = endTime - durationMillis
772 atomicState.start = shiftedStart
773 updateDimmers(percentComplete)
774 sendTimeRemainingEvent(percentComplete)
778 int dynamicEndLevel() {
779 if (usesOldSettings()) {
780 if (direction && direction == "Down") {
785 return endLevel as int
788 def getHue(dimmer, level) {
789 def start = atomicState.startLevels[dimmer.id] as int
790 def end = dynamicEndLevel()
792 return getDownHue(level)
794 return getUpHue(level)
798 def getUpHue(level) {
802 def getDownHue(level) {
806 private getBlueHue(level) {
807 if (level < 5) return 72
808 if (level < 10) return 71
809 if (level < 15) return 70
810 if (level < 20) return 69
811 if (level < 25) return 68
812 if (level < 30) return 67
813 if (level < 35) return 66
814 if (level < 40) return 65
815 if (level < 45) return 64
816 if (level < 50) return 63
817 if (level < 55) return 62
818 if (level < 60) return 61
819 if (level < 65) return 60
820 if (level < 70) return 59
821 if (level < 75) return 58
822 if (level < 80) return 57
823 if (level < 85) return 56
824 if (level < 90) return 55
825 if (level < 95) return 54
826 if (level >= 95) return 53
829 private getRedHue(level) {
830 if (level < 6) return 1
831 if (level < 12) return 2
832 if (level < 18) return 3
833 if (level < 24) return 4
834 if (level < 30) return 5
835 if (level < 36) return 6
836 if (level < 42) return 7
837 if (level < 48) return 8
838 if (level < 54) return 9
839 if (level < 60) return 10
840 if (level < 66) return 11
841 if (level < 72) return 12
842 if (level < 78) return 13
843 if (level < 84) return 14
844 if (level < 90) return 15
845 if (level < 96) return 16
846 if (level >= 96) return 17
849 private dimmersContainUnsupportedDevices() {
850 def found = dimmers.find { hasSetLevelCommand(it) == false }
854 private hasSetLevelCommand(device) {
855 return hasCommand(device, "setLevel")
858 private hasSetColorCommand(device) {
859 return hasCommand(device, "setColor")
862 private hasCommand(device, String command) {
863 return (device.supportedCommands.find { it.name == command } != null)
866 private dimmersWithSetColorCommand() {
867 def colorDimmers = []
868 dimmers.each { dimmer ->
869 //if (hasSetColorCommand(dimmer)) {
870 colorDimmers << dimmer
876 private int sanitizeInt(i, int defaultValue = 0) {
884 catch (Exception e) {
890 private completionDelaySeconds() {
891 int completionDelayMinutes = sanitizeInt(completionDelay)
892 int completionDelaySeconds = (completionDelayMinutes * 60)
893 return completionDelaySeconds ?: 0
896 private stepDuration() {
897 int minutes = sanitizeInt(duration, 30)
898 int stepDuration = (minutes * 60) / 100
899 return stepDuration ?: 1
902 private debug(message) {
903 log.debug "${message}\nstate: ${state}"
906 public smartThingsDateFormat() { "yyyy-MM-dd'T'HH:mm:ss.SSSZ" }
908 public humanReadableStartDate() {
909 new Date().parse(smartThingsDateFormat(), startTime).format("h:mm a", timeZone(startTime))
912 def fancyString(listOfStrings) {
914 def fancify = { list ->
915 return list.collect {
917 if (list.size() > 1 && it == list[-1]) {
918 label = "and ${label}"
924 return fancify(listOfStrings)
927 def fancyDeviceString(devices = []) {
928 fancyString(devices.collect { deviceLabel(it) })
931 def deviceLabel(device) {
932 return device.label ?: device.name
935 def schedulingHrefDescription() {
937 def descriptionParts = []
939 if (days == weekdays()) {
940 descriptionParts << "On weekdays,"
941 } else if (days == weekends()) {
942 descriptionParts << "On weekends,"
944 descriptionParts << "On ${fancyString(days)},"
948 descriptionParts << "${fancyDeviceString(dimmers)} will start dimming"
951 descriptionParts << "at ${humanReadableStartDate()}"
956 descriptionParts << "or"
958 descriptionParts << "when ${location.name} enters '${modeStart}' mode"
961 if (descriptionParts.size() <= 1) {
962 // dimmers will be in the list no matter what. No rules are set if only dimmers are in the list
966 return descriptionParts.join(" ")
969 def completionHrefDescription() {
971 def descriptionParts = []
972 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"
974 if (completionSwitches) {
975 def switchesList = []
979 completionSwitches.each {
980 def isDimmer = completionSwitchesLevel ? hasSetLevelCommand(it) : false
983 dimmersList << deviceLabel(it)
987 switchesList << deviceLabel(it)
993 descriptionParts << "${fancyString(switchesList)} will be turned ${completionSwitchesState ?: 'on'}."
997 descriptionParts << "${fancyString(dimmersList)} will be dimmed to ${completionSwitchesLevel}%."
1002 if (completionMessage && (completionPhoneNumber || completionPush || completionMusicPlayer)) {
1003 def messageParts = []
1005 if (completionMusicPlayer) {
1006 messageParts << "spoken"
1008 if (completionPhoneNumber) {
1009 messageParts << "sent as a text"
1011 if (completionPush) {
1012 messageParts << "sent as a push notification"
1015 descriptionParts << "The message '${completionMessage}' will be ${fancyString(messageParts)}."
1018 if (completionMode) {
1019 descriptionParts << "The mode will be changed to '${completionMode}'."
1022 if (completionPhrase) {
1023 descriptionParts << "The phrase '${completionPhrase}' will be executed."
1026 return descriptionParts.join(" ")
1029 def numbersPageHrefDescription() {
1030 def title = "All dimmers will dim for ${duration ?: '30'} minutes from ${startLevelLabel()} to ${endLevelLabel()}"
1032 def colorDimmers = dimmersWithSetColorCommand()
1033 if (colorDimmers == dimmers) {
1034 title += " and will gradually change color."
1036 title += ".\n${fancyDeviceString(colorDimmers)} will gradually change color."
1042 def hueSatToHex(h, s) {
1043 def convertedRGB = hslToRgb(h, s, 0.5)
1044 return rgbToHex(convertedRGB)
1047 def hslToRgb(h, s, l) {
1051 r = g = b = l; // achromatic
1053 def hue2rgb = { p, q, t ->
1056 if (t < 1 / 6) return p + (q - p) * 6 * t;
1057 if (t < 1 / 2) return q;
1058 if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
1062 def q = l < 0.5 ? l * (1 + s) : l + s - l * s;
1065 r = hue2rgb(p, q, h + 1 / 3);
1066 g = hue2rgb(p, q, h);
1067 b = hue2rgb(p, q, h - 1 / 3);
1070 return [r * 255, g * 255, b * 255];
1073 def rgbToHex(red, green, blue) {
1076 n = Math.max(0, Math.min(n, 255));
1077 def hexOptions = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"]
1079 def firstDecimal = ((n - n % 16) / 16) as int
1080 def secondDecimal = (n % 16) as int
1082 return "${hexOptions[firstDecimal]}${hexOptions[secondDecimal]}"
1085 def rgbToHex = { r, g, b ->
1086 return toHex(r) + toHex(g) + toHex(b)
1089 return rgbToHex(red, green, blue)
1092 def usesOldSettings() {
1096 def hasStartLevel() {
1097 return (startLevel != null && startLevel != "")
1101 return (endLevel != null && endLevel != "")