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