--- /dev/null
+/**
+ * ecobeeGenerateWeeklyStats
+ *
+ * Copyright 2015 Yves Racine
+ * LinkedIn profile: ca.linkedin.com/pub/yves-racine-m-sc-a/0/406/4b/
+ *
+ * Developer retains all right, title, copyright, and interest, including all copyright, patent rights, trade secret
+ * in the Background technology. May be subject to consulting fees under the Agreement between the Developer and the Customer.
+ * Developer grants a non exclusive perpetual license to use the Background technology in the Software developed for and delivered
+ * to Customer under this Agreement. However, the Customer shall make no commercial use of the Background technology without
+ * Developer's written consent.
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ * Software Distribution is restricted and shall be done only with Developer's written approval.
+ *
+ * N.B. Requires MyEcobee device (v5.0 and higher) available at
+ * http://www.ecomatiqhomes.com/#!store/tc3yr
+ */
+import java.text.SimpleDateFormat
+definition(
+ name: "${get_APP_NAME()}",
+ namespace: "yracine",
+ author: "Yves Racine",
+ 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. ",
+ category: "My Apps",
+ iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee.png",
+ iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee@2x.png"
+)
+
+preferences {
+ section("About") {
+ paragraph "${get_APP_NAME()}, the smartapp that generates weekly runtime reports about your ecobee components"
+ paragraph "Version 1.7"
+ paragraph "If you like this smartapp, please support the developer via PayPal and click on the Paypal link below "
+ href url: "https://www.paypal.me/ecomatiqhomes",
+ title:"Paypal donation..."
+ paragraph "Copyright©2016 Yves Racine"
+ href url:"http://github.com/yracine/device-type.myecobee", style:"embedded", required:false, title:"More information..."
+ description: "http://github.com/yracine/device-type.myecobee/blob/master/README.md"
+ }
+
+ section("Generate weekly stats for this ecobee thermostat") {
+ input "ecobee", "device.myEcobeeDevice", title: "Ecobee?"
+
+ }
+ section("Date for the initial run = YYYY-MM-DD") {
+ input "givenEndDate", "text", title: "End Date [default=today]", required: false
+ }
+ section("Time for the initial run (24HR)" ) {
+ input "givenEndTime", "text", title: "End time [default=00:00]", required: false
+ }
+ section( "Notifications" ) {
+ input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes", "No"]], required: false, default:"No"
+ input "phoneNumber", "phone", title: "Send a text message?", required: false
+ }
+ section("Detailed Notifications") {
+ input "detailedNotif", "bool", title: "Detailed Notifications?", required:false
+ }
+ section("Enable Amazon Echo/Ask Alexa Notifications [optional, default=false]") {
+ input (name:"askAlexaFlag", title: "Ask Alexa verbal Notifications?", type:"bool",
+ description:"optional",required:false)
+ input (name:"listOfMQs", type:"enum", title: "List of the Ask Alexa Message Queues (default=Primary)", options: state?.askAlexaMQ, multiple: true, required: false,
+ description:"optional")
+ input "AskAlexaExpiresInDays", "number", title: "Ask Alexa's messages expiration in days (default=2 days)?", required: false
+ }
+
+}
+
+def installed() {
+ log.debug "Installed with settings: ${settings}"
+
+ initialize()
+}
+
+def updated() {
+ log.debug "Updated with settings: ${settings}"
+
+ unsubscribe()
+ unschedule()
+ initialize()
+}
+
+def initialize() {
+
+
+ atomicState?.timestamp=''
+ atomicState?.componentAlreadyProcessed=''
+ atomicState?.retries=0
+
+ runIn((1*60), "generateStats") // run 1 minute later as it requires notification.
+ subscribe(app, appTouch)
+ atomicState?.poll = [ last: 0, rescheduled: now() ]
+
+ //Subscribe to different events (ex. sunrise and sunset events) to trigger rescheduling if needed
+ subscribe(location, "sunset", rescheduleIfNeeded)
+ subscribe(location, "mode", rescheduleIfNeeded)
+ subscribe(location, "sunsetTime", rescheduleIfNeeded)
+ subscribe(location, "askAlexaMQ", askAlexaMQHandler)
+ rescheduleIfNeeded()
+}
+
+def askAlexaMQHandler(evt) {
+ if (!evt) return
+ switch (evt.value) {
+ case "refresh":
+ state?.askAlexaMQ = evt.jsonData && evt.jsonData?.queues ? evt.jsonData.queues : []
+ log.info("askAlexaMQHandler>refresh value=$state?.askAlexaMQ")
+ break
+ }
+}
+
+
+def rescheduleIfNeeded(evt) {
+ if (evt) log.debug("rescheduleIfNeeded>$evt.name=$evt.value")
+ Integer delay = (24*60) // By default, do it every day
+ BigDecimal currentTime = now()
+ BigDecimal lastPollTime = (currentTime - (atomicState?.poll["last"]?:0))
+
+ if (lastPollTime != currentTime) {
+ Double lastPollTimeInMinutes = (lastPollTime/60000).toDouble().round(1)
+ log.info "rescheduleIfNeeded>last poll was ${lastPollTimeInMinutes.toString()} minutes ago"
+ }
+ if (((atomicState?.poll["last"]?:0) + (delay * 60000) < currentTime) && canSchedule()) {
+ log.info "rescheduleIfNeeded>scheduling dailyRun in ${delay} minutes.."
+// generate the stats every day at 0:15
+ schedule("0 15 0 * * ?", dailyRun)
+ }
+
+ // Update rescheduled state
+
+ if (!evt) atomicState?.poll["rescheduled"] = now()
+}
+
+
+def appTouch(evt) {
+ atomicState?.timestamp=''
+ atomicState?.componentAlreadyProcessed=''
+ generateStats()
+}
+
+void reRunIfNeeded() {
+ if (detailedNotif) {
+ log.debug("reRunIfNeeded>About to call generateStats() with state.componentAlreadyProcessed=${atomicState?.componentAlreadyProcessed}")
+ }
+ generateStats()
+
+}
+
+
+void dailyRun() {
+ Integer delay = (24*60) // By default, do it every day
+ atomicState?.poll["last"] = now()
+
+ //schedule the rescheduleIfNeeded() function
+
+ if (((atomicState?.poll["rescheduled"]?:0) + (delay * 60000)) < now()) {
+ log.info "takeAction>scheduling rescheduleIfNeeded() in ${delay} minutes.."
+ schedule("0 15 0 * * ?", rescheduleIfNeeded)
+ // Update rescheduled state
+ atomicState?.poll["rescheduled"] = now()
+ }
+ settings.givenEndDate=null
+ settings.givenEndTime=null
+ if (detailedNotif) {
+ log.debug("dailyRun>for $ecobee,about to call generateStats() with settings.givenEndDate=${settings.givenEndDate}")
+ }
+ atomicState?.componentAlreadyProcessed=''
+ atomicState?.retries=0
+ generateStats()
+
+}
+
+
+private String formatISODateInLocalTime(dateInString, timezone='') {
+ def myTimezone=(timezone)?TimeZone.getTimeZone(timezone):location.timeZone
+ if ((dateInString==null) || (dateInString.trim()=="")) {
+ return (new Date().format("yyyy-MM-dd HH:mm:ss", myTimezone))
+ }
+ SimpleDateFormat ISODateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
+ Date ISODate = ISODateFormat.parse(dateInString)
+ String dateInLocalTime =new Date(ISODate.getTime()).format("yyyy-MM-dd HH:mm:ss", myTimezone)
+ log.debug("formatDateInLocalTime>dateInString=$dateInString, dateInLocalTime=$dateInLocalTime")
+ return dateInLocalTime
+}
+
+
+private def formatDate(dateString) {
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm zzz")
+ Date aDate = sdf.parse(dateString)
+ return aDate
+}
+
+private def get_nextComponentStats(component='') {
+ if (detailedNotif) {
+ log.debug "get_nextComponentStats>About to get ${component}'s next component from components table"
+ }
+
+ def nextInLine=[:]
+
+ def components = [
+ '':
+ [position:1, next: 'auxHeat1'
+ ],
+ 'auxHeat1':
+ [position:2, next: 'auxHeat2'
+ ],
+ 'auxHeat2':
+ [position:3, next: 'auxHeat3'
+ ],
+ 'auxHeat3':
+ [position:4, next: 'compCool2'
+ ],
+ 'compCool2':
+ [position:5, next: 'compCool1'
+ ],
+ 'compCool1':
+ [position:6, next: 'done'
+ ],
+ ]
+ try {
+ nextInLine = components.getAt(component)
+ } catch (any) {
+ nextInLine=[position:1, next:'auxHeat1']
+ if (detailedNotif) {
+ log.debug "get_nextComponentStats>${component} not found, nextInLine=${nextInLine}"
+ }
+ }
+ if (detailedNotif) {
+ log.debug "get_nextComponentStats>got ${component}'s next component from components table= ${nextInLine}"
+ }
+ return nextInLine
+
+}
+
+
+void generateStats() {
+ String dateInLocalTime = new Date().format("yyyy-MM-dd", location.timeZone)
+ def MAX_POSITION=6
+ def MAX_RETRIES=4
+ float runtimeTotalAvgWeekly
+
+ def delay = 2 // 2-minute delay for rerun
+ atomicState?.retries= ((atomicState?.retries==null) ?:0) +1
+
+ try {
+ unschedule(reRunIfNeeded)
+ } catch (e) {
+
+ if (detailedNotif) {
+ log.debug("${get_APP_NAME()}>Exception $e while unscheduling reRunIfNeeded")
+ }
+ }
+ if (atomicState?.retries >= MAX_RETRIES) {
+ if (detailedNotif) {
+ log.debug("${get_APP_NAME()}>Max retries reached, exiting")
+ send("max retries reached ${atomicState?.retries}), exiting")
+ }
+ }
+
+ def component = atomicState?.componentAlreadyProcessed
+ def nextComponent = get_nextComponentStats(component) // get nextComponentToBeProcessed
+ if (detailedNotif) {
+ log.debug("${get_APP_NAME()}>For ${ecobee}, about to process nextComponent=${nextComponent}, state.componentAlreadyProcessed=${atomicState?.componentAlreadyProcessed}")
+ }
+ if (atomicState?.timestamp == dateInLocalTime && nextComponent.position >=MAX_POSITION) {
+ return // the weekly stats are already generated
+ } else {
+ // schedule a rerun till the stats are generated properly
+ schedule("0 0/${delay} * * * ?", reRunIfNeeded)
+ }
+
+ String timezone = new Date().format("zzz", location.timeZone)
+ String dateAtMidnight = dateInLocalTime + " 00:00 " + timezone
+ if (detailedNotif) {
+ log.debug("${get_APP_NAME()}>date at Midnight= ${dateAtMidnight}")
+ }
+ Date endDate = formatDate(dateAtMidnight)
+
+ def reportEndDate = (settings.givenEndDate) ?: endDate.format("yyyy-MM-dd", location.timeZone)
+ def reportEndTime=(settings.givenEndTime) ?:"00:00"
+ def dateTime = reportEndDate + " " + reportEndTime + " " + timezone
+ endDate = formatDate(dateTime)
+ Date aWeekAgo= endDate -7
+ if (detailedNotif) {
+ log.debug("${get_APP_NAME()}>end dateTime = ${dateTime}, endDate in UTC =${endDate.format("yyyy-MM-dd HH:mm:ss", TimeZone.getTimeZone("UTC"))}")
+ }
+
+
+ // Get the auxHeat1's runtime for startDate-endDate period
+ component = 'auxHeat1'
+
+ if (nextComponent.position <= 1) {
+ generateRuntimeReport(component,aWeekAgo, endDate,'weekly') // generate stats for the last 7 days
+ runtimeTotalAvgWeekly = (ecobee.currentAuxHeat1RuntimeAvgWeekly)? ecobee.currentAuxHeat1RuntimeAvgWeekly.toFloat().round(2):0
+ atomicState?.componentAlreadyProcessed=component
+ if (runtimeTotalAvgWeekly) {
+ send ("${ecobee} ${component}'s average weekly runtime stats=${runtimeTotalAvgWeekly} minutes since ${String.format('%tF', aWeekAgo)}", settings.askAlexaFlag)
+ }
+ }
+ int heatStages = ecobee.currentHeatStages.toInteger()
+
+
+ component = 'auxHeat2'
+ if (heatStages >1 && nextComponent.position <= 2) {
+
+// Get the auxHeat2's runtime for startDate-endDate period
+
+ generateRuntimeReport(component,aWeekAgo, endDate,'weekly') // generate stats for the last 7 days
+ runtimeTotalAvgWeekly = (ecobee.currentAuxHeat2RuntimeAvgWeekly)? ecobee.currentAuxHeat2RuntimeAvgWeekly.toFloat().round(2):0
+ atomicState?.componentAlreadyProcessed=component
+ if (runtimeTotalAvgWeekly) {
+ send ("${ecobee} ${component}'s average weekly runtime stats=${runtimeTotalAvgWeekly} minutes since ${String.format('%tF', aWeekAgo)}", settings.askAlexaFlag)
+ }
+
+ }
+
+ component = 'auxHeat3'
+ if (heatStages >2 && nextComponent.position <= 3) {
+
+// Get the auxHeat3's runtime for startDate-endDate period
+
+ generateRuntimeReport(component,aWeekAgo, endDate,'weekly') // generate stats for the last 7 days
+ runtimeTotalAvgWeekly = (ecobee.currentAuxHeat3RuntimeAvgWeekly)? ecobee.currentAuxHeat3RuntimeAvgWeekly.toFloat().round(2):0
+ atomicState?.componentAlreadyProcessed=component
+ if (runtimeTotalAvgWeekly) {
+ send ("${ecobee} ${component}'s average weekly runtime stats=${runtimeTotalAvgWeekly} minutes since ${String.format('%tF', aWeekAgo)}", settings.askAlexaFlag)
+ }
+ }
+
+// Get the compCool1's runtime for startDate-endDate period
+
+ int coolStages = ecobee.currentCoolStages.toInteger()
+
+
+// Get the compCool2's runtime for startDate-endDate period
+ component = 'compCool2'
+
+ if (coolStages >1 && nextComponent.position <= 4) {
+ generateRuntimeReport(component,aWeekAgo, endDate,'weekly') // generate stats for the last 7 days
+ runtimeTotalAvgWeekly = (ecobee.currentCompCool2RuntimeAvgWeekly)? ecobee.currentCompCool2RuntimeAvgWeekly.toFloat().round(2):0
+ atomicState?.componentAlreadyProcessed=component
+ if (runtimeTotalAvgWeekly) {
+ send ("${ecobee} ${component}'s average weekly runtime stats=${runtimeTotalAvgWeekly} minutes since ${String.format('%tF', aWeekAgo)}", settings.askAlexaFlag)
+ }
+ }
+
+ component = 'compCool1'
+ if (nextComponent.position <= 5) {
+ generateRuntimeReport(component,aWeekAgo, endDate,'weekly') // generate stats for the last 7 days
+ runtimeTotalAvgWeekly = (ecobee.currentCompCool1RuntimeAvgWeekly)? ecobee.currentCompCool1RuntimeAvgWeekly.toFloat().round(2):0
+ atomicState?.componentAlreadyProcessed=component
+ if (runtimeTotalAvgWeekly) {
+ send ("${ecobee} ${component}'s average weekly runtime stats=${runtimeTotalAvgWeekly} minutes since ${String.format('%tF', aWeekAgo)}", settings.askAlexaFlag)
+ }
+ }
+
+ component= atomicState?.componentAlreadyProcessed
+ nextComponent = get_nextComponentStats(component) // get nextComponentToBeProcessed
+ if (nextComponent?.position >=MAX_POSITION) {
+ send " generated all ${ecobee}'s weekly stats since ${String.format('%tF', aWeekAgo)}"
+ unschedule(reRunIfNeeded) // No need to reschedule again as the stats are completed.
+ atomicState?.timestamp = dateInLocalTime // save the date to avoid re-execution.
+ atomicState?.retries=0
+
+ }
+
+}
+
+void generateRuntimeReport(component, startDate, endDate, frequence='daily') {
+
+ if (detailedNotif) {
+ 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"))}")
+ send ("For component ${component}, about to call getReportData with aWeekAgo in UTC =${endDate.format("yyyy-MM-dd HH:mm:ss", TimeZone.getTimeZone("UTC"))}")
+ }
+ ecobee.getReportData("", startDate, endDate, null, null, component,false)
+ if (detailedNotif) {
+ 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"))}")
+ send ("For component ${component}, about to call generateReportRuntimeEvents with aWeekAgo in UTC =${endDate.format("yyyy-MM-dd HH:mm:ss", TimeZone.getTimeZone("UTC"))}")
+ }
+ ecobee.generateReportRuntimeEvents(component, startDate,endDate, 0, null,frequence)
+
+}
+
+private send(msg, askAlexa=false) {
+ def message = "${get_APP_NAME()}>${msg}"
+ if (sendPushMessage == "Yes") {
+ sendPush(message)
+ }
+ if (askAlexa) {
+ def expiresInDays=(AskAlexaExpiresInDays)?:2
+ sendLocationEvent(
+ name: "AskAlexaMsgQueue",
+ value: "${get_APP_NAME()}",
+ isStateChange: true,
+ descriptionText: msg,
+ data:[
+ queues: listOfMQs,
+ expires: (expiresInDays*24*60*60) /* Expires after 2 days by default */
+ ]
+ )
+ } /* End if Ask Alexa notifications*/
+
+ if (phone) {
+ log.debug("sending text message")
+ sendSms(phone, message)
+ }
+
+ log.debug msg
+
+}
+
+private def get_APP_NAME() {
+ return "ecobeeGenerateWeeklyStats"
+}