2 * ecobeeGenerateMonthlyStats
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 monthly 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 monthly runtime reports about your ecobee components"
35 paragraph "Version 1.8"
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 monthly 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 for ecobee stats reporting (optional)") {
62 input (name:"askAlexaFlag", title: "Ask Alexa verbal Notifications [default=false]?", 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 (optional,default=2 days)?", required: false
72 log.debug "Installed with settings: ${settings}"
78 log.debug "Updated with settings: ${settings}"
86 atomicState?.timestamp=''
87 atomicState?.componentAlreadyProcessed=''
88 atomicState?.retries=0
91 runIn((1*60), "generateStats") // run 1 minute later as it requires notification.
92 subscribe(app, appTouch)
93 atomicState?.poll = [ last: 0, rescheduled: now() ]
95 //Subscribe to different events (ex. sunrise and sunset events) to trigger rescheduling if needed
96 subscribe(location, "sunset", rescheduleIfNeeded)
97 subscribe(location, "mode", rescheduleIfNeeded)
98 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 schedule("0 30 0 * * ?", dailyRun)
130 // Update rescheduled state
132 if (!evt) atomicState?.poll["rescheduled"] = now()
137 atomicState?.timestamp=''
138 atomicState?.componentAlreadyProcessed=''
143 void reRunIfNeeded() {
145 log.debug("reRunIfNeeded>About to call generateStats() with state.componentAlreadyProcessed=${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 // generate the stats every day at 0:30
160 schedule("0 30 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)
240 def delay = 2 // 2-minute delay for rerun
243 float runtimeTotalAvgMonthly
244 String mode= ecobee.currentThermostatMode
245 atomicState?.retries= ((atomicState?.retries==null) ?:0) +1
248 unschedule(reRunIfNeeded)
252 log.debug("${get_APP_NAME()}>Exception $e while unscheduling reRunIfNeeded")
256 if (atomicState?.retries >= MAX_RETRIES) {
258 log.debug("${get_APP_NAME()}>Max retries reached, exiting")
259 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 monthly 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)
282 def reportEndDate = (settings.givenEndDate) ?: endDate.format("yyyy-MM-dd", location.timeZone)
283 def reportEndTime=(settings.givenEndTime) ?:"00:00"
284 def dateTime = reportEndDate + " " + reportEndTime + " " + timezone
285 endDate = formatDate(dateTime)
286 Date aMonthAgo= endDate -30
289 component = 'auxHeat1'
290 // if ((mode in ['auto','heat','off']) && (nextComponent?.position <= 1)) {
291 if (nextComponent?.position <= 1) {
292 generateRuntimeReport(component,aMonthAgo, endDate,'monthly') // generate stats for the last 30 days
293 runtimeTotalAvgMonthly = (ecobee.currentAuxHeat1RuntimeAvgMonthly)? ecobee.currentAuxHeat1RuntimeAvgMonthly.toFloat().round(2):0
294 atomicState?.componentAlreadyProcessed=component
295 if (runtimeTotalAvgMonthly) {
296 send "${ecobee} ${component}'s average monthly runtime stats=${runtimeTotalAvgMonthly} minutes since ${String.format('%tF', aMonthAgo)}", settings.askAlexaFlag
300 int heatStages = ecobee.currentHeatStages.toInteger()
303 component = 'auxHeat2'
304 // if ((mode in ['auto','heat', 'off']) && (heatStages >1) && (nextComponent.position <= 2)) {
305 if ((heatStages >1) && (nextComponent.position <= 2)) {
306 generateRuntimeReport(component,aMonthAgo, endDate,'monthly') // generate stats for the last 30 days
307 runtimeTotalAvgMonthly = (ecobee.currentAuxHeat2RuntimeAvgMonthly)? ecobee.currentAuxHeat2RuntimeAvgMonthly.toFloat().round(2):0
308 atomicState?.componentAlreadyProcessed=component
309 if (runtimeTotalAvgMonthly) {
310 send "${ecobee} ${component}'s average monthly runtime stats=${runtimeTotalAvgMonthly} minutes since ${String.format('%tF', aMonthAgo)}", settings.askAlexaFlag
314 component = 'auxHeat3'
315 // if ((mode in ['auto','heat', 'off']) && (heatStages >2) && (nextComponent.position <= 3)) {
316 if ((heatStages >2) && (nextComponent.position <= 3)) {
317 generateRuntimeReport(component,aMonthAgo, endDate,'monthly') // generate stats for the last 30 days
318 runtimeTotalAvgMonthly = (ecobee.currentAuxHeat3RuntimeAvgMonthly)? ecobee.currentAuxHeat3RuntimeAvgMonthly.toFloat().round(2):0
319 atomicState?.componentAlreadyProcessed=component
320 if (runtimeTotalAvgMonthly) {
321 send "${ecobee} ${component}'s average monthly runtime stats=${runtimeTotalAvgMonthly} minutes since ${String.format('%tF', aMonthAgo)}", settings.askAlexaFlag
325 // Get the compCool1's runtime for startDate-endDate period
327 int coolStages = ecobee.currentCoolStages.toInteger()
330 // Get the compCool2's runtime for startDate-endDate period
331 component = 'compCool2'
332 // if ((mode in ['auto','cool', 'off']) && (coolStages >1) && (nextComponent.position <= 4)) {
333 if ((coolStages >1) && (nextComponent.position <= 4)) {
334 generateRuntimeReport(component,aMonthAgo, endDate,'monthly') // generate stats for the last 30 days
335 runtimeTotalAvgMonthly = (ecobee.currentCompCool2RuntimeAvgMonthly)? ecobee.currentCompCool2RuntimeAvgMonthly.toFloat().round(2):0
336 atomicState?.componentAlreadyProcessed=component
337 if (runtimeTotalAvgMonthly) {
338 send "${ecobee} ${component}'s average monthly runtime stats=${runtimeTotalAvgMonthly} minutes since ${String.format('%tF', aMonthAgo)}", settings.askAlexaFlag
341 component = 'compCool1'
342 // if ((mode in ['auto','cool', 'off']) && (nextComponent.position <= 5)) {
343 if (nextComponent.position <= 5) {
344 generateRuntimeReport(component,aMonthAgo, endDate,'monthly') // generate stats for the last 30 days
345 runtimeTotalAvgMonthly = (ecobee.currentCompCool1RuntimeAvgMonthly)? ecobee.currentCompCool1RuntimeAvgMonthly.toFloat().round(2):0
346 atomicState?.componentAlreadyProcessed=component
347 if (runtimeTotalAvgMonthly) {
348 send "${ecobee} ${component}'s average monthly runtime stats=${runtimeTotalAvgMonthly} minutes since ${String.format('%tF', aMonthAgo)}", settings.askAlexaFlag
353 component=atomicState?.componentAlreadyProcessed
354 nextComponent = get_nextComponentStats(component) // get nextComponentToBeProcessed
355 // int endPosition= (mode=='heat') ? ((heatStages>2) ? 4 : (heatStages>1)? 3:2) :MAX_POSITION
356 if (nextComponent.position >=MAX_POSITION) {
357 send "generated all ${ecobee}'s monthly stats since ${String.format('%tF', aMonthAgo)}"
358 unschedule(reRunIfNeeded) // No need to reschedule again as the stats are completed.
359 atomicatomicState?.timestamp = dateInLocalTime // save the date to avoid re-execution.
365 void generateRuntimeReport(component, startDate, endDate, frequence='daily') {
368 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"))}")
369 send ("For component ${component}, about to call getReportData with aMonthAgo in UTC =${endDate.format("yyyy-MM-dd HH:mm:ss", TimeZone.getTimeZone("UTC"))}")
371 ecobee.getReportData("", startDate, endDate, null, null, component,false)
373 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"))}")
374 send ("For component ${component}, about to call generateReportRuntimeEvents with aMonthAgo in UTC =${endDate.format("yyyy-MM-dd HH:mm:ss", TimeZone.getTimeZone("UTC"))}")
376 ecobee.generateReportRuntimeEvents(component, startDate,endDate, 0, null,frequence)
381 private send(msg, askAlexa=false) {
382 def message = "${get_APP_NAME()}>${msg}"
383 if (sendPushMessage == "Yes") {
387 def expiresInDays=(AskAlexaExpiresInDays)?:2
389 name: "AskAlexaMsgQueue",
390 value: "${get_APP_NAME()}",
392 descriptionText: msg,
395 expires: (expiresInDays*24*60*60) /* Expires after 2 days by default */
398 } /* End if Ask Alexa notifications*/
401 log.debug("sending text message")
402 sendSms(phone, message)
409 private def get_APP_NAME() {
410 return "ecobeeGenerateMonthlyStats"