From 39c3feee225b1b0f913d9f6e8b56081fda6344e8 Mon Sep 17 00:00:00 2001 From: Sean Schneyer Date: Fri, 5 Feb 2016 02:47:51 -0600 Subject: [PATCH 1/2] Updates to the watchdog handling. Hopefully this version is stable. --- .../ecobee-connect.src/ecobee-connect.groovy | 234 +++++++++++++----- 1 file changed, 168 insertions(+), 66 deletions(-) diff --git a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy index 3252d1993ef..9c7a4a51641 100644 --- a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy +++ b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy @@ -25,7 +25,7 @@ * */ def getVersionNum() { return "0.9.0" } -private def getVersionLabel() { return "ecobee (Connect) Version ${getVersionNum()}-RC5" } +private def getVersionLabel() { return "ecobee (Connect) Version ${getVersionNum()}-RC6" } private def getHelperSmartApps() { return [ [name: "ecobeeRoutinesChild", appName: "ecobee Routines", @@ -69,14 +69,21 @@ mappings { // Begin Preference Pages def mainPage() { - def deviceHandlersInstalled = testForDeviceHandlers() - def readyToInstall = deviceHandlersInstalled + def deviceHandlersInstalled + def readyToInstall + + // Only create the dummy devices if we aren't initialized yet + if (!state.initialized) { + deviceHandlersInstalled = testForDeviceHandlers() + readyToInstall = deviceHandlersInstalled + } + if (state.initialized) { readyToInstall = true } dynamicPage(name: "mainPage", title: "Welcome to ecobee (Connect)", install: readyToInstall, uninstall: false, submitOnChange: true) { def ecoAuthDesc = (state.authToken != null) ? "[Connected]\n" :"[Not Connected]\n" // If not device Handlers we cannot proceed - if(!deviceHandlersInstalled) { + if(!state.initialized && !deviceHandlersInstalled) { section() { paragraph "ERROR!\n\nYou MUST add the ${getChildThermostatName()} and ${getChildSensorName()} Device Handlers to the IDE BEFORE running the setup." } @@ -90,7 +97,14 @@ def mainPage() { } } - if(state.authToken != null && deviceHandlersInstalled) { + if(state.authToken != null && state.initialized != true) { + section() { + paragraph "Please click 'Done' to save your credentials. Then re-open the SmartApp to continue the setup." + } + } + + // Need to save the initial login to setup the device without timeouts + if(state.authToken != null && state.initialized) { if (settings.thermostats?.size() > 0 && state.initialized) { section("Helper SmartApps") { href ("helperSmartAppsPage", title: "Helper SmartApps", description: "Tap to manage Helper SmartApps") @@ -272,11 +286,22 @@ def debugDashboardPage() { section("Settings Information") { paragraph "debugLevel: ${settings.debugLevel} (default=3 if null)" paragraph "holdType: ${settings.holdType} (default='Until I Change' if null)" - paragraph "pollingInterval: ${settings.pollingInterval} {default=5 if null)" - paragraph "showThermsAsSensor: ${settings.showThermsAsSensor} {default=false if null)" + paragraph "pollingInterval: ${settings.pollingInterval} (default=5 if null)" + paragraph "showThermsAsSensor: ${settings.showThermsAsSensor} (default=false if null)" paragraph "smartAuto: ${settings.smartAuto} (default=false if null)" paragraph "Selected Thermostats: ${settings.thermostats}" } + section("Dump of Debug Variables") { + def debugParamList = getDebugDump() + LOG("debugParamList: ${debugParamList}", 4, null, "debug") + //if ( debugParamList?.size() > 0 ) { + if ( debugParamList != null ) { + debugParamList.each { key, value -> + LOG("Adding paragraph: key:${key} value:${value}", 5, null, "trace") + paragraph "${key}: ${value}" + } + } + } section("Commands") { href(name: "pollChildrenPage", title: "", required: false, page: "pollChildrenPage", description: "Tap to execute command: pollChildren()") href ("removePage", description: "Tap to remove ecobee (Connect) ", title: "") @@ -302,7 +327,8 @@ def helperSmartAppsPage() { LOG("helperSmartAppsPage() entered", 5) LOG("SmartApps available are ${getHelperSmartApps()}", 5, null, "info") - //getHelperSmartApps() { + + //getHelperSmartApps() { dynamicPage(name: "helperSmartAppsPage", title: "Helper Smart Apps", install: true, uninstall: false, submitOnChange: true) { getHelperSmartApps().each { oneApp -> LOG("Processing the app: ${oneApp}", 4, null, "trace") @@ -524,7 +550,7 @@ def getEcobeeThermostats() { LOG("Storing the failed action to try later") state.action = "getEcobeeThermostats" LOG("Refreshing your auth_token!", 1) - refreshAuthToken() + refreshAuthToken(true) } else { LOG("Other error. Status: ${resp.status} Response data: ${resp.data} ", 1) } @@ -532,7 +558,7 @@ def getEcobeeThermostats() { } } catch(Exception e) { LOG("___exception getEcobeeThermostats(): ${e}", 1, null, "error") - refreshAuthToken() + refreshAuthToken(true) } state.thermostatsWithNames = stats LOG("state.thermostatsWithNames == ${state.thermostatsWithNames}", 4) @@ -632,11 +658,18 @@ def initialize() { LOG("updated() - Exception encountered trying to unschedule(). Exception: ${e}", 2, null, "error") } - // Initialize several variables - state.lastScheduledPoll = now() - state.lastScheduledTokenRefresh = now() - state.lastScheduledWatchdog = now() - state.lastPoll = now() + def nowTime = now() + def nowDate = getTimestamp() + + // Initialize several variables + state.lastScheduledPoll = nowTime + state.lastScheduledPollDate = nowDate + state.lastScheduledTokenRefresh = nowTime + state.lastScheduledTokenRefreshDate = nowDate + state.lastScheduledWatchdog = nowTime + state.lastScheduledWatchdogDate = nowDate + state.lastPoll = nowTime + state.lastPollDate = nowDate // Setup initial polling and determine polling intervals state.pollingInterval = getPollingInterval() @@ -677,8 +710,12 @@ def initialize() { //send activity feeds to tell that device is connected def notificationMessage = aOK ? "is connected to SmartThings" : "had an error during setup of devices" sendActivityFeeds(notificationMessage) - state.timeSendPush = null - state.initialized = true + state.timeSendPush = null + if (!state.initialized) { + state.initialized = true + state.initializedEpic = nowTime + state.initializedDate = nowDate + } return aOK } @@ -752,16 +789,27 @@ private def deleteUnusedChildren() { def childrenToDelete = allMyChildren.findAll { !childrenToKeep.contains(it.deviceNetworkId) } LOG("Ready to delete these devices. ${childrenToDelete}", 4, null, "trace") - childrenToDelete.each { deleteChildDevice(it.deviceNetworkId) } //inherits from SmartApp (data-management) + if (childrenToDelete.size() > 0) childrenToDelete?.each { deleteChildDevice(it.deviceNetworkId) } //inherits from SmartApp (data-management) } } -def scheduleWatchdog(evt=null) { +def scheduleWatchdog(evt=null, local=false) { def results = true + LOG("scheduleWhatdog() called with: evt (${evt}) & local (${local})", 4, null, "trace") + // Only update the Scheduled timestamp if it is not a local action or from a subscription + if ( (evt == null) && (local==false) ) { + state.lastScheduledWatchdog = now() + state.lastScheduledWatchdogDate = getTimestamp() + } + + state.lastWatchdog = now() + state.lastWatchdogDate = getTimestamp() + + LOG("After watchdog tagging") if(apiConnected() == "lost") { // Possibly a false alarm? Check if we can update the token with one last fleeting try... - if( refreshAuthToken() ) { + if( refreshAuthToken(true) ) { // We are back in business! LOG("scheduleWatchdog() - Was able to recover the lost connection. Please ignore any notifications received.", 1, null, "error") } else { @@ -792,13 +840,15 @@ private def Boolean isDaemonAlive(daemon="all") { daemon = daemon.toLowerCase() def result = true - def timeSinceLastScheduledPoll = (state.lastScheduledPoll == 0 || state.lastScheduledPoll == null) ? 0 : ((now() - state.lastScheduledPoll?.toDouble()) / 1000 / 60) - def timeSinceLastScheduledRefresh = (state.lastScheduledTokenRefresh == 0 || state.lastScheduledTokenRefresh == null) ? 0 : ((now() - state.lastScheduledTokenRefresh?.toDouble()) / 1000 / 60) - def timeSinceLastScheduledWatchdog = (state.lastScheduledWatchdog == 0 || state.lastScheduledWatchdog == null) ? 0 : ((now() - state.lastScheduledWatchdog?.toDouble()) / 1000 / 60) - def timeBeforeExpiry = state.authTokenExpires ? ((state.authTokenExpires - now()) / 1000 / 60) : 0 + def timeSinceLastScheduledPoll = (state.lastScheduledPoll == 0 || state.lastScheduledPoll == null) ? 0 : ((now() - state.lastScheduledPoll) / 1000 / 60) // TODO: Removed toDouble() will this impact? + def timeSinceLastScheduledRefresh = (state.lastScheduledTokenRefresh == 0 || state.lastScheduledTokenRefresh == null) ? 0 : ((now() - state.lastScheduledTokenRefresh) / 1000 / 60) + def timeSinceLastScheduledWatchdog = (state.lastScheduledWatchdog == 0 || state.lastScheduledWatchdog == null) ? 0 : ((now() - state.lastScheduledWatchdog) / 1000 / 60) + def timeBeforeExpiry = state.authTokenExpires ? ((state.authTokenExpires - now()) / 1000 / 60) : 0 + LOG("isDaemonAlive() - now() == ${now()} for daemon (${daemon})", 5, null, "trace") LOG("isDaemonAlive() - Time since last poll? ${timeSinceLastScheduledPoll} -- state.lastScheduledPoll == ${state.lastScheduledPoll}", 4, null, "info") LOG("isDaemonAlive() - Time since last token refresh? ${timeSinceLastScheduledRefresh} -- state.lastScheduledTokenRefresh == ${state.lastScheduledTokenRefresh}", 4, null, "info") + LOG("isDaemonAlive() - Time since watchdog activation? ${timeSinceLastScheduledWatchdog} -- state.lastScheduledWatchdog == ${state.lastScheduledWatchdog}", 4, null, "info") LOG("isDaemonAlive() - Time left (timeBeforeExpiry) until expiry (in min): ${timeBeforeExpiry}", 4, null, "info") if (daemon == "poll" || daemon == "all") { @@ -816,8 +866,9 @@ private def Boolean isDaemonAlive(daemon="all") { if (daemon == "watchdog" || daemon == "all") { LOG("isDaemonAlive() - Checking daemon (${daemon}) in 'watchdog'", 4, null, "trace") - def maxInterval = state.watchdogInterval + 3 - // if ( (timeSinceLastScheduledWatchdog == 0) || (timeSinceLastScheduledWatchdog >= (maxInterval)) || (state.initialized != true) ) { result = false } + def maxInterval = state.watchdogInterval + 6 + //if ( (timeSinceLastScheduledWatchdog == 0) || (timeSinceLastScheduledWatchdog >= (maxInterval)) || (state.initialized != true) ) { result = false } + LOG("isDaemonAlive(watchdog) - timeSinceLastScheduledWatchdog=(${timeSinceLastScheduledWatchdog}) Timestamps: (${state.lastScheduledWatchdogDate}) (epic: ${state.lastScheduledWatchdog}) now-(${now()})", 4, null, "trace") if ( timeSinceLastScheduledWatchdog >= maxInterval ) { result = false } } @@ -826,6 +877,7 @@ private def Boolean isDaemonAlive(daemon="all") { LOG("isDaemonAlive() - Unknown daemon: ${daemon} received. Do not know how to check this daemon.", 1, null, "error") result = false } + LOG("isDaemonAlive() - result is ${result}", 4, null, "trace") return result } @@ -840,12 +892,13 @@ private def Boolean spawnDaemon(daemon="all") { LOG("spawnDaemon() - Performing seance for daemon (${daemon}) in 'poll'", 4, null, "trace") // Reschedule the daemon try { - result = result && unschedule("pollScheduled") + // result = result && unschedule("pollScheduled") if ( canSchedule() ) { "runEvery${state.pollingInterval}Minutes"("pollScheduled") - // if ( canSchedule() ) { runIn(30, "pollScheduled") } // Don't count this against the results - pollScheduled() - result = result && true + // if ( canSchedule() ) { runIn(30, "pollScheduled") } // This will wipe out the existing scheduler! + // Web Services taking too long. Go ahead and only schedule here for now + + result = result && pollScheduled() } else { LOG("canSchedule() is NOT allowed! Unable to schedule daemon!", 1, null, "error") result = false @@ -860,12 +913,13 @@ private def Boolean spawnDaemon(daemon="all") { LOG("spawnDaemon() - Performing seance for daemon (${daemon}) in 'auth'", 4, null, "trace") // Reschedule the daemon try { - result = result && unschedule("refreshAuthTokenScheduled") + // result = result && unschedule("refreshAuthTokenScheduled") if ( canSchedule() ) { runEvery15Minutes("refreshAuthTokenScheduled") // if ( canSchedule() ) { runIn(30, "refreshAuthTokenScheduled") } // Don't count this against the results - refreshAuthTokenScheduled() - result = result && true + // Web Services taking too long. Go ahead and only schedule here for now + + result = result && refreshAuthTokenScheduled() } else { LOG("canSchedule() is NOT allowed! Unable to schedule daemon!", 1, null, "error") result = false @@ -880,7 +934,7 @@ private def Boolean spawnDaemon(daemon="all") { LOG("spawnDaemon() - Performing seance for daemon (${daemon}) in 'watchdog'", 4, null, "trace") // Reschedule the daemon try { - result = result && unschedule("scheduleWatchdog") + // result = result && unschedule("scheduleWatchdog") if ( canSchedule() ) { runEvery15Minutes("scheduleWatchdog") result = result && true @@ -906,10 +960,10 @@ private def Boolean spawnDaemon(daemon="all") { def updateLastPoll(Boolean isScheduled=false) { if (isScheduled) { state.lastScheduledPoll = now() - state.lastScheduledPollDate = new Date().toString() + state.lastScheduledPollDate = getTimestamp() } else { state.lastPoll = now() - state.lastPollDate = new Date().toString() + state.lastPollDate = getTimestamp() } } @@ -921,22 +975,24 @@ def pollScheduled() { } -def updateLastTokenRefresh(Boolean isScheduled=false) { - if (isScheduled) { +def updateLastTokenRefresh(isScheduled=false) { + if (isScheduled) { state.lastScheduledTokenRefresh = now() - state.lastScheduledTokenRefreshDate = new Date().toString() + state.lastScheduledTokenRefreshDate = getTimestamp() + LOG("updateLastTokenRefresh(true) - Updated timestamps: state.lastScheduledTokenRefreshDate=(${state.lastScheduledTokenRefreshDate}) state.lastScheduledTokenRefresh=${state.lastScheduledTokenRefresh} ", 5, null, "trace") } else { state.lastTokenRefresh = now() - state.lastTokenRefreshDate = new Date().toString() + state.lastTokenRefreshDate = getTimestamp() } } // Called by scheduled() event handler def refreshAuthTokenScheduled() { updateLastTokenRefresh(true) - LOG("refreshAuthTokenScheduled() - Running at ${state.lastScheduledTokenRefreshDate} (epic: ${state.lastScheduledTokenRefresh})", 3, null, "trace") - scheduleWatchdog() - return refreshAuthToken() + LOG("refreshAuthTokenScheduled() - Running at ${state.lastScheduledTokenRefreshDate} (epic: ${state.lastScheduledTokenRefresh})", 3, null, "trace") + + def result = refreshAuthToken() + return result } @@ -964,14 +1020,14 @@ def pollChildren(child = null) { } } + // Run a watchdog checker here + scheduleWatchdog(null, true) if (settings.thermostats?.size() < 1) { LOG("pollChildren() - Nothing to poll as there are no thermostats currently selected", 1, child, "warn") return true } - // Run a watchdog checker here - scheduleWatchdog() // Check to see if it is time to do an full poll to the Ecobee servers. If so, execute the API call and update ALL children def timeSinceLastPoll = (state.forcePoll == true) ? 0 : ((now() - state.lastPoll?.toDouble()) / 1000 / 60) @@ -1092,7 +1148,7 @@ private def pollEcobeeAPI(thermostatIdsString = "") { def reAttemptPeriod = 45 // in sec if ( (e.statusCode == 500 && e.getResponse()?.data.status.code == 14) || (e.statusCode == 401 && e.getResponse()?.data.status.code == 14) ) { // Not possible to recover from status.code == 14 - if ( refreshAuthToken() ) { LOG("We have recovered the token a from the code 14.", 2, null, "warn") } + // if ( refreshAuthToken() ) { LOG("We have recovered the token a from the code 14.", 2, null, "warn") } LOG("In HttpResponseException: Received data.stat.code of 14", 1, null, "error") apiLost("pollEcobeeAPI() - In HttpResponseException: Received data.stat.code of 14") } else if (e.statusCode != 401) { //this issue might comes from exceed 20sec app execution, connectivity issue etc. @@ -1132,7 +1188,7 @@ def poll() { // Check to see if we are connected to the API or not if (apiConnected() == "lost") { LOG("poll() - Attempting to recover poll() due to lost API Connection", 1, null, "warn") - scheduleWatchdog() + scheduleWatchdog(null, true) if ( refreshAuthToken() ) { // We were able to recover apiRestored() @@ -1144,7 +1200,7 @@ def poll() { } state.lastPoll = now() - state.lastPollDate = new Date().toString() + state.lastPollDate = getTimestamp() LOG("poll() - Polling children with pollChildren(null)", 4) return pollChildren(null) // Poll ALL the children at the same time for efficiency } @@ -1418,14 +1474,16 @@ def toQueryString(Map m) { return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") } -private def Boolean refreshAuthToken() { - +private def Boolean refreshAuthToken(force=false) { + // Update the timestamp + updateLastTokenRefresh() + if(!state.refreshToken) { LOG("refreshing auth token", 2) LOG("refreshAuthToken() - There is no refreshToken stored! Unable to refresh OAuth token.", 1, null, "error") apiLost("refreshAuthToken() - No refreshToken") return false - } else if ( !readyForAuthRefresh() ) { + } else if ( (force != true) && !readyForAuthRefresh() ) { // Not ready to refresh yet LOG("refreshAuthToken() - Not time to refresh yet, there is still time left before expiration.") return true @@ -1506,31 +1564,44 @@ private def Boolean refreshAuthToken() { if ( (e.statusCode == 500 && e.getResponse()?.data.status.code == 14) || (e.statusCode == 401 && e.getResponse()?.data.status.code == 14) ) { LOG("refreshAuthToken() - Received data.status.code = 14", 1, null, "error") apiLost("refreshAuthToken() - Received data.status.code = 14" ) + return false } else if (e.statusCode != 401) { //this issue might comes from exceed 20sec app execution, connectivity issue etc. LOG("refreshAuthToken() - e.statusCode: ${e.statusCode}", 1, null, "warn") state.connected = "warn" generateEventLocalParams() // Update the connected state at the thermostat devices - if(canSchedule()) { runIn(reAttemptPeriod, "refreshAuthToken") } else { refreshAuthTokens() } + if(canSchedule()) { runIn(reAttemptPeriod, "refreshAuthToken") } else { refreshAuthTokens(true) } + return false } else if (e.statusCode == 401) { // status.code other than 14 state.reAttempt = state.reAttempt + 1 LOG("reAttempt refreshAuthToken to try = ${state.reAttempt}", 1, null, "warn") if (state.reAttempt <= 3) { state.connected = "warn" generateEventLocalParams() // Update the connected state at the thermostat devices - if(canSchedule()) { runIn(reAttemptPeriod, "refreshAuthToken") } else { refreshAuthToken() } + if(canSchedule()) { runIn(reAttemptPeriod, "refreshAuthToken") } else { refreshAuthToken(true) } + return false } else { // More than 3 attempts, time to give up and notify the end user LOG("More than 3 attempts to refresh tokens. Giving up", 1, null, "error") debugEvent("More than 3 attempts to refresh tokens. Giving up") apiLost("refreshAuthToken() - More than 3 attempts to refresh token. Have to give up") + return false } } } catch (java.util.concurrent.TimeoutException e) { LOG("refreshAuthToken(), TimeoutException: ${e}.", 1, null, "error") // Likely bad luck and network overload, move on and let it try again - if(canSchedule()) { runIn(300, "refreshAuthToken") } else { refreshAuth() } + state.connected = "warn" + generateEventLocalParams() // Update the connected state at the thermostat devices + def reAttemptPeriod = 300 // in sec + if(canSchedule()) { runIn(reAttemptPeriod, "refreshAuthToken") } else { refreshAuthTokens(true) } + return false + } catch (groovy.lang.StringWriterIOException e) { + LOG("refreshAuthToken(), groovy.lang.StringWriterIOException encountered: ${e}", 1, null, "error") + apiLost("refreshAuthToken(), groovy.lang.StringWriterIOException encountered: ${e}") + return false } catch (Exception e) { LOG("refreshAuthToken(), General Exception: ${e}.", 1, null, "error") + return false } } } @@ -1696,7 +1767,7 @@ private def sendJson(child = null, String jsonBody) { //refresh the auth token if (resp.status == 500 && resp.status.code == 14) { LOG("Refreshing your auth_token!") - if(refreshAuthToken()) { + if(refreshAuthToken(true)) { LOG("Successfully performed a refreshAuthToken() after a Code 14!", 2, child, "warn") } return false // No way to recover from a status.code 14 @@ -1705,7 +1776,7 @@ private def sendJson(child = null, String jsonBody) { state.connected = "warn" generateEventLocalParams() if (j == 2) { // Go ahead and refresh on the second pass through - refreshAuthToken() + refreshAuthToken(true) return false } } @@ -1716,7 +1787,7 @@ private def sendJson(child = null, String jsonBody) { LOG("sendJson() >> HttpResponseException occured. Exception info: ${e} StatusCode: ${e.statusCode} response? data: ${e.getResponse()?.getData()}", 1, child, "error") state.connected = "warn" generateEventLocalParams() - refreshAuthToken() + refreshAuthToken(true) return false } catch(Exception e) { // Might need to further break down @@ -1724,7 +1795,7 @@ private def sendJson(child = null, String jsonBody) { state.connected = "warn" generateEventLocalParams() if (j == 2) { // Go ahead and refresh on the second pass through - refreshAuthToken() + refreshAuthToken(true) return false } } @@ -1753,14 +1824,22 @@ private def getSmartThingsClientId() { } -private def LOG(message, level=3, child=null, logType="debug", event=true, displayEvent=true) { +private def LOG(message, level=3, child=null, logType="debug", event=false, displayEvent=true) { def prefix = "" + def logTypes = ["error", "debug", "info", "trace"] + + if(!logTypes.contains(logType)) { + log.error "LOG() - Received logType ${logType} which is not in the list of allowed types." + if (event && child) { debugEventFromParent(child, "LOG() - Received logType ${logType} which is not in the list of allowed types.") } + logType = "debug" + } + + if ( logType == "error" ) { state.lastLOGerror = message } if ( settings.debugLevel?.toInteger() == 5 ) { prefix = "LOG: " } if ( debugLevel(level) ) { log."${logType}" "${prefix}${message}" - // log.debug message if (event) { debugEvent(message, displayEvent) } - if (event && child) { debugEventFromParent(child, message) } + if (child) { debugEventFromParent(child, message) } } } @@ -1868,6 +1947,10 @@ private def getPollingInterval() { return (settings.pollingInterval?.toInteger() >= 5) ? settings.pollingInterval.toInteger() : 5 } +private def String getTimestamp() { + return new Date().format("yyyy-MM-dd HH:mm:ss z", location.timeZone) +} + // Are we connected with the Ecobee service? private String apiConnected() { @@ -1882,10 +1965,29 @@ private def apiRestored() { unschedule("notifyApiLost") } + +private def getDebugDump() { + def debugParams = [when:"${getTimestamp()}", whenEpic:"${now()}", + lastPollDate:"${state.lastPollDate}", lastScheduledPollDate:"${state.lastScheduledPollDate}", + lastScheduledTokenRefreshDate:"${state.lastScheduledTokenRefreshDate}", lastScheduledWatchdogDate:"${state.lastScheduledWatchdogDate}", + lastTokenRefreshDate:"${state.lastTokenRefreshDate}", initializedEpic:"${state.initializedEpic}", initializedDate:"${state.initializedDate}", + lastLOGerror:"${state.lastLOGerror}" + ] + return debugParams +} + private def apiLost(where = "not specified") { - LOG("apiLost() - ${where}: Lost connection with APIs. unscheduling Polling and refreshAuthToken. User MUST reintialize the connection with Ecobee by running the SmartApp and logging in again", 1, null, "error", true, true) + LOG("apiLost() - ${where}: Lost connection with APIs. unscheduling Polling and refreshAuthToken. User MUST reintialize the connection with Ecobee by running the SmartApp and logging in again", 1, null, "error") // TODO: Add a state.apiLostDump variable and populate it with useful troubleshooting information to make it easier to grab everything in one place. Then also add this to the Debug Dashboard - // state.apiLostDump = [blah:blah, yup:yup] + state.apiLostDump = getDebugDump() + + // Has out token really expired yet? + if ( !readyForAuthRefresh() ) { + LOG("apiLost() - Still time left on expiry of Auth Token. Gonna wait until full expiry to actually declare the api as fully lost. Setting it to warn instead.", 1, null, "error") + state.connected = "warn" + generateEventLocalParams() + return + } // provide cleanup steps when API Connection is lost def notificationMessage = "is disconnected from SmartThings/Ecobee, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials." @@ -1895,13 +1997,13 @@ private def apiLost(where = "not specified") { sendPushAndFeeds(notificationMessage) generateEventLocalParams() - LOG("Unscheduling Polling and refreshAuthToken. User MUST reintialize the connection with Ecobee by running the SmartApp and logging in again", 0, null, "error", true, true) + LOG("Unscheduling Polling and refreshAuthToken. User MUST reintialize the connection with Ecobee by running the SmartApp and logging in again", 0, null, "error") // Notify each child that we lost so it gets logged if ( debugLevel(3) ) { def d = getChildDevices() d?.each { oneChild -> - LOG("apiLost() - notifying each child: ${oneChild} of loss", 0, child, "error", true, true) + LOG("apiLost() - notifying each child: ${oneChild} of loss", 0, child, "error") } } @@ -1915,7 +2017,7 @@ def notifyApiLost() { if ( state.connected == "lost" ) { generateEventLocalParams() sendPushAndFeeds(notificationMessage) - LOG("notifyApiLost() - API Connection Previously Lost. User MUST reintialize the connection with Ecobee by running the SmartApp and logging in again", 0, null, "error", true, true) + LOG("notifyApiLost() - API Connection Previously Lost. User MUST reintialize the connection with Ecobee by running the SmartApp and logging in again", 0, null, "error") } else { // Must have restored connection unschedule("notifyApiLost") From 1605eb8963a398706a7cffce041e4f50f63c7879 Mon Sep 17 00:00:00 2001 From: Sean Schneyer Date: Fri, 5 Feb 2016 02:48:06 -0600 Subject: [PATCH 2/2] Updates to the watchdog handling. Hopefully this version is stable. --- .../ecobee-thermostat.groovy | 350 ++++++------------ 1 file changed, 122 insertions(+), 228 deletions(-) diff --git a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy index 09267c95267..747a9ffc193 100644 --- a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy +++ b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy @@ -25,7 +25,7 @@ */ def getVersionNum() { return "0.9.0" } -private def getVersionLabel() { return "Ecobee Thermostat Version 0.9.0-RC2" } +private def getVersionLabel() { return "Ecobee Thermostat Version 0.9.0-RC6" } metadata { @@ -64,203 +64,57 @@ metadata { attribute "thermostatStatus","string" attribute "apiConnected","string" attribute "averagedTemperature","number" - attribute "currentProgram","string" - attribute "currentProgramId","string" - - attribute "weatherSymbol", "string" - + attribute "currentProgramId","string" + attribute "weatherSymbol", "string" attribute "debugEventFromParent","string" - - - /* - attribute "thermostatName", "string" - attribute "temperatureDisplay", "string" - attribute "coolingSetpointDisplay", "string" - attribute "heatingSetpointDisplay", "string" - attribute "heatLevelUp", "string" - attribute "heatLevelDown", "string" - attribute "coolLevelUp", "string" - attribute "coolLevelDown", "string" - attribute "verboseTrace", "string" - attribute "fanMinOnTime", "string" - attribute "humidifierMode", "string" - attribute "dehumidifierMode", "string" - attribute "humidifierLevel", "string" - attribute "dehumidifierLevel", "string" - attribute "condensationAvoid", "string" - attribute "groups", "string" - attribute "equipmentStatus", "string" - attribute "alerts", "string" - attribute "programScheduleName", "string" - attribute "programFanMode", "string" - attribute "programType", "string" - attribute "programCoolTemp", "string" - attribute "programHeatTemp", "string" - attribute "programCoolTempDisplay", "string" - attribute "programHeatTempDisplay", "string" - attribute "programEndTimeMsg", "string" - - attribute "weatherDateTime", "string" - attribute "weatherSymbol", "string" - attribute "weatherStation", "string" - attribute "weatherCondition", "string" - attribute "weatherTemperatureDisplay", "string" - attribute "weatherPressure", "string" - attribute "weatherRelativeHumidity", "string" - attribute "weatherWindSpeed", "string" - attribute "weatherWindDirection", "string" - attribute "weatherPop", "string" - attribute "weatherTempHigh", "string" - attribute "weatherTempLow", "string" - attribute "weatherTempHighDisplay", "string" - attribute "weatherTempLowDisplay", "string" - - attribute "plugName", "string" - attribute "plugState", "string" - attribute "plugSettings", "string" - attribute "hasHumidifier", "string" - attribute "hasDehumidifier", "string" - attribute "hasErv", "string" - attribute "hasHrv", "string" - attribute "ventilatorMinOnTime", "string" - attribute "ventilatorMode", "string" - attribute "programNameForUI", "string" - // Passed in via the SmartApp - // attribute "thermostatOperatingState", "string" - attribute "climateList", "string" - attribute "modelNumber", "string" - attribute "followMeComfort", "string" - attribute "autoAway", "string" - attribute "intervalRevision", "string" - attribute "runtimeRevision", "string" - attribute "thermostatRevision", "string" - attribute "heatStages", "string" - attribute "coolStages", "string" - attribute "climateName", "string" - attribute "setClimate", "string" - - // Report Runtime events - attribute "auxHeat1RuntimeInPeriod", "string" - attribute "auxHeat2RuntimeInPeriod", "string" - attribute "auxHeat3RuntimeInPeriod", "string" - attribute "compCool1RuntimeInPeriod", "string" - attribute "compCool2RuntimeInPeriod", "string" - attribute "dehumidifierRuntimeInPeriod", "string" - attribute "humidifierRuntimeInPeriod", "string" - attribute "ventilatorRuntimeInPeriod", "string" - attribute "fanRuntimeInPeriod", "string" - - attribute "auxHeat1RuntimeDaily", "string" - attribute "auxHeat2RuntimeDaily", "string" - attribute "auxHeat3RuntimeDaily", "string" - attribute "compCool1RuntimeDaily", "string" - attribute "compCool2RuntimeDaily", "string" - attribute "dehumidifierRuntimeDaily", "string" - attribute "humidifierRuntimeDaily", "string" - attribute "ventilatorRuntimeDaily", "string" - attribute "fanRuntimeDaily", "string" - attribute "reportData", "string" - - // Report Sensor Data & Stats - attribute "reportSensorMetadata", "string" - attribute "reportSensorData", "string" - attribute "reportSensorAvgInPeriod", "string" - attribute "reportSensorMinInPeriod", "string" - attribute "reportSensorMaxInPeriod", "string" - attribute "reportSensorTotalInPeriod", "string" - - // Remote Sensor Data & Stats - attribute "remoteSensorData", "string" - attribute "remoteSensorTmpData", "string" - attribute "remoteSensorHumData", "string" - attribute "remoteSensorOccData", "string" - attribute "remoteSensorAvgTemp", "string" - attribute "remoteSensorAvgHumidity", "string" - attribute "remoteSensorMinTemp", "string" - attribute "remoteSensorMinHumidity", "string" - attribute "remoteSensorMaxTemp", "string" - attribute "remoteSensorMaxHumidity", "string" - */ + } + simulator { } + tiles(scale: 2) { + + multiAttributeTile(name:"tempSummaryBlack", type:"thermostat", width:6, height:4) { + tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { + attributeState("default", label:'${currentValue}', unit:"dF") + } - /* - command "setFanMinOnTime" - command "setCondensationAvoid" - command "createVacation" - command "deleteVacation" - command "getEcobeePinAndAuth" - command "getThermostatInfo" - command "getThermostatSummary" - command "iterateCreateVacation" - command "iterateDeleteVacation" - command "iterateResumeProgram" - command "iterateSetHold" - command "resumeProgram" - command "resumeThisTstat" - command "setAuthTokens" - command "setHold" - command "setHoldExtraParams" - command "heatLevelUp" - command "heatLevelDown" - command "coolLevelUp" - command "coolLevelDown" - command "auxHeatOnly" - command "setThermostatFanMode" - command "dehumidifierOff" - command "dehumidifierOn" - command "humidifierOff" - command "humidifierAuto" - command "humidifierManual" - command "setHumidifierLevel" - command "setDehumidifierLevel" - command "updateGroup" - command "getGroups" - command "iterateUpdateGroup" - command "createGroup" - command "deleteGroup" - command "updateClimate" - command "iterateUpdateClimate" - command "createClimate" - command "deleteClimate" - command "setClimate" - command "iterateSetClimate" - command "controlPlug" - command "ventilatorOn" - command "ventilatorAuto" - command "ventilatorOff" - command "ventilatorAuto" - command "setVentilatorMinOnTime" - command "awake" - command "away" - command "present" - command "home" - command "asleep" - command "quickSave" - command "setThisTstatClimate" - command "setThermostatSettings" - command "iterateSetThermostatSettings" - command "getEquipmentStatus" - command "refreshChildTokens" - command "autoAway" - command "followMeComfort" - command "getReportData" - command "generateReportRuntimeEvents" - command "generateReportSensorStatsEvents" - command "getThermostatRevision" - command "generateRemoteSensorEvents" - */ - + tileAttribute("device.temperature", key: "VALUE_CONTROL") { + attributeState("default", action: "setTemperature") + } + tileAttribute("device.humidity", key: "SECONDARY_CONTROL") { + attributeState("default", label:'${currentValue}%', unit:"%") + } - } + + // Use this one if you want ALL BLACK + tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") { + // TODO: Change this to a preference so the use can select green over grey from within the app + // Uncomment the below if you prefer green for idle + attributeState("idle", backgroundColor:"#000000") + // Or uncomment this one if you prefer grey for idle + // attributeState("idle", backgroundColor:"#C0C0C0") + attributeState("heating", backgroundColor:"#000000") + attributeState("cooling", backgroundColor:"#000000") + } - simulator { } - tiles(scale: 2) { + tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") { + attributeState("off", label:'${name}') + attributeState("heat", label:'${name}') + attributeState("cool", label:'${name}') + attributeState("auto", label:'${name}') + } + tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") { + attributeState("default", label:'${currentValue}', unit:"F") + } + tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") { + attributeState("default", label:'${currentValue}', unit:"F") + } - + } // End multiAttributeTile + multiAttributeTile(name:"tempSummary", type:"thermostat", width:6, height:4) { tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { attributeState("default", label:'${currentValue}', unit:"dF") @@ -273,6 +127,7 @@ metadata { attributeState("default", label:'${currentValue}%', unit:"%") } + // Use this one if you want colors for the multiAttributeTile tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") { // TODO: Change this to a preference so the use can select green over grey from within the app // Uncomment the below if you prefer green for idle @@ -282,7 +137,6 @@ metadata { attributeState("heating", backgroundColor:"#ffa81e") attributeState("cooling", backgroundColor:"#269bd2") } - tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") { attributeState("off", label:'${name}') attributeState("heat", label:'${name}') @@ -393,10 +247,16 @@ metadata { standardTile("refresh", "device.thermostatMode", width: 2, height: 2,inactiveLabel: false, decoration: "flat") { state "default", action:"refresh.refresh", icon:"st.secondary.refresh" } + standardTile("resumeProgram", "device.resumeProgram", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { - state "resume", action:"resumeProgram", nextState: "updating", label:'Resume Schedule', icon:"st.Office.office7" + state "resume", action:"resumeProgram", nextState: "updating", label:'Resume Program', icon:"st.Office.office7" state "updating", label:"Working", icon: "st.samsung.da.oven_ic_send" } + + standardTile("resumeProgramBlack", "device.resumeProgram", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "resume", action:"resumeProgram", nextState: "updating", label:'Resume Program', icon:"https://raw.githubusercontent.com/StrykerSKS/SmartThings/master/smartapp-icons/ecobee/dark/ecobee_sched_square.png" + state "updating", label:"Working...", icon: "https://raw.githubusercontent.com/StrykerSKS/SmartThings/master/smartapp-icons/ecobee/dark/ecobee_blank_square.png" + } valueTile("currentProgram", "device.currentProgramName", height: 2, width: 4, inactiveLabel: false, decoration: "flat") { state "default", label:'Comfort Setting:\n${currentValue}' } @@ -416,6 +276,15 @@ metadata { state "updating", label:"Working...", icon: "st.samsung.da.oven_ic_send" } + standardTile("operatingStateBlack", "device.thermostatOperatingState", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "idle", label: "Idle", backgroundColor:"#c0c0c0", icon: "https://raw.githubusercontent.com/StrykerSKS/SmartThings/master/smartapp-icons/ecobee/dark/ecobee_idle_square.png" + state "fan only", backgroundColor:"#87ceeb", icon: "https://raw.githubusercontent.com/StrykerSKS/SmartThings/master/smartapp-icons/ecobee/dark/ecobee_fanonly_square.png" + state "heating", backgroundColor:"#ffa81e", icon: "https://raw.githubusercontent.com/StrykerSKS/SmartThings/master/smartapp-icons/ecobee/dark/ecobee_heat_square.png" + state "cooling", backgroundColor:"#269bd2", icon: "https://raw.githubusercontent.com/StrykerSKS/SmartThings/master/smartapp-icons/ecobee/dark/ecobee_cool_square.png" + // Issue reported that the label overlaps. Need to remove the icon + state "default", label: '${currentValue}', backgroundColor:"c0c0c0", icon: "st.nest.empty" + } + standardTile("operatingState", "device.thermostatOperatingState", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { //state "idle", label: "Idle", backgroundColor:"#44b621", icon: "st.nest.empty" state "idle", label: "Idle", backgroundColor:"#c0c0c0", icon: "st.nest.empty" @@ -431,12 +300,19 @@ metadata { } + standardTile("motionBlack", "device.motion", width: 2, height: 2) { + state("active", label:'motion', backgroundColor:"#53a7c0", icon:"https://raw.githubusercontent.com/StrykerSKS/SmartThings/master/smartapp-icons/ecobee/dark/ecobee_sensor_motion.png") + state("inactive", label:'no motion', backgroundColor:"#ffffff", icon:"https://raw.githubusercontent.com/StrykerSKS/SmartThings/master/smartapp-icons/ecobee/dark/ecobee_sensor_nomotion.png") + } + + standardTile("motion", "device.motion", width: 2, height: 2) { state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0") state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff") } + // Additional tiles based on Yves Racine's device type // Weather Tiles and other Forecast related tiles standardTile("weatherIcon", "device.weatherSymbol", inactiveLabel: false, width: 2, height: 2, @@ -445,7 +321,7 @@ metadata { state "0", label: 'Sunny', icon: "st.Weather.weather14" state "1", label: 'Few Clouds', icon: "st.Weather.weather11" state "2", label: 'Partly Cloudy', icon: "st.Weather.weather11" - state "3", label: 'Mostly Cloudy', icon: "st.Weather.weather13" + state "3", label: 'Mostly Cloudy', icon: "st.Weather.weather11" state "4", label: 'Overcast', icon: "st.Weather.weather13" state "5", label: 'Drizzle', icon: "st.Weather.weather10" state "6", label: 'Rain', icon: "st.Weather.weather10" @@ -465,53 +341,65 @@ metadata { state "20", label: 'Smoke', icon: "st.Weather.weather13" state "21", label: 'Dust', icon: "st.Weather.weather13" } - valueTile("weatherDateTime", "device.weatherDateTime", inactiveLabel: false, - width: 3, height: 2, decoration: "flat") { - state "default", label: '${currentValue}' + + standardTile("weatherIconBlack", "device.weatherSymbol", inactiveLabel: false, width: 2, height: 2, + decoration: "flat") { + state "-2", label: 'updating...', icon: "st.unknown.unknown.unknown" + state "0", label: '', icon: "https://raw.githubusercontent.com/StrykerSKS/SmartThings/master/smartapp-icons/ecobee/dark/e3_weather_0.png" + state "1", label: 'Few Clouds', icon: "https://raw.githubusercontent.com/StrykerSKS/SmartThings/master/smartapp-icons/ecobee/light/e3_weather_1.png" + state "2", label: 'Partly Cloudy', icon: "st.Weather.weather11" + state "3", label: 'Mostly Cloudy', icon: "https://raw.githubusercontent.com/StrykerSKS/SmartThings/master/smartapp-icons/ecobee/dark/e3_weather_3.png" + state "4", label: 'Overcast', icon: "st.Weather.weather13" + state "5", label: 'Drizzle', icon: "st.Weather.weather10" + state "6", label: 'Rain', icon: "st.Weather.weather10" + state "7", label: 'Freezing Rain', icon: "st.Weather.weather6" + state "8", label: 'Showers', icon: "st.Weather.weather10" + state "9", label: 'Hail', icon: "st.custom.wuk.sleet" + state "10", label: 'Snow', icon: "st.Weather.weather6" + state "11", label: 'Flurries', icon: "st.Weather.weather6" + state "12", label: 'Sleet', icon: "st.Weather.weather6" + state "13", label: 'Blizzard', icon: "st.Weather.weather7" + state "14", label: 'Pellets', icon: "st.custom.wuk.sleet" + state "15", label: 'Thunder Storms',icon: "st.custom.wuk.tstorms" + state "16", label: 'Windy', icon: "st.Transportation.transportation5" + state "17", label: 'Tornado', icon: "st.Weather.weather1" + state "18", label: 'Fog', icon: "st.Weather.weather13" + state "19", label: 'Hazy', icon: "st.Weather.weather13" + state "20", label: 'Smoke', icon: "st.Weather.weather13" + state "21", label: 'Dust', icon: "st.Weather.weather13" } - valueTile("weatherConditions", "device.weatherCondition", - inactiveLabel: false, width: 3, height: 2, decoration: "flat") { - state "default", label: 'Forecast\n${currentValue}' + + standardTile("weatherTemperatureBlack", "device.weatherTemperature", width: 2, height: 2, decoration: "flat") { + state "default", label: 'Outside: ${currentValue}°', unit: "dF", icon: "https://raw.githubusercontent.com/StrykerSKS/SmartThings/master/smartapp-icons/ecobee/dark/ecobee_thermometer_square.png" } - standardTile("weatherTemperature", "device.weatherTemperature", inactiveLabel: - false, width: 2, height: 2, decoration: "flat") { + + standardTile("weatherTemperature", "device.weatherTemperature",width: 2, height: 2, decoration: "flat") { state "default", label: 'Outside: ${currentValue}°', unit: "dF", icon: "st.Weather.weather2" } - valueTile("weatherRelativeHumidity", "device.weatherRelativeHumidity", - inactiveLabel: false, width: 2, height: 2,decoration: "flat") { - state "default", label: 'Out Hum\n${currentValue}%', unit: "humidity" - } - valueTile("weatherTempHigh", "device.weatherTempHigh", inactiveLabel: false, - width: 2, height: 2, decoration: "flat") { - state "default", label: 'ForecastH\n${currentValue}°', unit: "dF" - } - valueTile("weatherTempLow", "device.weatherTempLow", inactiveLabel: false, - width: 2, height: 2, decoration: "flat") { - state "default", label: 'ForecastL\n${currentValue}°', unit: "dF" - } - valueTile("weatherPressure", "device.weatherPressure", inactiveLabel: false, - width: 2, height: 2, decoration: "flat") { - state "default", label: 'Pressure\n${currentValue}', unit: "hpa" - } - valueTile("weatherWindDirection", "device.weatherWindDirection", - inactiveLabel: false, width: 2, height: 2, decoration: "flat") { - state "default", label: 'W.Dir\n${currentValue}' - } - valueTile("weatherWindSpeed", "device.weatherWindSpeed", inactiveLabel: false, - width: 2, height: 2, decoration: "flat") { - state "default", label: 'W.Speed\n${currentValue}' - } - valueTile("weatherPop", "device.weatherPop", inactiveLabel: false, width: 2, - height: 2, decoration: "flat") { - state "default", label: 'PoP\n${currentValue}%', unit: "%" + + + standardTile("ecoLogo", "device.motion", width: 2, height: 2) { + state "default", icon:"https://s3.amazonaws.com/smartapp-icons/MiscHacking/ecobee-smartapp-icn@2x.png" } + main(["temperature", "tempSummary"]) // details(["summary","temperature", "upButtonControl", "thermostatSetpoint", "currentStatus", "downButtonControl", "mode", "weatherIcon", "resumeProgram", "refresh"]) // details(["summary","apiStatus", "upButtonControl", "thermostatSetpoint", "currentStatus", "downButtonControl", "mode", "weatherIcon", "resumeProgram", "refresh"]) - details(["tempSummary", + /* details(["tempSummaryBlack", + "operatingStateBlack", "weatherIconBlack", "weatherTemperatureBlack", + "motion", "resumeProgram", "mode", + "coolSliderControl", "coolingSetpoint", + "heatSliderControl", "heatingSetpoint", + "currentStatus", "apiStatus", + "currentProgram", "fanMode", + "setHome", "setAway", "setSleep", + "refresh", "ecoLogo" + ]) +*/ + details(["tempSummary", "operatingState", "weatherIcon", "weatherTemperature", "motion", "resumeProgram", "mode", "coolSliderControl", "coolingSetpoint", @@ -519,8 +407,10 @@ metadata { "currentStatus", "apiStatus", "currentProgram", "fanMode", "setHome", "setAway", "setSleep", - "refresh" + "refresh", "ecoLogo" ]) + + } preferences { @@ -1301,6 +1191,10 @@ private def milesToKm(distance) { private def get_URI_ROOT() { return "https://api.ecobee.com" } + +private def getImageURLRoot() { + return "https://raw.githubusercontent.com/StrykerSKS/SmartThings/master/smartapp-icons/ecobee/dark/" +} // Maximum tstat batch size (25 thermostats max may be processed in batch) private def get_MAX_TSTAT_BATCH() { return 25