Update double-tap.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
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     def inputDimmersA = [
95         name:       "A_dimmers",
96         type:       "capability.switchLevel",
97         title:      "Dim the following...",
98         multiple:   true,
99         required:   false
100     ]
101
102     def inputMotionA = [
103         name:       "A_motion",
104         type:       "capability.motionSensor",
105         title:      "Using these motion sensors...",
106         multiple:   true,
107         required:   false
108     ]
109     
110         def inputAccelerationA = [
111                 name:       "A_acceleration",
112                 type:       "capability.accelerationSensor",
113                 title:      "Or using these acceleration sensors...",
114                 multiple:   true,
115                 required:   false
116         ]
117     def inputContactA = [
118         name:       "A_contact",
119         type:       "capability.contactSensor",
120         title:      "Or using these contact sensors...",
121         multiple:   true,
122         required:   false
123     ]
124     
125     def inputTriggerOnceA = [
126         name:       "A_triggerOnce",
127         type:       "bool",
128         title:      "Trigger only once per day...",
129         defaultValue:false
130     ]
131     
132     def inputSwitchDisableA = [
133         name:       "A_switchDisable",
134         type:       "bool",
135         title:      "Stop triggering if physical switches/dimmers are turned off...",
136         defaultValue:false
137     ]
138     
139     def inputLockA = [
140         name:       "A_lock",
141         type:       "capability.lock",
142         title:      "Or using these locks...",
143         multiple:   true,
144         required:   false
145     ]
146     
147     def inputModeA = [
148         name:       "A_mode",
149         type:       "mode",
150         title:      "Only during the following modes...",
151         multiple:   true,
152         required:   false
153     ]
154     
155     def inputDayA = [
156         name:       "A_day",
157         type:       "enum",
158         options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
159         title:      "Only on certain days of the week...",
160         multiple:   true,
161         required:   false
162     ]
163     
164     
165     def inputLevelA = [
166         name:       "A_level",
167         type:       "enum",
168         options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]],
169         title:      "Set dimmers to this level",
170         multiple:   false,
171         required:   false
172     ]
173     
174     def inputTurnOnLuxA = [
175         name:       "A_turnOnLux",
176         type:       "number",
177         title:      "Only run this scenario if lux is below...",
178         multiple:   false,
179         required:   false
180     ]
181     
182     def inputLuxSensorsA = [
183         name:       "A_luxSensors",
184         type:       "capability.illuminanceMeasurement",
185         title:      "On these lux sensors",
186         multiple:   false,
187         required:   false
188     ]
189     
190     def inputTurnOffA = [
191         name:       "A_turnOff",
192         type:       "number",
193         title:      "Turn off this scenario after motion stops or doors close/lock (minutes)...",
194         multiple:   false,
195         required:   false
196     ]
197     
198     def inputScenarioNameA = [
199         name:       "ScenarioNameA",
200         type:       "text",
201         title:      "Scenario Name",
202         multiple:   false,
203         required:   false
204     ]
205     
206     def pageProperties = [
207         name:       "pageSetupScenarioA",
208     ]
209
210     return dynamicPage(pageProperties) {
211 section("Name your scenario") {
212             input inputScenarioNameA
213         }
214
215 section("Devices included in the scenario") {
216             input inputMotionA
217                         input inputAccelerationA
218             input inputContactA
219             input inputLockA
220             input inputLightsA
221             input inputDimmersA
222             }
223
224 section("Scenario settings") {
225             input inputLevelA
226             input inputTurnOnLuxA
227             input inputLuxSensorsA
228             input inputTurnOffA
229             }
230             
231 section("Scenario restrictions") {            
232             input inputTriggerOnceA
233             input inputSwitchDisableA
234             href "timeIntervalInputA", title: "Only during a certain time...", description: getTimeLabel(A_timeStart, A_timeEnd), state: greyedOutTime(A_timeStart, A_timeEnd), refreshAfterSelection:true
235             input inputDayA
236             input inputModeA
237             }
238
239 section("Help") {
240             paragraph helpText()
241             }
242     }
243     
244 }
245
246
247
248 def installed() {
249     initialize()
250 }
251
252 def updated() {
253     unschedule()
254     unsubscribe()
255     initialize()
256 }
257
258 def initialize() {
259
260         midNightReset()
261
262         if(A_motion) {
263                 subscribe(settings.A_motion, "motion", onEventA)
264         }
265
266         if(A_acceleration) {
267                 subscribe(settings.A_acceleration, "acceleration", onEventA)
268         }
269
270         if(A_contact) {
271                 subscribe(settings.A_contact, "contact", onEventA)
272         }
273
274         if(A_lock) {
275                 subscribe(settings.A_lock, "lock", onEventA)
276         }
277
278         if(A_switchDisable) {
279                 subscribe(A_switches, "switch.off", onPressA)
280             subscribe(A_dimmers, "switch.off", onPressA)
281         }
282
283 }
284
285 def onEventA(evt) {
286
287 if ((!A_triggerOnce || (A_triggerOnce && !state.A_triggered)) && (!A_switchDisable || (A_switchDisable && !state.A_triggered))) { //Checks to make sure this scenario should be triggered more then once in a day
288 if ((!A_mode || A_mode.contains(location.mode)) && getTimeOk (A_timeStart, A_timeEnd) && getDayOk(A_day)) { //checks to make sure we are not opperating outside of set restrictions.
289 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
290 def A_levelOn = A_level as Integer
291
292 //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.
293 if (getInputOk(A_motion, A_contact, A_lock, A_acceleration)) {
294                 log.debug("Motion, Door Open or Unlock Detected Running '${ScenarioNameA}'")
295             settings.A_dimmers?.setLevel(A_levelOn)
296             settings.A_switches?.on()
297             if (A_triggerOnce){
298                 state.A_triggered = true
299                 if (!A_turnOff) {
300                                         runOnce (getMidnight(), midNightReset)
301                 }
302             }
303                 if (state.A_timerStart){
304                 unschedule(delayTurnOffA)
305                 state.A_timerStart = false
306                 }
307 }
308
309 //if none of the above paramenters meet the expectation of the app then turn off
310 else {
311                 
312         if (settings.A_turnOff) {
313                         runIn(A_turnOff * 60, "delayTurnOffA")
314                 state.A_timerStart = true
315         }
316         else {
317                 settings.A_switches?.off()
318                         settings.A_dimmers?.setLevel(0)
319                 if (state.A_triggered) {
320                         runOnce (getMidnight(), midNightReset)
321                 }
322         }
323 }
324 }
325 }
326 else{
327 log.debug("Motion, Contact or Unlock detected outside of mode or time/day restriction.  Not running scenario.")
328 }
329 }
330 }
331
332 def delayTurnOffA(){
333         settings.A_switches?.off()
334         settings.A_dimmers?.setLevel(0)
335         state.A_timerStart = false
336         if (state.A_triggered) {
337         runOnce (getMidnight(), midNightReset)
338     }
339
340 }
341
342 //when physical switch is actuated disable the scenario
343 def onPressA(evt) {
344 if ((!A_mode || A_mode.contains(location.mode)) && getTimeOk (A_timeStart, A_timeEnd) && getDayOk(A_day)) { //checks to make sure we are not opperating outside of set restrictions.
345 if ((!A_luxSensors) || (A_luxSensors.latestValue("illuminance") <= A_turnOnLux)){ 
346 if ((!A_triggerOnce || (A_triggerOnce && !state.A_triggered)) && (!A_switchDisable || (A_switchDisable && !state.A_triggered))) {    
347     if (evt.physical){
348         state.A_triggered = true
349         unschedule(delayTurnOffA)
350         runOnce (getMidnight(), midNightReset)
351         log.debug "Physical switch in '${ScenarioNameA}' pressed. Triggers for this scenario disabled."
352         }
353 }
354 }}}
355
356 //Common Methods
357
358 //resets once a day trigger at midnight so trigger can be ran again the next day.
359 def midNightReset() {
360         state.A_triggered = false
361     state.B_triggered = false
362     state.C_triggered = false
363     state.D_triggered = false
364 }
365
366 private def helpText() {
367         def text =
368                 "Select motion sensors, acceleration sensors, contact sensors or locks to control a set of lights. " +
369         "Each scenario can control dimmers and switches but can also be " +
370         "restricted to modes or between certain times and turned off after " +
371         "motion stops, doors close or lock. Scenarios can also be limited to  " +
372         "running once or to stop running if the physical switches are turned off."
373         text
374 }
375
376 //should scenario be marked complete or not
377 def greyOut(scenario){
378         def result = ""
379     if (scenario) {
380         result = "complete"     
381     }
382     result
383 }
384
385 //should i mark the time restriction green or grey
386 def greyedOutTime(start, end){
387         def result = ""
388     if (start || end) {
389         result = "complete"     
390     }
391     result
392 }
393
394
395 def getTitle(scenario) {
396         def title = "Empty"
397         if (scenario) {
398                 title = scenario
399     }
400         title
401 }
402
403 //recursively applies label to each scenario depending on if the scenario has deatils inside it or not
404 def getDesc(scenario) {
405         def desc = "Tap to create a scenario"
406         if (scenario) {
407                 desc = "Tap to edit scenario"
408     }
409         desc    
410 }
411
412
413 def getMidnight() {
414         def midnightToday = timeToday("2000-01-01T23:59:59.999-0000", location.timeZone)
415         midnightToday
416 }
417
418 //used to recursively check device states when methods are triggered 
419 private getInputOk(motion, contact, lock, acceleration) {
420
421 def motionDetected = false
422 def accelerationDetected = false
423 def contactDetected = false
424 def unlockDetected = false
425 def result = false
426
427 if (motion) {
428         if (motion.latestValue("motion").contains("active")) {
429                 motionDetected = true
430         }
431 }
432
433 if (acceleration) {
434         if (acceleration.latestValue("acceleration").contains("active")) {
435                 accelerationDetected = true
436         }
437 }
438
439 if (contact) {
440         if (contact.latestValue("contact").contains("open")) {
441                 contactDetected = true
442         }
443 }
444
445 if (lock) {
446         if (lock.latestValue("lock").contains("unlocked")) {
447                 unlockDetected = true
448         }
449 }
450
451 result = motionDetected || contactDetected || unlockDetected || accelerationDetected
452 result
453
454 }
455
456 private getTimeOk(starting, ending) {
457         def result = true
458         if (starting && ending) {
459                 def currTime = now()
460                 def start = timeToday(starting).time
461                 def stop = timeToday(ending).time
462                 result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
463         }
464     
465     else if (starting){
466         result = currTime >= start
467     }
468     else if (ending){
469         result = currTime <= stop
470     }
471     
472         log.trace "timeOk = $result"
473         result
474 }
475
476 def getTimeLabel(start, end){
477         def timeLabel = "Tap to set"
478         
479     if(start && end){
480         timeLabel = "Between" + " " + hhmm(start) + " "  + "and" + " " +  hhmm(end)
481     }
482     else if (start) {
483                 timeLabel = "Start at" + " " + hhmm(start)
484     }
485     else if(end){
486     timeLabel = "End at" + hhmm(end)
487     }
488         timeLabel       
489 }
490
491 private hhmm(time, fmt = "h:mm a")
492 {
493         def t = timeToday(time, location.timeZone)
494         def f = new java.text.SimpleDateFormat(fmt)
495         f.setTimeZone(location.timeZone ?: timeZone(time))
496         f.format(t)
497 }
498
499 private getDayOk(dayList) {
500         def result = true
501     if (dayList) {
502                 def df = new java.text.SimpleDateFormat("EEEE")
503                 if (location.timeZone) {
504                         df.setTimeZone(location.timeZone)
505                 }
506                 else {
507                         df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
508                 }
509                 def day = df.format(new Date())
510                 result = dayList.contains(day)
511         }
512     result
513 }