Update beacon-control.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: "schedulingPage")
39         page(name: "completionPage")
40         page(name: "numbersPage")
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", description: numbersPageHrefDescription(), 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", description: schedulingHrefDescription() ?: "Set rules for when to start", state: schedulingHrefDescription() ? "complete" : "")
66                                 input(name: "manualOverride", type: "enum", options: ["cancel": "Cancel dimming", "jumpTo": "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", state: completionHrefDescription() ? "complete" : "", description: completionHrefDescription() ?: "Set rules for what to do when dimming completes")
68                         }
69
70                         section {
71                                 // TODO: fancy label
72                                 label(title: "Label This SmartApp", required: false, defaultValue: "", description: "Highly recommended", submitOnChange: true)
73                         }
74                 }
75         }
76 }
77
78 def unsupportedDevicesPage() {
79
80         def unsupportedDimmers = dimmers.findAll { !hasSetLevelCommand(it) }
81
82         dynamicPage(name: "unsupportedDevicesPage") {
83                 if (unsupportedDimmers) {
84                         section("These devices do not support the setLevel command") {
85                                 unsupportedDimmers.each {
86                                         paragraph deviceLabel(it)
87                                 }
88                         }
89                         section {
90                                 input(name: "dimmers", type: "capability.sensor", title: "Please remove the above devices from this list.", submitOnChange: true, multiple: true)
91                         }
92                         section {
93                                 paragraph "If you think there is a mistake here, please contact support."
94                         }
95                 } else {
96                         section {
97                                 paragraph "You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)"
98                         }
99                 }
100         }
101 }
102
103 def controllerExplanationPage() {
104         dynamicPage(name: "controllerExplanationPage", title: "How To Control Gentle Wake Up") {
105
106                 section("With other SmartApps", hideable: true, hidden: false) {
107                         paragraph "When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!"
108                         paragraph "The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!"
109                         paragraph "Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up."
110                 }
111
112                 section("More about the controller", hideable: true, hidden: true) {
113                         paragraph "You can find the controller with your other 'Things'. It will look like this."
114                         image "http://f.cl.ly/items/2O0v0h41301U14042z3i/GentleWakeUpController-tile-stopped.png"
115                         paragraph "You can start and stop Gentle Wake up by tapping the control on the right."
116                         image "http://f.cl.ly/items/3W323J3M1b3K0k0V3X3a/GentleWakeUpController-tile-running.png"
117                         paragraph "If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls."
118                         image "http://f.cl.ly/items/291s3z2I2Q0r2q0x171H/GentleWakeUpController-richTile-stopped.png"
119                         paragraph "The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep."
120                         image "http://f.cl.ly/items/0F0N2G0S3v1q0L0R3J3Y/GentleWakeUpController-richTile-running.png"
121                         paragraph "In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle."
122                         paragraph "Of course, you may also tap the middle to start or stop the dimming cycle at any time."
123                 }
124
125                 section("Starting and stopping the SmartApp itself", hideable: true, hidden: true) {
126                         paragraph "Tap the 'play' button on the SmartApp to start or stop dimming."
127                         image "http://f.cl.ly/items/0R2u1Z2H30393z2I2V3S/GentleWakeUp-appTouch2.png"
128                 }
129
130                 section("Turning off devices while dimming", hideable: true, hidden: true) {
131                         paragraph "It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option."
132                         paragraph "If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings."
133                         paragraph "Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop."
134                         paragraph "That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)"
135                 }
136         }
137 }
138
139 def numbersPage() {
140         dynamicPage(name:"numbersPage", title:"") {
141
142                 section {
143                         paragraph(name: "pGraph", title: "These lights will dim", fancyDeviceString(dimmers))
144                 }
145
146                 section {
147                         input(name: "duration", type: "number", title: "For this many minutes", description: "30", required: false, defaultValue: 30)
148                 }
149
150                 section {
151                         input(name: "startLevel", type: "number", range: "0..99", title: "From this level", defaultValue: defaultStart(), description: "Current Level", required: false, multiple: false)
152                         input(name: "endLevel", type: "number", range: "0..99", title: "To this level", defaultValue: defaultEnd(), description: "Between 0 and 99", required: true, multiple: false)
153                 }
154
155                 def colorDimmers = dimmersWithSetColorCommand()
156                 if (colorDimmers) {
157                         section {
158                                 input(name: "colorize", type: "bool", title: "Gradually change the color of ${fancyDeviceString(colorDimmers)}", description: null, required: false, defaultValue: "true")
159                         }
160                 }
161         }
162 }
163
164 def defaultStart() {
165         if (usesOldSettings() && direction && direction == "Down") {
166                 return 99
167         }
168         return 0
169 }
170
171 def defaultEnd() {
172         if (usesOldSettings() && direction && direction == "Down") {
173                 return 0
174         }
175         return 99
176 }
177
178 def startLevelLabel() {
179         if (usesOldSettings()) { // using old settings
180                 if (direction && direction == "Down") { // 99 -> 1
181                         return "99%"
182                 }
183                 return "0%"
184         }
185         return hasStartLevel() ? "${startLevel}%" : "Current Level"
186 }
187
188 def endLevelLabel() {
189         if (usesOldSettings()) {
190                 if (direction && direction == "Down") { // 99 -> 1
191                         return "0%"
192                 }
193                 return "99%"
194         }
195         return "${endLevel}%"
196 }
197
198 def weekdays() {
199         ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
200 }
201
202 def weekends() {
203         ["Saturday", "Sunday"]
204 }
205
206 def schedulingPage() {
207         dynamicPage(name: "schedulingPage", title: "Rules For Automatically Dimming Your Lights") {
208
209                 section("Use Other SmartApps!") {
210                         href(title: "Learn how to control Gentle Wake Up", page: "controllerExplanationPage", description: null)
211                 }
212
213                 section("Allow Automatic Dimming") {
214                         input(name: "days", type: "enum", title: "On These Days", description: "Every day", required: false, multiple: true, options: weekdays() + weekends())
215                 }
216
217                 section("Start Dimming...") {
218                         input(name: "startTime", type: "time", title: "At This Time", description: null, required: false)
219                         input(name: "modeStart", title: "When Entering This Mode", type: "mode", required: false, mutliple: false, submitOnChange: true, description: null)
220                         if (modeStart) {
221                                 input(name: "modeStop", title: "Stop when leaving '${modeStart}' mode", type: "bool", required: false)
222                         }
223                 }
224
225         }
226 }
227
228 def completionPage() {
229         dynamicPage(name: "completionPage", title: "Completion Rules") {
230
231                 section("Switches") {
232                         input(name: "completionSwitches", type: "capability.switch", title: "Set these switches", description: null, required: false, multiple: true, submitOnChange: true)
233                         if (completionSwitches) {
234                                 input(name: "completionSwitchesState", type: "enum", title: "To", description: null, required: false, multiple: false, options: ["on", "off"], defaultValue: "on")
235                                 input(name: "completionSwitchesLevel", type: "number", title: "Optionally, Set Dimmer Levels To", description: null, required: false, multiple: false, range: "(0..99)")
236                         }
237                 }
238
239                 section("Notifications") {
240                         input("recipients", "contact", title: "Send notifications to", required: false) {
241                                 input(name: "completionPhoneNumber", type: "phone", title: "Text This Number", description: "Phone number", required: false)
242                                 input(name: "completionPush", type: "bool", title: "Send A Push Notification", description: "Phone number", required: false)
243                         }
244                         input(name: "completionMusicPlayer", type: "capability.musicPlayer", title: "Speak Using This Music Player", required: false)
245                         input(name: "completionMessage", type: "text", title: "With This Message", description: null, required: false)
246                 }
247
248                 section("Modes and Phrases") {
249                         input(name: "completionMode", type: "mode", title: "Change ${location.name} Mode To", description: null, required: false)
250                         input(name: "completionPhrase", type: "enum", title: "Execute The Phrase", description: null, required: false, multiple: false, options: location.helloHome.getPhrases().label)
251                 }
252
253                 section("Delay") {
254                         input(name: "completionDelay", type: "number", title: "Delay This Many Minutes Before Executing These Actions", description: "0", required: false)
255                 }
256         }
257 }
258
259 // ========================================================
260 // Handlers
261 // ========================================================
262
263 def installed() {
264         log.debug "Installing 'Gentle Wake Up' with settings: ${settings}"
265
266         initialize()
267 }
268
269 def updated() {
270         log.debug "Updating 'Gentle Wake Up' with settings: ${settings}"
271         unschedule()
272
273         def controller = getController()
274         if (controller) {
275                 controller.label = app.label
276         }
277
278         initialize()
279 }
280
281 private initialize() {
282         stop("settingsChange")
283
284         if (startTime) {
285                 log.debug "scheduling dimming routine to run at $startTime"
286                 schedule(startTime, "scheduledStart")
287         }
288
289         // TODO: make this an option
290         subscribe(app, appHandler)
291
292         subscribe(location, locationHandler)
293
294         if (manualOverride) {
295                 subscribe(dimmers, "switch.off", stopDimmersHandler)
296         }
297
298         if (!getAllChildDevices()) {
299                 // create controller device and set name to the label used here
300                 def dni = "${new Date().getTime()}"
301                 log.debug "app.label: ${app.label}"
302                 addChildDevice("smartthings", "Gentle Wake Up Controller", dni, null, ["label": app.label])
303                 state.controllerDni = dni
304         }
305 }
306
307 def appHandler(evt) {
308         log.debug "appHandler evt: ${evt.value}"
309         if (evt.value == "touch") {
310                 if (atomicState.running) {
311                         stop("appTouch")
312                 } else {
313                         start("appTouch")
314                 }
315         }
316 }
317
318 def locationHandler(evt) {
319         log.debug "locationHandler evt: ${evt.value}"
320
321         if (!modeStart) {
322                 return
323         }
324
325         def isSpecifiedMode = (evt.value == modeStart)
326         def modeStopIsTrue = (modeStop && modeStop != "false")
327
328         if (isSpecifiedMode && canStartAutomatically()) {
329                 start("modeChange")
330         } else if (!isSpecifiedMode && modeStopIsTrue) {
331                 stop("modeChange")
332         }
333
334 }
335
336 def stopDimmersHandler(evt) {
337         log.trace "stopDimmersHandler evt: ${evt.value}"
338         def percentComplete = completionPercentage()
339         // Often times, the first thing we do is turn lights on or off so make sure we don't stop as soon as we start
340         if (percentComplete > 2 && percentComplete < 98) {
341                 if (manualOverride == "cancel") {
342                         log.debug "STOPPING in stopDimmersHandler"
343                         stop("manualOverride")
344                 } else if (manualOverride == "jumpTo") {
345                         def end = dynamicEndLevel()
346                         log.debug "Jumping to 99% complete in stopDimmersHandler"
347                         jumpTo(99)
348                 }
349
350         } else {
351                 log.debug "not stopping in stopDimmersHandler"
352         }
353 }
354
355 // ========================================================
356 // Scheduling
357 // ========================================================
358
359 def scheduledStart() {
360         if (canStartAutomatically()) {
361                 start("schedule")
362         }
363 }
364
365 public def start(source) {
366         log.trace "START"
367
368         sendStartEvent(source)
369
370         setLevelsInState()
371
372         atomicState.running = true
373
374         atomicState.start = new Date().getTime()
375
376         schedule("0 * * * * ?", "healthCheck")
377         increment()
378 }
379
380 public def stop(source) {
381         log.trace "STOP"
382
383         sendStopEvent(source)
384
385         atomicState.running = false
386         atomicState.start = 0
387
388         unschedule("healthCheck")
389 }
390
391 private healthCheck() {
392         log.trace "'Gentle Wake Up' healthCheck"
393
394         if (!atomicState.running) {
395                 return
396         }
397
398         increment()
399 }
400
401 // ========================================================
402 // Controller
403 // ========================================================
404
405 def sendStartEvent(source) {
406         log.trace "sendStartEvent(${source})"
407         def eventData = [
408                         name: "sessionStatus",
409                         value: "running",
410                         descriptionText: "${app.label} has started dimming",
411                         displayed: true,
412                         linkText: app.label,
413                         isStateChange: true
414         ]
415         if (source == "modeChange") {
416                 eventData.descriptionText += " because of a mode change"
417         } else if (source == "schedule") {
418                 eventData.descriptionText += " as scheduled"
419         } else if (source == "appTouch") {
420                 eventData.descriptionText += " because you pressed play on the app"
421         } else if (source == "controller") {
422                 eventData.descriptionText += " because you pressed play on the controller"
423         }
424
425         sendControllerEvent(eventData)
426 }
427
428 def sendStopEvent(source) {
429         log.trace "sendStopEvent(${source})"
430         def eventData = [
431                         name: "sessionStatus",
432                         value: "stopped",
433                         descriptionText: "${app.label} has stopped dimming",
434                         displayed: true,
435                         linkText: app.label,
436                         isStateChange: true
437         ]
438         if (source == "modeChange") {
439                 eventData.descriptionText += " because of a mode change"
440                 eventData.value += "cancelled"
441         } else if (source == "schedule") {
442                 eventData.descriptionText = "${app.label} has finished dimming"
443         } else if (source == "appTouch") {
444                 eventData.descriptionText += " because you pressed play on the app"
445                 eventData.value += "cancelled"
446         } else if (source == "controller") {
447                 eventData.descriptionText += " because you pressed stop on the controller"
448                 eventData.value += "cancelled"
449         } else if (source == "settingsChange") {
450                 eventData.descriptionText += " because the settings have changed"
451                 eventData.value += "cancelled"
452         } else if (source == "manualOverride") {
453                 eventData.descriptionText += " because the dimmer was manually turned off"
454                 eventData.value += "cancelled"
455         }
456
457         // send 100% completion event
458         sendTimeRemainingEvent(100)
459
460         // send a non-displayed 0% completion to reset tiles
461         sendTimeRemainingEvent(0, false)
462
463         // send sessionStatus event last so the event feed is ordered properly
464         sendControllerEvent(eventData)
465 }
466
467 def sendTimeRemainingEvent(percentComplete, displayed = true) {
468         log.trace "sendTimeRemainingEvent(${percentComplete})"
469
470         def percentCompleteEventData = [
471                         name: "percentComplete",
472                         value: percentComplete as int,
473                         displayed: displayed,
474                         isStateChange: true
475         ]
476         sendControllerEvent(percentCompleteEventData)
477
478         def duration = sanitizeInt(duration, 30)
479         def timeRemaining = duration - (duration * (percentComplete / 100))
480         def timeRemainingEventData = [
481                         name: "timeRemaining",
482                         value: displayableTime(timeRemaining),
483                         displayed: displayed,
484                         isStateChange: true
485         ]
486         sendControllerEvent(timeRemainingEventData)
487 }
488
489 def sendControllerEvent(eventData) {
490         def controller = getController()
491         if (controller) {
492                 controller.controllerEvent(eventData)
493         }
494 }
495
496 def getController() {
497         def dni = state.controllerDni
498         if (!dni) {
499                 log.warn "no controller dni"
500                 return null
501         }
502         def controller = getChildDevice(dni)
503         if (!controller) {
504                 log.warn "no controller"
505                 return null
506         }
507         log.debug "controller: ${controller}"
508         return controller
509 }
510
511 // ========================================================
512 // Setting levels
513 // ========================================================
514
515
516 private increment() {
517
518         if (!atomicState.running) {
519                 return
520         }
521
522         def percentComplete = completionPercentage()
523
524         if (percentComplete > 99) {
525                 percentComplete = 99
526         }
527
528         updateDimmers(percentComplete)
529
530         if (percentComplete < 99) {
531
532                 def runAgain = stepDuration()
533                 log.debug "Rescheduling to run again in ${runAgain} seconds"
534
535                 runIn(runAgain, 'increment', [overwrite: true])
536
537         } else {
538
539                 int completionDelay = completionDelaySeconds()
540                 if (completionDelay) {
541                         log.debug "Finished with steps. Scheduling completion for ${completionDelay} second(s) from now"
542                         runIn(completionDelay, 'completion', [overwrite: true])
543                         unschedule("healthCheck")
544                         // don't let the health check start incrementing again while we wait for the delayed execution of completion
545                 } else {
546                         log.debug "Finished with steps. Execution completion"
547                         completion()
548                 }
549
550         }
551 }
552
553
554 def updateDimmers(percentComplete) {
555         dimmers.each { dimmer ->
556
557                 def nextLevel = dynamicLevel(dimmer, percentComplete)
558
559                 if (nextLevel == 0) {
560
561                         dimmer.off()
562
563                 } else {
564
565                         def shouldChangeColors = (colorize && colorize != "false")
566
567                         if (shouldChangeColors && hasSetColorCommand(dimmer)) {
568                                 def hue = getHue(dimmer, nextLevel)
569                                 log.debug "Setting ${deviceLabel(dimmer)} level to ${nextLevel} and hue to ${hue}"
570                                 dimmer.setColor([hue: hue, saturation: 100, level: nextLevel])
571                         } else if (hasSetLevelCommand(dimmer)) {
572                                 log.debug "Setting ${deviceLabel(dimmer)} level to ${nextLevel}"
573                                 dimmer.setLevel(nextLevel)
574                         } else {
575                                 log.warn "${deviceLabel(dimmer)} does not have setColor or setLevel commands."
576                         }
577
578                 }
579         }
580
581         sendTimeRemainingEvent(percentComplete)
582 }
583
584 int dynamicLevel(dimmer, percentComplete) {
585         def start = atomicState.startLevels[dimmer.id]
586         def end = dynamicEndLevel()
587
588         if (!percentComplete) {
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 }
733
734 int totalRunTimeMillis() {
735         int minutes = sanitizeInt(duration, 30)
736         convertToMillis(minutes)
737 }
738
739 int convertToMillis(minutes) {
740         def seconds = minutes * 60
741         def millis = seconds * 1000
742         return millis
743 }
744
745 def timeRemaining(percentComplete) {
746         def normalizedPercentComplete = percentComplete / 100
747         def duration = sanitizeInt(duration, 30)
748         def timeElapsed = duration * normalizedPercentComplete
749         def timeRemaining = duration - timeElapsed
750         return timeRemaining
751 }
752
753 int millisToEnd(percentComplete) {
754         convertToMillis(timeRemaining(percentComplete))
755 }
756
757 String displayableTime(timeRemaining) {
758         def timeString = "${timeRemaining}"
759         def parts = timeString.split(/\./)
760         if (!parts.size()) {
761                 return "0:00"
762         }
763         def minutes = parts[0]
764         if (parts.size() == 1) {
765                 return "${minutes}:00"
766         }
767         def fraction = "0.${parts[1]}" as double
768         def seconds = "${60 * fraction as int}".padLeft(2, "0")
769         return "${minutes}:${seconds}"
770 }
771
772 def jumpTo(percentComplete) {
773         def millisToEnd = millisToEnd(percentComplete)
774         def endTime = new Date().getTime() + millisToEnd
775         def duration = sanitizeInt(duration, 30)
776         def durationMillis = convertToMillis(duration)
777         def shiftedStart = endTime - durationMillis
778         atomicState.start = shiftedStart
779         updateDimmers(percentComplete)
780         sendTimeRemainingEvent(percentComplete)
781 }
782
783
784 int dynamicEndLevel() {
785         if (usesOldSettings()) {
786                 if (direction && direction == "Down") {
787                         return 0
788                 }
789                 return 99
790         }
791         return endLevel as int
792 }
793
794 def getHue(dimmer, level) {
795         def start = atomicState.startLevels[dimmer.id] as int
796         def end = dynamicEndLevel()
797         if (start > end) {
798                 return getDownHue(level)
799         } else {
800                 return getUpHue(level)
801         }
802 }
803
804 def getUpHue(level) {
805         getBlueHue(level)
806 }
807
808 def getDownHue(level) {
809         getRedHue(level)
810 }
811
812 private getBlueHue(level) {
813         if (level < 5) return 72
814         if (level < 10) return 71
815         if (level < 15) return 70
816         if (level < 20) return 69
817         if (level < 25) return 68
818         if (level < 30) return 67
819         if (level < 35) return 66
820         if (level < 40) return 65
821         if (level < 45) return 64
822         if (level < 50) return 63
823         if (level < 55) return 62
824         if (level < 60) return 61
825         if (level < 65) return 60
826         if (level < 70) return 59
827         if (level < 75) return 58
828         if (level < 80) return 57
829         if (level < 85) return 56
830         if (level < 90) return 55
831         if (level < 95) return 54
832         if (level >= 95) return 53
833 }
834
835 private getRedHue(level) {
836         if (level < 6) return 1
837         if (level < 12) return 2
838         if (level < 18) return 3
839         if (level < 24) return 4
840         if (level < 30) return 5
841         if (level < 36) return 6
842         if (level < 42) return 7
843         if (level < 48) return 8
844         if (level < 54) return 9
845         if (level < 60) return 10
846         if (level < 66) return 11
847         if (level < 72) return 12
848         if (level < 78) return 13
849         if (level < 84) return 14
850         if (level < 90) return 15
851         if (level < 96) return 16
852         if (level >= 96) return 17
853 }
854
855 private dimmersContainUnsupportedDevices() {
856         def found = dimmers.find { hasSetLevelCommand(it) == false }
857         return found != null
858 }
859
860 private hasSetLevelCommand(device) {
861         return hasCommand(device, "setLevel")
862 }
863
864 private hasSetColorCommand(device) {
865         return hasCommand(device, "setColor")
866 }
867
868 private hasCommand(device, String command) {
869         return (device.supportedCommands.find { it.name == command } != null)
870 }
871
872 private dimmersWithSetColorCommand() {
873         def colorDimmers = []
874         dimmers.each { dimmer ->
875                 if (hasSetColorCommand(dimmer)) {
876                         colorDimmers << dimmer
877                 }
878         }
879         return colorDimmers
880 }
881
882 private int sanitizeInt(i, int defaultValue = 0) {
883         try {
884                 if (!i) {
885                         return defaultValue
886                 } else {
887                         return i as int
888                 }
889         }
890         catch (Exception e) {
891                 log.debug e
892                 return defaultValue
893         }
894 }
895
896 private completionDelaySeconds() {
897         int completionDelayMinutes = sanitizeInt(completionDelay)
898         int completionDelaySeconds = (completionDelayMinutes * 60)
899         return completionDelaySeconds ?: 0
900 }
901
902 private stepDuration() {
903         int minutes = sanitizeInt(duration, 30)
904         int stepDuration = (minutes * 60) / 100
905         return stepDuration ?: 1
906 }
907
908 private debug(message) {
909         log.debug "${message}\nstate: ${state}"
910 }
911
912 public smartThingsDateFormat() { "yyyy-MM-dd'T'HH:mm:ss.SSSZ" }
913
914 public humanReadableStartDate() {
915         new Date().parse(smartThingsDateFormat(), startTime).format("h:mm a", timeZone(startTime))
916 }
917
918 def fancyString(listOfStrings) {
919
920         def fancify = { list ->
921                 return list.collect {
922                         def label = it
923                         if (list.size() > 1 && it == list[-1]) {
924                                 label = "and ${label}"
925                         }
926                         label
927                 }.join(", ")
928         }
929
930         return fancify(listOfStrings)
931 }
932
933 def fancyDeviceString(devices = []) {
934         fancyString(devices.collect { deviceLabel(it) })
935 }
936
937 def deviceLabel(device) {
938         return device.label ?: device.name
939 }
940
941 def schedulingHrefDescription() {
942
943         def descriptionParts = []
944         if (days) {
945                 if (days == weekdays()) {
946                         descriptionParts << "On weekdays,"
947                 } else if (days == weekends()) {
948                         descriptionParts << "On weekends,"
949                 } else {
950                         descriptionParts << "On ${fancyString(days)},"
951                 }
952         }
953
954         descriptionParts << "${fancyDeviceString(dimmers)} will start dimming"
955
956         if (startTime) {
957                 descriptionParts << "at ${humanReadableStartDate()}"
958         }
959
960         if (modeStart) {
961                 if (startTime) {
962                         descriptionParts << "or"
963                 }
964                 descriptionParts << "when ${location.name} enters '${modeStart}' mode"
965         }
966
967         if (descriptionParts.size() <= 1) {
968                 // dimmers will be in the list no matter what. No rules are set if only dimmers are in the list
969                 return null
970         }
971
972         return descriptionParts.join(" ")
973 }
974
975 def completionHrefDescription() {
976
977         def descriptionParts = []
978         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"
979
980         if (completionSwitches) {
981                 def switchesList = []
982                 def dimmersList = []
983
984
985                 completionSwitches.each {
986                         def isDimmer = completionSwitchesLevel ? hasSetLevelCommand(it) : false
987
988                         if (isDimmer) {
989                                 dimmersList << deviceLabel(it)
990                         }
991
992                         if (!isDimmer) {
993                                 switchesList << deviceLabel(it)
994                         }
995                 }
996
997
998                 if (switchesList) {
999                         descriptionParts << "${fancyString(switchesList)} will be turned ${completionSwitchesState ?: 'on'}."
1000                 }
1001
1002                 if (dimmersList) {
1003                         descriptionParts << "${fancyString(dimmersList)} will be dimmed to ${completionSwitchesLevel}%."
1004                 }
1005
1006         }
1007
1008         if (completionMessage && (completionPhoneNumber || completionPush || completionMusicPlayer)) {
1009                 def messageParts = []
1010
1011                 if (completionMusicPlayer) {
1012                         messageParts << "spoken"
1013                 }
1014                 if (completionPhoneNumber) {
1015                         messageParts << "sent as a text"
1016                 }
1017                 if (completionPush) {
1018                         messageParts << "sent as a push notification"
1019                 }
1020
1021                 descriptionParts << "The message '${completionMessage}' will be ${fancyString(messageParts)}."
1022         }
1023
1024         if (completionMode) {
1025                 descriptionParts << "The mode will be changed to '${completionMode}'."
1026         }
1027
1028         if (completionPhrase) {
1029                 descriptionParts << "The phrase '${completionPhrase}' will be executed."
1030         }
1031
1032         return descriptionParts.join(" ")
1033 }
1034
1035 def numbersPageHrefDescription() {
1036         def title = "All dimmers will dim for ${duration ?: '30'} minutes from ${startLevelLabel()} to ${endLevelLabel()}"
1037         if (colorize) {
1038                 def colorDimmers = dimmersWithSetColorCommand()
1039                 if (colorDimmers == dimmers) {
1040                         title += " and will gradually change color."
1041                 } else {
1042                         title += ".\n${fancyDeviceString(colorDimmers)} will gradually change color."
1043                 }
1044         }
1045         return title
1046 }
1047
1048 def hueSatToHex(h, s) {
1049         def convertedRGB = hslToRgb(h, s, 0.5)
1050         return rgbToHex(convertedRGB)
1051 }
1052
1053 def hslToRgb(h, s, l) {
1054         def r, g, b;
1055
1056         if (s == 0) {
1057                 r = g = b = l; // achromatic
1058         } else {
1059                 def hue2rgb = { p, q, t ->
1060                         if (t < 0) t += 1;
1061                         if (t > 1) t -= 1;
1062                         if (t < 1 / 6) return p + (q - p) * 6 * t;
1063                         if (t < 1 / 2) return q;
1064                         if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
1065                         return p;
1066                 }
1067
1068                 def q = l < 0.5 ? l * (1 + s) : l + s - l * s;
1069                 def p = 2 * l - q;
1070
1071                 r = hue2rgb(p, q, h + 1 / 3);
1072                 g = hue2rgb(p, q, h);
1073                 b = hue2rgb(p, q, h - 1 / 3);
1074         }
1075
1076         return [r * 255, g * 255, b * 255];
1077 }
1078
1079 def rgbToHex(red, green, blue) {
1080         def toHex = {
1081                 int n = it as int;
1082                 n = Math.max(0, Math.min(n, 255));
1083                 def hexOptions = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"]
1084
1085                 def firstDecimal = ((n - n % 16) / 16) as int
1086                 def secondDecimal = (n % 16) as int
1087
1088                 return "${hexOptions[firstDecimal]}${hexOptions[secondDecimal]}"
1089         }
1090
1091         def rgbToHex = { r, g, b ->
1092                 return toHex(r) + toHex(g) + toHex(b)
1093         }
1094
1095         return rgbToHex(red, green, blue)
1096 }
1097
1098 def usesOldSettings() {
1099         !hasEndLevel()
1100 }
1101
1102 def hasStartLevel() {
1103         return (startLevel != null && startLevel != "")
1104 }
1105
1106 def hasEndLevel() {
1107         return (endLevel != null && endLevel != "")
1108 }