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