diff --git a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy
index 7b08b14..5021089 100644
--- a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy
+++ b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy
@@ -234,7 +234,7 @@ 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(this, heatingValue, coolingValue, deviceId, sendHoldType)) {
+ if (parent.setHold(heatingValue, coolingValue, deviceId, sendHoldType)) {
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
log.debug "Done setHeatingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}"
@@ -271,7 +271,7 @@ 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(this, heatingValue, coolingValue, deviceId, sendHoldType)) {
+ if (parent.setHold(heatingValue, coolingValue, deviceId, sendHoldType)) {
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
log.debug "Done setCoolingSetpoint>> coolingSetpoint = ${coolingSetpoint}, heatingSetpoint = ${heatingSetpoint}"
@@ -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(this, deviceId)) {
+ if (parent.resumeProgram(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(this, deviceId)"
+ log.error "Error resumeProgram() check parent.resumeProgram(deviceId)"
}
}
@@ -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(this, heatingValue, coolingValue, deviceId, sendHoldType, fanMode)) {
+ if (parent.setFanMode(heatingValue, coolingValue, deviceId, sendHoldType, fanMode)) {
generateFanModeEvent(fanMode)
} else {
log.debug "Error setting new mode."
@@ -690,7 +690,7 @@ void alterSetpoint(temp) {
def coolingValue = location.temperatureScale == "C"? convertCtoF(targetCoolingSetpoint) : targetCoolingSetpoint
def heatingValue = location.temperatureScale == "C"? convertCtoF(targetHeatingSetpoint) : targetHeatingSetpoint
- if (parent.setHold(this, heatingValue, coolingValue, deviceId, sendHoldType)) {
+ if (parent.setHold(heatingValue, coolingValue, deviceId, sendHoldType)) {
sendEvent("name": "thermostatSetpoint", "value": temp.value, displayed: false)
sendEvent("name": "heatingSetpoint", "value": targetHeatingSetpoint)
sendEvent("name": "coolingSetpoint", "value": targetCoolingSetpoint)
diff --git a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy
index 848b7b7..c50651e 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,13 +115,12 @@ 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)}"
@@ -148,8 +147,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)
}
@@ -171,64 +170,63 @@ def connectionStatus(message, redirectUrl = null) {
}
def html = """
-
-
-
-
-Ecobee & SmartThings connection
-
-
-
-
+
+
+
+
+
Ecobee & SmartThings connection
+
+
+
+
-
-
-"""
+
+
+
+ """
render contentType: 'text/html', data: html
}
@@ -237,19 +235,26 @@ def getEcobeeThermostats() {
log.debug "getting device list"
atomicState.remoteSensors = []
- def requestBody = '{"selection":{"selectionType":"registered","selectionMatch":"","includeRuntime":true,"includeSensors":true}}'
-
+ def bodyParams = [
+ selection: [
+ selectionType: "registered",
+ selectionMatch: "",
+ includeRuntime: true,
+ includeSensors: true
+ ]
+ ]
def deviceListParams = [
- uri: apiEndpoint,
- path: "/1/thermostat",
- headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"],
- query: [format: 'json', body: requestBody]
+ 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)]
]
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
@@ -289,8 +294,9 @@ Map sensorsDiscovered() {
}
def getThermostatDisplayName(stat) {
- if(stat?.name)
- return stat.name.toString()
+ if(stat?.name) {
+ return stat.name.toString()
+ }
return (getThermostatTypeName(stat) + " (${stat.identifier})").toString()
}
@@ -310,7 +316,6 @@ def updated() {
}
def initialize() {
-
log.debug "initialize"
def devices = thermostats.collect { dni ->
def d = getChildDevice(dni)
@@ -350,8 +355,6 @@ 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)
@@ -383,16 +386,27 @@ def pollHandler() {
def pollChildren(child = null) {
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 requestBody = [
+ 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}"],
- query: [format: 'json', body: jsonRequestBody]
+ 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)]
]
try{
@@ -400,53 +414,8 @@ def pollChildren(child = null) {
if(resp.status == 200) {
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
- }
+ storeThermostatData(resp.data.thermostatList)
result = true
log.debug "updated ${atomicState.thermostats?.size()} stats: ${atomicState.thermostats}"
}
@@ -463,13 +432,12 @@ 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}"
@@ -492,36 +460,7 @@ 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)
- {
- 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")
-
- modes
-
-}
-
-def currentMode(child) {
- debugEvent ("atomicState.Thermos = ${atomicState.thermostats}")
-
debugEvent ("Child DNI = ${child.device.deviceNetworkId}")
def tData = atomicState.thermostats[child.device.deviceNetworkId]
@@ -530,14 +469,42 @@ def currentMode(child) {
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")
+ }
+
+ return modes
+}
+
+def currentMode(child) {
+ debugEvent ("atomicState.Thermos = ${atomicState.thermostats}")
+ debugEvent ("Child DNI = ${child.device.deviceNetworkId}")
+
+ def tData = atomicState.thermostats[child.device.deviceNetworkId]
+
+ debugEvent("Data = ${tData}")
+
+ if(!tData) {
+ log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling"
return null
}
def mode = tData.data.thermostatMode
-
- mode
+ return mode
}
def updateSensorData() {
@@ -558,12 +525,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
@@ -582,7 +549,7 @@ def getChildDeviceIdsString() {
}
def toJson(Map m) {
- return new org.json.JSONObject(m).toString()
+ return groovy.json.JsonOutput.toJson(m)
}
def toQueryString(Map m) {
@@ -595,54 +562,26 @@ 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
+ log.debug "making refresh request with paramaters: $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!")
-
- 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 = ""
- }
- }
+ saveTokenAndResumeAction(resp.data)
+ }
+ }
} catch (groovyx.net.http.HttpResponseException e) {
log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}"
def reAttemptPeriod = 300 // in sec
@@ -662,64 +601,174 @@ private refreshAuthToken() {
}
}
-def resumeProgram(child, deviceId) {
-
-
- def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{"type": "resumeProgram"}]}'
- def result = sendJson(jsonRequestBody)
- return result
+/**
+ * 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 "ASYNC refreshTokenHandler: 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 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 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 setFanMode(child, heating, cooling, deviceId, sendHoldType, fanMode) {
+/**
+ * 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
- int h = heating * 10
- int c = cooling * 10
+ def payload = [
+ selection: [
+ selectionType: "thermostats",
+ selectionMatch: deviceId,
+ includeRuntime: true
+ ],
+ functions: [
+ [
+ type: "setHold",
+ params: [
+ coolHoldTemp: c,
+ heatHoldTemp: h,
+ holdType: sendHoldType
+ ]
+ ]
+ ]
+ ]
-
- 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
+ return sendCommandToEcobee(payload)
}
-def setMode(child, mode, deviceId) {
- def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"thermostat": {"settings":{"hvacMode":"'+"${mode}"+'"}}}'
+/**
+ * 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 result = sendJson(jsonRequestBody)
- return result
+ 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 sendJson(child = null, String jsonBody) {
+/**
+ * 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 returnStatus = false
+/**
+ * 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 cmdParams = [
- uri: apiEndpoint,
- path: "/1/thermostat",
- headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
- body: jsonBody
+ uri: apiEndpoint,
+ path: "/1/thermostat",
+ headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
+ body: toJson(bodyParams)
]
try{
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}")
+ 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}")
}
}
}
@@ -727,53 +776,45 @@ def sendJson(child = null, String jsonBody) {
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."
}
}
- if (returnStatus == 0)
- return true
- else
- return false
+ return isSuccess
}
-def getChildName() { "Ecobee Thermostat" }
-def getSensorChildName() { "Ecobee Sensor" }
+def getChildName() { return "Ecobee Thermostat" }
+def getSensorChildName() { return "Ecobee Sensor" }
def getServerUrl() { return "https://graph.api.smartthings.com" }
def getShardUrl() { return getApiServerUrl() }
-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 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 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()
@@ -786,6 +827,58 @@ 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 ->
@@ -793,14 +886,6 @@ 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()
-}