Update influxdb-logger.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: "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 = switches
249         for (int i = 0 ; i < number_of_active_lights ; i++) {
250             // if there are no inactive switches to turn on then let's break
251             if (inactive_switches.size() == 0){
252                 break
253             }
254             
255             // grab a random switch and turn it on
256             def random_int = random.nextInt(inactive_switches.size())
257             inactive_switches[random_int].on()
258             
259             // then remove that switch from the pool off switches that can be turned on
260             inactive_switches.remove(random_int)
261         }
262         
263         // re-run again when the frequency demands it
264         schedule("0 0/${frequency_minutes} * 1/1 * ? *", scheduleCheck)
265     }
266     //Check to see if mode is ok but not time/day.  If mode is still ok, check again after frequency period.
267     else if (modeOk) {
268         log.debug("mode OK.  Running again")
269         switches.off()
270     }
271     //if none is ok turn off frequency check and turn off lights.
272     else {
273         if(people){
274                 //don't turn off lights if anyone is home
275                 if(someoneIsHome){
276                     log.debug("Stopping Check for Light")
277                     unschedule()
278                 }
279                 else{
280                     log.debug("Stopping Check for Light and turning off all lights")
281                     switches.off()
282                     unschedule()
283                 }
284         }
285         else if (!modeOk) {
286                 unschedule()
287         }
288     }
289 }      
290
291
292 //below is used to check restrictions
293 private getAllOk() {
294         modeOk && daysOk && timeOk && homeIsEmpty
295 }
296
297
298 private getModeOk() {
299         def result = !newMode || newMode.contains(location.mode)
300         log.trace "modeOk = $result"
301         result
302 }
303
304 private getDaysOk() {
305         def result = true
306         if (days) {
307                 def df = new java.text.SimpleDateFormat("EEEE")
308                 if (location.timeZone) {
309                         df.setTimeZone(location.timeZone)
310                 }
311                 else {
312                         df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
313                 }
314                 def day = df.format(new Date())
315                 result = days.contains(day)
316         }
317         log.trace "daysOk = $result"
318         result
319 }
320
321 private getHomeIsEmpty() {
322   def result = true
323
324   if(people?.findAll { it?.currentPresence == "present" }) {
325     result = false
326   }
327
328   log.debug("homeIsEmpty: ${result}")
329
330   return result
331 }
332
333 private getSomeoneIsHome() {
334   def result = false
335
336   if(people?.findAll { it?.currentPresence == "present" }) {
337     result = true
338   }
339
340   log.debug("anyoneIsHome: ${result}")
341
342   return result
343 }
344
345 private getTimeOk() {
346         def result = true
347         def start = timeWindowStart()
348         def stop = timeWindowStop()
349         if (start && stop && location.timeZone) {
350                 result = timeOfDayIsBetween(start, stop, new Date(), location.timeZone)
351         }
352         log.trace "timeOk = $result"
353         result
354 }
355
356 private timeWindowStart() {
357         def result = null
358         if (startTimeType == "sunrise") {
359                 result = location.currentState("sunriseTime")?.dateValue
360                 if (result && startTimeOffset) {
361                         result = new Date(result.time + Math.round(startTimeOffset * 60000))
362                 }
363         }
364         else if (startTimeType == "sunset") {
365                 result = location.currentState("sunsetTime")?.dateValue
366                 if (result && startTimeOffset) {
367                         result = new Date(result.time + Math.round(startTimeOffset * 60000))
368                 }
369         }
370         else if (starting && location.timeZone) {
371                 result = timeToday(starting, location.timeZone)
372         }
373         log.trace "timeWindowStart = ${result}"
374         result
375 }
376
377 private timeWindowStop() {
378         def result = null
379         if (endTimeType == "sunrise") {
380                 result = location.currentState("sunriseTime")?.dateValue
381                 if (result && endTimeOffset) {
382                         result = new Date(result.time + Math.round(endTimeOffset * 60000))
383                 }
384         }
385         else if (endTimeType == "sunset") {
386                 result = location.currentState("sunsetTime")?.dateValue
387                 if (result && endTimeOffset) {
388                         result = new Date(result.time + Math.round(endTimeOffset * 60000))
389                 }
390         }
391         else if (ending && location.timeZone) {
392                 result = timeToday(ending, location.timeZone)
393         }
394         log.trace "timeWindowStop = ${result}"
395         result
396 }
397
398 private hhmm(time, fmt = "h:mm a")
399 {
400         def t = timeToday(time, location.timeZone)
401         def f = new java.text.SimpleDateFormat(fmt)
402         f.setTimeZone(location.timeZone ?: timeZone(time))
403         f.format(t)
404 }
405
406 private timeIntervalLabel() {
407         def start = ""
408         switch (startTimeType) {
409                 case "time":
410                         if (ending) {
411                 start += hhmm(starting)
412             }
413                         break
414                 case "sunrise":
415                 case "sunset":
416                 start += startTimeType[0].toUpperCase() + startTimeType[1..-1]
417                         if (startTimeOffset) {
418                                 start += startTimeOffset > 0 ? "+${startTimeOffset} min" : "${startTimeOffset} min"
419                         }
420                         break
421         }
422
423     def finish = ""
424         switch (endTimeType) {
425                 case "time":
426                         if (ending) {
427                 finish += hhmm(ending)
428             }
429                         break
430                 case "sunrise":
431                 case "sunset":
432                 finish += endTimeType[0].toUpperCase() + endTimeType[1..-1]
433                         if (endTimeOffset) {
434                                 finish += endTimeOffset > 0 ? "+${endTimeOffset} min" : "${endTimeOffset} min"
435                         }
436                         break
437         }
438         start && finish ? "${start} to ${finish}" : ""
439 }
440
441 //sets complete/not complete for the setup section on the main dynamic page
442 def greyedOut(){
443         def result = ""
444     if (switches) {
445         result = "complete"     
446     }
447     result
448 }
449
450 //sets complete/not complete for the settings section on the main dynamic page
451 def greyedOutSettings(){
452         def result = ""
453     if (people || days || falseAlarmThreshold ) {
454         result = "complete"     
455     }
456     result
457 }