Update groveStreams.groovy
[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:"pageSetup"
41     page name:"Setup"
42     page name:"Settings"
43     page name: "timeIntervalInput"
44
45 }
46
47 // Show setup page
48 def pageSetup() {
49
50     def pageProperties = [
51         name:       "pageSetup",
52         title:      "Status",
53         nextPage:   null,
54         install:    true,
55         uninstall:  true
56     ]
57
58         return dynamicPage(pageProperties) {
59         section(""){
60                 paragraph "This app can be used to make your home seem occupied anytime you are away from your home. " +
61                         "Please use each of the the sections below to setup the different preferences to your liking. " 
62         }
63         section("Setup Menu") {
64             href "Setup", title: "Setup", description: "", state:greyedOut()
65             href "Settings", title: "Settings", description: "", state: greyedOutSettings()
66             }
67         section([title:"Options", mobileOnly:true]) {
68             label title:"Assign a name", required:false
69         }
70     }
71 }
72
73 // Show "Setup" page
74 def Setup() {
75
76     def newMode = [
77         name:           "newMode",
78         type:           "mode",
79         title:          "Modes",
80         multiple:       true,
81         required:       true
82     ]
83     def switches = [
84         name:           "switches",
85         type:           "capability.switch",
86         title:          "Switches",
87         multiple:       true,
88         required:       true
89     ]
90     
91     def frequency_minutes = [
92         name:           "frequency_minutes",
93         type:           "number",
94         title:          "Minutes?",
95         required:       true
96     ]
97     
98     def number_of_active_lights = [
99         name:           "number_of_active_lights",
100         type:           "number",
101         title:          "Number of active lights",
102         required:       true,
103     ]
104     
105     def pageName = "Setup"
106     
107     def pageProperties = [
108         name:       "Setup",
109         title:      "Setup",
110         nextPage:   "pageSetup"
111     ]
112
113     return dynamicPage(pageProperties) {
114
115                 section(""){            
116                     paragraph "In this section you need to setup the deatils of how you want your lighting to be affected while " +
117                     "you are away.  All of these settings are required in order for the simulator to run correctly."
118         }
119         section("Simulator Triggers") {
120                     input newMode  
121                     href "timeIntervalInput", title: "Times", description: timeIntervalLabel(), refreshAfterSelection:true
122         }
123         section("Light switches to turn on/off") {
124                     input switches           
125         }
126         section("How often to cycle the lights") {
127                     input frequency_minutes            
128         }
129         section("Number of active lights at any given time") {
130                     input number_of_active_lights           
131         }    
132     }
133     
134 }
135
136 // Show "Setup" page
137 def Settings() {
138
139     def falseAlarmThreshold = [
140         name:       "falseAlarmThreshold",
141         type:       "decimal",
142         title:      "Default is 2 minutes",
143         required:       false
144     ]
145     def days = [
146         name:       "days",
147         type:       "enum",
148         title:      "Only on certain days of the week",
149         multiple:   true,
150         required:   false,
151         options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
152     ]
153     
154     def pageName = "Settings"
155     
156     def pageProperties = [
157         name:       "Settings",
158         title:      "Settings",
159         nextPage:   "pageSetup"
160     ]
161     
162     def people = [
163         name:       "people",
164         type:       "capability.presenceSensor",
165         title:      "If these people are home do not change light status",
166         required:       false,
167         multiple:       true
168     ]
169
170     return dynamicPage(pageProperties) {
171
172                 section(""){              
173                     paragraph "In this section you can restrict how your simulator runs.  For instance you can restrict on which days it will run " +
174                     "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."
175         }
176         section("Delay to start simulator") {
177                     input falseAlarmThreshold
178         }
179         section("People") {
180                                 paragraph "Not using this setting may cause some lights to remain on when you arrive home"
181                     input people            
182         }
183         section("More options") {
184                     input days
185         } 
186     }   
187 }
188
189 def timeIntervalInput() {
190         dynamicPage(name: "timeIntervalInput") {
191                 section {
192                         input "startTimeType", "enum", title: "Starting at", options: [["time": "A specific time"], ["sunrise": "Sunrise"], ["sunset": "Sunset"]], defaultValue: "time", submitOnChange: true
193                         if (startTimeType in ["sunrise","sunset"]) {
194                                 input "startTimeOffset", "number", title: "Offset in minutes (+/-)", range: "*..*", required: false
195                         }
196                         else {
197                                 input "starting", "time", title: "Start time", required: false
198                         }
199                 }
200                 section {
201                         input "endTimeType", "enum", title: "Ending at", options: [["time": "A specific time"], ["sunrise": "Sunrise"], ["sunset": "Sunset"]], defaultValue: "time", submitOnChange: true
202                         if (endTimeType in ["sunrise","sunset"]) {
203                                 input "endTimeOffset", "number", title: "Offset in minutes (+/-)", range: "*..*", required: false
204                         }
205                         else {
206                                 input "ending", "time", title: "End time", required: false
207                         }
208                 }
209         }
210 }
211
212
213 def installed() {
214 initialize()
215 }
216
217 def updated() {
218   unsubscribe();
219   unschedule();
220   initialize()
221 }
222
223 def initialize(){
224
225         if (newMode != null) {
226                 subscribe(location, modeChangeHandler)
227     }
228     if (starting != null) {
229         schedule(starting, modeChangeHandler)
230     }
231     log.debug "Installed with settings: ${settings}"
232 }
233
234 def modeChangeHandler(evt) {
235                 def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 2 * 60  
236         runIn(delay, scheduleCheck)
237 }
238
239
240 //Main logic to pick a random set of lights from the large set of lights to turn on and then turn the rest off
241 def scheduleCheck(evt) {
242     if(allOk){
243         log.debug("Running")
244         // turn off all the switches
245         switches.off()
246         
247         // grab a random switch
248         def random = new Random()
249         def inactive_switches = 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 && daysOk && timeOk && homeIsEmpty
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 }