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                                 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 != "false")
564                         if (colorize == "false")
565                                 colorize = "true"
566                         else
567                                 colorize = "false"
568
569                         if (shouldChangeColors/*&& hasSetColorCommand(dimmer)*/) {
570                                 def hue = getHue(dimmer, nextLevel)
571                                 log.debug "Setting ${deviceLabel(dimmer)} level to ${nextLevel} and hue to ${hue}"
572                                 dimmer.setColor([hue: hue, saturation: 100, level: nextLevel])
573                         } else {
574                                 log.debug "Setting ${deviceLabel(dimmer)} level to ${nextLevel}"
575                                 dimmer.setLevel(nextLevel)
576                         }
577                 }
578         }
579
580         sendTimeRemainingEvent(percentComplete)
581 }
582
583 int dynamicLevel(dimmer, percentComplete) {
584         def start = atomicState.startLevels[dimmer.id]
585         def end = dynamicEndLevel()
586
587         if (!percentComplete) {
588                 return 77       
589                 //return start
590         }
591
592         def totalDiff = end - start
593         def actualPercentage = percentComplete / 100
594         def percentOfTotalDiff = totalDiff * actualPercentage
595
596         (start + percentOfTotalDiff) as int
597 }
598
599 // ========================================================
600 // Completion
601 // ========================================================
602
603 private completion() {
604         log.trace "Starting completion block"
605
606         if (!atomicState.running) {
607                 return
608         }
609
610         stop("schedule")
611
612         //handleCompletionSwitches()
613
614         //handleCompletionMessaging()
615
616         handleCompletionModesAndPhrases()
617 }
618
619 private handleCompletionSwitches() {
620         completionSwitches.each { completionSwitch ->
621
622                 def isDimmer = hasSetLevelCommand(completionSwitch)
623
624                 if (completionSwitchesLevel && isDimmer) {
625                         completionSwitch.setLevel(completionSwitchesLevel)
626                 } else {
627                         def command = completionSwitchesState ?: "on"
628                         completionSwitch."${command}"()
629                 }
630         }
631 }
632
633 private handleCompletionMessaging() {
634         if (completionMessage) {
635                 if (location.contactBookEnabled) {
636                         sendNotificationToContacts(completionMessage, recipients)
637                 } else {
638                         if (completionPhoneNumber) {
639                                 sendSms(completionPhoneNumber, completionMessage)
640                         }
641                         if (completionPush) {
642                                 sendPush(completionMessage)
643                         }
644                 }
645                 if (completionMusicPlayer) {
646                         speak(completionMessage)
647                 }
648         }
649 }
650
651 private handleCompletionModesAndPhrases() {
652
653         if (completionMode) {
654                 setLocationMode(completionMode)
655         }
656
657         if (completionPhrase) {
658                 location.helloHome.execute(completionPhrase)
659         }
660
661 }
662
663 def speak(message) {
664         def sound = textToSpeech(message)
665         def soundDuration = (sound.duration as Integer) + 2
666         log.debug "Playing $sound.uri"
667         completionMusicPlayer.playTrack(sound.uri)
668         log.debug "Scheduled resume in $soundDuration sec"
669         runIn(soundDuration, resumePlaying, [overwrite: true])
670 }
671
672 def resumePlaying() {
673         log.trace "resumePlaying()"
674         def sonos = completionMusicPlayer
675         if (sonos) {
676                 def currentTrack = sonos.currentState("trackData").jsonValue
677                 if (currentTrack.status == "playing") {
678                         sonos.playTrack(currentTrack)
679                 } else {
680                         sonos.setTrack(currentTrack)
681                 }
682         }
683 }
684
685 // ========================================================
686 // Helpers
687 // ========================================================
688
689 def setLevelsInState() {
690         def startLevels = [:]
691         dimmers.each { dimmer ->
692                 if (usesOldSettings()) {
693                         startLevels[dimmer.id] = defaultStart()
694                 } else if (hasStartLevel()) {
695                         startLevels[dimmer.id] = startLevel
696                 } else {
697                         def dimmerIsOff = dimmer.currentValue("switch") == "off"
698                         startLevels[dimmer.id] = dimmerIsOff ? 0 : dimmer.currentValue("level")
699                 }
700         }
701
702         atomicState.startLevels = startLevels
703 }
704
705 def canStartAutomatically() {
706
707         def today = new Date().format("EEEE")
708         log.debug "today: ${today}, days: ${days}"
709
710         //if (!days || days.contains(today)) {// if no days, assume every day
711                 return true
712         //}
713
714         log.trace "should not run"
715         return false
716 }
717
718 def completionPercentage() {
719         log.trace "checkingTime"
720
721         if (!atomicState.running) {
722                 return
723         }
724
725         def now = new Date().getTime()
726         def timeElapsed = now - atomicState.start
727         def totalRunTime = totalRunTimeMillis() ?: 1
728         def percentComplete = timeElapsed / totalRunTime * 100
729         log.debug "percentComplete: ${percentComplete}"
730
731         //return percentComplete
732         // We do not have the notion of time for model-checking
733         return 0
734 }
735
736 int totalRunTimeMillis() {
737         int minutes = sanitizeInt(duration, 30)
738         convertToMillis(minutes)
739 }
740
741 int convertToMillis(minutes) {
742         def seconds = minutes * 60
743         def millis = seconds * 1000
744         return millis
745 }
746
747 def timeRemaining(percentComplete) {
748         def normalizedPercentComplete = percentComplete / 100
749         def duration = sanitizeInt(duration, 30)
750         def timeElapsed = duration * normalizedPercentComplete
751         def timeRemaining = duration - timeElapsed
752         return timeRemaining
753 }
754
755 int millisToEnd(percentComplete) {
756         convertToMillis(timeRemaining(percentComplete))
757 }
758
759 String displayableTime(timeRemaining) {
760         def timeString = "${timeRemaining}"
761         def parts = timeString.split(/\./)
762         if (!parts.size()) {
763                 return "0:00"
764         }
765         def minutes = parts[0]
766         if (parts.size() == 1) {
767                 return "${minutes}:00"
768         }
769         def fraction = "0.${parts[1]}" as double
770         def seconds = "${60 * fraction as int}".padLeft(2, "0")
771         return "${minutes}:${seconds}"
772 }
773
774 def jumpTo(percentComplete) {
775         def millisToEnd = millisToEnd(percentComplete)
776         def endTime = new Date().getTime() + millisToEnd
777         def duration = sanitizeInt(duration, 30)
778         def durationMillis = convertToMillis(duration)
779         def shiftedStart = endTime - durationMillis
780         atomicState.start = shiftedStart
781         updateDimmers(percentComplete)
782         sendTimeRemainingEvent(percentComplete)
783 }
784
785
786 int dynamicEndLevel() {
787         if (usesOldSettings()) {
788                 if (direction && direction == "Down") {
789                         return 0
790                 }
791                 return 99
792         }
793         return endLevel as int
794 }
795
796 def getHue(dimmer, level) {
797         def start = atomicState.startLevels[dimmer.id] as int
798         def end = dynamicEndLevel()
799         if (start > end) {
800                 return getDownHue(level)
801         } else {
802                 return getUpHue(level)
803         }
804 }
805
806 def getUpHue(level) {
807         getBlueHue(level)
808 }
809
810 def getDownHue(level) {
811         getRedHue(level)
812 }
813
814 private getBlueHue(level) {
815         if (level < 5) return 72
816         if (level < 10) return 71
817         if (level < 15) return 70
818         if (level < 20) return 69
819         if (level < 25) return 68
820         if (level < 30) return 67
821         if (level < 35) return 66
822         if (level < 40) return 65
823         if (level < 45) return 64
824         if (level < 50) return 63
825         if (level < 55) return 62
826         if (level < 60) return 61
827         if (level < 65) return 60
828         if (level < 70) return 59
829         if (level < 75) return 58
830         if (level < 80) return 57
831         if (level < 85) return 56
832         if (level < 90) return 55
833         if (level < 95) return 54
834         if (level >= 95) return 53
835 }
836
837 private getRedHue(level) {
838         if (level < 6) return 1
839         if (level < 12) return 2
840         if (level < 18) return 3
841         if (level < 24) return 4
842         if (level < 30) return 5
843         if (level < 36) return 6
844         if (level < 42) return 7
845         if (level < 48) return 8
846         if (level < 54) return 9
847         if (level < 60) return 10
848         if (level < 66) return 11
849         if (level < 72) return 12
850         if (level < 78) return 13
851         if (level < 84) return 14
852         if (level < 90) return 15
853         if (level < 96) return 16
854         if (level >= 96) return 17
855 }
856
857 private dimmersContainUnsupportedDevices() {
858         def found = dimmers.find { hasSetLevelCommand(it) == false }
859         return found != null
860 }
861
862 private hasSetLevelCommand(device) {
863         return hasCommand(device, "setLevel")
864 }
865
866 private hasSetColorCommand(device) {
867         return hasCommand(device, "setColor")
868 }
869
870 private hasCommand(device, String command) {
871         return (device.supportedCommands.find { it.name == command } != null)
872 }
873
874 private dimmersWithSetColorCommand() {
875         def colorDimmers = []
876         dimmers.each { dimmer ->
877                 //if (hasSetColorCommand(dimmer)) {
878                         colorDimmers << dimmer
879                 //}
880         }
881         return colorDimmers
882 }
883
884 private int sanitizeInt(i, int defaultValue = 0) {
885         try {
886                 if (!i) {
887                         return defaultValue
888                 } else {
889                         return i as int
890                 }
891         }
892         catch (Exception e) {
893                 log.debug e
894                 return defaultValue
895         }
896 }
897
898 private completionDelaySeconds() {
899         int completionDelayMinutes = sanitizeInt(completionDelay)
900         int completionDelaySeconds = (completionDelayMinutes * 60)
901         return completionDelaySeconds ?: 0
902 }
903
904 private stepDuration() {
905         int minutes = sanitizeInt(duration, 30)
906         int stepDuration = (minutes * 60) / 100
907         return stepDuration ?: 1
908 }
909
910 private debug(message) {
911         log.debug "${message}\nstate: ${state}"
912 }
913
914 public smartThingsDateFormat() { "yyyy-MM-dd'T'HH:mm:ss.SSSZ" }
915
916 public humanReadableStartDate() {
917         new Date().parse(smartThingsDateFormat(), startTime).format("h:mm a", timeZone(startTime))
918 }
919
920 def fancyString(listOfStrings) {
921
922         def fancify = { list ->
923                 return list.collect {
924                         def label = it
925                         if (list.size() > 1 && it == list[-1]) {
926                                 label = "and ${label}"
927                         }
928                         label
929                 }.join(", ")
930         }
931
932         return fancify(listOfStrings)
933 }
934
935 def fancyDeviceString(devices = []) {
936         fancyString(devices.collect { deviceLabel(it) })
937 }
938
939 def deviceLabel(device) {
940         return device.label ?: device.name
941 }
942
943 def schedulingHrefDescription() {
944
945         def descriptionParts = []
946         if (days) {
947                 if (days == weekdays()) {
948                         descriptionParts << "On weekdays,"
949                 } else if (days == weekends()) {
950                         descriptionParts << "On weekends,"
951                 } else {
952                         descriptionParts << "On ${fancyString(days)},"
953                 }
954         }
955
956         descriptionParts << "${fancyDeviceString(dimmers)} will start dimming"
957
958         if (startTime) {
959                 descriptionParts << "at ${humanReadableStartDate()}"
960         }
961
962         if (modeStart) {
963                 if (startTime) {
964                         descriptionParts << "or"
965                 }
966                 descriptionParts << "when ${location.name} enters '${modeStart}' mode"
967         }
968
969         if (descriptionParts.size() <= 1) {
970                 // dimmers will be in the list no matter what. No rules are set if only dimmers are in the list
971                 return null
972         }
973
974         return descriptionParts.join(" ")
975 }
976
977 def completionHrefDescription() {
978
979         def descriptionParts = []
980         def example = "Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '<message>' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to '<mode>'. The phrase '<phrase>' will be executed"
981
982         if (completionSwitches) {
983                 def switchesList = []
984                 def dimmersList = []
985
986
987                 completionSwitches.each {
988                         def isDimmer = completionSwitchesLevel ? hasSetLevelCommand(it) : false
989
990                         if (isDimmer) {
991                                 dimmersList << deviceLabel(it)
992                         }
993
994                         if (!isDimmer) {
995                                 switchesList << deviceLabel(it)
996                         }
997                 }
998
999
1000                 if (switchesList) {
1001                         descriptionParts << "${fancyString(switchesList)} will be turned ${completionSwitchesState ?: 'on'}."
1002                 }
1003
1004                 if (dimmersList) {
1005                         descriptionParts << "${fancyString(dimmersList)} will be dimmed to ${completionSwitchesLevel}%."
1006                 }
1007
1008         }
1009
1010         if (completionMessage && (completionPhoneNumber || completionPush || completionMusicPlayer)) {
1011                 def messageParts = []
1012
1013                 if (completionMusicPlayer) {
1014                         messageParts << "spoken"
1015                 }
1016                 if (completionPhoneNumber) {
1017                         messageParts << "sent as a text"
1018                 }
1019                 if (completionPush) {
1020                         messageParts << "sent as a push notification"
1021                 }
1022
1023                 descriptionParts << "The message '${completionMessage}' will be ${fancyString(messageParts)}."
1024         }
1025
1026         if (completionMode) {
1027                 descriptionParts << "The mode will be changed to '${completionMode}'."
1028         }
1029
1030         if (completionPhrase) {
1031                 descriptionParts << "The phrase '${completionPhrase}' will be executed."
1032         }
1033
1034         return descriptionParts.join(" ")
1035 }
1036
1037 def numbersPageHrefDescription() {
1038         def title = "All dimmers will dim for ${duration ?: '30'} minutes from ${startLevelLabel()} to ${endLevelLabel()}"
1039         if (colorize) {
1040                 def colorDimmers = dimmersWithSetColorCommand()
1041                 if (colorDimmers == dimmers) {
1042                         title += " and will gradually change color."
1043                 } else {
1044                         title += ".\n${fancyDeviceString(colorDimmers)} will gradually change color."
1045                 }
1046         }
1047         return title
1048 }
1049
1050 def hueSatToHex(h, s) {
1051         def convertedRGB = hslToRgb(h, s, 0.5)
1052         return rgbToHex(convertedRGB)
1053 }
1054
1055 def hslToRgb(h, s, l) {
1056         def r, g, b;
1057
1058         if (s == 0) {
1059                 r = g = b = l; // achromatic
1060         } else {
1061                 def hue2rgb = { p, q, t ->
1062                         if (t < 0) t += 1;
1063                         if (t > 1) t -= 1;
1064                         if (t < 1 / 6) return p + (q - p) * 6 * t;
1065                         if (t < 1 / 2) return q;
1066                         if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
1067                         return p;
1068                 }
1069
1070                 def q = l < 0.5 ? l * (1 + s) : l + s - l * s;
1071                 def p = 2 * l - q;
1072
1073                 r = hue2rgb(p, q, h + 1 / 3);
1074                 g = hue2rgb(p, q, h);
1075                 b = hue2rgb(p, q, h - 1 / 3);
1076         }
1077
1078         return [r * 255, g * 255, b * 255];
1079 }
1080
1081 def rgbToHex(red, green, blue) {
1082         def toHex = {
1083                 int n = it as int;
1084                 n = Math.max(0, Math.min(n, 255));
1085                 def hexOptions = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"]
1086
1087                 def firstDecimal = ((n - n % 16) / 16) as int
1088                 def secondDecimal = (n % 16) as int
1089
1090                 return "${hexOptions[firstDecimal]}${hexOptions[secondDecimal]}"
1091         }
1092
1093         def rgbToHex = { r, g, b ->
1094                 return toHex(r) + toHex(g) + toHex(b)
1095         }
1096
1097         return rgbToHex(red, green, blue)
1098 }
1099
1100 def usesOldSettings() {
1101         !hasEndLevel()
1102 }
1103
1104 def hasStartLevel() {
1105         return (startLevel != null && startLevel != "")
1106 }
1107
1108 def hasEndLevel() {
1109         return (endLevel != null && endLevel != "")
1110 }