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 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 runtime stats (daily 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 daily runtime reports about your ecobee components"
35 paragraph "Version 2.5.1"
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©2014 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 daily stats for this ecobee thermostat") {
45 input "ecobee", "device.myEcobeeDevice", title: "Ecobee?"
48 section("Start date for the initial run, format = YYYY-MM-DD") {
49 input "givenStartDate", "text", title: "Beginning Date [default=yesterday]", required: false
51 section("Start time for initial run HH:MM (24HR)") {
52 input "givenStartTime", "text", title: "Beginning time [default=00:00]" , required: false
54 section("End date for the initial run = YYYY-MM-DD") {
55 input "givenEndDate", "text", title: "End Date [default=today]", required: false
57 section("End time for the initial run (24HR)" ) {
58 input "givenEndTime", "text", title: "End time [default=00:00]", required: false
60 section( "Notifications" ) {
61 input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes", "No"]], required: false, default:"No"
62 input "phoneNumber", "phone", title: "Send a text message?", required: false
64 section("Detailed Notifications") {
65 input "detailedNotif", "bool", title: "Detailed Notifications?", required:false
67 section("Enable Amazon Echo/Ask Alexa Notifications [optional, default=false]") {
68 input (name:"askAlexaFlag", title: "Ask Alexa verbal Notifications?", type:"bool",
69 description:"optional",required:false)
70 input (name:"listOfMQs", type:"enum", title: "List of the Ask Alexa Message Queues (default=Primary)", options: state?.askAlexaMQ, multiple: true, required: false,
71 description:"optional")
72 input "AskAlexaExpiresInDays", "number", title: "Ask Alexa's messages expiration in days (default=2 days)?", required: false
79 log.debug "Installed with settings: ${settings}"
85 log.debug "Updated with settings: ${settings}"
94 atomicState?.timestamp=''
95 atomicState?.componentAlreadyProcessed=''
96 atomicState?.retries=0
98 runIn((1*60), "generateStats") // run 1 minute later as it requires notification.
99 subscribe(app, appTouch)
100 atomicState?.poll = [ last: 0, rescheduled: now() ]
102 //Subscribe to different events (ex. sunrise and sunset events) to trigger rescheduling if needed
103 subscribe(location, "sunset", rescheduleIfNeeded)
104 subscribe(location, "mode", rescheduleIfNeeded)
105 subscribe(location, "sunsetTime", rescheduleIfNeeded)
106 subscribe(location, "askAlexaMQ", askAlexaMQHandler)
110 def askAlexaMQHandler(evt) {
114 state?.askAlexaMQ = evt.jsonData && evt.jsonData?.queues ? evt.jsonData.queues : []
115 log.info("askAlexaMQHandler>refresh value=$state?.askAlexaMQ")
120 def rescheduleIfNeeded(evt) {
121 if (evt) log.debug("rescheduleIfNeeded>$evt.name=$evt.value")
122 Integer delay = (24*60) // By default, do it every day
123 BigDecimal currentTime = now()
124 BigDecimal lastPollTime = (currentTime - (atomicState?.poll["last"]?:0))
126 if (lastPollTime != currentTime) {
127 Double lastPollTimeInMinutes = (lastPollTime/60000).toDouble().round(1)
128 log.info "rescheduleIfNeeded>last poll was ${lastPollTimeInMinutes.toString()} minutes ago"
130 if (((atomicState?.poll["last"]?:0) + (delay * 60000) < currentTime) && canSchedule()) {
131 log.info "rescheduleIfNeeded>scheduling dailyRun in ${delay} minutes.."
132 // generate the stats every day at 0:10
134 schedule("0 10 0 * * ?", dailyRun)
137 // Update rescheduled state
139 if (!evt) atomicState?.poll["rescheduled"] = now()
144 atomicState?.timestamp=''
145 atomicState?.componentAlreadyProcessed=''
152 Integer delay = (24*60) // By default, do it every day
153 atomicState?.poll["last"] = now()
155 //schedule the rescheduleIfNeeded() function
157 if (((atomicState?.poll["rescheduled"]?:0) + (delay * 60000)) < now()) {
158 log.info "takeAction>scheduling rescheduleIfNeeded() in ${delay} minutes.."
159 schedule("0 10 0 * * ?", rescheduleIfNeeded)
160 // Update rescheduled state
161 atomicState?.poll["rescheduled"] = now()
163 settings.givenStartDate=null
164 settings.givenStartTime=null
165 settings.givenEndDate=null
166 settings.givenEndTime=null
168 log.debug("dailyRun>for $ecobee, about to call generateStats() with settings.givenEndDate=${settings.givenEndDate}")
170 atomicState?.componentAlreadyProcessed=''
171 atomicState?.retries=0
176 void reRunIfNeeded() {
178 log.debug("reRunIfNeeded>About to call generateStats() with state.componentAlreadyProcessed=${atomicState?.componentAlreadyProcessed}")
186 private String formatISODateInLocalTime(dateInString, timezone='') {
187 def myTimezone=(timezone)?TimeZone.getTimeZone(timezone):location.timeZone
188 if ((dateInString==null) || (dateInString.trim()=="")) {
189 return (new Date().format("yyyy-MM-dd HH:mm:ss", myTimezone))
191 SimpleDateFormat ISODateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
192 Date ISODate = ISODateFormat.parse(dateInString)
193 String dateInLocalTime =new Date(ISODate.getTime()).format("yyyy-MM-dd HH:mm:ss", myTimezone)
194 log.debug("formatDateInLocalTime>dateInString=$dateInString, dateInLocalTime=$dateInLocalTime")
195 return dateInLocalTime
199 private def formatDate(dateString) {
200 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm zzz")
201 Date aDate = sdf.parse(dateString)
205 private def get_nextComponentStats(component='') {
210 [position:1, next: 'auxHeat1'
213 [position:2, next: 'auxHeat2'
216 [position:3, next: 'auxHeat3'
219 [position:4, next: 'compCool1'
222 [position:5, next: 'compCool2'
225 [position:6, next: 'humidifier'
228 [position:7, next: 'dehumidifier'
231 [position:8, next: 'ventilator'
234 [position:9, next: 'fan'
237 [position:10, next: 'done'
241 nextInLine = components.getAt(component)
244 log.debug "get_nextComponentStats>${component} not found"
246 nextInLine=[position:1,next:'auxHeat1']
250 log.debug "get_nextComponentStats>got ${component}'s next component from components table= ${nextInLine}"
257 void generateStats() {
260 float runtimeTotalYesterday,runtimeTotalDaily
261 String dateInLocalTime = new Date().format("yyyy-MM-dd", location.timeZone)
264 atomicState?.retries= ((atomicState?.retries==null) ?:0) +1
267 unschedule(reRunIfNeeded)
271 log.debug("${get_APP_NAME()}>Exception $e while unscheduling reRunIfNeeded")
275 if (atomicState?.retries >= MAX_RETRIES) {
277 log.debug("${get_APP_NAME()}>Max retries reached, exiting")
278 send("max retries reached ${atomicState?.retries}), exiting")
282 def component=atomicState?.componentAlreadyProcessed // use logic to restart the batch process if needed due to ST rate limiting
283 def nextComponent = get_nextComponentStats(component) // get next Component To Be Processed
285 log.debug("${get_APP_NAME()}>for $ecobee, about to process nextComponent=${nextComponent}, state.componentAlreadyProcessed=${atomicState?.componentAlreadyProcessed}")
287 if (atomicState?.timestamp == dateInLocalTime && nextComponent.position >=MAX_POSITION) {
288 return // the daily stats are already generated
290 // schedule a rerun till the stats are generated properly
291 schedule("0 0/${delay} * * * ?", reRunIfNeeded)
295 String timezone = new Date().format("zzz", location.timeZone)
296 String dateAtMidnight = dateInLocalTime + " 00:00 " + timezone
298 log.debug("${get_APP_NAME()}>date at Midnight= ${dateAtMidnight}")
300 Date endDate = formatDate(dateAtMidnight)
301 Date startDate = endDate -1
303 def reportStartDate = (settings.givenStartDate) ?: startDate.format("yyyy-MM-dd", location.timeZone)
304 def reportStartTime=(settings.givenStartTime) ?:"00:00"
305 def dateTime = reportStartDate + " " + reportStartTime + " " + timezone
306 startDate = formatDate(dateTime)
307 Date yesterday = startDate-1
310 log.debug("${get_APP_NAME()}>start dateTime = ${dateTime}, startDate in UTC = ${startDate.format("yyyy-MM-dd HH:mm:ss", TimeZone.getTimeZone("UTC"))}")
312 def reportEndDate = (settings.givenEndDate) ?: endDate.format("yyyy-MM-dd", location.timeZone)
313 def reportEndTime=(settings.givenEndTime) ?:"00:00"
314 dateTime = reportEndDate + " " + reportEndTime + " " + timezone
315 endDate = formatDate(dateTime)
317 log.debug("${get_APP_NAME()}>end dateTime = ${dateTime}, endDate in UTC =${endDate.format("yyyy-MM-dd HH:mm:ss", TimeZone.getTimeZone("UTC"))}")
321 // Get the auxHeat1's runtime for startDate-endDate period
322 component = 'auxHeat1'
323 if (nextComponent.position <= 1) {
324 generateRuntimeReport(component,startDate, endDate)
325 runtimeTotalDaily = (ecobee.currentAuxHeat1RuntimeDaily) ? ecobee.currentAuxHeat1RuntimeDaily.toFloat().round(2):0
326 if (runtimeTotalDaily) {
327 send "On ${String.format('%tF', startDate)}, ${ecobee} ${component}'s runtime stats=${runtimeTotalDaily} minutes", settings.askAlexaFlag
330 generateRuntimeReport(component,yesterday, startDate,'yesterday') // generate stats for yesterday
331 runtimeTotalYesterday = (ecobee.currentAuxHeat1RuntimeYesterday)? ecobee.currentAuxHeat1RuntimeYesterday.toFloat().round(2):0
332 atomicState?.componentAlreadyProcessed=component
333 if (detailedNotif && runtimeTotalYesterday) {
334 send "And, on ${String.format('%tF', yesterday)}, ${ecobee} ${component}'s runtime stats for the day before=${runtimeTotalYesterday} minutes"
338 int heatStages = ecobee.currentHeatStages.toInteger()
340 component = 'auxHeat2'
341 if (heatStages >1 && (nextComponent.position <= 2) ) {
343 // Get the auxHeat2's runtime for startDate-endDate period
345 generateRuntimeReport(component,startDate, endDate)
346 runtimeTotalDaily = (ecobee.currentAuxHeat2RuntimeDaily)? ecobee.currentAuxHeat2RuntimeDaily.toFloat().round(2):0
347 if (runtimeTotalDaily) {
348 send "On ${String.format('%tF', startDate)}, ${ecobee} ${component}'s runtime stats=${runtimeTotalDaily} minutes", settings.askAlexaFlag
350 generateRuntimeReport(component,yesterday, startDate,'yesterday') // generate stats for yesterday
351 runtimeTotalYesterday = (ecobee.currentAuxHeat2RuntimeYesterday)? ecobee.currentAuxHeat2RuntimeYesterday.toFloat().round(2):0
352 atomicState?.componentAlreadyProcessed=component
353 if (detailedNotif && runtimeTotalYesterday) {
354 send "And, on ${String.format('%tF', yesterday)}, ${ecobee} ${component}'s runtime stats for the day before=${runtimeTotalYesterday} minutes"
358 component = 'auxHeat3'
359 if (heatStages >2 && nextComponent.position <= 3) {
361 // Get the auxHeat3's runtime for startDate-endDate period
363 generateRuntimeReport(component,startDate, endDate)
364 runtimeTotalDaily = (ecobee.currentAuxHeat3RuntimeDaily)? ecobee.currentAuxHeat3RuntimeDaily.toFloat().round(2):0
365 if (runtimeTotalDaily) {
366 send "On ${String.format('%tF', startDate)},${ecobee} ${component}'s runtime stats=${runtimeTotalDaily} minutes", settings.askAlexaFlag
368 generateRuntimeReport(component,yesterday, startDate,'yesterday') // generate stats for yesterday
369 runtimeTotalYesterday = (ecobee.currentAuxHeat3RuntimeYesterday)? ecobee.currentAuxHeat3RuntimeYesterday.toFloat().round(2):0
370 if (detailedNotif && runtimeTotalYesterday) {
371 send "And, on ${String.format('%tF', yesterday)}, ${ecobee} ${component}'s runtime stats for the day before=${runtimeTotalYesterday} minutes"
375 // Get the compCool1's runtime for startDate-endDate period
377 int coolStages = ecobee.currentCoolStages.toInteger()
378 component = 'compCool1'
380 if (nextComponent.position <= 4) {
381 generateRuntimeReport(component,startDate, endDate)
382 runtimeTotalDaily = (ecobee.currentCompCool1RuntimeDaily)? ecobee.currentCompCool1RuntimeDaily.toFloat().round(2):0
383 if (runtimeTotalDaily) {
384 send "On ${String.format('%tF', startDate)}, ${ecobee} ${component}'s runtime stats=${runtimeTotalDaily} minutes", settings.askAlexaFlag
386 generateRuntimeReport(component,yesterday, startDate,'yesterday') // generate stats for the day before
387 runtimeTotalYesterday = (ecobee.currentCompCool1RuntimeYesterday)? ecobee.currentCompCool1RuntimeYesterday.toFloat().round(2):0
388 atomicState?.componentAlreadyProcessed=component
389 if (detailedNotif && runtimeTotalYesterday) {
390 send "And, on ${String.format('%tF', yesterday)}, ${ecobee} ${component}'s runtime stats for the day before=${runtimeTotalYesterday} minutes"
394 // Get the compCool2's runtime for startDate-endDate period
396 component = 'compCool2'
397 if (coolStages >1 && nextComponent.position <= 5) {
398 generateRuntimeReport(component,startDate, endDate)
399 runtimeTotalDaily = (ecobee.currentCompCool2RuntimeDaily)? ecobee.currentCompCool2RuntimeDaily.toFloat().round(2):0
400 if (runtimeTotalDaily) {
401 send "On ${String.format('%tF', startDate)}, ${ecobee} ${component}'s runtime stats=${runtimeTotalDaily} minutes", settings.askAlexaFlag
403 generateRuntimeReport(component,yesterday, startDate,'yesterday') // generate stats for the day before
404 runtimeTotalYesterday = (ecobee.currentCompCool2RuntimeYesterday)? ecobee.currentCompCool2RuntimeYesterday.toFloat().round(2):0
405 atomicState?.componentAlreadyProcessed=component
406 if (detailedNotif && runtimeTotalYesterday ) {
407 send "And, on ${String.format('%tF', yesterday)}, ${ecobee} ${component}'s runtime stats for the day before=${runtimeTotalYesterday} minutes"
412 def hasDehumidifier = (ecobee.currentHasDehumidifier) ? ecobee.currentHasDehumidifier : 'false'
413 def hasHumidifier = (ecobee.currentHasHumidifier) ? ecobee.currentHasHumidifier : 'false'
414 def hasHrv = (ecobee.currentHasHrv)? ecobee.currentHasHrv : 'false'
415 def hasErv = (ecobee.currentHasErv)? ecobee.currentHasErv : 'false'
417 component = "humidifier"
418 if (hasHumidifier=='true' && (nextComponent.position <= 6)) {
419 // Get the humidifier's runtime for startDate-endDate period
420 generateRuntimeReport(component,startDate, endDate)
421 runtimeTotalDaily = (ecobee.currentHumidifierRuntimeDaily)? ecobee.currentHumidifierRuntimeDaily.toFloat().round(2):0
422 atomicState?.componentAlreadyProcessed=component
423 if (runtimeTotalDaily) {
424 send "On ${String.format('%tF', startDate)}, ${ecobee} ${component}'s runtime stats=${runtimeTotalDaily} minutes", settings.askAlexaFlag
429 component = 'dehumidifier'
430 if (hasDehumidifier=='true' && (nextComponent.position <= 7)) {
431 // Get the dehumidifier's for startDate-endDate period
432 generateRuntimeReport(component,startDate, endDate)
433 runtimeTotalDaily = (ecobee.currentDehumidifierRuntimeDaily)? ecobee.currentDehumidifierRuntimeDaily.toFloat().round(2):0
434 atomicState?.componentAlreadyProcessed=component
435 if (runtimeTotalDaily) {
436 send "On ${String.format('%tF', startDate)}, ${ecobee} ${component}'s runtime stats=${runtimeTotalDaily} minutes", settings.askAlexaFlag
440 component = 'ventilator'
441 if (hasHrv=='true' || hasErv=='true' && (nextComponent.position <= 8)) {
442 // Get the ventilator's runtime for startDate-endDate period
443 generateRuntimeReport(component,startDate, endDate)
444 runtimeTotalDaily = (ecobee.currentVentilatorRuntimeDaily)? ecobee.currentVentilatorRuntimeDaily.toFloat().round(2):0
445 atomicState?.componentAlreadyProcessed=component
446 if (runtimeTotalDaily) {
447 send "On ${String.format('%tF', startDate)}, ${ecobee} ${component}'s runtime stats=${runtimeTotalDaily} minutes", settings.askAlexaFlag
453 // Get the fan's runtime for startDate-endDate period
454 if (nextComponent.position <= 9) {
456 generateRuntimeReport(component,startDate, endDate)
457 runtimeTotalDaily = (ecobee.currentFanRuntimeDaily)? ecobee.currentFanRuntimeDaily.toFloat().round(2):0
458 if (runtimeTotalDaily) {
459 send "On ${String.format('%tF', startDate)},${ecobee} ${component}'s runtime stats=${runtimeTotalDaily} minutes", settings.askAlexaFlag
461 generateRuntimeReport(component,yesterday, startDate,'yesterday') // generate stats for the day before
462 runtimeTotalYesterday = (ecobee.currentFanRuntimeYesterday)? ecobee.currentFanRuntimeYesterday.toFloat().round(2):0
463 atomicState?.componentAlreadyProcessed=component
464 if (detailedNotif && runtimeTotalYesterday) {
465 send "And, on ${String.format('%tF', yesterday)}, ${ecobee} ${component}'s runtime stats for the day before=${runtimeTotalYesterday} minutes"
469 component=atomicState?.componentAlreadyProcessed
470 nextComponent = get_nextComponentStats(component) // get nextComponentToBeProcessed
471 if (nextComponent.position >= MAX_POSITION) {
472 send "generated ${ecobee}'s daily stats done for ${String.format('%tF', startDate)} - ${String.format('%tF', endDate)} period"
473 atomicState?.timestamp = dateInLocalTime // save the local date to avoid re-execution
474 unschedule(reRunIfNeeded) // No need to reschedule again as the stats are completed.
475 atomicState?.retries=0
480 void generateRuntimeReport(component, startDate, endDate, frequence='daily') {
483 log.debug("${get_APP_NAME()}>For ${ecobee} ${component}, about to call getReportData with endDate in UTC =${endDate.format("yyyy-MM-dd HH:mm:ss", TimeZone.getTimeZone("UTC"))}")
485 ecobee.getReportData("", startDate, endDate, null, null, component,false)
487 log.debug("${get_APP_NAME()}>For ${ecobee} ${component}, about to call generateRuntimeReportEvents with endDate in UTC =${endDate.format("yyyy-MM-dd HH:mm:ss", TimeZone.getTimeZone("UTC"))}")
489 ecobee.generateReportRuntimeEvents(component, startDate,endDate, 0, null,frequence)
493 private send(msg, askAlexa=false) {
494 def message = "${get_APP_NAME()}>${msg}"
495 if (sendPushMessage == "Yes") {
499 def expiresInDays=(AskAlexaExpiresInDays)?:2
501 name: "AskAlexaMsgQueue",
502 value: "${get_APP_NAME()}",
504 descriptionText: msg,
507 expires: (expiresInDays*24*60*60) /* Expires after 2 days by default */
510 } /* End if Ask Alexa notifications*/
513 log.debug("sending text message")
514 sendSms(phone, message)
521 private def get_APP_NAME() {
522 return "ecobeeGenerateStats"