Changing remote branch to PLRG Git server.
[smartapps.git] / third-party / ecobeeGenerateWeeklyStats.groovy
1 /**
2  *  ecobeeGenerateWeeklyStats
3  *
4  *  Copyright 2015 Yves Racine
5  *  LinkedIn profile: ca.linkedin.com/pub/yves-racine-m-sc-a/0/406/4b/
6  *
7  *  Developer retains all right, title, copyright, and interest, including all copyright, patent rights, trade secret 
8  *  in the Background technology. May be subject to consulting fees under the Agreement between the Developer and the Customer. 
9  *  Developer grants a non exclusive perpetual license to use the Background technology in the Software developed for and delivered 
10  *  to Customer under this Agreement. However, the Customer shall make no commercial use of the Background technology without
11  *  Developer's written consent.
12  *
13  *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
14  *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
15  *
16  *  Software Distribution is restricted and shall be done only with Developer's written approval.
17  * 
18  *  N.B. Requires MyEcobee device (v5.0 and higher) available at 
19  *          http://www.ecomatiqhomes.com/#!store/tc3yr 
20  */
21 import java.text.SimpleDateFormat 
22 definition(
23     name: "${get_APP_NAME()}",
24     namespace: "yracine",
25     author: "Yves Racine",
26     description: "This smartapp allows a ST user to generate weekly runtime stats (by scheduling or based on custom dates) on their devices controlled by ecobee such as a heating & cooling component,fan, dehumidifier/humidifier/HRV/ERV. ",
27     category: "My Apps",
28     iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee.png",
29     iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee@2x.png"
30 )
31
32 preferences {
33         section("About") {
34                 paragraph "${get_APP_NAME()}, the smartapp that generates weekly runtime reports about your ecobee components"
35                 paragraph "Version 1.7" 
36                 paragraph "If you like this smartapp, please support the developer via PayPal and click on the Paypal link below " 
37                         href url: "https://www.paypal.me/ecomatiqhomes",
38                                 title:"Paypal donation..."
39                 paragraph "Copyright©2016 Yves Racine"
40                         href url:"http://github.com/yracine/device-type.myecobee", style:"embedded", required:false, title:"More information..."  
41                                 description: "http://github.com/yracine/device-type.myecobee/blob/master/README.md"
42         }
43
44         section("Generate weekly stats for this ecobee thermostat") {
45                 input "ecobee", "device.myEcobeeDevice", title: "Ecobee?"
46
47         }
48         section("Date for the initial run = YYYY-MM-DD") {
49                 input "givenEndDate", "text", title: "End Date [default=today]", required: false
50         }        
51         section("Time for the initial run (24HR)" ) {
52                 input "givenEndTime", "text", title: "End time [default=00:00]", required: false
53         }        
54         section( "Notifications" ) {
55                 input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes", "No"]], required: false, default:"No"
56                 input "phoneNumber", "phone", title: "Send a text message?", required: false
57         }
58         section("Detailed Notifications") {
59                 input "detailedNotif", "bool", title: "Detailed Notifications?", required:false
60         }
61         section("Enable Amazon Echo/Ask Alexa Notifications [optional, default=false]") {
62                 input (name:"askAlexaFlag", title: "Ask Alexa verbal Notifications?", type:"bool",
63                         description:"optional",required:false)
64                 input (name:"listOfMQs",  type:"enum", title: "List of the Ask Alexa Message Queues (default=Primary)", options: state?.askAlexaMQ, multiple: true, required: false,
65                         description:"optional")            
66                 input "AskAlexaExpiresInDays", "number", title: "Ask Alexa's messages expiration in days (default=2 days)?", required: false
67         }
68      
69 }
70
71 def installed() {
72         log.debug "Installed with settings: ${settings}"
73
74         initialize()
75 }
76
77 def updated() {
78         log.debug "Updated with settings: ${settings}"
79
80         unsubscribe()
81         unschedule()    
82         initialize()
83 }
84
85 def initialize() {
86
87
88         atomicState?.timestamp=''
89         atomicState?.componentAlreadyProcessed=''
90         atomicState?.retries=0
91
92         runIn((1*60),   "generateStats") // run 1 minute later as it requires notification.     
93         subscribe(app, appTouch)
94         atomicState?.poll = [ last: 0, rescheduled: now() ]
95
96         //Subscribe to different events (ex. sunrise and sunset events) to trigger rescheduling if needed
97         subscribe(location, "sunset", rescheduleIfNeeded)
98         subscribe(location, "mode", rescheduleIfNeeded)
99         subscribe(location, "sunsetTime", rescheduleIfNeeded)
100         subscribe(location, "askAlexaMQ", askAlexaMQHandler)
101         rescheduleIfNeeded()   
102 }
103
104 def askAlexaMQHandler(evt) {
105         if (!evt) return
106         switch (evt.value) {
107                 case "refresh":
108                 state?.askAlexaMQ = evt.jsonData && evt.jsonData?.queues ? evt.jsonData.queues : []
109                 log.info("askAlexaMQHandler>refresh value=$state?.askAlexaMQ")        
110                 break
111         }
112 }
113
114
115 def rescheduleIfNeeded(evt) {
116         if (evt) log.debug("rescheduleIfNeeded>$evt.name=$evt.value")
117         Integer delay = (24*60) // By default, do it every day
118         BigDecimal currentTime = now()    
119         BigDecimal lastPollTime = (currentTime - (atomicState?.poll["last"]?:0))  
120  
121         if (lastPollTime != currentTime) {    
122                 Double lastPollTimeInMinutes = (lastPollTime/60000).toDouble().round(1)      
123                 log.info "rescheduleIfNeeded>last poll was  ${lastPollTimeInMinutes.toString()} minutes ago"
124         }
125         if (((atomicState?.poll["last"]?:0) + (delay * 60000) < currentTime) && canSchedule()) {
126                 log.info "rescheduleIfNeeded>scheduling dailyRun in ${delay} minutes.."
127 //              generate the stats every day at 0:15
128                 schedule("0 15 0 * * ?", dailyRun)    
129         }
130     
131         // Update rescheduled state
132     
133         if (!evt) atomicState?.poll["rescheduled"] = now()
134 }
135    
136
137 def appTouch(evt) {
138         atomicState?.timestamp=''
139         atomicState?.componentAlreadyProcessed=''
140         generateStats()
141 }
142
143 void reRunIfNeeded() {
144         if (detailedNotif) {    
145                 log.debug("reRunIfNeeded>About to call generateStats() with state.componentAlreadyProcessed=${atomicState?.componentAlreadyProcessed}")
146         }    
147         generateStats()
148    
149 }
150
151
152 void dailyRun() {
153         Integer delay = (24*60) // By default, do it every day
154         atomicState?.poll["last"] = now()
155                 
156         //schedule the rescheduleIfNeeded() function
157     
158         if (((atomicState?.poll["rescheduled"]?:0) + (delay * 60000)) < now()) {
159                 log.info "takeAction>scheduling rescheduleIfNeeded() in ${delay} minutes.."
160                 schedule("0 15 0 * * ?", rescheduleIfNeeded)    
161                 // Update rescheduled state
162                 atomicState?.poll["rescheduled"] = now()
163         }
164         settings.givenEndDate=null
165         settings.givenEndTime=null
166         if (detailedNotif) {    
167                 log.debug("dailyRun>for $ecobee,about to call generateStats() with settings.givenEndDate=${settings.givenEndDate}")
168         }    
169         atomicState?.componentAlreadyProcessed=''
170         atomicState?.retries=0
171         generateStats()
172     
173 }
174
175
176 private String formatISODateInLocalTime(dateInString, timezone='') {
177         def myTimezone=(timezone)?TimeZone.getTimeZone(timezone):location.timeZone 
178         if ((dateInString==null) || (dateInString.trim()=="")) {
179                 return (new Date().format("yyyy-MM-dd HH:mm:ss", myTimezone))
180         }    
181         SimpleDateFormat ISODateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
182         Date ISODate = ISODateFormat.parse(dateInString)
183         String dateInLocalTime =new Date(ISODate.getTime()).format("yyyy-MM-dd HH:mm:ss", myTimezone)
184         log.debug("formatDateInLocalTime>dateInString=$dateInString, dateInLocalTime=$dateInLocalTime")    
185         return dateInLocalTime
186 }
187
188
189 private def formatDate(dateString) {
190         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm zzz")
191         Date aDate = sdf.parse(dateString)
192         return aDate
193 }
194
195 private def get_nextComponentStats(component='') {
196         if (detailedNotif) {
197                 log.debug "get_nextComponentStats>About to get ${component}'s next component from components table"
198         }
199
200         def nextInLine=[:]
201
202         def components = [
203                 '': 
204                         [position:1, next: 'auxHeat1'
205                         ], 
206                 'auxHeat1': 
207                         [position:2, next: 'auxHeat2'
208                         ], 
209                 'auxHeat2': 
210                         [position:3, next: 'auxHeat3'
211                         ], 
212                 'auxHeat3': 
213                         [position:4, next: 'compCool2'
214                         ], 
215                 'compCool2': 
216                         [position:5, next: 'compCool1'
217                         ],
218                 'compCool1': 
219                         [position:6, next: 'done'
220                         ], 
221                 ]
222         try {
223                 nextInLine = components.getAt(component)
224         } catch (any) {
225                 nextInLine=[position:1, next:'auxHeat1']
226                 if (detailedNotif) {
227                         log.debug "get_nextComponentStats>${component} not found, nextInLine=${nextInLine}"
228                 }
229         }          
230         if (detailedNotif) {
231                 log.debug "get_nextComponentStats>got ${component}'s next component from components table= ${nextInLine}"
232         }
233         return nextInLine
234                             
235 }
236
237
238 void generateStats() {
239         String dateInLocalTime = new Date().format("yyyy-MM-dd", location.timeZone) 
240         def MAX_POSITION=6    
241         def MAX_RETRIES=4    
242         float runtimeTotalAvgWeekly
243     
244         def delay = 2 // 2-minute delay for rerun
245         atomicState?.retries=   ((atomicState?.retries==null) ?:0) +1
246
247         try {
248                 unschedule(reRunIfNeeded)
249         } catch (e) {
250     
251                 if (detailedNotif) {    
252                         log.debug("${get_APP_NAME()}>Exception $e while unscheduling reRunIfNeeded")
253                 }       
254         }    
255         if (atomicState?.retries >= MAX_RETRIES) { 
256                 if (detailedNotif) {    
257                         log.debug("${get_APP_NAME()}>Max retries reached, exiting")
258                         send("max retries reached ${atomicState?.retries}), exiting")
259                 }       
260         }       
261       
262         def component = atomicState?.componentAlreadyProcessed   
263         def nextComponent  = get_nextComponentStats(component) // get nextComponentToBeProcessed        
264         if (detailedNotif) {    
265                 log.debug("${get_APP_NAME()}>For ${ecobee}, about to process nextComponent=${nextComponent}, state.componentAlreadyProcessed=${atomicState?.componentAlreadyProcessed}")
266         }       
267         if (atomicState?.timestamp == dateInLocalTime && nextComponent.position >=MAX_POSITION) {
268                 return // the weekly stats are already generated 
269         } else {        
270                 // schedule a rerun till the stats are generated properly
271                 schedule("0 0/${delay} * * * ?", reRunIfNeeded)
272         }       
273     
274         String timezone = new Date().format("zzz", location.timeZone)
275         String dateAtMidnight = dateInLocalTime + " 00:00 " + timezone    
276         if (detailedNotif) {    
277                 log.debug("${get_APP_NAME()}>date at Midnight= ${dateAtMidnight}")
278         }        
279         Date endDate = formatDate(dateAtMidnight) 
280             
281         def reportEndDate = (settings.givenEndDate) ?: endDate.format("yyyy-MM-dd", location.timeZone) 
282         def reportEndTime=(settings.givenEndTime) ?:"00:00"    
283         def dateTime = reportEndDate + " " + reportEndTime + " " + timezone
284         endDate = formatDate(dateTime)
285         Date aWeekAgo= endDate -7   
286         if (detailedNotif) {    
287                 log.debug("${get_APP_NAME()}>end dateTime = ${dateTime}, endDate in UTC =${endDate.format("yyyy-MM-dd HH:mm:ss", TimeZone.getTimeZone("UTC"))}")
288         }
289
290
291         // Get the auxHeat1's runtime for startDate-endDate period
292         component = 'auxHeat1'
293
294         if (nextComponent.position <= 1) { 
295                 generateRuntimeReport(component,aWeekAgo, endDate,'weekly') // generate stats for the last 7 days
296                 runtimeTotalAvgWeekly = (ecobee.currentAuxHeat1RuntimeAvgWeekly)? ecobee.currentAuxHeat1RuntimeAvgWeekly.toFloat().round(2):0
297                 atomicState?.componentAlreadyProcessed=component
298                 if (runtimeTotalAvgWeekly) {
299                         send ("${ecobee} ${component}'s average weekly runtime stats=${runtimeTotalAvgWeekly} minutes since ${String.format('%tF', aWeekAgo)}", settings.askAlexaFlag)
300                 }     
301         }
302         int heatStages = ecobee.currentHeatStages.toInteger()
303     
304     
305         component = 'auxHeat2'
306         if (heatStages >1 && nextComponent.position <= 2) { 
307     
308 //      Get the auxHeat2's runtime for startDate-endDate period
309         
310                 generateRuntimeReport(component,aWeekAgo, endDate,'weekly') // generate stats for the last 7 days
311                 runtimeTotalAvgWeekly = (ecobee.currentAuxHeat2RuntimeAvgWeekly)? ecobee.currentAuxHeat2RuntimeAvgWeekly.toFloat().round(2):0
312                 atomicState?.componentAlreadyProcessed=component
313                 if (runtimeTotalAvgWeekly) {
314                         send ("${ecobee} ${component}'s average weekly runtime stats=${runtimeTotalAvgWeekly} minutes since ${String.format('%tF', aWeekAgo)}", settings.askAlexaFlag)
315                 }     
316
317         }     
318
319         component = 'auxHeat3'
320         if (heatStages >2 && nextComponent.position <= 3) { 
321     
322 //      Get the auxHeat3's runtime for startDate-endDate period
323         
324                 generateRuntimeReport(component,aWeekAgo, endDate,'weekly') // generate stats for the last 7 days
325                 runtimeTotalAvgWeekly = (ecobee.currentAuxHeat3RuntimeAvgWeekly)? ecobee.currentAuxHeat3RuntimeAvgWeekly.toFloat().round(2):0
326                 atomicState?.componentAlreadyProcessed=component
327                 if (runtimeTotalAvgWeekly) {
328                         send ("${ecobee} ${component}'s average weekly runtime stats=${runtimeTotalAvgWeekly} minutes since ${String.format('%tF', aWeekAgo)}", settings.askAlexaFlag)
329                 }     
330         }     
331
332 // Get the compCool1's runtime for startDate-endDate period
333
334         int coolStages = ecobee.currentCoolStages.toInteger()
335
336             
337 //      Get the compCool2's runtime for startDate-endDate period
338         component = 'compCool2'
339
340         if (coolStages >1 && nextComponent.position <= 4) {
341                 generateRuntimeReport(component,aWeekAgo, endDate,'weekly') // generate stats for the last 7 days
342                 runtimeTotalAvgWeekly = (ecobee.currentCompCool2RuntimeAvgWeekly)? ecobee.currentCompCool2RuntimeAvgWeekly.toFloat().round(2):0
343                 atomicState?.componentAlreadyProcessed=component
344                 if (runtimeTotalAvgWeekly) {
345                         send ("${ecobee} ${component}'s average weekly runtime stats=${runtimeTotalAvgWeekly} minutes since ${String.format('%tF', aWeekAgo)}", settings.askAlexaFlag)
346                 }     
347         } 
348     
349         component = 'compCool1'
350         if (nextComponent.position <= 5) {
351                 generateRuntimeReport(component,aWeekAgo, endDate,'weekly') // generate stats for the last 7 days
352                 runtimeTotalAvgWeekly = (ecobee.currentCompCool1RuntimeAvgWeekly)? ecobee.currentCompCool1RuntimeAvgWeekly.toFloat().round(2):0
353                 atomicState?.componentAlreadyProcessed=component
354                 if (runtimeTotalAvgWeekly) {
355                         send ("${ecobee} ${component}'s average weekly runtime stats=${runtimeTotalAvgWeekly} minutes since ${String.format('%tF', aWeekAgo)}", settings.askAlexaFlag)
356                 }     
357         }        
358     
359         component= atomicState?.componentAlreadyProcessed   
360         nextComponent  = get_nextComponentStats(component) // get nextComponentToBeProcessed    
361         if (nextComponent?.position >=MAX_POSITION) {
362                 send " generated all ${ecobee}'s weekly stats since ${String.format('%tF', aWeekAgo)}"
363                 unschedule(reRunIfNeeded) // No need to reschedule again as the stats are completed.
364                 atomicState?.timestamp = dateInLocalTime // save the date to avoid re-execution.
365                 atomicState?.retries=0
366         
367         }
368
369 }
370
371 void generateRuntimeReport(component, startDate, endDate, frequence='daily') {
372
373         if (detailedNotif) {
374                 log.debug("generateRuntimeReport>For component ${component}, about to call getReportData with endDate in UTC =${endDate.format("yyyy-MM-dd HH:mm:ss", TimeZone.getTimeZone("UTC"))}")
375                 send ("For component ${component}, about to call getReportData with aWeekAgo in UTC =${endDate.format("yyyy-MM-dd HH:mm:ss", TimeZone.getTimeZone("UTC"))}")
376         }        
377         ecobee.getReportData("", startDate, endDate, null, null, component,false)
378         if (detailedNotif) {
379                 log.debug("generateRuntimeReport>For component ${component}, about to call generateReportRuntimeEvents with endDate in UTC =${endDate.format("yyyy-MM-dd HH:mm:ss", TimeZone.getTimeZone("UTC"))}")
380                 send ("For component ${component}, about to call generateReportRuntimeEvents with aWeekAgo in UTC =${endDate.format("yyyy-MM-dd HH:mm:ss", TimeZone.getTimeZone("UTC"))}")
381         }        
382         ecobee.generateReportRuntimeEvents(component, startDate,endDate, 0, null,frequence)
383
384 }
385
386 private send(msg, askAlexa=false) {
387         def message = "${get_APP_NAME()}>${msg}"
388         if (sendPushMessage == "Yes") {
389                         sendPush(message)
390         }
391         if (askAlexa) {
392                 def expiresInDays=(AskAlexaExpiresInDays)?:2    
393                 sendLocationEvent(
394                         name: "AskAlexaMsgQueue", 
395                         value: "${get_APP_NAME()}", 
396                         isStateChange: true, 
397                         descriptionText: msg, 
398                         data:[
399                                 queues: listOfMQs,
400                                 expires: (expiresInDays*24*60*60)  /* Expires after 2 days by default */
401                         ]
402                 )
403         } /* End if Ask Alexa notifications*/
404
405         if (phone) {
406                 log.debug("sending text message")
407                 sendSms(phone, message)
408         }
409     
410         log.debug msg
411     
412 }
413
414 private def get_APP_NAME() {
415         return "ecobeeGenerateWeeklyStats"
416 }