3dfe9fa581678b576ffcb416c0fa37857a9b5568
[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                                 paragraph "If you think there is a mistake here, please contact support."
91                         }
92                 } else {
93                         section {
94                                 paragraph "You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)"
95                         }
96                 }
97         }
98 }
99
100 def controllerExplanationPage() {
101         dynamicPage(name: "controllerExplanationPage", title: "How To Control Gentle Wake Up") {
102
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."
107                 }
108
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."
120                 }
121
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"
125                 }
126
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. :)"
132                 }
133         }
134 }
135
136 def numbersPage() {
137         dynamicPage(name:"numbersPage", title:"") {
138
139                 section {
140                         paragraph(name: "pGraph", title: "These lights will dim", fancyDeviceString(dimmers))
141                 }
142
143                 section {
144                         input(name: "duration", type: "number", title: "For this many minutes", description: "30", required: false, defaultValue: 30)
145                 }
146
147                 section {
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)
150                 }
151
152                 def colorDimmers = dimmersWithSetColorCommand()
153                 if (colorDimmers) {
154                         section {
155                                 input(name: "colorize", type: "bool", title: "Gradually change the color of ${fancyDeviceString(colorDimmers)}", description: null, required: false, defaultValue: "true")
156                         }
157                 }
158         }
159 }
160
161 def defaultStart() {
162         if (usesOldSettings() && direction && direction == "Down") {
163                 return 99
164         }
165         return 0
166 }
167
168 def defaultEnd() {
169         if (usesOldSettings() && direction && direction == "Down") {
170                 return 0
171         }
172         return 99
173 }
174
175 def startLevelLabel() {
176         if (usesOldSettings()) { // using old settings
177                 if (direction && direction == "Down") { // 99 -> 1
178                         return "99%"
179                 }
180                 return "0%"
181         }
182         return hasStartLevel() ? "${startLevel}%" : "Current Level"
183 }
184
185 def endLevelLabel() {
186         if (usesOldSettings()) {
187                 if (direction && direction == "Down") { // 99 -> 1
188                         return "0%"
189                 }
190                 return "99%"
191         }
192         return "${endLevel}%"
193 }
194
195 def weekdays() {
196         ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
197 }
198
199 def weekends() {
200         ["Saturday", "Sunday"]
201 }
202
203 def schedulingPage() {
204         dynamicPage(name: "schedulingPage", title: "Rules For Automatically Dimming Your Lights") {
205
206                 section("Use Other SmartApps!") {
207                         href(title: "Learn how to control Gentle Wake Up", page: "controllerExplanationPage", description: null)
208                 }
209
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())
212                 }
213
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)
217                         if (modeStart) {
218                                 input(name: "modeStop", title: "Stop when leaving '${modeStart}' mode", type: "bool", required: false)
219                         }
220                 }
221
222         }
223 }
224
225 def completionPage() {
226         dynamicPage(name: "completionPage", title: "Completion Rules") {
227
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)")
233                         }
234                 }
235
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)
240                         }
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)
243                 }
244
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)
248                 }
249
250                 section("Delay") {
251                         input(name: "completionDelay", type: "number", title: "Delay This Many Minutes Before Executing These Actions", description: "0", required: false)
252                 }
253         }
254 }
255
256 // ========================================================
257 // Handlers
258 // ========================================================
259
260 def installed() {
261         log.debug "Installing 'Gentle Wake Up' with settings: ${settings}"
262
263         initialize()
264 }
265
266 def updated() {
267         log.debug "Updating 'Gentle Wake Up' with settings: ${settings}"
268         unschedule()
269
270         def controller = getController()
271         if (controller) {
272                 controller.label = app.label
273         }
274
275         initialize()
276 }
277
278 private initialize() {
279         startLevel = 0//Chagne start level to 0 to make it possible for the light to be off!
280         stop("settingsChange")
281
282         if (startTime) {
283                 log.debug "scheduling dimming routine to run at $startTime"
284                 schedule(startTime, "scheduledStart")
285         }
286
287         // TODO: make this an option
288         subscribe(app, appHandler)
289
290         subscribe(location, locationHandler)
291
292         if (manualOverride) {
293                 subscribe(dimmers, "switch.off", stopDimmersHandler)
294         }
295
296         /*if (!getAllChildDevices()) {
297                 // create controller device and set name to the label used here
298                 def dni = "${new Date().getTime()}"
299                 log.debug "app.label: ${app.label}"
300                 addChildDevice("smartthings", "Gentle Wake Up Controller", dni, null, ["label": app.label])
301                 state.controllerDni = dni
302         }*/
303 }
304
305 def appHandler(evt) {
306         log.debug "appHandler evt: ${evt.value}"
307         if (evt.value == "touch") {
308                 if (atomicState.running) {
309                         stop("appTouch")
310                 } else {
311                         start("appTouch")
312                 }
313         }
314 }
315
316 def locationHandler(evt) {
317         log.debug "locationHandler evt: ${evt.value}"
318
319         if (!modeStart) {
320                 return
321         }
322
323         def isSpecifiedMode = (evt.value == modeStart)
324         def modeStopIsTrue = (modeStop && modeStop != "false")
325
326         if (isSpecifiedMode && canStartAutomatically()) {
327                 start("modeChange")
328         } else if (!isSpecifiedMode && modeStopIsTrue) {
329                 stop("modeChange")
330         }
331
332 }
333
334 def stopDimmersHandler(evt) {
335         log.trace "stopDimmersHandler evt: ${evt.value}"
336         def percentComplete = completionPercentage()
337         // Often times, the first thing we do is turn lights on or off so make sure we don't stop as soon as we start
338         if (percentComplete > 2 && percentComplete < 98) {
339                 if (manualOverride == "cancel") {
340                         log.debug "STOPPING in stopDimmersHandler"
341                         stop("manualOverride")
342                 } else if (manualOverride == "jumpTo") {
343                         def end = dynamicEndLevel()
344                         log.debug "Jumping to 99% complete in stopDimmersHandler"
345                         jumpTo(99)
346                 }
347
348         } else {
349                 log.debug "not stopping in stopDimmersHandler"
350         }
351 }
352
353 // ========================================================
354 // Scheduling
355 // ========================================================
356
357 def scheduledStart() {
358         if (canStartAutomatically()) {
359                 start("schedule")
360         }
361 }
362
363 public def start(source) {
364         log.trace "START"
365
366         sendStartEvent(source)
367
368         setLevelsInState()
369
370         atomicState.running = true
371
372         atomicState.start = new Date().getTime()
373
374         schedule("0 * * * * ?", "healthCheck")
375         increment()
376 }
377
378 public def stop(source) {
379         log.trace "STOP"
380
381         sendStopEvent(source)
382
383         atomicState.running = false
384         atomicState.start = 0
385
386         unschedule("healthCheck")
387 }
388
389 private healthCheck() {
390         log.trace "'Gentle Wake Up' healthCheck"
391
392         if (!atomicState.running) {
393                 return
394         }
395
396         increment()
397 }
398
399 // ========================================================
400 // Controller
401 // ========================================================
402
403 def sendStartEvent(source) {
404         log.trace "sendStartEvent(${source})"
405         def eventData = [
406                         name: "sessionStatus",
407                         value: "running",
408                         descriptionText: "${app.label} has started dimming",
409                         displayed: true,
410                         linkText: app.label,
411                         isStateChange: true
412         ]
413         if (source == "modeChange") {
414                 eventData.descriptionText += " because of a mode change"
415         } else if (source == "schedule") {
416                 eventData.descriptionText += " as scheduled"
417         } else if (source == "appTouch") {
418                 eventData.descriptionText += " because you pressed play on the app"
419         } else if (source == "controller") {
420                 eventData.descriptionText += " because you pressed play on the controller"
421         }
422
423         sendControllerEvent(eventData)
424 }
425
426 def sendStopEvent(source) {
427         log.trace "sendStopEvent(${source})"
428         def eventData = [
429                         name: "sessionStatus",
430                         value: "stopped",
431                         descriptionText: "${app.label} has stopped dimming",
432                         displayed: true,
433                         linkText: app.label,
434                         isStateChange: true
435         ]
436         if (source == "modeChange") {
437                 eventData.descriptionText += " because of a mode change"
438                 eventData.value += "cancelled"
439         } else if (source == "schedule") {
440                 eventData.descriptionText = "${app.label} has finished dimming"
441         } else if (source == "appTouch") {
442                 eventData.descriptionText += " because you pressed play on the app"
443                 eventData.value += "cancelled"
444         } else if (source == "controller") {
445                 eventData.descriptionText += " because you pressed stop on the controller"
446                 eventData.value += "cancelled"
447         } else if (source == "settingsChange") {
448                 eventData.descriptionText += " because the settings have changed"
449                 eventData.value += "cancelled"
450         } else if (source == "manualOverride") {
451                 eventData.descriptionText += " because the dimmer was manually turned off"
452                 eventData.value += "cancelled"
453         }
454
455         // send 100% completion event
456         sendTimeRemainingEvent(100)
457
458         // send a non-displayed 0% completion to reset tiles
459         sendTimeRemainingEvent(0, false)
460
461         // send sessionStatus event last so the event feed is ordered properly
462         sendControllerEvent(eventData)
463 }
464
465 def sendTimeRemainingEvent(percentComplete, displayed = true) {
466         log.trace "sendTimeRemainingEvent(${percentComplete})"
467
468         def percentCompleteEventData = [
469                         name: "percentComplete",
470                         value: percentComplete as int,
471                         displayed: displayed,
472                         isStateChange: true
473         ]
474         sendControllerEvent(percentCompleteEventData)
475
476         def duration = sanitizeInt(duration, 30)
477         def timeRemaining = duration - (duration * (percentComplete / 100))
478         def timeRemainingEventData = [
479                         name: "timeRemaining",
480                         value: displayableTime(timeRemaining),
481                         displayed: displayed,
482                         isStateChange: true
483         ]
484         sendControllerEvent(timeRemainingEventData)
485 }
486
487 def sendControllerEvent(eventData) {
488         def controller = getController()
489         if (controller) {
490                 controller.controllerEvent(eventData)
491         }
492 }
493
494 def getController() {
495         def dni = state.controllerDni
496         if (!dni) {
497                 log.warn "no controller dni"
498                 return null
499         }
500         def controller = getChildDevice(dni)
501         if (!controller) {
502                 log.warn "no controller"
503                 return null
504         }
505         log.debug "controller: ${controller}"
506         return controller
507 }
508
509 // ========================================================
510 // Setting levels
511 // ========================================================
512
513
514 private increment() {
515
516         if (!atomicState.running) {
517                 return
518         }
519
520         def percentComplete = completionPercentage()
521
522         if (percentComplete > 99) {
523                 percentComplete = 99
524         }
525
526         updateDimmers(percentComplete)
527
528         if (percentComplete < 99) {
529
530                 def runAgain = stepDuration()
531                 log.debug "Rescheduling to run again in ${runAgain} seconds"
532
533                 //runIn(runAgain, 'increment', [overwrite: true])
534
535         } else {
536
537                 int completionDelay = completionDelaySeconds()
538                 if (completionDelay) {
539                         log.debug "Finished with steps. Scheduling completion for ${completionDelay} second(s) from now"
540                         runIn(completionDelay, 'completion', [overwrite: true])
541                         unschedule("healthCheck")
542                         // don't let the health check start incrementing again while we wait for the delayed execution of completion
543                 } else {
544                         log.debug "Finished with steps. Execution completion"
545                         completion()
546                 }
547
548         }
549 }
550
551
552 def updateDimmers(percentComplete) {
553         dimmers.each { dimmer ->
554
555                 def nextLevel = dynamicLevel(dimmer, percentComplete)
556
557                 if (nextLevel == 0) {
558
559                         dimmer.off()
560
561                 } else {
562
563                         def shouldChangeColors = (colorize && colorize != "false")
564
565                         if (shouldChangeColors/*&& hasSetColorCommand(dimmer)*/) {
566                                 def hue = getHue(dimmer, nextLevel)
567                                 log.debug "Setting ${deviceLabel(dimmer)} level to ${nextLevel} and hue to ${hue}"
568                                 dimmer.setColor([hue: hue, saturation: 100, level: nextLevel])
569                         } else {
570                                 log.debug "Setting ${deviceLabel(dimmer)} level to ${nextLevel}"
571                                 dimmer.setLevel(nextLevel)
572                         }
573                 }
574         }
575
576         sendTimeRemainingEvent(percentComplete)
577 }
578
579 int dynamicLevel(dimmer, percentComplete) {
580         def start = atomicState.startLevels[dimmer.id]
581         def end = dynamicEndLevel()
582
583         if (!percentComplete) {
584                 return start
585         }
586
587         def totalDiff = end - start
588         def actualPercentage = percentComplete / 100
589         def percentOfTotalDiff = totalDiff * actualPercentage
590
591         (start + percentOfTotalDiff) as int
592 }
593
594 // ========================================================
595 // Completion
596 // ========================================================
597
598 private completion() {
599         log.trace "Starting completion block"
600
601         if (!atomicState.running) {
602                 return
603         }
604
605         stop("schedule")
606
607         handleCompletionSwitches()
608
609         handleCompletionMessaging()
610
611         handleCompletionModesAndPhrases()
612 }
613
614 private handleCompletionSwitches() {
615         completionSwitches.each { completionSwitch ->
616
617                 def isDimmer = hasSetLevelCommand(completionSwitch)
618
619                 if (completionSwitchesLevel && isDimmer) {
620                         completionSwitch.setLevel(completionSwitchesLevel)
621                 } else {
622                         def command = completionSwitchesState ?: "on"
623                         completionSwitch."${command}"()
624                 }
625         }
626 }
627
628 private handleCompletionMessaging() {
629         if (completionMessage) {
630                 if (location.contactBookEnabled) {
631                         sendNotificationToContacts(completionMessage, recipients)
632                 } else {
633                         if (completionPhoneNumber) {
634                                 sendSms(completionPhoneNumber, completionMessage)
635                         }
636                         if (completionPush) {
637                                 sendPush(completionMessage)
638                         }
639                 }
640                 if (completionMusicPlayer) {
641                         speak(completionMessage)
642                 }
643         }
644 }
645
646 private handleCompletionModesAndPhrases() {
647
648         if (completionMode) {
649                 setLocationMode(completionMode)
650         }
651
652         if (completionPhrase) {
653                 location.helloHome.execute(completionPhrase)
654         }
655
656 }
657
658 def speak(message) {
659         def sound = textToSpeech(message)
660         def soundDuration = (sound.duration as Integer) + 2
661         log.debug "Playing $sound.uri"
662         completionMusicPlayer.playTrack(sound.uri)
663         log.debug "Scheduled resume in $soundDuration sec"
664         runIn(soundDuration, resumePlaying, [overwrite: true])
665 }
666
667 def resumePlaying() {
668         log.trace "resumePlaying()"
669         def sonos = completionMusicPlayer
670         if (sonos) {
671                 def currentTrack = sonos.currentState("trackData").jsonValue
672                 if (currentTrack.status == "playing") {
673                         sonos.playTrack(currentTrack)
674                 } else {
675                         sonos.setTrack(currentTrack)
676                 }
677         }
678 }
679
680 // ========================================================
681 // Helpers
682 // ========================================================
683
684 def setLevelsInState() {
685         def startLevels = [:]
686         dimmers.each { dimmer ->
687                 if (usesOldSettings()) {
688                         startLevels[dimmer.id] = defaultStart()
689                 } else if (hasStartLevel()) {
690                         startLevels[dimmer.id] = startLevel
691                 } else {
692                         def dimmerIsOff = dimmer.currentValue("switch") == "off"
693                         startLevels[dimmer.id] = dimmerIsOff ? 0 : dimmer.currentValue("level")
694                 }
695         }
696
697         atomicState.startLevels = startLevels
698 }
699
700 def canStartAutomatically() {
701
702         def today = new Date().format("EEEE")
703         log.debug "today: ${today}, days: ${days}"
704
705         //if (!days || days.contains(today)) {// if no days, assume every day
706                 return true
707         //}
708
709         log.trace "should not run"
710         return false
711 }
712
713 def completionPercentage() {
714         log.trace "checkingTime"
715
716         if (!atomicState.running) {
717                 return
718         }
719
720         def now = new Date().getTime()
721         def timeElapsed = now - atomicState.start
722         def totalRunTime = totalRunTimeMillis() ?: 1
723         def percentComplete = timeElapsed / totalRunTime * 100
724         log.debug "percentComplete: ${percentComplete}"
725
726         return percentComplete
727 }
728
729 int totalRunTimeMillis() {
730         int minutes = sanitizeInt(duration, 30)
731         convertToMillis(minutes)
732 }
733
734 int convertToMillis(minutes) {
735         def seconds = minutes * 60
736         def millis = seconds * 1000
737         return millis
738 }
739
740 def timeRemaining(percentComplete) {
741         def normalizedPercentComplete = percentComplete / 100
742         def duration = sanitizeInt(duration, 30)
743         def timeElapsed = duration * normalizedPercentComplete
744         def timeRemaining = duration - timeElapsed
745         return timeRemaining
746 }
747
748 int millisToEnd(percentComplete) {
749         convertToMillis(timeRemaining(percentComplete))
750 }
751
752 String displayableTime(timeRemaining) {
753         def timeString = "${timeRemaining}"
754         def parts = timeString.split(/\./)
755         if (!parts.size()) {
756                 return "0:00"
757         }
758         def minutes = parts[0]
759         if (parts.size() == 1) {
760                 return "${minutes}:00"
761         }
762         def fraction = "0.${parts[1]}" as double
763         def seconds = "${60 * fraction as int}".padLeft(2, "0")
764         return "${minutes}:${seconds}"
765 }
766
767 def jumpTo(percentComplete) {
768         def millisToEnd = millisToEnd(percentComplete)
769         def endTime = new Date().getTime() + millisToEnd
770         def duration = sanitizeInt(duration, 30)
771         def durationMillis = convertToMillis(duration)
772         def shiftedStart = endTime - durationMillis
773         atomicState.start = shiftedStart
774         updateDimmers(percentComplete)
775         sendTimeRemainingEvent(percentComplete)
776 }
777
778
779 int dynamicEndLevel() {
780         if (usesOldSettings()) {
781                 if (direction && direction == "Down") {
782                         return 0
783                 }
784                 return 99
785         }
786         return endLevel as int
787 }
788
789 def getHue(dimmer, level) {
790         def start = atomicState.startLevels[dimmer.id] as int
791         def end = dynamicEndLevel()
792         if (start > end) {
793                 return getDownHue(level)
794         } else {
795                 return getUpHue(level)
796         }
797 }
798
799 def getUpHue(level) {
800         getBlueHue(level)
801 }
802
803 def getDownHue(level) {
804         getRedHue(level)
805 }
806
807 private getBlueHue(level) {
808         if (level < 5) return 72
809         if (level < 10) return 71
810         if (level < 15) return 70
811         if (level < 20) return 69
812         if (level < 25) return 68
813         if (level < 30) return 67
814         if (level < 35) return 66
815         if (level < 40) return 65
816         if (level < 45) return 64
817         if (level < 50) return 63
818         if (level < 55) return 62
819         if (level < 60) return 61
820         if (level < 65) return 60
821         if (level < 70) return 59
822         if (level < 75) return 58
823         if (level < 80) return 57
824         if (level < 85) return 56
825         if (level < 90) return 55
826         if (level < 95) return 54
827         if (level >= 95) return 53
828 }
829
830 private getRedHue(level) {
831         if (level < 6) return 1
832         if (level < 12) return 2
833         if (level < 18) return 3
834         if (level < 24) return 4
835         if (level < 30) return 5
836         if (level < 36) return 6
837         if (level < 42) return 7
838         if (level < 48) return 8
839         if (level < 54) return 9
840         if (level < 60) return 10
841         if (level < 66) return 11
842         if (level < 72) return 12
843         if (level < 78) return 13
844         if (level < 84) return 14
845         if (level < 90) return 15
846         if (level < 96) return 16
847         if (level >= 96) return 17
848 }
849
850 private dimmersContainUnsupportedDevices() {
851         def found = dimmers.find { hasSetLevelCommand(it) == false }
852         return found != null
853 }
854
855 private hasSetLevelCommand(device) {
856         return hasCommand(device, "setLevel")
857 }
858
859 private hasSetColorCommand(device) {
860         return hasCommand(device, "setColor")
861 }
862
863 private hasCommand(device, String command) {
864         return (device.supportedCommands.find { it.name == command } != null)
865 }
866
867 private dimmersWithSetColorCommand() {
868         def colorDimmers = []
869         dimmers.each { dimmer ->
870                 //if (hasSetColorCommand(dimmer)) {
871                         colorDimmers << dimmer
872                 //}
873         }
874         return colorDimmers
875 }
876
877 private int sanitizeInt(i, int defaultValue = 0) {
878         try {
879                 if (!i) {
880                         return defaultValue
881                 } else {
882                         return i as int
883                 }
884         }
885         catch (Exception e) {
886                 log.debug e
887                 return defaultValue
888         }
889 }
890
891 private completionDelaySeconds() {
892         int completionDelayMinutes = sanitizeInt(completionDelay)
893         int completionDelaySeconds = (completionDelayMinutes * 60)
894         return completionDelaySeconds ?: 0
895 }
896
897 private stepDuration() {
898         int minutes = sanitizeInt(duration, 30)
899         int stepDuration = (minutes * 60) / 100
900         return stepDuration ?: 1
901 }
902
903 private debug(message) {
904         log.debug "${message}\nstate: ${state}"
905 }
906
907 public smartThingsDateFormat() { "yyyy-MM-dd'T'HH:mm:ss.SSSZ" }
908
909 public humanReadableStartDate() {
910         new Date().parse(smartThingsDateFormat(), startTime).format("h:mm a", timeZone(startTime))
911 }
912
913 def fancyString(listOfStrings) {
914
915         def fancify = { list ->
916                 return list.collect {
917                         def label = it
918                         if (list.size() > 1 && it == list[-1]) {
919                                 label = "and ${label}"
920                         }
921                         label
922                 }.join(", ")
923         }
924
925         return fancify(listOfStrings)
926 }
927
928 def fancyDeviceString(devices = []) {
929         fancyString(devices.collect { deviceLabel(it) })
930 }
931
932 def deviceLabel(device) {
933         return device.label ?: device.name
934 }
935
936 def schedulingHrefDescription() {
937
938         def descriptionParts = []
939         if (days) {
940                 if (days == weekdays()) {
941                         descriptionParts << "On weekdays,"
942                 } else if (days == weekends()) {
943                         descriptionParts << "On weekends,"
944                 } else {
945                         descriptionParts << "On ${fancyString(days)},"
946                 }
947         }
948
949         descriptionParts << "${fancyDeviceString(dimmers)} will start dimming"
950
951         if (startTime) {
952                 descriptionParts << "at ${humanReadableStartDate()}"
953         }
954
955         if (modeStart) {
956                 if (startTime) {
957                         descriptionParts << "or"
958                 }
959                 descriptionParts << "when ${location.name} enters '${modeStart}' mode"
960         }
961
962         if (descriptionParts.size() <= 1) {
963                 // dimmers will be in the list no matter what. No rules are set if only dimmers are in the list
964                 return null
965         }
966
967         return descriptionParts.join(" ")
968 }
969
970 def completionHrefDescription() {
971
972         def descriptionParts = []
973         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
975         if (completionSwitches) {
976                 def switchesList = []
977                 def dimmersList = []
978
979
980                 completionSwitches.each {
981                         def isDimmer = completionSwitchesLevel ? hasSetLevelCommand(it) : false
982
983                         if (isDimmer) {
984                                 dimmersList << deviceLabel(it)
985                         }
986
987                         if (!isDimmer) {
988                                 switchesList << deviceLabel(it)
989                         }
990                 }
991
992
993                 if (switchesList) {
994                         descriptionParts << "${fancyString(switchesList)} will be turned ${completionSwitchesState ?: 'on'}."
995                 }
996
997                 if (dimmersList) {
998                         descriptionParts << "${fancyString(dimmersList)} will be dimmed to ${completionSwitchesLevel}%."
999                 }
1000
1001         }
1002
1003         if (completionMessage && (completionPhoneNumber || completionPush || completionMusicPlayer)) {
1004                 def messageParts = []
1005
1006                 if (completionMusicPlayer) {
1007                         messageParts << "spoken"
1008                 }
1009                 if (completionPhoneNumber) {
1010                         messageParts << "sent as a text"
1011                 }
1012                 if (completionPush) {
1013                         messageParts << "sent as a push notification"
1014                 }
1015
1016                 descriptionParts << "The message '${completionMessage}' will be ${fancyString(messageParts)}."
1017         }
1018
1019         if (completionMode) {
1020                 descriptionParts << "The mode will be changed to '${completionMode}'."
1021         }
1022
1023         if (completionPhrase) {
1024                 descriptionParts << "The phrase '${completionPhrase}' will be executed."
1025         }
1026
1027         return descriptionParts.join(" ")
1028 }
1029
1030 def numbersPageHrefDescription() {
1031         def title = "All dimmers will dim for ${duration ?: '30'} minutes from ${startLevelLabel()} to ${endLevelLabel()}"
1032         if (colorize) {
1033                 def colorDimmers = dimmersWithSetColorCommand()
1034                 if (colorDimmers == dimmers) {
1035                         title += " and will gradually change color."
1036                 } else {
1037                         title += ".\n${fancyDeviceString(colorDimmers)} will gradually change color."
1038                 }
1039         }
1040         return title
1041 }
1042
1043 def hueSatToHex(h, s) {
1044         def convertedRGB = hslToRgb(h, s, 0.5)
1045         return rgbToHex(convertedRGB)
1046 }
1047
1048 def hslToRgb(h, s, l) {
1049         def r, g, b;
1050
1051         if (s == 0) {
1052                 r = g = b = l; // achromatic
1053         } else {
1054                 def hue2rgb = { p, q, t ->
1055                         if (t < 0) t += 1;
1056                         if (t > 1) t -= 1;
1057                         if (t < 1 / 6) return p + (q - p) * 6 * t;
1058                         if (t < 1 / 2) return q;
1059                         if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
1060                         return p;
1061                 }
1062
1063                 def q = l < 0.5 ? l * (1 + s) : l + s - l * s;
1064                 def p = 2 * l - q;
1065
1066                 r = hue2rgb(p, q, h + 1 / 3);
1067                 g = hue2rgb(p, q, h);
1068                 b = hue2rgb(p, q, h - 1 / 3);
1069         }
1070
1071         return [r * 255, g * 255, b * 255];
1072 }
1073
1074 def rgbToHex(red, green, blue) {
1075         def toHex = {
1076                 int n = it as int;
1077                 n = Math.max(0, Math.min(n, 255));
1078                 def hexOptions = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"]
1079
1080                 def firstDecimal = ((n - n % 16) / 16) as int
1081                 def secondDecimal = (n % 16) as int
1082
1083                 return "${hexOptions[firstDecimal]}${hexOptions[secondDecimal]}"
1084         }
1085
1086         def rgbToHex = { r, g, b ->
1087                 return toHex(r) + toHex(g) + toHex(b)
1088         }
1089
1090         return rgbToHex(red, green, blue)
1091 }
1092
1093 def usesOldSettings() {
1094         !hasEndLevel()
1095 }
1096
1097 def hasStartLevel() {
1098         return (startLevel != null && startLevel != "")
1099 }
1100
1101 def hasEndLevel() {
1102         return (endLevel != null && endLevel != "")
1103 }