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