Update keep-me-cozy.groovy
[smartapps.git] / official / smart-home-ventilation.groovy
1 /**
2  *      Smart Home Ventilation
3  *      Version 2.1.2 - 5/31/15
4  *
5  *      Copyright 2015 Michael Struck
6  *
7  *      Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
8  *      in compliance with the License. You may obtain a copy of the License at:
9  *
10  *              http://www.apache.org/licenses/LICENSE-2.0
11  *
12  *      Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
13  *      on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
14  *      for the specific language governing permissions and limitations under the License.
15  *
16  */
17
18 definition(
19         name: "Smart Home Ventilation",
20     namespace: "MichaelStruck",
21     author: "Michael Struck",
22     description: "Allows for setting up various schedule scenarios for turning on and off home ventilation switches.",
23     category: "Convenience",
24     iconUrl: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Smart-Home-Ventilation/HomeVent.png",
25     iconX2Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Smart-Home-Ventilation/HomeVent@2x.png",
26     iconX3Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Smart-Home-Ventilation/HomeVent@2x.png")
27
28 preferences {
29         page name: "mainPage"
30 }
31
32 def mainPage() {
33         dynamicPage(name: "mainPage", title: "", install: true, uninstall: true) {
34         section("Select ventilation switches..."){
35                         input "switches", title: "Switches", "capability.switch", multiple: true
36                 }
37         section ("Scheduling scenarios...") {
38                 href(name: "toA_Scenario", page: "A_Scenario", title: getTitle (titleA, "A"), description: schedDesc(timeOnA1,timeOffA1,timeOnA2,timeOffA2,timeOnA3,timeOffA3,timeOnA4,timeOffA4, modeA, daysA), state: greyOut(timeOnA1,timeOnA2,timeOnA3,timeOnA4))
39                 href(name: "toB_Scenario", page: "B_Scenario", title: getTitle (titleB, "B"), description: schedDesc(timeOnB1,timeOffB1,timeOnB2,timeOffB2,timeOnB3,timeOffB3,timeOnB4,timeOffB4, modeB, daysB), state: greyOut(timeOnB1,timeOnB2,timeOnB3,timeOnB4))
40                 href(name: "toC_Scenario", page: "C_Scenario", title: getTitle (titleC, "C"), description: schedDesc(timeOnC1,timeOffC1,timeOnC2,timeOffC2,timeOnC3,timeOffC3,timeOnC4,timeOffC4, modeC, daysC), state: greyOut(timeOnC1,timeOnC2,timeOnC3,timeOnC4))
41                 href(name: "toD_Scenario", page: "D_Scenario", title: getTitle (titleD, "D"), description: schedDesc(timeOnD1,timeOffD1,timeOnD2,timeOffD2,timeOnD3,timeOffD3,timeOnD4,timeOffD4, modeD, daysD), state: greyOut(timeOnD1,timeOnD2,timeOnD3,timeOnD4))
42         }
43         section([mobileOnly:true], "Options") {
44                         label(title: "Assign a name", required: false, defaultValue: "Smart Home Ventilation")
45                         href "pageAbout", title: "About ${textAppName()}", description: "Tap to get application version, license and instructions"
46         }
47     }
48 }
49 //----Scheduling Pages
50 page(name: "A_Scenario", title: getTitle (titleA, "A")) {
51         section{
52                         input "timeOnA1", title: "Schedule 1 time to turn on", "time", required: false
53                 input "timeOffA1", title: "Schedule 1 time to turn off", "time", required: false
54                 }
55         section{
56                         input "timeOnA2", title: "Schedule 2 time to turn on", "time", required: false
57                 input "timeOffA2", title: "Schedule 2 time to turn off", "time", required: false
58                 }
59         section{
60                 input "timeOnA3", title: "Schedule 3 time to turn on", "time", required: false
61                 input "timeOffA3", title: "Schedule 3 time to turn off", "time", required: false
62                 }
63         section{
64                 input "timeOnA4", title: "Schedule 4 time to turn on", "time", required: false
65                 input "timeOffA4", title: "Schedule 4 time to turn off", "time", required: false
66                 }
67                 section ("Options") {
68                 input "titleA", title: "Assign a scenario name", "text", required: false
69             input "modeA", "mode", required: false, multiple: true, title: "Run in specific mode(s)", description: "Choose Modes"
70                         input "daysA", "enum", multiple: true, title: "Run on specific day(s)", description: "Choose Days", required: false, options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
71                 }
72     }
73
74 page(name: "B_Scenario", title: getTitle (titleB, "B")) {
75         section{
76                         input "timeOnB1", title: "Schedule 1 time to turn on", "time", required: false
77                 input "timeOffB1", title: "Schedule 1 time to turn off", "time", required: false
78                 }
79         section{
80                         input "timeOnB2", title: "Schedule 2 time to turn on", "time", required: false
81                 input "timeOffB2", title: "Schedule 2 time to turn off", "time", required: false
82                 }
83         section{
84                 input "timeOnB3", title: "Schedule 3 time to turn on", "time", required: false
85                 input "timeOffB3", title: "Schedule 3 time to turn off", "time", required: false
86                 }
87         section{
88                 input "timeOnB4", title: "Schedule 4 time to turn on", "time", required: false
89                 input "timeOffB4", title: "Schedule 4 time to turn off", "time", required: false
90                 }
91                 section("Options") {
92                 input "titleB", title: "Assign a scenario name", "text", required: false
93             input "modeB", "mode", required: false, multiple: true, title: "Run in specific mode(s)", description: "Choose Modes"
94                         input "daysB", "enum", multiple: true, title: "Run on specific day(s)", description: "Choose Days", required: false, options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
95                 }
96     }
97
98 page(name: "C_Scenario", title: getTitle (titleC, "C")) {
99         section{
100                         input "timeOnC1", title: "Schedule 1 time to turn on", "time", required: false
101                 input "timeOffC1", title: "Schedule 1 time to turn off", "time", required: false
102                 }
103         section{
104                         input "timeOnC2", title: "Schedule 2 time to turn on", "time", required: false
105                 input "timeOffC2", title: "Schedule 2 time to turn off", "time", required: false
106                 }
107         section{
108                 input "timeOnC3", title: "Schedule 3 time to turn on", "time", required: false
109                 input "timeOffC3", title: "Schedule 3 time to turn off", "time", required: false
110                 }
111         section{
112                 input "timeOnC4", title: "Schedule 4 time to turn on", "time", required: false
113                 input "timeOffC4", title: "Schedule 4 time to turn off", "time", required: false
114                 }
115                 section("Options") {
116                 input "titleC", title: "Assign a scenario name", "text", required: false
117             input "modeC", "mode", required: false, multiple: true, title: "Run in specific mode(s)", description: "Choose Modes"
118                         input "daysC", "enum", multiple: true, title: "Run on specific day(s)", description: "Choose Days", required: false, options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
119                 }
120     }
121
122
123 page(name: "D_Scenario", title: getTitle (titleD, "D")) {
124         section{
125                         input "timeOnD1", title: "Schedule 1 time to turn on", "time", required: false
126                 input "timeOffD1", title: "Schedule 1 time to turn off", "time", required: false
127                 }
128         section{
129                         input "timeOnD2", title: "Schedule 2 time to turn on", "time", required: false
130                 input "timeOffD2", title: "Schedule 2 time to turn off", "time", required: false
131                 }
132         section{
133                 input "timeOnD3", title: "Schedule 3 time to turn on", "time", required: false
134                 input "timeOffD3", title: "Schedule 3 time to turn off", "time", required: false
135                 }
136         section{
137                 input "timeOnD4", title: "Schedule 4 time to turn on", "time", required: false
138                 input "timeOffD4", title: "Schedule 4 time to turn off", "time", required: false
139                 }
140         section("Options") {
141                 input "titleD", title: "Assign a scenario name", "text", required: false
142             input "modeD", "mode", required: false, multiple: true, title: "Run in specific mode(s)", description: "Choose Modes"
143                         input "daysD", "enum", multiple: true, title: "Run on specific day(s)", description: "Choose Days", required: false, options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
144                 }
145     }
146
147
148 page(name: "pageAbout", title: "About ${textAppName()}") {
149         section {
150             paragraph "${textVersion()}\n${textCopyright()}\n\n${textLicense()}\n"
151         }
152         section("Instructions") {
153             paragraph textHelp()
154         }
155 }
156
157 // Install and initiate
158
159 def installed() {
160     log.debug "Installed with settings: ${settings}"
161     init()
162 }
163
164 def updated() {
165     unschedule()
166     turnOffSwitch() //Turn off all switches if the schedules are changed while in mid-schedule
167     unsubscribe()
168     log.debug "Updated with settings: ${settings}"
169     init()
170 }
171
172 def init() {
173         def midnightTime = timeToday("2000-01-01T00:01:00.999-0000", location.timeZone)
174     schedule (midnightTime, midNight)
175         subscribe(location, "mode", locationHandler)
176     startProcess()
177 }
178
179 // Common methods
180
181 def startProcess () {
182     createDayArray()
183         state.dayCount=state.data.size()
184     if (state.dayCount){
185                 state.counter = 0
186         startDay()
187     }
188 }
189
190 def startDay() {
191         def start = convertEpoch(state.data[state.counter].start)
192         def stop = convertEpoch(state.data[state.counter].stop)
193
194     runOnce(start, turnOnSwitch, [overwrite: true])
195     runOnce(stop, incDay, [overwrite: true])
196 }
197
198 def incDay() {
199     turnOffSwitch()
200     if (state.modeChange) {
201         startProcess()
202     }
203     else {
204         state.counter = state.counter + 1
205         if (state.counter < state.dayCount) {
206                 startDay()
207         }
208     }
209 }
210
211 def locationHandler(evt) {
212         def result = false
213     state.modeChange = true
214     switches.each {
215         if (it.currentValue("switch")=="on"){
216            result = true
217         }
218     }
219         if (!result) {
220         startProcess()
221     }
222 }
223
224 def midNight(){
225     startProcess()
226 }
227
228 def turnOnSwitch() {
229     switches.on()
230     log.debug "Home ventilation switches are on."
231 }
232
233 def turnOffSwitch() {
234     switches.each {
235         if (it.currentValue("switch")=="on"){
236                         it.off()
237         }
238     }
239     log.debug "Home ventilation switches are off."
240 }
241
242 def schedDesc(on1, off1, on2, off2, on3, off3, on4, off4, modeList, dayList) {
243         def title = ""
244         def dayListClean = "On "
245     def modeListClean ="Scenario runs in "
246     if (dayList && dayList.size() < 7) {
247         def dayListSize = dayList.size()
248         for (dayName in dayList) {
249                 dayListClean = "${dayListClean}"+"${dayName}"
250                 dayListSize = dayListSize -1
251             if (dayListSize) {
252                 dayListClean = "${dayListClean}, "
253             }
254         }
255         }
256     else {
257         dayListClean = "Every day"
258     }
259     if (modeList) {
260         def modeListSize = modeList.size()
261         def modePrefix ="modes"
262         if (modeListSize == 1) {
263                 modePrefix = "mode"
264         }
265         for (modeName in modeList) {
266                 modeListClean = "${modeListClean}"+"'${modeName}'"
267                 modeListSize = modeListSize -1
268             if (modeListSize) {
269                 modeListClean = "${modeListClean}, "
270             }
271             else {
272                 modeListClean = "${modeListClean} ${modePrefix}"
273                 }
274         }
275         }
276     else {
277         modeListClean = "${modeListClean}all modes"
278     }
279     if (on1 && off1){
280         title += "Schedule 1: ${humanReadableTime(on1)} to ${humanReadableTime(off1)}"
281     }
282     if (on2 && off2) {
283         title += "\nSchedule 2: ${humanReadableTime(on2)} to ${humanReadableTime(off2)}"
284     }
285     if (on3 && off3) {
286         title += "\nSchedule 3: ${humanReadableTime(on3)} to ${humanReadableTime(off3)}"
287     }
288     if (on4 && off4) {
289         title += "\nSchedule 4: ${humanReadableTime(on4)} to ${humanReadableTime(off4)}"
290     }
291     if (on1 || on2 || on3 || on4) {
292         title += "\n$modeListClean"
293         title += "\n$dayListClean"
294     }
295
296     if (!on1 && !on2 && !on3 && !on4) {
297         title="Click to configure scenario"
298     }
299     title
300 }
301
302 def greyOut(on1, on2, on3, on4){
303     def result = on1 || on2 || on3 || on4 ? "complete" : ""
304 }
305
306 public humanReadableTime(dateTxt) {
307         new Date().parse("yyyy-MM-dd'T'HH:mm:ss.SSSZ", dateTxt).format("h:mm a", timeZone(dateTxt))
308 }
309
310 public convertEpoch(epochDate) {
311     new Date(epochDate).format("yyyy-MM-dd'T'HH:mm:ss.SSSZ", location.timeZone)
312 }
313
314 private getTitle(txt, scenario) {
315     def title = txt ? txt : "Scenario ${scenario}"
316 }
317
318 private daysOk(dayList) {
319         def result = true
320     if (dayList) {
321                 def df = new java.text.SimpleDateFormat("EEEE")
322                 if (location.timeZone) {
323                         df.setTimeZone(location.timeZone)
324                 }
325                 else {
326                         df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
327                 }
328                 def day = df.format(new Date())
329                 result = dayList.contains(day)
330         }
331     result
332 }
333
334 private timeOk(starting, ending) {
335     if (starting && ending) {
336         def currTime = now()
337                 def start = timeToday(starting).time
338                 def stop = timeToday(ending).time
339         if (start < stop && start >= currTime && stop>=currTime) {
340                 state.data << [start:start, stop:stop]
341         }
342     }
343 }
344
345 def createDayArray() {
346         state.modeChange = false
347     state.data = []
348     if (modeA && modeA.contains(location.mode)) {
349         if (daysOk(daysA)){
350             timeOk(timeOnA1, timeOffA1)
351                         timeOk(timeOnA2, timeOffA2)
352                         timeOk(timeOnA3, timeOffA3)
353                         timeOk(timeOnA4, timeOffA4)
354         }
355     }
356     if (modeB && modeB.contains(location.mode)) {
357         if (daysOk(daysB)){
358                         timeOk(timeOnB1, timeOffB1)
359             timeOk(timeOnB2, timeOffB2)
360             timeOk(timeOnB3, timeOffB3)
361             timeOk(timeOnB4, timeOffB4)
362         }
363     }
364     if (modeC && modeC.contains(location.mode)) {
365         if (daysOk(daysC)){
366             timeOk(timeOnC1, timeOffC1)
367             timeOk(timeOnC2, timeOffC2)
368             timeOk(timeOnC3, timeOffC3)
369             timeOk(timeOnC4, timeOffC4)
370         }
371     }
372     if (modeD && modeD.contains(location.mode)) {
373         if (daysOk(daysD)){
374            timeOk(timeOnD1, timeOffD1)
375            timeOk(timeOnD2, timeOffD2)
376            timeOk(timeOnD3, timeOffD3)
377            timeOk(timeOnD4, timeOffD4)
378         }
379     }
380     state.data.sort{it.start}
381 }
382
383 //Version/Copyright/Information/Help
384
385 private def textAppName() {
386         def text = "Smart Home Ventilation"
387 }
388
389 private def textVersion() {
390     def text = "Version 2.1.2 (05/31/2015)"
391 }
392
393 private def textCopyright() {
394     def text = "Copyright © 2015 Michael Struck"
395 }
396
397 private def textLicense() {
398     def text =
399                 "Licensed under the Apache License, Version 2.0 (the 'License'); "+
400                 "you may not use this file except in compliance with the License. "+
401                 "You may obtain a copy of the License at"+
402                 "\n\n"+
403                 "    http://www.apache.org/licenses/LICENSE-2.0"+
404                 "\n\n"+
405                 "Unless required by applicable law or agreed to in writing, software "+
406                 "distributed under the License is distributed on an 'AS IS' BASIS, "+
407                 "WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. "+
408                 "See the License for the specific language governing permissions and "+
409                 "limitations under the License."
410 }
411
412 private def textHelp() {
413         def text =
414         "Within each scenario, choose a start and end time for the ventilation fan. You can have up to 4 different " +
415         "venting scenarios, and 4 schedules within each scenario. Each scenario can be restricted to specific modes or certain days of the week. It is recommended "+
416         "that each scenario does not overlap and run in separate modes (i.e. Home, Out of town, etc). Also note that you should  " +
417         "avoid scheduling the ventilation fan at exactly midnight; the app resets itself at that time. It is suggested to start any new schedule " +
418         "at 12:15 am or later."
419 }