Update routine-director.groovy
[smartapps.git] / official / routine-director.groovy
1 /**
2  *  Rotuine Director
3  *
4  *
5  *  Changelog
6  *
7  *  2015-09-01
8  *     --Added Contact Book
9  *     --Removed references to phrases and replaced with routines
10  *     --Added bool logic to inputs instead of enum for "yes" "no" options
11  *     --Fixed halting error with code installation
12  *
13  *  Copyright 2015 Tim Slagle
14  *
15  *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
16  *  in compliance with the License. You may obtain a copy of the License at:
17  *
18  *      http://www.apache.org/licenses/LICENSE-2.0
19  *
20  *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
21  *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
22  *  for the specific language governing permissions and limitations under the License.
23  *
24  */
25 definition(
26     name: "Routine Director",
27     namespace: "tslagle13",
28     author: "Tim Slagle",
29     description: "Monitor a set of presence sensors and activate routines based on whether your home is empty or occupied.  Each presence status change will check against the current 'sun state' to run routines based on occupancy and whether the sun is up or down.",
30     category: "Convenience",
31     iconUrl: "http://icons.iconarchive.com/icons/icons8/ios7/512/Very-Basic-Home-Filled-icon.png",
32     iconX2Url: "http://icons.iconarchive.com/icons/icons8/ios7/512/Very-Basic-Home-Filled-icon.png"
33 )
34
35 preferences {
36     page(name: "selectRoutines")
37
38     page(name: "Settings", title: "Settings", uninstall: true, install: true) {
39         section("False alarm threshold (defaults to 10 min)") {
40             input "falseAlarmThreshold", "decimal", title: "Number of minutes", required: false
41         }
42
43         section("Zip code (for sunrise/sunset)") {
44             input "zip", "text", required: true
45         }
46
47         section("Notifications") {
48             input "sendPushMessage", "bool", title: "Send notifications when house is empty?"
49             input "sendPushMessageHome", "bool", title: "Send notifications when home is occupied?"
50         }
51         section("Send Notifications?") {
52             input("recipients", "contact", title: "Send notifications to") {
53                 input "phone", "phone", title: "Send an SMS to this number?", required:false
54             }
55         }
56
57         section(title: "More options", hidden: hideOptionsSection(), hideable: true) {
58             label title: "Assign a name", required: false
59             input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
60                 options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
61             input "modes", "mode", title: "Only when mode is", multiple: true, required: false
62         }
63             
64         section {
65                 input "starting", "time", title: "Starting", required: false
66                 input "ending", "time", title: "Ending", required: false
67         }
68     }
69 }
70
71 def selectRoutines() {
72     def configured = (settings.awayDay && settings.awayNight && settings.homeDay && settings.homeNight)
73     dynamicPage(name: "selectRoutines", title: "Configure", nextPage: "Settings", uninstall: true) {
74         section("Who?") {
75             input "people", "capability.presenceSensor", title: "Monitor These Presences", required: true, multiple: true, submitOnChange: true
76         }
77
78         def routines = location.helloHome?.getPhrases()*.label
79         if (routines) {
80             routines.sort()
81             section("Run This Routine When...") {
82                     log.trace routines
83                     input "awayDay", "enum", title: "Everyone Is Away And It's Day", required: true, options: routines, submitOnChange: true
84                     input "awayNight", "enum", title: "Everyone Is Away And It's Night", required: true, options: routines, submitOnChange: true
85                     input "homeDay", "enum", title: "At Least One Person Is Home And It's Day", required: true, options: routines, submitOnChange: true
86                     input "homeNight", "enum", title: "At Least One Person Is Home And It's Night", required: true, options: routines, submitOnChange: true
87                 }
88                 /*    section("Select modes used for each condition.") { This allows the director to know which rotuine has already been ran so it does not run again if someone else comes home. 
89         input "homeModeDay", "mode", title: "Select Mode Used for 'Home Day'", required: true
90         input "homeModeNight", "mode", title: "Select Mode Used for 'Home Night'", required: true
91     }*/
92         }
93     }
94 }
95
96 def installed() {
97         log.debug "Updated with settings: ${settings}"
98     initialize()
99 }
100
101 def updated() {
102         log.debug "Updated with settings: ${settings}"
103     unsubscribe()
104     initialize()
105 }
106
107 def initialize() {
108     subscribe(people, "presence", presence)
109     checkSun()
110     subscribe(location, "sunrise", setSunrise)
111     subscribe(location, "sunset", setSunset)
112     state.homestate = null
113 }
114
115 //check current sun state when installed.
116 def checkSun() {
117     def zip = settings.zip as String
118     def sunInfo = getSunriseAndSunset(zipCode: zip)
119     def current = now()
120
121     if (sunInfo.sunrise.time < current && sunInfo.sunset.time > current) {
122         state.sunMode = "sunrise"
123         runIn(60,"setSunrise")
124     }
125     else {
126         state.sunMode = "sunset"
127         runIn(60,"setSunset")
128     }
129 }
130
131 //change to sunrise mode on sunrise event
132 def setSunrise(evt) {
133     state.sunMode = "sunrise";
134     changeSunMode(newMode);
135     log.debug "Current sun mode is ${state.sunMode}"
136 }
137
138 //change to sunset mode on sunset event
139 def setSunset(evt) {
140     state.sunMode = "sunset";
141     changeSunMode(newMode)
142     log.debug "Current sun mode is ${state.sunMode}"
143 }
144
145 //change mode on sun event
146 def changeSunMode(newMode) {
147     if (allOk) {
148
149         if (everyoneIsAway()) /*&& (state.sunMode == "sunrise")*/ {
150             log.info("Home is Empty  Setting New Away Mode")
151             def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 10 * 60
152             setAway()
153         }
154 /*
155         else if (everyoneIsAway() && (state.sunMode == "sunset")) {
156             log.info("Home is Empty  Setting New Away Mode")
157             def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 10 * 60
158             setAway()
159         }*/
160         else if (anyoneIsHome()) {
161             log.info("Home is Occupied Setting New Home Mode")
162             setHome()
163
164
165         }
166     }
167 }
168
169 //presence change run logic based on presence state of home
170 def presence(evt) {
171     if (allOk) {
172         if (evt.value == "not present") {
173             log.debug("Checking if everyone is away")
174
175             if (everyoneIsAway()) {
176                 log.info("Nobody is home, running away sequence")
177                 def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 10 * 60
178                 runIn(delay, "setAway")
179             }
180         }
181         else {
182             def lastTime = state[evt.deviceId]
183             if (lastTime == null || now() - lastTime >= 1 * 60000) {
184                 log.info("Someone is home, running home sequence")
185                 setHome()
186             }
187             state[evt.deviceId] = now()
188
189         }
190     }
191 }
192
193 //if empty set home to one of the away modes
194 def setAway() {
195     if (everyoneIsAway()) {
196         if (state.sunMode == "sunset") {
197             def message = "Performing \"${awayNight}\" for you as requested."
198             log.info(message)
199             sendAway(message)
200             location.helloHome.execute(settings.awayNight)
201             state.homestate = "away"
202
203         }
204         else if (state.sunMode == "sunrise") {
205             def message = "Performing \"${awayDay}\" for you as requested."
206             log.info(message)
207             sendAway(message)
208             location.helloHome.execute(settings.awayDay)
209             state.homestate = "away"
210         }
211         else {
212             log.debug("Mode is the same, not evaluating")
213         }
214     }
215 }
216
217 //set home mode when house is occupied
218 def setHome() {
219     log.info("Setting Home Mode!!")
220     if (anyoneIsHome()) {
221         if (state.sunMode == "sunset") {
222             if (state.homestate != "homeNight") {
223                 def message = "Performing \"${homeNight}\" for you as requested."
224                 log.info(message)
225                 sendHome(message)
226                 location.helloHome.execute(settings.homeNight)
227                 state.homestate = "homeNight"
228             }
229         }
230
231         if (state.sunMode == "sunrise") {
232             if (state.homestate != "homeDay") {
233                 def message = "Performing \"${homeDay}\" for you as requested."
234                 log.info(message)
235                 sendHome(message)
236                 location.helloHome.execute(settings.homeDay)
237                 state.homestate = "homeDay"
238             }
239         }
240     }
241 }
242
243 private everyoneIsAway() {
244   def result = true
245
246   if(people.findAll { it?.currentPresence == "present" }) {
247     result = false
248   }
249
250   log.debug("everyoneIsAway: ${result}")
251
252   return result
253 }
254
255 private anyoneIsHome() {
256   def result = false
257
258   if(people.findAll { it?.currentPresence == "present" }) {
259     result = true
260   }
261
262   log.debug("anyoneIsHome: ${result}")
263
264   return result
265 }
266
267 def sendAway(msg) {
268     if (sendPushMessage) {
269         if (recipients) {
270                 sendNotificationToContacts(msg, recipients)
271         }
272         else {
273                 sendPush(msg)
274                 if(phone){
275                         sendSms(phone, msg)
276                 }       
277         }    
278     }
279
280     log.debug(msg)
281 }
282
283 def sendHome(msg) {
284     if (sendPushMessageHome) {
285         if (recipients) {
286                 sendNotificationToContacts(msg, recipients)
287         }
288         else {
289                 sendPush(msg)
290                 if(phone){
291                         sendSms(phone, msg)
292                 }
293         }    
294     }
295
296     log.debug(msg)
297 }
298
299 private getAllOk() {
300     modeOk && daysOk && timeOk
301 }
302
303 private getModeOk() {
304     def result = !modes || modes.contains(location.mode)
305     log.trace "modeOk = $result"
306     result
307 }
308
309 private getDaysOk() {
310     def result = true
311     if (days) {
312         def df = new java.text.SimpleDateFormat("EEEE")
313         if (location.timeZone) {
314             df.setTimeZone(location.timeZone)
315         }
316         else {
317             df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
318         }
319         def day = df.format(new Date())
320         result = days.contains(day)
321     }
322     log.trace "daysOk = $result"
323     result
324 }
325
326 private getTimeOk() {
327     def result = true
328     if (starting && ending) {
329         def currTime = now()
330         def start = timeToday(starting, location?.timeZone).time
331         def stop = timeToday(ending, location?.timeZone).time
332         result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
333     }
334     log.trace "timeOk = $result"
335     result
336 }
337
338 private hhmm(time, fmt = "h:mm a") {
339     def t = timeToday(time, location.timeZone)
340     def f = new java.text.SimpleDateFormat(fmt)
341     f.setTimeZone(location.timeZone?:timeZone(time))
342     f.format(t)
343 }
344
345 private getTimeIntervalLabel() {
346     (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z"): ""
347 }
348
349 private hideOptionsSection() {
350     (starting || ending || days || modes) ? false: true
351 }