Update gentle-wake-up.groovy
[smartapps.git] / official / gentle-wake-up.groovy
1 /**
2  *  Copyright 2016 SmartThings
3  *
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:
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
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.
12  *
13  *  Gentle Wake Up
14  *
15  *  Author: Steve Vlaminck
16  *  Date: 2013-03-11
17  *
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).
24  *
25  */
26 definition(
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"
34 )
35
36 preferences {
37         page(name: "rootPage")
38         page(name: "numbersPage")
39         page(name: "schedulingPage")
40         page(name: "completionPage")
41         page(name: "controllerExplanationPage")
42         //page(name: "unsupportedDevicesPage")
43 }
44
45 def rootPage() {
46         dynamicPage(name: "rootPage", title: "", install: true, uninstall: true) {
47
48                 section("What to dim") {
49                         input(name: "dimmers", type: "capability.switchLevel", title: "Dimmers", description: null, multiple: true, required: true, submitOnChange: true)
50                         if (dimmers) {
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)
53                                 }*/
54                                 href(name: "toNumbersPage", page: "numbersPage", title: "Duration & Direction", state: "complete")
55                         }
56                 }
57
58                 if (dimmers) {
59
60                         section("Gentle Wake Up Has A Controller") {
61                                 href(title: "Learn how to control Gentle Wake Up", page: "controllerExplanationPage", description: null)
62                         }
63
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")
68                         }
69
70                         section {
71                                 // TODO: fancy label
72                                 label(title: "Label This SmartApp", required: false, defaultValue: "", description: "Highly recommended", submitOnChange: true)
73                         }
74                 }
75         }
76 }
77
78 def unsupportedDevicesPage() {
79
80         def unsupportedDimmers = dimmers.findAll { !hasSetLevelCommand(it) }
81
82         dynamicPage(name: "unsupportedDevicesPage") {
83                 if (unsupportedDimmers) {
84                         section("These devices do not support the setLevel command") {
85                                 unsupportedDimmers.each {
86                                         paragraph deviceLabel(it)
87                                 }
88                         }
89                         /*section {
90                                 input(name: "dimmers", type: "capability.sensor", title: "Please remove the above devices from this list.", submitOnChange: true, multiple: true)
91                         }*/
92                         section {
93                                 paragraph "If you think there is a mistake here, please contact support."
94                         }
95                 } else {
96                         section {
97                                 paragraph "You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)"
98                         }
99                 }
100         }
101 }
102
103 def controllerExplanationPage() {
104         dynamicPage(name: "controllerExplanationPage", title: "How To Control Gentle Wake Up") {
105
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."
110                 }
111
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."
123                 }
124
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"
128                 }
129
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. :)"
135                 }
136         }
137 }
138
139 def numbersPage() {
140         dynamicPage(name:"numbersPage", title:"") {
141
142                 section {
143                         paragraph(name: "pGraph", title: "These lights will dim", fancyDeviceString(dimmers))
144                 }
145
146                 section {
147                         input(name: "duration", type: "number", title: "For this many minutes", description: "30", required: false, defaultValue: 30)
148                 }
149
150                 section {
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)
153                 }
154
155                 def colorDimmers = dimmersWithSetColorCommand()
156                 if (colorDimmers) {
157                         section {
158                                 input(name: "colorize", type: "bool", title: "Gradually change the color of ${fancyDeviceString(colorDimmers)}", description: null, required: false, defaultValue: "true")
159                         }
160                 }
161         }
162 }
163
164 def defaultStart() {
165         if (usesOldSettings() && direction && direction == "Down") {
166                 return 99
167         }
168         return 0
169 }
170
171 def defaultEnd() {
172         if (usesOldSettings() && direction && direction == "Down") {
173                 return 0
174         }
175         return 99
176 }
177
178 def startLevelLabel() {
179         if (usesOldSettings()) { // using old settings
180                 if (direction && direction == "Down") { // 99 -> 1
181                         return "99%"
182                 }
183                 return "0%"
184         }
185         return hasStartLevel() ? "${startLevel}%" : "Current Level"
186 }
187
188 def endLevelLabel() {
189         if (usesOldSettings()) {
190                 if (direction && direction == "Down") { // 99 -> 1
191                         return "0%"
192                 }
193                 return "99%"
194         }
195         return "${endLevel}%"
196 }
197
198 def weekdays() {
199         ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
200 }
201
202 def weekends() {
203         ["Saturday", "Sunday"]
204 }
205
206 def schedulingPage() {
207         dynamicPage(name: "schedulingPage", title: "Rules For Automatically Dimming Your Lights") {
208
209                 section("Use Other SmartApps!") {
210                         href(title: "Learn how to control Gentle Wake Up", page: "controllerExplanationPage", description: null)
211                 }
212
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())
215                 }
216
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)
220                         if (modeStart) {
221                                 input(name: "modeStop", title: "Stop when leaving '${modeStart}' mode", type: "bool", required: false)
222                         }
223                 }
224
225         }
226 }
227
228 def completionPage() {
229         dynamicPage(name: "completionPage", title: "Completion Rules") {
230
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)")
236                         }
237                 }
238
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)
243                         }
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)
246                 }
247
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)
251                 }
252
253                 section("Delay") {
254                         input(name: "completionDelay", type: "number", title: "Delay This Many Minutes Before Executing These Actions", description: "0", required: false)
255                 }
256         }
257 }
258
259 // ========================================================
260 // Handlers
261 // ========================================================
262
263 def installed() {
264         log.debug "Installing 'Gentle Wake Up' with settings: ${settings}"
265
266         initialize()
267 }
268
269 def updated() {
270         log.debug "Updating 'Gentle Wake Up' with settings: ${settings}"
271         unschedule()
272
273         def controller = getController()
274         if (controller) {
275                 controller.label = app.label
276         }
277
278         initialize()
279 }
280
281 private initialize() {
282         stop("settingsChange")
283
284         if (startTime) {
285                 log.debug "scheduling dimming routine to run at $startTime"
286                 schedule(startTime, "scheduledStart")
287         }
288
289         // TODO: make this an option
290         subscribe(app, appHandler)
291
292         subscribe(location, locationHandler)
293
294         if (manualOverride) {
295                 subscribe(dimmers, "switch.off", stopDimmersHandler)
296         }
297
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
304         }*/
305 }
306
307 def appHandler(evt) {
308         log.debug "appHandler evt: ${evt.value}"
309         if (evt.value == "touch") {
310                 if (atomicState.running) {
311                         stop("appTouch")
312                 } else {
313                         start("appTouch")
314                 }
315         }
316 }
317
318 def locationHandler(evt) {
319         log.debug "locationHandler evt: ${evt.value}"
320
321         if (!modeStart) {
322                 return
323         }
324
325         def isSpecifiedMode = (evt.value == modeStart)
326         def modeStopIsTrue = (modeStop && modeStop != "false")
327
328         if (isSpecifiedMode && canStartAutomatically()) {
329                 start("modeChange")
330         } else if (!isSpecifiedMode && modeStopIsTrue) {
331                 stop("modeChange")
332         }
333
334 }
335
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"
347                         jumpTo(99)
348                 }
349
350         } else {
351                 log.debug "not stopping in stopDimmersHandler"
352         }
353 }
354
355 // ========================================================
356 // Scheduling
357 // ========================================================
358
359 def scheduledStart() {
360         if (canStartAutomatically()) {
361                 start("schedule")
362         }
363 }
364
365 public def start(source) {
366         log.trace "START"
367
368         sendStartEvent(source)
369
370         setLevelsInState()
371
372         atomicState.running = true
373
374         atomicState.start = new Date().getTime()
375
376         schedule("0 * * * * ?", "healthCheck")
377         increment()
378 }
379
380 public def stop(source) {
381         log.trace "STOP"
382
383         sendStopEvent(source)
384
385         atomicState.running = false
386         atomicState.start = 0
387
388         unschedule("healthCheck")
389 }
390
391 private healthCheck() {
392         log.trace "'Gentle Wake Up' healthCheck"
393
394         if (!atomicState.running) {
395                 return
396         }
397
398         increment()
399 }
400
401 // ========================================================
402 // Controller
403 // ========================================================
404
405 def sendStartEvent(source) {
406         log.trace "sendStartEvent(${source})"
407         def eventData = [
408                         name: "sessionStatus",
409                         value: "running",
410                         descriptionText: "${app.label} has started dimming",
411                         displayed: true,
412                         linkText: app.label,
413                         isStateChange: true
414         ]
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"
423         }
424
425         sendControllerEvent(eventData)
426 }
427
428 def sendStopEvent(source) {
429         log.trace "sendStopEvent(${source})"
430         def eventData = [
431                         name: "sessionStatus",
432                         value: "stopped",
433                         descriptionText: "${app.label} has stopped dimming",
434                         displayed: true,
435                         linkText: app.label,
436                         isStateChange: true
437         ]
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"
455         }
456
457         // send 100% completion event
458         sendTimeRemainingEvent(100)
459
460         // send a non-displayed 0% completion to reset tiles
461         sendTimeRemainingEvent(0, false)
462
463         // send sessionStatus event last so the event feed is ordered properly
464         sendControllerEvent(eventData)
465 }
466
467 def sendTimeRemainingEvent(percentComplete, displayed = true) {
468         log.trace "sendTimeRemainingEvent(${percentComplete})"
469
470         def percentCompleteEventData = [
471                         name: "percentComplete",
472                         value: percentComplete as int,
473                         displayed: displayed,
474                         isStateChange: true
475         ]
476         sendControllerEvent(percentCompleteEventData)
477
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,
484                         isStateChange: true
485         ]
486         sendControllerEvent(timeRemainingEventData)
487 }
488
489 def sendControllerEvent(eventData) {
490         def controller = getController()
491         if (controller) {
492                 controller.controllerEvent(eventData)
493         }
494 }
495
496 def getController() {
497         def dni = state.controllerDni
498         if (!dni) {
499                 log.warn "no controller dni"
500                 return null
501         }
502         def controller = getChildDevice(dni)
503         if (!controller) {
504                 log.warn "no controller"
505                 return null
506         }
507         log.debug "controller: ${controller}"
508         return controller
509 }
510
511 // ========================================================
512 // Setting levels
513 // ========================================================
514
515
516 private increment() {
517
518         if (!atomicState.running) {
519                 return
520         }
521
522         def percentComplete = completionPercentage()
523
524         if (percentComplete > 99) {
525                 percentComplete = 99
526         }
527
528         updateDimmers(percentComplete)
529
530         if (percentComplete < 99) {
531
532                 def runAgain = stepDuration()
533                 log.debug "Rescheduling to run again in ${runAgain} seconds"
534
535                 runIn(runAgain, 'increment', [overwrite: true])
536
537         } else {
538
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
545                 } else {
546                         log.debug "Finished with steps. Execution completion"
547                         completion()
548                 }
549
550         }
551 }
552
553
554 def updateDimmers(percentComplete) {
555         dimmers.each { dimmer ->
556
557                 def nextLevel = dynamicLevel(dimmer, percentComplete)
558
559                 if (nextLevel == 0) {
560
561                         dimmer.off()
562
563                 } else {
564
565                         def shouldChangeColors = (colorize && colorize != "false")
566
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 {
572                                 log.debug "Setting ${deviceLabel(dimmer)} level to ${nextLevel}"
573                                 dimmer.setLevel(nextLevel)
574                         }
575                 }
576         }
577
578         sendTimeRemainingEvent(percentComplete)
579 }
580
581 int dynamicLevel(dimmer, percentComplete) {
582         def start = atomicState.startLevels[dimmer.id]
583         def end = dynamicEndLevel()
584
585         if (!percentComplete) {
586                 return start
587         }
588
589         def totalDiff = end - start
590         def actualPercentage = percentComplete / 100
591         def percentOfTotalDiff = totalDiff * actualPercentage
592
593         (start + percentOfTotalDiff) as int
594 }
595
596 // ========================================================
597 // Completion
598 // ========================================================
599
600 private completion() {
601         log.trace "Starting completion block"
602
603         if (!atomicState.running) {
604                 return
605         }
606
607         stop("schedule")
608
609         handleCompletionSwitches()
610
611         handleCompletionMessaging()
612
613         handleCompletionModesAndPhrases()
614 }
615
616 private handleCompletionSwitches() {
617         completionSwitches.each { completionSwitch ->
618
619                 def isDimmer = hasSetLevelCommand(completionSwitch)
620
621                 if (completionSwitchesLevel && isDimmer) {
622                         completionSwitch.setLevel(completionSwitchesLevel)
623                 } else {
624                         def command = completionSwitchesState ?: "on"
625                         completionSwitch."${command}"()
626                 }
627         }
628 }
629
630 private handleCompletionMessaging() {
631         if (completionMessage) {
632                 if (location.contactBookEnabled) {
633                         sendNotificationToContacts(completionMessage, recipients)
634                 } else {
635                         if (completionPhoneNumber) {
636                                 sendSms(completionPhoneNumber, completionMessage)
637                         }
638                         if (completionPush) {
639                                 sendPush(completionMessage)
640                         }
641                 }
642                 if (completionMusicPlayer) {
643                         speak(completionMessage)
644                 }
645         }
646 }
647
648 private handleCompletionModesAndPhrases() {
649
650         if (completionMode) {
651                 setLocationMode(completionMode)
652         }
653
654         if (completionPhrase) {
655                 location.helloHome.execute(completionPhrase)
656         }
657
658 }
659
660 def speak(message) {
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])
667 }
668
669 def resumePlaying() {
670         log.trace "resumePlaying()"
671         def sonos = completionMusicPlayer
672         if (sonos) {
673                 def currentTrack = sonos.currentState("trackData").jsonValue
674                 if (currentTrack.status == "playing") {
675                         sonos.playTrack(currentTrack)
676                 } else {
677                         sonos.setTrack(currentTrack)
678                 }
679         }
680 }
681
682 // ========================================================
683 // Helpers
684 // ========================================================
685
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
693                 } else {
694                         def dimmerIsOff = dimmer.currentValue("switch") == "off"
695                         startLevels[dimmer.id] = dimmerIsOff ? 0 : dimmer.currentValue("level")
696                 }
697         }
698
699         atomicState.startLevels = startLevels
700 }
701
702 def canStartAutomatically() {
703
704         def today = new Date().format("EEEE")
705         log.debug "today: ${today}, days: ${days}"
706
707         if (!days || days.contains(today)) {// if no days, assume every day
708                 return true
709         }
710
711         log.trace "should not run"
712         return false
713 }
714
715 def completionPercentage() {
716         log.trace "checkingTime"
717
718         if (!atomicState.running) {
719                 return
720         }
721
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}"
727
728         return percentComplete
729 }
730
731 int totalRunTimeMillis() {
732         int minutes = sanitizeInt(duration, 30)
733         convertToMillis(minutes)
734 }
735
736 int convertToMillis(minutes) {
737         def seconds = minutes * 60
738         def millis = seconds * 1000
739         return millis
740 }
741
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
747         return timeRemaining
748 }
749
750 int millisToEnd(percentComplete) {
751         convertToMillis(timeRemaining(percentComplete))
752 }
753
754 String displayableTime(timeRemaining) {
755         def timeString = "${timeRemaining}"
756         def parts = timeString.split(/\./)
757         if (!parts.size()) {
758                 return "0:00"
759         }
760         def minutes = parts[0]
761         if (parts.size() == 1) {
762                 return "${minutes}:00"
763         }
764         def fraction = "0.${parts[1]}" as double
765         def seconds = "${60 * fraction as int}".padLeft(2, "0")
766         return "${minutes}:${seconds}"
767 }
768
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)
778 }
779
780
781 int dynamicEndLevel() {
782         if (usesOldSettings()) {
783                 if (direction && direction == "Down") {
784                         return 0
785                 }
786                 return 99
787         }
788         return endLevel as int
789 }
790
791 def getHue(dimmer, level) {
792         def start = atomicState.startLevels[dimmer.id] as int
793         def end = dynamicEndLevel()
794         if (start > end) {
795                 return getDownHue(level)
796         } else {
797                 return getUpHue(level)
798         }
799 }
800
801 def getUpHue(level) {
802         getBlueHue(level)
803 }
804
805 def getDownHue(level) {
806         getRedHue(level)
807 }
808
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
830 }
831
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
850 }
851
852 private dimmersContainUnsupportedDevices() {
853         def found = dimmers.find { hasSetLevelCommand(it) == false }
854         return found != null
855 }
856
857 private hasSetLevelCommand(device) {
858         return hasCommand(device, "setLevel")
859 }
860
861 private hasSetColorCommand(device) {
862         return hasCommand(device, "setColor")
863 }
864
865 private hasCommand(device, String command) {
866         return (device.supportedCommands.find { it.name == command } != null)
867 }
868
869 private dimmersWithSetColorCommand() {
870         def colorDimmers = []
871         dimmers.each { dimmer ->
872                 //if (hasSetColorCommand(dimmer)) {
873                         colorDimmers << dimmer
874                 //}
875         }
876         return colorDimmers
877 }
878
879 private int sanitizeInt(i, int defaultValue = 0) {
880         try {
881                 if (!i) {
882                         return defaultValue
883                 } else {
884                         return i as int
885                 }
886         }
887         catch (Exception e) {
888                 log.debug e
889                 return defaultValue
890         }
891 }
892
893 private completionDelaySeconds() {
894         int completionDelayMinutes = sanitizeInt(completionDelay)
895         int completionDelaySeconds = (completionDelayMinutes * 60)
896         return completionDelaySeconds ?: 0
897 }
898
899 private stepDuration() {
900         int minutes = sanitizeInt(duration, 30)
901         int stepDuration = (minutes * 60) / 100
902         return stepDuration ?: 1
903 }
904
905 private debug(message) {
906         log.debug "${message}\nstate: ${state}"
907 }
908
909 public smartThingsDateFormat() { "yyyy-MM-dd'T'HH:mm:ss.SSSZ" }
910
911 public humanReadableStartDate() {
912         new Date().parse(smartThingsDateFormat(), startTime).format("h:mm a", timeZone(startTime))
913 }
914
915 def fancyString(listOfStrings) {
916
917         def fancify = { list ->
918                 return list.collect {
919                         def label = it
920                         if (list.size() > 1 && it == list[-1]) {
921                                 label = "and ${label}"
922                         }
923                         label
924                 }.join(", ")
925         }
926
927         return fancify(listOfStrings)
928 }
929
930 def fancyDeviceString(devices = []) {
931         fancyString(devices.collect { deviceLabel(it) })
932 }
933
934 def deviceLabel(device) {
935         return device.label ?: device.name
936 }
937
938 def schedulingHrefDescription() {
939
940         def descriptionParts = []
941         if (days) {
942                 if (days == weekdays()) {
943                         descriptionParts << "On weekdays,"
944                 } else if (days == weekends()) {
945                         descriptionParts << "On weekends,"
946                 } else {
947                         descriptionParts << "On ${fancyString(days)},"
948                 }
949         }
950
951         descriptionParts << "${fancyDeviceString(dimmers)} will start dimming"
952
953         if (startTime) {
954                 descriptionParts << "at ${humanReadableStartDate()}"
955         }
956
957         if (modeStart) {
958                 if (startTime) {
959                         descriptionParts << "or"
960                 }
961                 descriptionParts << "when ${location.name} enters '${modeStart}' mode"
962         }
963
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
966                 return null
967         }
968
969         return descriptionParts.join(" ")
970 }
971
972 def completionHrefDescription() {
973
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"
976
977         if (completionSwitches) {
978                 def switchesList = []
979                 def dimmersList = []
980
981
982                 completionSwitches.each {
983                         def isDimmer = completionSwitchesLevel ? hasSetLevelCommand(it) : false
984
985                         if (isDimmer) {
986                                 dimmersList << deviceLabel(it)
987                         }
988
989                         if (!isDimmer) {
990                                 switchesList << deviceLabel(it)
991                         }
992                 }
993
994
995                 if (switchesList) {
996                         descriptionParts << "${fancyString(switchesList)} will be turned ${completionSwitchesState ?: 'on'}."
997                 }
998
999                 if (dimmersList) {
1000                         descriptionParts << "${fancyString(dimmersList)} will be dimmed to ${completionSwitchesLevel}%."
1001                 }
1002
1003         }
1004
1005         if (completionMessage && (completionPhoneNumber || completionPush || completionMusicPlayer)) {
1006                 def messageParts = []
1007
1008                 if (completionMusicPlayer) {
1009                         messageParts << "spoken"
1010                 }
1011                 if (completionPhoneNumber) {
1012                         messageParts << "sent as a text"
1013                 }
1014                 if (completionPush) {
1015                         messageParts << "sent as a push notification"
1016                 }
1017
1018                 descriptionParts << "The message '${completionMessage}' will be ${fancyString(messageParts)}."
1019         }
1020
1021         if (completionMode) {
1022                 descriptionParts << "The mode will be changed to '${completionMode}'."
1023         }
1024
1025         if (completionPhrase) {
1026                 descriptionParts << "The phrase '${completionPhrase}' will be executed."
1027         }
1028
1029         return descriptionParts.join(" ")
1030 }
1031
1032 def numbersPageHrefDescription() {
1033         def title = "All dimmers will dim for ${duration ?: '30'} minutes from ${startLevelLabel()} to ${endLevelLabel()}"
1034         if (colorize) {
1035                 def colorDimmers = dimmersWithSetColorCommand()
1036                 if (colorDimmers == dimmers) {
1037                         title += " and will gradually change color."
1038                 } else {
1039                         title += ".\n${fancyDeviceString(colorDimmers)} will gradually change color."
1040                 }
1041         }
1042         return title
1043 }
1044
1045 def hueSatToHex(h, s) {
1046         def convertedRGB = hslToRgb(h, s, 0.5)
1047         return rgbToHex(convertedRGB)
1048 }
1049
1050 def hslToRgb(h, s, l) {
1051         def r, g, b;
1052
1053         if (s == 0) {
1054                 r = g = b = l; // achromatic
1055         } else {
1056                 def hue2rgb = { p, q, t ->
1057                         if (t < 0) t += 1;
1058                         if (t > 1) t -= 1;
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;
1062                         return p;
1063                 }
1064
1065                 def q = l < 0.5 ? l * (1 + s) : l + s - l * s;
1066                 def p = 2 * l - q;
1067
1068                 r = hue2rgb(p, q, h + 1 / 3);
1069                 g = hue2rgb(p, q, h);
1070                 b = hue2rgb(p, q, h - 1 / 3);
1071         }
1072
1073         return [r * 255, g * 255, b * 255];
1074 }
1075
1076 def rgbToHex(red, green, blue) {
1077         def toHex = {
1078                 int n = it as int;
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"]
1081
1082                 def firstDecimal = ((n - n % 16) / 16) as int
1083                 def secondDecimal = (n % 16) as int
1084
1085                 return "${hexOptions[firstDecimal]}${hexOptions[secondDecimal]}"
1086         }
1087
1088         def rgbToHex = { r, g, b ->
1089                 return toHex(r) + toHex(g) + toHex(b)
1090         }
1091
1092         return rgbToHex(red, green, blue)
1093 }
1094
1095 def usesOldSettings() {
1096         !hasEndLevel()
1097 }
1098
1099 def hasStartLevel() {
1100         return (startLevel != null && startLevel != "")
1101 }
1102
1103 def hasEndLevel() {
1104         return (endLevel != null && endLevel != "")
1105 }