Update gentle-wake-up.groovy
[smartapps.git] / official / lighting-director.groovy
1 /**
2  *  Lighting Director
3  *
4  * Source: https://github.com/tslagle13/SmartThings/blob/master/Director-Series-Apps/Lighting-Director/Lighting%20Director.groovy
5  *
6  *  Current Version: 2.9.4
7  *
8  *
9  *  Changelog:
10  *  Version - 1.3
11  *  Version - 1.30.1 Modification by Michael Struck - Fixed syntax of help text and titles of scenarios, along with a new icon
12  *  Version - 1.40.0 Modification by Michael Struck - Code optimization and added door contact sensor capability                
13  *  Version - 1.41.0 Modification by Michael Struck - Code optimization and added time restrictions to each scenario
14  *  Version - 2.0  Tim Slagle - Moved to only have 4 slots.  Code was to heavy and needed to be trimmed.
15  *  Version - 2.1  Tim Slagle - Moved time interval inputs inline with STs design.
16  *  Version - 2.2  Michael Struck - Added the ability to activate switches via the status locks and fixed some syntax issues
17  *  Version - 2.5  Michael Struck - Changed the way the app unschedules re-triggered events
18  *  Version - 2.5.1 Tim Slagle - Fixed Time Logic
19  *  Version - 2.6 Michael Struck - Added the additional restriction of running triggers once per day and misc cleanup of code
20  *  Version - 2.7 Michael Struck - Added feature that turns off triggering if the physical switch is pressed.
21  *  Version - 2.81 Michael Struck - Fixed an issue with dimmers not stopping light action
22  *  Version - 2.9 Michael Struck - Fixed issue where button presses outside of the time restrictions prevent the triggers from firing and code optimization 
23  *  Version - 2.9.1 Tim Slagle - Further enhanced time interval logic.  
24  *  Version - 2.9.2 Brandon Gordon - Added support for acceleration sensors.
25  *  Version - 2.9.3 Brandon Gordon - Added mode change subscriptions.
26  *  Version - 2.9.4 Michael Struck - Code Optimization when triggers are tripped
27  *
28 *  Source code can be found here: https://github.com/tslagle13/SmartThings/blob/master/smartapps/tslagle13/vacation-lighting-director.groovy
29  *
30  *  Copyright 2015 Tim Slagle and Michael Struck
31  *
32  *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
33  *  in compliance with the License. You may obtain a copy of the License at:
34  *
35  *      http://www.apache.org/licenses/LICENSE-2.0
36  *
37  *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
38  *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
39  *  for the specific language governing permissions and limitations under the License.
40  *
41  */
42  
43 definition(
44     name: "Lighting Director",
45     namespace: "tslagle13",
46     author: "Tim Slagle & Michael Struck",
47     description: "Control up to 4 sets (scenarios) of lights based on motion, door contacts and illuminance levels.",
48     category: "Convenience",
49     iconUrl: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Lighting-Director/LightingDirector.png",
50     iconX2Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Lighting-Director/LightingDirector@2x.png",
51     iconX3Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Lighting-Director/LightingDirector@2x.png")
52
53 preferences {
54     page(name: "timeIntervalInputA", title: "Only during a certain time", refreshAfterSelection:true) {
55                 section {
56                         input "A_timeStart", "time", title: "Starting", required: false, refreshAfterSelection:true
57                         input "A_timeEnd", "time", title: "Ending", required: false, refreshAfterSelection:true
58                 }
59         }
60     page name:"pageSetup"
61     page name:"pageSetupScenarioA"
62 }
63
64 // Show setup page
65 def pageSetup() {
66
67     def pageProperties = [
68         name:       "pageSetup",
69         nextPage:   null,
70         install:    true,
71         uninstall:  true
72     ]
73
74         return dynamicPage(pageProperties) {
75         section("Setup Menu") {
76             href "pageSetupScenarioA", title: getTitle(settings.ScenarioNameA), description: getDesc(settings.ScenarioNameA), state: greyOut(settings.ScenarioNameA)
77             }
78         section([title:"Options", mobileOnly:true]) {
79             label title:"Assign a name", required:false
80         }
81     }
82 }
83
84 // Show "pageSetupScenarioA" page
85 def pageSetupScenarioA() {
86     //input name: "A_switches", type: "capability.switch", title: "Control the following switches...", multiple: true, required: false
87     def inputLightsA = [
88         name:       "A_switches",
89         type:       "capability.switch",
90         title:      "Control the following switches...",
91         multiple:   true,
92         required:   false
93     ]
94     //input name: "A_dimmers", type: "capability.switchLevel", title: "Dim the following...", multiple: true, required: false
95     def inputDimmersA = [
96         name:       "A_dimmers",
97         type:       "capability.switchLevel",
98         title:      "Dim the following...",
99         multiple:   true,
100         required:   false
101     ]
102     //input name: "A_motion", type: "capability.motionSensor", title: "Using these motion sensors...", multiple: true, required: false
103     def inputMotionA = [
104         name:       "A_motion",
105         type:       "capability.motionSensor",
106         title:      "Using these motion sensors...",
107         multiple:   true,
108         required:   false
109     ]
110     //input name: "A_acceleration", type: "capability.accelerationSensor", title: "Or using these acceleration sensors...", multiple: true, required: false
111         def inputAccelerationA = [
112                 name:       "A_acceleration",
113                 type:       "capability.accelerationSensor",
114                 title:      "Or using these acceleration sensors...",
115                 multiple:   true,
116                 required:   false
117         ]
118     //input name: "A_contact", type: "capability.contactSensor", title: "Or using these contact sensors...", multiple: true, required: false
119     def inputContactA = [
120         name:       "A_contact",
121         type:       "capability.contactSensor",
122         title:      "Or using these contact sensors...",
123         multiple:   true,
124         required:   false
125     ]
126     //input name: "A_triggerOnce", type: "bool", title: "Trigger only once per day...", defaultValue: false
127     def inputTriggerOnceA = [
128         name:       "A_triggerOnce",
129         type:       "bool",
130         title:      "Trigger only once per day...",
131         defaultValue:false
132     ]
133     //input name: "A_switchDisable", type: "bool", title: "Stop triggering if physical switches/dimmers are turned off...", defaultValue: false
134     def inputSwitchDisableA = [
135         name:       "A_switchDisable",
136         type:       "bool",
137         title:      "Stop triggering if physical switches/dimmers are turned off...",
138         defaultValue:false
139     ]
140     //input name: "A_lock", type: "capability.lock", title: "Or using these locks....", multiple: true, required: false
141     def inputLockA = [
142         name:       "A_lock",
143         type:       "capability.lock",
144         title:      "Or using these locks...",
145         multiple:   true,
146         required:   false
147     ]
148     //input name: "A_mode", type: "mode", title: "Only during the following modes...", multiple: true, required: false
149     def inputModeA = [
150         name:       "A_mode",
151         type:       "mode",
152         title:      "Only during the following modes...",
153         multiple:   true,
154         required:   false
155     ]
156     //input name: "A_day", type: "enum", options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], title: "Only on certain days of the week...", multiple: true, required: false
157     def inputDayA = [
158         name:       "A_day",
159         type:       "enum",
160         options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
161         title:      "Only on certain days of the week...",
162         multiple:   true,
163         required:   false
164     ]
165     
166     //input name: "A_level", type: "enum", options: [10,20,30,40,50,60,70,80,90,100], title: "Set dimmers to this level", multiple: false, required: false
167     def inputLevelA = [
168         name:       "A_level",
169         type:       "enum",
170         options: [10,20,30,40,50,60,70,80,90,100],
171         title:      "Set dimmers to this level",
172         multiple:   false,
173         required:   false
174     ]
175     
176     //input name: "A_turnOnLux", type: "number", title: "Only run this scenario if lux is below...", multiple: false, required: false
177     def inputTurnOnLuxA = [
178         name:       "A_turnOnLux",
179         type:       "number",
180         title:      "Only run this scenario if lux is below...",
181         multiple:   false,
182         required:   false
183     ]
184     //input name: "A_luxSensors", type: "capability.illuminanceMeasurement", title: "On these lux sensors", multiple: false, required: false
185     def inputLuxSensorsA = [
186         name:       "A_luxSensors",
187         type:       "capability.illuminanceMeasurement",
188         title:      "On these lux sensors",
189         multiple:   false,
190         required:   false
191     ]
192      //input name: "A_turnOff", type: "number", title: "Turn off this scenario after motion stops or doors close/lock (minutes)...", multiple: false, required: false
193     def inputTurnOffA = [
194         name:       "A_turnOff",
195         type:       "number",
196         title:      "Turn off this scenario after motion stops or doors close/lock (minutes)...",
197         multiple:   false,
198         required:   false
199     ]
200     //input name: "ScenarioNameA", type: "text", title: "Scenario Name", multiple: false, required: false
201     def inputScenarioNameA = [
202         name:       "ScenarioNameA",
203         type:       "text",
204         title:      "Scenario Name",
205         multiple:   false,
206         required:   false
207     ]
208     
209     def pageProperties = [
210         name:       "pageSetupScenarioA",
211     ]
212
213     return dynamicPage(pageProperties) {
214 section("Name your scenario") {
215             input inputScenarioNameA
216         }
217
218 section("Devices included in the scenario") {
219             input inputMotionA
220                         input inputAccelerationA
221             input inputContactA
222             input inputLockA
223             input inputLightsA
224             input inputDimmersA
225             }
226
227 section("Scenario settings") {
228             input inputLevelA
229             input inputTurnOnLuxA
230             input inputLuxSensorsA
231             input inputTurnOffA
232             }
233             
234 section("Scenario restrictions") {            
235             input inputTriggerOnceA
236             input inputSwitchDisableA
237             href "timeIntervalInputA", title: "Only during a certain time...", description: getTimeLabel(A_timeStart, A_timeEnd), state: greyedOutTime(A_timeStart, A_timeEnd), refreshAfterSelection:true
238             input inputDayA
239             input inputModeA
240             }
241
242 section("Help") {
243             paragraph helpText()
244             }
245     }
246     
247 }
248
249
250
251 def installed() {
252     initialize()
253 }
254
255 def updated() {
256     unschedule()
257     unsubscribe()
258     initialize()
259 }
260
261 def initialize() {
262
263         midNightReset()
264
265         if(A_motion) {
266                 //subscribe(A_motion, "motion", onEventA)
267                 subscribe(A_motion, "motion", onEventA)
268         }
269
270         if(A_acceleration) {
271                 subscribe(A_acceleration, "acceleration", onEventA)
272         }
273
274         if(A_contact) {
275                 subscribe(A_contact, "contact", onEventA)
276         }
277
278         if(A_lock) {
279                 subscribe(A_lock, "lock", onEventA)
280         }
281
282         if(A_switchDisable) {
283                 subscribe(A_switches, "switch.off", onPressA)
284             subscribe(A_dimmers, "switch.off", onPressA)
285         }
286
287 }
288
289 def onEventA(evt) {
290
291 if (/*(!A_triggerOnce || (A_triggerOnce && !state.A_triggered)) && (!A_switchDisable || (A_switchDisable && !state.A_triggered))*/true) { //Checks to make sure this scenario should be triggered more then once in a day
292 if ((!A_mode || A_mode.contains(location.mode)) && true/*getTimeOk (A_timeStart, A_timeEnd) && getDayOk(A_day)*/) { //checks to make sure we are not opperating outside of set restrictions.
293 if ((!A_luxSensors) || (A_luxSensors.latestValue("illuminance") <= A_turnOnLux)){ //checks to make sure illimunance is either not cared about or if the value is within the restrictions
294 def A_levelOn = A_level as Integer
295
296 //Check states of each device to see if they are to be ignored or if they meet the requirments of the app to produce an action.
297 if (getInputOk(A_motion, A_contact, A_lock, A_acceleration)) {
298                 log.debug("Motion, Door Open or Unlock Detected Running '${ScenarioNameA}'")
299             settings.A_dimmers?.setLevel(A_levelOn)
300             settings.A_switches?.on()
301             if (A_triggerOnce){
302                 state.A_triggered = true
303                 if (!A_turnOff) {
304                                         runOnce (getMidnight(), midNightReset)
305                 }
306             }
307                 if (state.A_timerStart){
308                 unschedule(delayTurnOffA)
309                 state.A_timerStart = false
310                 }
311 }
312
313 //if none of the above paramenters meet the expectation of the app then turn off
314 else {
315                 
316         if (settings.A_turnOff) {
317                         runIn(A_turnOff * 60, "delayTurnOffA")
318                 state.A_timerStart = true
319         }
320         else {
321                 settings.A_switches?.off()
322                         settings.A_dimmers?.setLevel(0)
323                 if (state.A_triggered) {
324                         runOnce (getMidnight(), midNightReset)
325                 }
326         }
327 }
328 }
329 }
330 else{
331 log.debug("Motion, Contact or Unlock detected outside of mode or time/day restriction.  Not running scenario.")
332 }
333 }
334 }
335
336 def delayTurnOffA(){
337         settings.A_switches?.off()
338         settings.A_dimmers?.setLevel(0)
339         state.A_timerStart = false
340         if (state.A_triggered) {
341         runOnce (getMidnight(), midNightReset)
342     }
343
344 }
345
346 //when physical switch is actuated disable the scenario
347 def onPressA(evt) {
348 if ((!A_mode || A_mode.contains(location.mode)) && /*getTimeOk (A_timeStart, A_timeEnd) && getDayOk(A_day)*/true) { //checks to make sure we are not opperating outside of set restrictions.
349 if ((!A_luxSensors) || (A_luxSensors.latestValue("illuminance") <= A_turnOnLux)){ 
350 if (/*(!A_triggerOnce || (A_triggerOnce && !state.A_triggered)) && (!A_switchDisable || (A_switchDisable && !state.A_triggered))*/true) {    
351     if (evt.physical){
352         state.A_triggered = true
353         unschedule(delayTurnOffA)
354         runOnce (getMidnight(), midNightReset)
355         log.debug "Physical switch in '${ScenarioNameA}' pressed. Triggers for this scenario disabled."
356         }
357 }
358 }}}
359
360 //Common Methods
361
362 //resets once a day trigger at midnight so trigger can be ran again the next day.
363 def midNightReset() {
364         state.A_triggered = false
365     state.B_triggered = false
366     state.C_triggered = false
367     state.D_triggered = false
368 }
369
370 private def helpText() {
371         def text =
372                 "Select motion sensors, acceleration sensors, contact sensors or locks to control a set of lights. " +
373         "Each scenario can control dimmers and switches but can also be " +
374         "restricted to modes or between certain times and turned off after " +
375         "motion stops, doors close or lock. Scenarios can also be limited to  " +
376         "running once or to stop running if the physical switches are turned off."
377         text
378 }
379
380 //should scenario be marked complete or not
381 def greyOut(scenario){
382         def result = ""
383     if (scenario) {
384         result = "complete"     
385     }
386     result
387 }
388
389 //should i mark the time restriction green or grey
390 def greyedOutTime(start, end){
391         def result = ""
392     if (start || end) {
393         result = "complete"     
394     }
395     result
396 }
397
398
399 def getTitle(scenario) {
400         def title = "Empty"
401         if (scenario) {
402                 title = scenario
403     }
404         title
405 }
406
407 //recursively applies label to each scenario depending on if the scenario has deatils inside it or not
408 def getDesc(scenario) {
409         def desc = "Tap to create a scenario"
410         if (scenario) {
411                 desc = "Tap to edit scenario"
412     }
413         desc    
414 }
415
416
417 def getMidnight() {
418         def midnightToday = timeToday("23:59", location.timeZone)
419         midnightToday
420 }
421
422 //used to recursively check device states when methods are triggered 
423 private getInputOk(motion, contact, lock, acceleration) {
424
425 def motionDetected = false
426 def accelerationDetected = false
427 def contactDetected = false
428 def unlockDetected = false
429 def result = false
430
431 if (motion) {
432         if (motion.latestValue("motion") == "active") {
433                 motionDetected = true
434         }
435 }
436
437 if (acceleration) {
438         if (acceleration.latestValue("acceleration") == "active") {
439                 accelerationDetected = true
440         }
441 }
442
443 if (contact) {
444         if (contact.latestValue("contact").contains("open")) {
445                 contactDetected = true
446         }
447 }
448
449 if (lock) {
450         if (lock.latestValue("lock").contains("unlocked")) {
451                 unlockDetected = true
452         }
453 }
454
455 result = motionDetected || contactDetected || unlockDetected || accelerationDetected
456 result
457
458 }
459
460 private getTimeOk(starting, ending) {
461         def result = true
462         if (starting && ending) {
463                 def currTime = now()
464                 def start = timeToday(starting).time
465                 def stop = timeToday(ending).time
466                 result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
467         }
468     
469     else if (starting){
470         result = currTime >= start
471     }
472     else if (ending){
473         result = currTime <= stop
474     }
475     
476         log.trace "timeOk = $result"
477         result
478 }
479
480 def getTimeLabel(start, end){
481         def timeLabel = "Tap to set"
482         
483     if(start && end){
484         timeLabel = "Between" + " " + hhmm(start) + " "  + "and" + " " +  hhmm(end)
485     }
486     else if (start) {
487                 timeLabel = "Start at" + " " + hhmm(start)
488     }
489     else if(end){
490     timeLabel = "End at" + hhmm(end)
491     }
492         timeLabel       
493 }
494
495 private hhmm(time, fmt = "h:mm a")
496 {
497         def t = timeToday(time, location.timeZone)
498         def f = new java.text.SimpleDateFormat(fmt)
499         f.setTimeZone(location.timeZone ?: timeZone(time))
500         f.format(t)
501 }
502
503 private getDayOk(dayList) {
504         def result = true
505     if (dayList) {
506                 def df = new java.text.SimpleDateFormat("EEEE")
507                 if (location.timeZone) {
508                         df.setTimeZone(location.timeZone)
509                 }
510                 else {
511                         df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
512                 }
513                 def day = df.format(new Date())
514                 result = dayList.contains(day)
515         }
516     result
517 }