2 * ecobeeGenerateWeeklyStats
4 * Copyright 2015 Yves Racine
5 * LinkedIn profile: ca.linkedin.com/pub/yves-racine-m-sc-a/0/406/4b/
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.
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.
16 * Software Distribution is restricted and shall be done only with Developer's written approval.
18 * N.B. Requires MyEcobee device (v5.0 and higher) available at
19 * http://www.ecomatiqhomes.com/#!store/tc3yr
21 import java.text.SimpleDateFormat
23 name: "${get_APP_NAME()}",
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. ",
28 iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee.png",
29 iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee@2x.png"
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"
44 section("Generate weekly stats for this ecobee thermostat") {
45 input "ecobee", "device.myEcobeeDevice", title: "Ecobee?"
48 section("Date for the initial run = YYYY-MM-DD") {
49 input "givenEndDate", "text", title: "End Date [default=today]", required: false
51 section("Time for the initial run (24HR)" ) {
52 input "givenEndTime", "text", title: "End time [default=00:00]", required: false
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
58 section("Detailed Notifications") {
59 input "detailedNotif", "bool", title: "Detailed Notifications?", required:false
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
72 log.debug "Installed with settings: ${settings}"
78 log.debug "Updated with settings: ${settings}"
88 atomicState?.timestamp=''
89 atomicState?.componentAlreadyProcessed=''
90 atomicState?.retries=0
92 runIn((1*60), "generateStats") // run 1 minute later as it requires notification.
93 subscribe(app, appTouch)
94 atomicState?.poll = [ last: 0, rescheduled: now() ]
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)
104 def askAlexaMQHandler(evt) {
108 state?.askAlexaMQ = evt.jsonData && evt.jsonData?.queues ? evt.jsonData.queues : []
109 log.info("askAlexaMQHandler>refresh value=$state?.askAlexaMQ")
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))
121 if (lastPollTime != currentTime) {
122 Double lastPollTimeInMinutes = (lastPollTime/60000).toDouble().round(1)
123 log.info "rescheduleIfNeeded>last poll was ${lastPollTimeInMinutes.toString()} minutes ago"
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)
131 // Update rescheduled state
133 if (!evt) atomicState?.poll["rescheduled"] = now()
138 atomicState?.timestamp=''
139 atomicState?.componentAlreadyProcessed=''
143 void reRunIfNeeded() {
145 log.debug("reRunIfNeeded>About to call generateStats() with state.componentAlreadyProcessed=${atomicState?.componentAlreadyProcessed}")
153 Integer delay = (24*60) // By default, do it every day
154 atomicState?.poll["last"] = now()
156 //schedule the rescheduleIfNeeded() function
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()
164 settings.givenEndDate=null
165 settings.givenEndTime=null
167 log.debug("dailyRun>for $ecobee,about to call generateStats() with settings.givenEndDate=${settings.givenEndDate}")
169 atomicState?.componentAlreadyProcessed=''
170 atomicState?.retries=0
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))
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
189 private def formatDate(dateString) {
190 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm zzz")
191 Date aDate = sdf.parse(dateString)
195 private def get_nextComponentStats(component='') {
197 log.debug "get_nextComponentStats>About to get ${component}'s next component from components table"
204 [position:1, next: 'auxHeat1'
207 [position:2, next: 'auxHeat2'
210 [position:3, next: 'auxHeat3'
213 [position:4, next: 'compCool2'
216 [position:5, next: 'compCool1'
219 [position:6, next: 'done'
223 nextInLine = components.getAt(component)
225 nextInLine=[position:1, next:'auxHeat1']
227 log.debug "get_nextComponentStats>${component} not found, nextInLine=${nextInLine}"
231 log.debug "get_nextComponentStats>got ${component}'s next component from components table= ${nextInLine}"
238 void generateStats() {
239 String dateInLocalTime = new Date().format("yyyy-MM-dd", location.timeZone)
242 float runtimeTotalAvgWeekly
244 def delay = 2 // 2-minute delay for rerun
245 atomicState?.retries= ((atomicState?.retries==null) ?:0) +1
248 unschedule(reRunIfNeeded)
252 log.debug("${get_APP_NAME()}>Exception $e while unscheduling reRunIfNeeded")
255 if (atomicState?.retries >= MAX_RETRIES) {
257 log.debug("${get_APP_NAME()}>Max retries reached, exiting")
258 send("max retries reached ${atomicState?.retries}), exiting")
262 def component = atomicState?.componentAlreadyProcessed
263 def nextComponent = get_nextComponentStats(component) // get nextComponentToBeProcessed
265 log.debug("${get_APP_NAME()}>For ${ecobee}, about to process nextComponent=${nextComponent}, state.componentAlreadyProcessed=${atomicState?.componentAlreadyProcessed}")
267 if (atomicState?.timestamp == dateInLocalTime && nextComponent.position >=MAX_POSITION) {
268 return // the weekly stats are already generated
270 // schedule a rerun till the stats are generated properly
271 schedule("0 0/${delay} * * * ?", reRunIfNeeded)
274 String timezone = new Date().format("zzz", location.timeZone)
275 String dateAtMidnight = dateInLocalTime + " 00:00 " + timezone
277 log.debug("${get_APP_NAME()}>date at Midnight= ${dateAtMidnight}")
279 Date endDate = formatDate(dateAtMidnight)
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
287 log.debug("${get_APP_NAME()}>end dateTime = ${dateTime}, endDate in UTC =${endDate.format("yyyy-MM-dd HH:mm:ss", TimeZone.getTimeZone("UTC"))}")
291 // Get the auxHeat1's runtime for startDate-endDate period
292 component = 'auxHeat1'
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)
302 int heatStages = ecobee.currentHeatStages.toInteger()
305 component = 'auxHeat2'
306 if (heatStages >1 && nextComponent.position <= 2) {
308 // Get the auxHeat2's runtime for startDate-endDate period
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)
319 component = 'auxHeat3'
320 if (heatStages >2 && nextComponent.position <= 3) {
322 // Get the auxHeat3's runtime for startDate-endDate period
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)
332 // Get the compCool1's runtime for startDate-endDate period
334 int coolStages = ecobee.currentCoolStages.toInteger()
337 // Get the compCool2's runtime for startDate-endDate period
338 component = 'compCool2'
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)
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)
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
371 void generateRuntimeReport(component, startDate, endDate, frequence='daily') {
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"))}")
377 ecobee.getReportData("", startDate, endDate, null, null, component,false)
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"))}")
382 ecobee.generateReportRuntimeEvents(component, startDate,endDate, 0, null,frequence)
386 private send(msg, askAlexa=false) {
387 def message = "${get_APP_NAME()}>${msg}"
388 if (sendPushMessage == "Yes") {
392 def expiresInDays=(AskAlexaExpiresInDays)?:2
394 name: "AskAlexaMsgQueue",
395 value: "${get_APP_NAME()}",
397 descriptionText: msg,
400 expires: (expiresInDays*24*60*60) /* Expires after 2 days by default */
403 } /* End if Ask Alexa notifications*/
406 log.debug("sending text message")
407 sendSms(phone, message)
414 private def get_APP_NAME() {
415 return "ecobeeGenerateWeeklyStats"