From 26df619b4f8ae9d4cb32e39632d3696b27a38a63 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Tue, 6 Sep 2016 11:22:06 -0500 Subject: [PATCH] revert changes for DVCSMP-1980 and SSVD-2534 on staging --- .../ecobee-thermostat.groovy | 50 +- .../ecobee-connect.src/ecobee-connect.groovy | 642 ++++++++---------- 2 files changed, 306 insertions(+), 386 deletions(-) diff --git a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy index a16d28c..7b08b14 100644 --- a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy +++ b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy @@ -152,11 +152,11 @@ def generateEvent(Map results) { sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue isChange = isTemperatureStateChange(device, name, value.toString()) isDisplayed = isChange - event << [value: sendValue, unit: temperatureScale, isStateChange: isChange, displayed: isDisplayed] + event << [value: sendValue, isStateChange: isChange, displayed: isDisplayed] } else if (name=="maxCoolingSetpoint" || name=="minCoolingSetpoint" || name=="maxHeatingSetpoint" || name=="minHeatingSetpoint") { def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue - event << [value: sendValue, unit: temperatureScale, displayed: false] + event << [value: sendValue, displayed: false] } else if (name=="heatMode" || name=="coolMode" || name=="autoMode" || name=="auxHeatMode"){ isChange = isStateChange(device, name, value.toString()) event << [value: value.toString(), isStateChange: isChange, displayed: false] @@ -234,9 +234,9 @@ void setHeatingSetpoint(setpoint) { def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite" - if (parent.setHold(heatingValue, coolingValue, deviceId, sendHoldType)) { - sendEvent("name":"heatingSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale) - sendEvent("name":"coolingSetpoint", "value":coolingSetpoint, "unit":location.temperatureScale) + if (parent.setHold(this, heatingValue, coolingValue, deviceId, sendHoldType)) { + sendEvent("name":"heatingSetpoint", "value":heatingSetpoint) + sendEvent("name":"coolingSetpoint", "value":coolingSetpoint) log.debug "Done setHeatingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}" generateSetpointEvent() generateStatusEvent() @@ -271,9 +271,9 @@ void setCoolingSetpoint(setpoint) { def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite" - if (parent.setHold(heatingValue, coolingValue, deviceId, sendHoldType)) { - sendEvent("name":"heatingSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale) - sendEvent("name":"coolingSetpoint", "value":coolingSetpoint, "unit":location.temperatureScale) + if (parent.setHold(this, heatingValue, coolingValue, deviceId, sendHoldType)) { + sendEvent("name":"heatingSetpoint", "value":heatingSetpoint) + sendEvent("name":"coolingSetpoint", "value":coolingSetpoint) log.debug "Done setCoolingSetpoint>> coolingSetpoint = ${coolingSetpoint}, heatingSetpoint = ${heatingSetpoint}" generateSetpointEvent() generateStatusEvent() @@ -287,14 +287,14 @@ void resumeProgram() { log.debug "resumeProgram() is called" sendEvent("name":"thermostatStatus", "value":"resuming schedule", "description":statusText, displayed: false) def deviceId = device.deviceNetworkId.split(/\./).last() - if (parent.resumeProgram(deviceId)) { + if (parent.resumeProgram(this, deviceId)) { sendEvent("name":"thermostatStatus", "value":"setpoint is updating", "description":statusText, displayed: false) runIn(5, "poll") log.debug "resumeProgram() is done" sendEvent("name":"resumeProgram", "value":"resume", descriptionText: "resumeProgram is done", displayed: false, isStateChange: true) } else { sendEvent("name":"thermostatStatus", "value":"failed resume click refresh", "description":statusText, displayed: false) - log.error "Error resumeProgram() check parent.resumeProgram(deviceId)" + log.error "Error resumeProgram() check parent.resumeProgram(this, deviceId)" } } @@ -406,7 +406,7 @@ def generateOperatingStateEvent(operatingState) { def off() { log.debug "off" def deviceId = device.deviceNetworkId.split(/\./).last() - if (parent.setMode ("off", deviceId)) + if (parent.setMode (this,"off", deviceId)) generateModeEvent("off") else { log.debug "Error setting new mode." @@ -420,7 +420,7 @@ def off() { def heat() { log.debug "heat" def deviceId = device.deviceNetworkId.split(/\./).last() - if (parent.setMode ("heat", deviceId)) + if (parent.setMode (this,"heat", deviceId)) generateModeEvent("heat") else { log.debug "Error setting new mode." @@ -438,7 +438,7 @@ def emergencyHeat() { def auxHeatOnly() { log.debug "auxHeatOnly" def deviceId = device.deviceNetworkId.split(/\./).last() - if (parent.setMode ("auxHeatOnly", deviceId)) + if (parent.setMode (this,"auxHeatOnly", deviceId)) generateModeEvent("auxHeatOnly") else { log.debug "Error setting new mode." @@ -452,7 +452,7 @@ def auxHeatOnly() { def cool() { log.debug "cool" def deviceId = device.deviceNetworkId.split(/\./).last() - if (parent.setMode ("cool", deviceId)) + if (parent.setMode (this,"cool", deviceId)) generateModeEvent("cool") else { log.debug "Error setting new mode." @@ -466,7 +466,7 @@ def cool() { def auto() { log.debug "auto" def deviceId = device.deviceNetworkId.split(/\./).last() - if (parent.setMode ("auto", deviceId)) + if (parent.setMode (this,"auto", deviceId)) generateModeEvent("auto") else { log.debug "Error setting new mode." @@ -489,7 +489,7 @@ def fanOn() { def coolingValue = location.temperatureScale == "C"? convertCtoF(coolingSetpoint) : coolingSetpoint def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint - if (parent.setFanMode(heatingValue, coolingValue, deviceId, sendHoldType, fanMode)) { + if (parent.setFanMode(this, heatingValue, coolingValue, deviceId, sendHoldType, fanMode)) { generateFanModeEvent(fanMode) } else { log.debug "Error setting new mode." @@ -510,7 +510,7 @@ def fanAuto() { def coolingValue = location.temperatureScale == "C"? convertCtoF(coolingSetpoint) : coolingSetpoint def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint - if (parent.setFanMode(heatingValue, coolingValue, deviceId, sendHoldType, fanMode)) { + if (parent.setFanMode(this, heatingValue, coolingValue, deviceId, sendHoldType, fanMode)) { generateFanModeEvent(fanMode) } else { log.debug "Error setting new mode." @@ -556,12 +556,12 @@ def generateSetpointEvent() { if (mode == "heat") { - sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale) + sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint ) } else if (mode == "cool") { - sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint, "unit":location.temperatureScale) + sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint) } else if (mode == "auto") { @@ -573,7 +573,7 @@ def generateSetpointEvent() { } else if (mode == "auxHeatOnly") { - sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale) + sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint) } @@ -608,7 +608,7 @@ void raiseSetpoint() { targetvalue = maxCoolingSetpoint } - sendEvent("name":"thermostatSetpoint", "value":targetvalue, "unit":location.temperatureScale, displayed: false) + sendEvent("name":"thermostatSetpoint", "value":targetvalue, displayed: false) log.info "In mode $mode raiseSetpoint() to $targetvalue" runIn(3, "alterSetpoint", [data: [value:targetvalue], overwrite: true]) //when user click button this runIn will be overwrite @@ -644,7 +644,7 @@ void lowerSetpoint() { targetvalue = minCoolingSetpoint } - sendEvent("name":"thermostatSetpoint", "value":targetvalue, "unit":location.temperatureScale, displayed: false) + sendEvent("name":"thermostatSetpoint", "value":targetvalue, displayed: false) log.info "In mode $mode lowerSetpoint() to $targetvalue" runIn(3, "alterSetpoint", [data: [value:targetvalue], overwrite: true]) //when user click button this runIn will be overwrite @@ -690,10 +690,10 @@ void alterSetpoint(temp) { def coolingValue = location.temperatureScale == "C"? convertCtoF(targetCoolingSetpoint) : targetCoolingSetpoint def heatingValue = location.temperatureScale == "C"? convertCtoF(targetHeatingSetpoint) : targetHeatingSetpoint - if (parent.setHold(heatingValue, coolingValue, deviceId, sendHoldType)) { + if (parent.setHold(this, heatingValue, coolingValue, deviceId, sendHoldType)) { sendEvent("name": "thermostatSetpoint", "value": temp.value, displayed: false) - sendEvent("name": "heatingSetpoint", "value": targetHeatingSetpoint, "unit": location.temperatureScale) - sendEvent("name": "coolingSetpoint", "value": targetCoolingSetpoint, "unit": location.temperatureScale) + sendEvent("name": "heatingSetpoint", "value": targetHeatingSetpoint) + sendEvent("name": "coolingSetpoint", "value": targetCoolingSetpoint) log.debug "alterSetpoint in mode $mode succeed change setpoint to= ${temp.value}" } else { log.error "Error alterSetpoint()" diff --git a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy index d8c2179..848b7b7 100644 --- a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy +++ b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy @@ -66,7 +66,7 @@ def authPage() { // get rid of next button until the user is actually auth'd if (!oauthTokenProvided) { return dynamicPage(name: "auth", title: "Login", nextPage: "", uninstall:uninstallAllowed) { - section() { + section(){ paragraph "Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button." href url:redirectUrl, style:"embedded", required:true, title:"ecobee", description:description } @@ -76,7 +76,7 @@ def authPage() { log.debug "thermostat list: $stats" log.debug "sensor list: ${sensorsDiscovered()}" return dynamicPage(name: "auth", title: "Select Your Thermostats", uninstall: true) { - section("") { + section(""){ paragraph "Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings." input(name: "thermostats", title:"", type: "enum", required:true, multiple:true, description: "Tap to choose", metadata:[values:stats]) } @@ -84,7 +84,7 @@ def authPage() { def options = sensorsDiscovered() ?: [] def numFound = options.size() ?: 0 if (numFound > 0) { - section("") { + section(""){ paragraph "Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings." input(name: "ecobeesensors", title:"Select Ecobee Sensors (${numFound} found)", type: "enum", required:false, description: "Tap to choose", multiple:true, options:options) } @@ -115,12 +115,13 @@ def callback() { def code = params.code def oauthState = params.state - if (oauthState == atomicState.oauthInitState) { + if (oauthState == atomicState.oauthInitState){ + def tokenParams = [ - grant_type: "authorization_code", - code : code, - client_id : smartThingsClientId, - redirect_uri: callbackUrl + grant_type: "authorization_code", + code : code, + client_id : smartThingsClientId, + redirect_uri: callbackUrl ] def tokenUrl = "https://www.ecobee.com/home/token?${toQueryString(tokenParams)}" @@ -128,6 +129,9 @@ def callback() { httpPost(uri: tokenUrl) { resp -> atomicState.refreshToken = resp.data.refresh_token atomicState.authToken = resp.data.access_token + log.debug "swapped token: $resp.data" + log.debug "atomicState.refreshToken: ${atomicState.refreshToken}" + log.debug "atomicState.authToken: ${atomicState.authToken}" } if (atomicState.authToken) { @@ -144,8 +148,8 @@ def callback() { def success() { def message = """ -

Your ecobee Account is now connected to SmartThings!

-

Click 'Done' to finish setup.

+

Your ecobee Account is now connected to SmartThings!

+

Click 'Done' to finish setup.

""" connectionStatus(message) } @@ -167,63 +171,64 @@ def connectionStatus(message, redirectUrl = null) { } def html = """ - - - - - Ecobee & SmartThings connection - - - -
+ + + + +Ecobee & SmartThings connection + + + +
ecobee icon connected device icon SmartThings logo ${message} -
- - - """ +
+ + +""" render contentType: 'text/html', data: html } @@ -232,26 +237,19 @@ def getEcobeeThermostats() { log.debug "getting device list" atomicState.remoteSensors = [] - def bodyParams = [ - selection: [ - selectionType: "registered", - selectionMatch: "", - includeRuntime: true, - includeSensors: true - ] - ] + def requestBody = '{"selection":{"selectionType":"registered","selectionMatch":"","includeRuntime":true,"includeSensors":true}}' + def deviceListParams = [ - uri: apiEndpoint, - path: "/1/thermostat", - headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], - // TODO - the query string below is not consistent with the Ecobee docs: - // https://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostats.shtml - query: [format: 'json', body: toJson(bodyParams)] + uri: apiEndpoint, + path: "/1/thermostat", + headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], + query: [format: 'json', body: requestBody] ] def stats = [:] try { httpGet(deviceListParams) { resp -> + if (resp.status == 200) { resp.data.thermostatList.each { stat -> atomicState.remoteSensors = atomicState.remoteSensors == null ? stat.remoteSensors : atomicState.remoteSensors << stat.remoteSensors @@ -291,10 +289,9 @@ Map sensorsDiscovered() { } def getThermostatDisplayName(stat) { - if(stat?.name) { - return stat.name.toString() - } - return (getThermostatTypeName(stat) + " (${stat.identifier})").toString() + if(stat?.name) + return stat.name.toString() + return (getThermostatTypeName(stat) + " (${stat.identifier})").toString() } def getThermostatTypeName(stat) { @@ -313,6 +310,7 @@ def updated() { } def initialize() { + log.debug "initialize" def devices = thermostats.collect { dni -> def d = getChildDevice(dni) @@ -352,6 +350,8 @@ def initialize() { log.warn "delete: ${delete}, deleting ${delete.size()} thermostats" delete.each { deleteChildDevice(it.deviceNetworkId) } //inherits from SmartApp (data-management) + atomicState.thermostatData = [:] //reset Map to store thermostat data + //send activity feeds to tell that device is connected def notificationMessage = "is connected to SmartThings" sendActivityFeeds(notificationMessage) @@ -381,41 +381,75 @@ def pollHandler() { } def pollChildren(child = null) { - def thermostatIdsString = getChildDeviceIdsString() - log.debug "polling children: $thermostatIdsString" - - def requestBody = [ - selection: [ - selectionType: "thermostats", - selectionMatch: thermostatIdsString, - includeExtendedRuntime: true, - includeSettings: true, - includeRuntime: true, - includeSensors: true - ] - ] + def thermostatIdsString = getChildDeviceIdsString() + log.debug "polling children: $thermostatIdsString" + def data = "" + def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeExtendedRuntime":"true","includeSettings":"true","includeRuntime":"true","includeSensors":true}}' def result = false def pollParams = [ - uri: apiEndpoint, - path: "/1/thermostat", - headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], - // TODO - the query string below is not consistent with the Ecobee docs: - // https://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostats.shtml - query: [format: 'json', body: toJson(requestBody)] - ] + uri: apiEndpoint, + path: "/1/thermostat", + headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], + query: [format: 'json', body: jsonRequestBody] + ] try{ httpGet(pollParams) { resp -> if(resp.status == 200) { - log.debug "poll results returned resp.data ${resp.data}" - atomicState.remoteSensors = resp.data.thermostatList.remoteSensors - updateSensorData() - storeThermostatData(resp.data.thermostatList) - result = true - log.debug "updated ${atomicState.thermostats?.size()} stats: ${atomicState.thermostats}" - } + log.debug "poll results returned resp.data ${resp.data}" + atomicState.remoteSensors = resp.data.thermostatList.remoteSensors + atomicState.thermostatData = resp.data + updateSensorData() + atomicState.thermostats = resp.data.thermostatList.inject([:]) { collector, stat -> + def dni = [ app.id, stat.identifier ].join('.') + + log.debug "updating dni $dni" + + data = [ + coolMode: (stat.settings.coolStages > 0), + heatMode: (stat.settings.heatStages > 0), + deviceTemperatureUnit: stat.settings.useCelsius, + minHeatingSetpoint: (stat.settings.heatRangeLow / 10), + maxHeatingSetpoint: (stat.settings.heatRangeHigh / 10), + minCoolingSetpoint: (stat.settings.coolRangeLow / 10), + maxCoolingSetpoint: (stat.settings.coolRangeHigh / 10), + autoMode: stat.settings.autoHeatCoolFeatureEnabled, + auxHeatMode: (stat.settings.hasHeatPump) && (stat.settings.hasForcedAir || stat.settings.hasElectric || stat.settings.hasBoiler), + temperature: (stat.runtime.actualTemperature / 10), + heatingSetpoint: stat.runtime.desiredHeat / 10, + coolingSetpoint: stat.runtime.desiredCool / 10, + thermostatMode: stat.settings.hvacMode, + humidity: stat.runtime.actualHumidity, + thermostatFanMode: stat.runtime.desiredFanMode + ] + + if (location.temperatureScale == "F") + { + data["temperature"] = data["temperature"] ? Math.round(data["temperature"].toDouble()) : data["temperature"] + data["heatingSetpoint"] = data["heatingSetpoint"] ? Math.round(data["heatingSetpoint"].toDouble()) : data["heatingSetpoint"] + data["coolingSetpoint"] = data["coolingSetpoint"] ? Math.round(data["coolingSetpoint"].toDouble()) : data["coolingSetpoint"] + data["minHeatingSetpoint"] = data["minHeatingSetpoint"] ? Math.round(data["minHeatingSetpoint"].toDouble()) : data["minHeatingSetpoint"] + data["maxHeatingSetpoint"] = data["maxHeatingSetpoint"] ? Math.round(data["maxHeatingSetpoint"].toDouble()) : data["maxHeatingSetpoint"] + data["minCoolingSetpoint"] = data["minCoolingSetpoint"] ? Math.round(data["minCoolingSetpoint"].toDouble()) : data["minCoolingSetpoint"] + data["maxCoolingSetpoint"] = data["maxCoolingSetpoint"] ? Math.round(data["maxCoolingSetpoint"].toDouble()) : data["maxCoolingSetpoint"] + + } + + if (data?.deviceTemperatureUnit == false && location.temperatureScale == "F") { + data["deviceTemperatureUnit"] = "F" + + } else { + data["deviceTemperatureUnit"] = "C" + } + + collector[dni] = [data:data] + return collector + } + result = true + log.debug "updated ${atomicState.thermostats?.size()} stats: ${atomicState.thermostats}" + } } } catch (groovyx.net.http.HttpResponseException e) { log.trace "Exception polling children: " + e.response.data.status @@ -429,12 +463,13 @@ def pollChildren(child = null) { } // Poll Child is invoked from the Child Device itself as part of the Poll Capability -def pollChild() { +def pollChild(){ + def devices = getChildDevices() - if (pollChildren()) { + if (pollChildren()){ devices.each { child -> - if (!child.device.deviceNetworkId.startsWith("ecobee_sensor")) { + if (!child.device.deviceNetworkId.startsWith("ecobee_sensor")){ if(atomicState.thermostats[child.device.deviceNetworkId] != null) { def tData = atomicState.thermostats[child.device.deviceNetworkId] log.info "pollChild(child)>> data for ${child.device.deviceNetworkId} : ${tData.data}" @@ -457,38 +492,36 @@ void poll() { } def availableModes(child) { + debugEvent ("atomicState.thermostats = ${atomicState.thermostats}") + debugEvent ("Child DNI = ${child.device.deviceNetworkId}") def tData = atomicState.thermostats[child.device.deviceNetworkId] debugEvent("Data = ${tData}") - if(!tData) { + if(!tData) + { log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling" + return null } def modes = ["off"] - if (tData.data.heatMode) { - modes.add("heat") - } - if (tData.data.coolMode) { - modes.add("cool") - } - if (tData.data.autoMode) { - modes.add("auto") - } - if (tData.data.auxHeatMode) { - modes.add("auxHeatOnly") - } + if (tData.data.heatMode) modes.add("heat") + if (tData.data.coolMode) modes.add("cool") + if (tData.data.autoMode) modes.add("auto") + if (tData.data.auxHeatMode) modes.add("auxHeatOnly") + + modes - return modes } def currentMode(child) { debugEvent ("atomicState.Thermos = ${atomicState.thermostats}") + debugEvent ("Child DNI = ${child.device.deviceNetworkId}") def tData = atomicState.thermostats[child.device.deviceNetworkId] @@ -497,11 +530,14 @@ def currentMode(child) { if(!tData) { log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling" + + return null } def mode = tData.data.thermostatMode - return mode + + mode } def updateSensorData() { @@ -522,12 +558,12 @@ def updateSensorData() { } } + } else if (it.type == "occupancy") { - if(it.value == "true") { - occupancy = "active" - } else { + if(it.value == "true") + occupancy = "active" + else occupancy = "inactive" - } } } def dni = "ecobee_sensor-"+ it?.id + "-" + it?.code @@ -546,7 +582,7 @@ def getChildDeviceIdsString() { } def toJson(Map m) { - return groovy.json.JsonOutput.toJson(m) + return new org.json.JSONObject(m).toString() } def toQueryString(Map m) { @@ -559,24 +595,54 @@ private refreshAuthToken() { if(!atomicState.refreshToken) { log.warn "Can not refresh OAuth token since there is no refreshToken stored" } else { + def refreshParams = [ - method: 'POST', - uri : apiEndpoint, - path : "/token", - query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId], + method: 'POST', + uri : apiEndpoint, + path : "/token", + query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId], ] + log.debug refreshParams + def notificationMessage = "is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials." //changed to httpPost try { def jsonMap httpPost(refreshParams) { resp -> + if(resp.status == 200) { log.debug "Token refreshed...calling saved RestAction now!" + debugEvent("Token refreshed ... calling saved RestAction now!") - saveTokenAndResumeAction(resp.data) - } - } + + log.debug resp + + jsonMap = resp.data + + if(resp.data) { + + log.debug resp.data + debugEvent("Response = ${resp.data}") + + atomicState.refreshToken = resp?.data?.refresh_token + atomicState.authToken = resp?.data?.access_token + + debugEvent("Refresh Token = ${atomicState.refreshToken}") + debugEvent("OAUTH Token = ${atomicState.authToken}") + + if(atomicState.action && atomicState.action != "") { + log.debug "Executing next action: ${atomicState.action}" + + "${atomicState.action}"() + + atomicState.action = "" + } + + } + atomicState.action = "" + } + } } catch (groovyx.net.http.HttpResponseException e) { log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}" def reAttemptPeriod = 300 // in sec @@ -596,220 +662,118 @@ private refreshAuthToken() { } } -/** - * Saves the refresh and auth token from the passed-in JSON object, - * and invokes any previously executing action that did not complete due to - * an expired token. - * - * @param json - an object representing the parsed JSON response from Ecobee - */ -private void saveTokenAndResumeAction(json) { - log.debug "token response json: $json" - if (json) { - debugEvent("Response = $json") - atomicState.refreshToken = json?.refresh_token - atomicState.authToken = json?.access_token - if (atomicState.action) { - log.debug "got refresh token, executing next action: ${atomicState.action}" - "${atomicState.action}"() - } - } else { - log.warn "did not get response body from refresh token response" - } - atomicState.action = "" +def resumeProgram(child, deviceId) { + + + def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{"type": "resumeProgram"}]}' + def result = sendJson(jsonRequestBody) + return result } -/** - * Executes the resume program command on the Ecobee thermostat - * @param deviceId - the ID of the device - * - * @retrun true if the command was successful, false otherwise. - */ -boolean resumeProgram(deviceId) { - def payload = [ - selection: [ - selectionType: "thermostats", - selectionMatch: deviceId, - includeRuntime: true - ], - functions: [ - [ - type: "resumeProgram" - ] - ] - ] - return sendCommandToEcobee(payload) +def setHold(child, heating, cooling, deviceId, sendHoldType) { + + int h = heating * 10 + int c = cooling * 10 + def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{ "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": '+sendHoldType+' } } ]}' + + def result = sendJson(child, jsonRequestBody) + return result } -/** - * Executes the set hold command on the Ecobee thermostat - * @param heating - The heating temperature to set in fahrenheit - * @param cooling - the cooling temperature to set in fahrenheit - * @param deviceId - the ID of the device - * @param sendHoldType - the hold type to execute - * - * @return true if the command was successful, false otherwise - */ -boolean setHold(heating, cooling, deviceId, sendHoldType) { - // Ecobee requires that temp values be in fahrenheit multiplied by 10. - int h = heating * 10 - int c = cooling * 10 +def setFanMode(child, heating, cooling, deviceId, sendHoldType, fanMode) { - def payload = [ - selection: [ - selectionType: "thermostats", - selectionMatch: deviceId, - includeRuntime: true - ], - functions: [ - [ - type: "setHold", - params: [ - coolHoldTemp: c, - heatHoldTemp: h, - holdType: sendHoldType - ] - ] - ] - ] + int h = heating * 10 + int c = cooling * 10 - return sendCommandToEcobee(payload) + + def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{ "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": '+sendHoldType+', "fan": '+fanMode+' } } ]}' + def result = sendJson(child, jsonRequestBody) + return result } -/** - * Executes the set fan mode command on the Ecobee thermostat - * @param heating - The heating temperature to set in fahrenheit - * @param cooling - the cooling temperature to set in fahrenheit - * @param deviceId - the ID of the device - * @param sendHoldType - the hold type to execute - * @param fanMode - the fan mode to set to - * - * @return true if the command was successful, false otherwise - */ -boolean setFanMode(heating, cooling, deviceId, sendHoldType, fanMode) { - // Ecobee requires that temp values be in fahrenheit multiplied by 10. - int h = heating * 10 - int c = cooling * 10 +def setMode(child, mode, deviceId) { + def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"thermostat": {"settings":{"hvacMode":"'+"${mode}"+'"}}}' - def payload = [ - selection: [ - selectionType: "thermostats", - selectionMatch: deviceId, - includeRuntime: true - ], - functions: [ - [ - type: "setHold", - params: [ - coolHoldTemp: c, - heatHoldTemp: h, - holdType: sendHoldType, - fan: fanMode - ] - ] - ] - ] - - return sendCommandToEcobee(payload) + def result = sendJson(jsonRequestBody) + return result } -/** - * Sets the mode of the Ecobee thermostat - * @param mode - the mode to set to - * @param deviceId - the ID of the device - * - * @return true if the command was successful, false otherwise - */ -boolean setMode(mode, deviceId) { - def payload = [ - selection: [ - selectionType: "thermostats", - selectionMatch: deviceId, - includeRuntime: true - ], - thermostat: [ - settings: [ - hvacMode: mode - ] - ] - ] - return sendCommandToEcobee(payload) -} +def sendJson(child = null, String jsonBody) { -/** - * Makes a request to the Ecobee API to actuate the thermostat. - * Used by command methods to send commands to Ecobee. - * - * @param bodyParams - a map of request parameters to send to Ecobee. - * - * @return true if the command was accepted by Ecobee without error, false otherwise. - */ -private boolean sendCommandToEcobee(Map bodyParams) { - def isSuccess = false + def returnStatus = false def cmdParams = [ - uri: apiEndpoint, - path: "/1/thermostat", - headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"], - body: toJson(bodyParams) + uri: apiEndpoint, + path: "/1/thermostat", + headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"], + body: jsonBody ] try{ - httpPost(cmdParams) { resp -> - if(resp.status == 200) { - log.debug "updated ${resp.data}" - def returnStatus = resp.data.status.code - if (returnStatus == 0) { - log.debug "Successful call to ecobee API." - isSuccess = true - } else { - log.debug "Error return code = ${returnStatus}" - debugEvent("Error return code = ${returnStatus}") - } - } - } + httpPost(cmdParams) { resp -> + + if(resp.status == 200) { + + log.debug "updated ${resp.data}" + returnStatus = resp.data.status.code + if (resp.data.status.code == 0) + log.debug "Successful call to ecobee API." + else { + log.debug "Error return code = ${resp.data.status.code}" + debugEvent("Error return code = ${resp.data.status.code}") + } + } + } } catch (groovyx.net.http.HttpResponseException e) { log.trace "Exception Sending Json: " + e.response.data.status debugEvent ("sent Json & got http status ${e.statusCode} - ${e.response.data.status.code}") if (e.response.data.status.code == 14) { - // TODO - figure out why we're setting the next action to be pollChildren - // after refreshing auth token. Is it to keep UI in sync, or just copy/paste error? atomicState.action = "pollChildren" log.debug "Refreshing your auth_token!" refreshAuthToken() - } else { + } + else { debugEvent("Authentication error, invalid authentication method, lack of credentials, etc.") log.error "Authentication error, invalid authentication method, lack of credentials, etc." } } - return isSuccess + if (returnStatus == 0) + return true + else + return false } -def getChildName() { return "Ecobee Thermostat" } -def getSensorChildName() { return "Ecobee Sensor" } +def getChildName() { "Ecobee Thermostat" } +def getSensorChildName() { "Ecobee Sensor" } def getServerUrl() { return "https://graph.api.smartthings.com" } def getShardUrl() { return getApiServerUrl() } -def getCallbackUrl() { return "https://graph.api.smartthings.com/oauth/callback" } -def getBuildRedirectUrl() { return "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}&apiServerUrl=${shardUrl}" } -def getApiEndpoint() { return "https://api.ecobee.com" } -def getSmartThingsClientId() { return appSettings.clientId } +def getCallbackUrl() { "https://graph.api.smartthings.com/oauth/callback" } +def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}&apiServerUrl=${shardUrl}" } +def getApiEndpoint() { "https://api.ecobee.com" } +def getSmartThingsClientId() { appSettings.clientId } def debugEvent(message, displayEvent = false) { + def results = [ - name: "appdebug", - descriptionText: message, - displayed: displayEvent + name: "appdebug", + descriptionText: message, + displayed: displayEvent ] log.debug "Generating AppDebug Event: ${results}" sendEvent (results) + +} + +def debugEventFromParent(child, message) { + if (child != null) { child.sendEvent("name":"debugEventFromParent", "value":message, "description":message, displayed: true, isStateChange: true)} } //send both push notification and mobile activity feeds -def sendPushAndFeeds(notificationMessage) { +def sendPushAndFeeds(notificationMessage){ log.warn "sendPushAndFeeds >> notificationMessage: ${notificationMessage}" log.warn "sendPushAndFeeds >> atomicState.timeSendPush: ${atomicState.timeSendPush}" - if (atomicState.timeSendPush) { - if (now() - atomicState.timeSendPush > 86400000) { // notification is sent to remind user once a day + if (atomicState.timeSendPush){ + if (now() - atomicState.timeSendPush > 86400000){ // notification is sent to remind user once a day sendPush("Your Ecobee thermostat " + notificationMessage) sendActivityFeeds(notificationMessage) atomicState.timeSendPush = now() @@ -822,58 +786,6 @@ def sendPushAndFeeds(notificationMessage) { atomicState.authToken = null } -/** - * Stores data about the thermostats in atomicState. - * @param thermostats - a list of thermostats as returned from the Ecobee API - */ -private void storeThermostatData(thermostats) { - log.trace "Storing thermostat data: $thermostats" - def data - atomicState.thermostats = thermostats.inject([:]) { collector, stat -> - def dni = [ app.id, stat.identifier ].join('.') - log.debug "updating dni $dni" - - data = [ - coolMode: (stat.settings.coolStages > 0), - heatMode: (stat.settings.heatStages > 0), - deviceTemperatureUnit: stat.settings.useCelsius, - minHeatingSetpoint: (stat.settings.heatRangeLow / 10), - maxHeatingSetpoint: (stat.settings.heatRangeHigh / 10), - minCoolingSetpoint: (stat.settings.coolRangeLow / 10), - maxCoolingSetpoint: (stat.settings.coolRangeHigh / 10), - autoMode: stat.settings.autoHeatCoolFeatureEnabled, - auxHeatMode: (stat.settings.hasHeatPump) && (stat.settings.hasForcedAir || stat.settings.hasElectric || stat.settings.hasBoiler), - temperature: (stat.runtime.actualTemperature / 10), - heatingSetpoint: stat.runtime.desiredHeat / 10, - coolingSetpoint: stat.runtime.desiredCool / 10, - thermostatMode: stat.settings.hvacMode, - humidity: stat.runtime.actualHumidity, - thermostatFanMode: stat.runtime.desiredFanMode - ] - if (location.temperatureScale == "F") { - data["temperature"] = data["temperature"] ? Math.round(data["temperature"].toDouble()) : data["temperature"] - data["heatingSetpoint"] = data["heatingSetpoint"] ? Math.round(data["heatingSetpoint"].toDouble()) : data["heatingSetpoint"] - data["coolingSetpoint"] = data["coolingSetpoint"] ? Math.round(data["coolingSetpoint"].toDouble()) : data["coolingSetpoint"] - data["minHeatingSetpoint"] = data["minHeatingSetpoint"] ? Math.round(data["minHeatingSetpoint"].toDouble()) : data["minHeatingSetpoint"] - data["maxHeatingSetpoint"] = data["maxHeatingSetpoint"] ? Math.round(data["maxHeatingSetpoint"].toDouble()) : data["maxHeatingSetpoint"] - data["minCoolingSetpoint"] = data["minCoolingSetpoint"] ? Math.round(data["minCoolingSetpoint"].toDouble()) : data["minCoolingSetpoint"] - data["maxCoolingSetpoint"] = data["maxCoolingSetpoint"] ? Math.round(data["maxCoolingSetpoint"].toDouble()) : data["maxCoolingSetpoint"] - - } - - if (data?.deviceTemperatureUnit == false && location.temperatureScale == "F") { - data["deviceTemperatureUnit"] = "F" - - } else { - data["deviceTemperatureUnit"] = "C" - } - - collector[dni] = [data:data] - return collector - } - log.debug "updated ${atomicState.thermostats?.size()} thermostats: ${atomicState.thermostats}" -} - def sendActivityFeeds(notificationMessage) { def devices = getChildDevices() devices.each { child -> @@ -881,6 +793,14 @@ def sendActivityFeeds(notificationMessage) { } } +def roundC (tempC) { + return String.format("%.1f", (Math.round(tempC * 2))/2) +} + def convertFtoC (tempF) { return String.format("%.1f", (Math.round(((tempF - 32)*(5/9)) * 2))/2) } + +def convertCtoF (tempC) { + return (Math.round(tempC * (9/5)) + 32).toInteger() +}