7354edd853d8a3f9d003999e697c7a868ae44af3
[smartapps.git] / official / vacation-lighting-director.groovy
1 /**
2  *  Vacation Lighting Director
3  * 
4  * Version  2.5 - Moved scheduling over to Cron and added time as a trigger. 
5  *                                Cleaned up formatting and some typos.
6  *                Updated license.
7  *                Made people option optional
8  *                                Added sttement to unschedule on mode change if people option is not selected
9  *
10  * Version  2.4 - Added information paragraphs
11  * 
12  *  Source code can be found here: https://github.com/tslagle13/SmartThings/blob/master/smartapps/tslagle13/vacation-lighting-director.groovy
13  *
14  *  Copyright 2016 Tim Slagle
15  *
16  *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
17  *  in compliance with the License. You may obtain a copy of the License at:
18  *
19  *      http://www.apache.org/licenses/LICENSE-2.0
20  *
21  *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
22  *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
23  *  for the specific language governing permissions and limitations under the License.
24  *
25  */
26
27
28 // Automatically generated. Make future change here.
29 definition(
30     name: "Vacation Lighting Director",
31     namespace: "tslagle13",
32     author: "Tim Slagle",
33     category: "Safety & Security",
34     description: "Randomly turn on/off lights to simulate the appearance of a occupied home while you are away.",
35     iconUrl: "http://icons.iconarchive.com/icons/custom-icon-design/mono-general-2/512/settings-icon.png",
36     iconX2Url: "http://icons.iconarchive.com/icons/custom-icon-design/mono-general-2/512/settings-icon.png"
37 )
38
39 preferences {
40     page name: "timeIntervalInput"
41     page name:"Settings1"
42     page name:"Setup"
43     page name:"pageSetup"
44 }
45
46 // Show setup page
47 def pageSetup() {
48
49     def pageProperties = [
50         name:       "pageSetup",
51         title:      "Status",
52         nextPage:   null,
53         install:    true,
54         uninstall:  true
55     ]
56
57         return dynamicPage(pageProperties) {
58         section(""){
59                 paragraph "This app can be used to make your home seem occupied anytime you are away from your home. " +
60                         "Please use each of the the sections below to setup the different preferences to your liking. " 
61         }
62         section("Setup Menu") {
63             href "Setup", title: "Setup", description: "", state:greyedOut()
64             href "Settings1", title: "Settings1", description: "", state: greyedOutSettings()
65             }
66         section([title:"Options", mobileOnly:true]) {
67             label title:"Assign a name", required:false
68         }
69     }
70 }
71
72 // Show "Setup" page
73 def Setup() {
74
75     def newMode = [
76         name:           "newMode",
77         type:           "mode",
78         title:          "Modes",
79         multiple:       true,
80         required:       true
81     ]
82     def switches = [
83         name:           "switches",
84         type:           "capability.switch",
85         title:          "Switches",
86         multiple:       true,
87         required:       true
88     ]
89     
90     def frequency_minutes = [
91         name:           "frequency_minutes",
92         type:           "number",
93         title:          "Minutes?",
94         required:       true
95     ]
96     
97     def number_of_active_lights = [
98         name:           "number_of_active_lights",
99         type:           "number",
100         title:          "Number of active lights",
101         required:       true,
102     ]
103     
104     def pageName = "Setup"
105     
106     def pageProperties = [
107         name:       "Setup",
108         title:      "Setup",
109         nextPage:   "pageSetup"
110     ]
111
112     return dynamicPage(pageProperties) {
113
114                 section(""){            
115                     paragraph "In this section you need to setup the deatils of how you want your lighting to be affected while " +
116                     "you are away.  All of these settings are required in order for the simulator to run correctly."
117         }
118         section("Simulator Triggers") {
119                     input newMode  
120                     href "timeIntervalInput", title: "Times", description: timeIntervalLabel(), refreshAfterSelection:true
121         }
122         section("Light switches to turn on/off") {
123                     input switches           
124         }
125         section("How often to cycle the lights") {
126                     input frequency_minutes            
127         }
128         section("Number of active lights at any given time") {
129                     input number_of_active_lights           
130         }    
131     }
132     
133 }
134
135 // Show "Setup" page
136 def Settings1() {
137
138     def falseAlarmThreshold = [
139         name:       "falseAlarmThreshold",
140         type:       "decimal",
141         title:      "Default is 2 minutes",
142         required:       false
143     ]
144     def days = [
145         name:       "days",
146         type:       "enum",
147         title:      "Only on certain days of the week",
148         multiple:   true,
149         required:   false,
150         options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
151     ]
152     
153     def pageName = "Settings1"
154     
155     def pageProperties = [
156         name:       "Settings1",
157         title:      "Settings1",
158         nextPage:   "pageSetup"
159     ]
160     
161     def people = [
162         name:       "people",
163         type:       "capability.presenceSensor",
164         title:      "If these people are home do not change light status",
165         required:       false,
166         multiple:       true
167     ]
168
169     return dynamicPage(pageProperties) {
170
171                 section(""){              
172                     paragraph "In this section you can restrict how your simulator runs.  For instance you can restrict on which days it will run " +
173                     "as well as a delay for the simulator to start after it is in the correct mode.  Delaying the simulator helps with false starts based on a incorrect mode change."
174         }
175         section("Delay to start simulator") {
176                     input falseAlarmThreshold
177         }
178         section("People") {
179                                 paragraph "Not using this setting may cause some lights to remain on when you arrive home"
180                     input people            
181         }
182         section("More options") {
183                     input days
184         } 
185     }   
186 }
187
188 def timeIntervalInput() {
189         dynamicPage(name: "timeIntervalInput") {
190                 section {
191                         input "startTimeType", "enum", title: "Starting at", options: [["time": "A specific time"], ["sunrise": "Sunrise"], ["sunset": "Sunset"]], defaultValue: "time", submitOnChange: true
192                         if (startTimeType in ["sunrise","sunset"]) {
193                                 input "startTimeOffset", "number", title: "Offset in minutes (+/-)", range: "*..*", required: false
194                         }
195                         else {
196                                 input "starting", "time", title: "Start time", required: false
197                         }
198                 }
199                 section {
200                         input "endTimeType", "enum", title: "Ending at", options: [["time": "A specific time"], ["sunrise": "Sunrise"], ["sunset": "Sunset"]], defaultValue: "time", submitOnChange: true
201                         if (endTimeType in ["sunrise","sunset"]) {
202                                 input "endTimeOffset", "number", title: "Offset in minutes (+/-)", range: "*..*", required: false
203                         }
204                         else {
205                                 input "ending", "time", title: "End time", required: false
206                         }
207                 }
208         }
209 }
210
211
212 def installed() {
213 initialize()
214 }
215
216 def updated() {
217   unsubscribe();
218   unschedule();
219   initialize()
220 }
221
222 def initialize(){
223
224         if (newMode != null) {
225                 subscribe(location, modeChangeHandler)
226     }
227     if (starting != null) {
228         schedule(starting, modeChangeHandler)
229     }
230     log.debug "Installed with settings: ${settings}"
231 }
232
233 def modeChangeHandler(evt) {
234                 def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 2 * 60  
235         runIn(delay, scheduleCheck)
236 }
237
238
239 //Main logic to pick a random set of lights from the large set of lights to turn on and then turn the rest off
240 def scheduleCheck(evt) {
241     if(allOk){
242         log.debug("Running")
243         // turn off all the switches
244         switches.off()
245         
246         // grab a random switch
247         def random = new Random()
248         def inactive_switches = []
249         inactive_switches.add(switches)
250         for (int i = 0 ; i < number_of_active_lights ; i++) {
251             // if there are no inactive switches to turn on then let's break
252             if (inactive_switches.size() == 0){
253                 break
254             }
255             
256             // grab a random switch and turn it on
257             def random_int = random.nextInt(inactive_switches.size())
258             inactive_switches[random_int].on()
259             
260             // then remove that switch from the pool off switches that can be turned on
261             inactive_switches.remove(random_int)
262         }
263         
264         // re-run again when the frequency demands it
265         //schedule("0 0/${frequency_minutes} * 1/1 * ? *", scheduleCheck)
266     }
267     //Check to see if mode is ok but not time/day.  If mode is still ok, check again after frequency period.
268     else if (modeOk) {
269         log.debug("mode OK.  Running again")
270         switches.off()
271     }
272     //if none is ok turn off frequency check and turn off lights.
273     else {
274         if(people){
275                 //don't turn off lights if anyone is home
276                 if(someoneIsHome){
277                     log.debug("Stopping Check for Light")
278                     unschedule()
279                 }
280                 else{
281                     log.debug("Stopping Check for Light and turning off all lights")
282                     switches.off()
283                     unschedule()
284                 }
285         }
286         else if (!modeOk) {
287                 unschedule()
288         }
289     }
290 }      
291
292
293 //below is used to check restrictions
294 private getAllOk() {
295         modeOk && homeIsEmpty/*&& daysOk && timeOk*/
296 }
297
298
299 private getModeOk() {
300         def result = !newMode || newMode.contains(location.mode)
301         log.trace "modeOk = $result"
302         result
303 }
304
305 private getDaysOk() {
306         def result = true
307         if (days) {
308                 def df = new java.text.SimpleDateFormat("EEEE")
309                 if (location.timeZone) {
310                         df.setTimeZone(location.timeZone)
311                 }
312                 else {
313                         df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
314                 }
315                 def day = df.format(new Date())
316                 result = days.contains(day)
317         }
318         log.trace "daysOk = $result"
319         result
320 }
321
322 private getHomeIsEmpty() {
323   def result = true
324
325   if(people?.findAll { it?.currentPresence == "present" }) {
326     result = false
327   }
328
329   log.debug("homeIsEmpty: ${result}")
330
331   return result
332 }
333
334 private getSomeoneIsHome() {
335   def result = false
336
337   if(people?.findAll { it?.currentPresence == "present" }) {
338     result = true
339   }
340
341   log.debug("anyoneIsHome: ${result}")
342
343   return result
344 }
345
346 private getTimeOk() {
347         def result = true
348         def start = timeWindowStart()
349         def stop = timeWindowStop()
350         if (start && stop && location.timeZone) {
351                 result = timeOfDayIsBetween(start, stop, new Date(), location.timeZone)
352         }
353         log.trace "timeOk = $result"
354         result
355 }
356
357 private timeWindowStart() {
358         def result = null
359         if (startTimeType == "sunrise") {
360                 result = location.currentState("sunriseTime")?.dateValue
361                 if (result && startTimeOffset) {
362                         result = new Date(result.time + Math.round(startTimeOffset * 60000))
363                 }
364         }
365         else if (startTimeType == "sunset") {
366                 result = location.currentState("sunsetTime")?.dateValue
367                 if (result && startTimeOffset) {
368                         result = new Date(result.time + Math.round(startTimeOffset * 60000))
369                 }
370         }
371         else if (starting && location.timeZone) {
372                 result = timeToday(starting, location.timeZone)
373         }
374         log.trace "timeWindowStart = ${result}"
375         result
376 }
377
378 private timeWindowStop() {
379         def result = null
380         if (endTimeType == "sunrise") {
381                 result = location.currentState("sunriseTime")?.dateValue
382                 if (result && endTimeOffset) {
383                         result = new Date(result.time + Math.round(endTimeOffset * 60000))
384                 }
385         }
386         else if (endTimeType == "sunset") {
387                 result = location.currentState("sunsetTime")?.dateValue
388                 if (result && endTimeOffset) {
389                         result = new Date(result.time + Math.round(endTimeOffset * 60000))
390                 }
391         }
392         else if (ending && location.timeZone) {
393                 result = timeToday(ending, location.timeZone)
394         }
395         log.trace "timeWindowStop = ${result}"
396         result
397 }
398
399 private hhmm(time, fmt = "h:mm a")
400 {
401         def t = timeToday(time, location.timeZone)
402         def f = new java.text.SimpleDateFormat(fmt)
403         f.setTimeZone(location.timeZone ?: timeZone(time))
404         f.format(t)
405 }
406
407 private timeIntervalLabel() {
408         def start = ""
409         switch (startTimeType) {
410                 case "time":
411                         if (ending) {
412                 start += hhmm(starting)
413             }
414                         break
415                 case "sunrise":
416                 case "sunset":
417                 start += startTimeType[0].toUpperCase() + startTimeType[1..-1]
418                         if (startTimeOffset) {
419                                 start += startTimeOffset > 0 ? "+${startTimeOffset} min" : "${startTimeOffset} min"
420                         }
421                         break
422         }
423
424     def finish = ""
425         switch (endTimeType) {
426                 case "time":
427                         if (ending) {
428                 finish += hhmm(ending)
429             }
430                         break
431                 case "sunrise":
432                 case "sunset":
433                 finish += endTimeType[0].toUpperCase() + endTimeType[1..-1]
434                         if (endTimeOffset) {
435                                 finish += endTimeOffset > 0 ? "+${endTimeOffset} min" : "${endTimeOffset} min"
436                         }
437                         break
438         }
439         start && finish ? "${start} to ${finish}" : ""
440 }
441
442 //sets complete/not complete for the setup section on the main dynamic page
443 def greyedOut(){
444         def result = ""
445     if (switches) {
446         result = "complete"     
447     }
448     result
449 }
450
451 //sets complete/not complete for the settings section on the main dynamic page
452 def greyedOutSettings(){
453         def result = ""
454     if (people || days || falseAlarmThreshold ) {
455         result = "complete"     
456     }
457     result
458 }