diff --git a/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy b/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy index e984bb6..2753e9b 100644 --- a/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy +++ b/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy @@ -22,10 +22,6 @@ metadata { capability "Polling" } - simulator { - // TODO: define status and reply messages here - } - tiles { valueTile("temperature", "device.temperature", width: 2, height: 2) { state("temperature", label:'${currentValue}°', unit:"F", @@ -56,16 +52,12 @@ metadata { } def refresh() { - log.debug "refresh..." + log.debug "refresh called" poll() } void poll() { log.debug "Executing 'poll' using parent SmartApp" - parent.pollChildren(this) -} + parent.pollChild(this) -//generate custom mobile activity feeds event -def generateActivityFeedsEvent(notificationMessage) { - sendEvent(name: "notificationMessage", value: "$device.displayName $notificationMessage", descriptionText: "$device.displayName $notificationMessage", displayed: true) } diff --git a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy index b439a63..7c718b3 100644 --- a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy +++ b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy @@ -19,34 +19,39 @@ metadata { definition (name: "Ecobee Thermostat", namespace: "smartthings", author: "SmartThings") { capability "Actuator" capability "Thermostat" + capability "Temperature Measurement" capability "Polling" capability "Sensor" - capability "Refresh" + capability "Refresh" + capability "Relative Humidity Measurement" - command "generateEvent" - command "raiseSetpoint" - command "lowerSetpoint" - command "resumeProgram" - command "switchMode" + command "generateEvent" + command "raiseSetpoint" + command "lowerSetpoint" + command "resumeProgram" + command "switchMode" - attribute "thermostatSetpoint","number" - attribute "thermostatStatus","string" + attribute "thermostatSetpoint","number" + attribute "thermostatStatus","string" + attribute "maxHeatingSetpoint", "number" + attribute "minHeatingSetpoint", "number" + attribute "maxCoolingSetpoint", "number" + attribute "minCoolingSetpoint", "number" + attribute "deviceTemperatureUnit", "number" } - simulator { } - - tiles { + tiles { valueTile("temperature", "device.temperature", width: 2, height: 2) { state("temperature", label:'${currentValue}°', unit:"F", - backgroundColors:[ - [value: 31, color: "#153591"], - [value: 44, color: "#1e9cbb"], - [value: 59, color: "#90d2a7"], - [value: 74, color: "#44b621"], - [value: 84, color: "#f1d801"], - [value: 95, color: "#d04e00"], - [value: 96, color: "#bc2323"] - ] + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] ) } standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") { @@ -94,8 +99,11 @@ metadata { state "resume", action:"resumeProgram", nextState: "updating", label:'Resume Schedule', icon:"st.samsung.da.oven_ic_send" state "updating", label:"Working", icon: "st.secondary.secondary" } + valueTile("humidity", "device.humidity", decoration: "flat") { + state "humidity", label:'${currentValue}% humidity' + } main "temperature" - details(["temperature", "upButtonControl", "thermostatSetpoint", "currentStatus", "downButtonControl", "mode", "resumeProgram", "refresh"]) + details(["temperature", "upButtonControl", "thermostatSetpoint", "currentStatus", "downButtonControl", "mode", "resumeProgram", "refresh", "humidity"]) } preferences { @@ -107,8 +115,6 @@ metadata { // parse events into attributes def parse(String description) { log.debug "Parsing '${description}'" - // TODO: handle '' attribute - } def refresh() { @@ -133,16 +139,24 @@ def generateEvent(Map results) { def isChange = false def isDisplayed = true def event = [name: name, linkText: linkText, descriptionText: getThermostatDescriptionText(name, value, linkText), - handlerName: name] + handlerName: name] - if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint") { - def sendValue = value? convertTemperatureIfNeeded(value.toDouble(), "F", 1): value //API return temperature value in F + if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint" ) { + def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F + sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue isChange = isTemperatureStateChange(device, name, value.toString()) isDisplayed = isChange event << [value: sendValue, isStateChange: isChange, displayed: isDisplayed] - } else if (name=="heatMode" || name=="coolMode" || name=="autoMode" || name=="auxHeatMode"){ + } 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, 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] + } else if (name=="humidity") { + isChange = isStateChange(device, name, value.toString()) + event << [value: value.toString(), isStateChange: isChange, displayed: false, unit: "%"] } else { isChange = isStateChange(device, name, value.toString()) isDisplayed = isChange @@ -158,13 +172,19 @@ def generateEvent(Map results) { //return descriptionText to be shown on mobile activity feed private getThermostatDescriptionText(name, value, linkText) { if(name == "temperature") { - return "$linkText temperature is $value°F" + def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F + sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue + return "$linkText temperature is $sendValue ${location.temperatureScale}" } else if(name == "heatingSetpoint") { - return "heating setpoint is $value°F" + def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F + sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue + return "heating setpoint is $sendValue ${location.temperatureScale}" } else if(name == "coolingSetpoint"){ - return "cooling setpoint is $value°F" + def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F + sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue + return "cooling setpoint is $sendValue ${location.temperatureScale}" } else if (name == "thermostatMode") { return "thermostat mode is ${value}" @@ -172,26 +192,26 @@ private getThermostatDescriptionText(name, value, linkText) { } else if (name == "thermostatFanMode") { return "thermostat fan mode is ${value}" + } else if (name == "humidity") { + return "humidity is ${value} %" } else { return "${name} = ${value}" } } void setHeatingSetpoint(setpoint) { - setHeatingSetpoint(setpoint.toDouble()) -} - -void setHeatingSetpoint(Double setpoint) { -// def mode = device.currentValue("thermostatMode") - def heatingSetpoint = setpoint + log.debug "***heating setpoint $setpoint" + def heatingSetpoint = setpoint.toDouble() def coolingSetpoint = device.currentValue("coolingSetpoint").toDouble() def deviceId = device.deviceNetworkId.split(/\./).last() + def maxHeatingSetpoint = device.currentValue("maxHeatingSetpoint").toDouble() + def minHeatingSetpoint = device.currentValue("minHeatingSetpoint").toDouble() //enforce limits of heatingSetpoint - if (heatingSetpoint > 79) { - heatingSetpoint = 79 - } else if (heatingSetpoint < 45) { - heatingSetpoint = 45 + if (heatingSetpoint > maxHeatingSetpoint) { + heatingSetpoint = maxHeatingSetpoint + } else if (heatingSetpoint < minHeatingSetpoint) { + heatingSetpoint = minHeatingSetpoint } //enforce limits of heatingSetpoint vs coolingSetpoint @@ -201,32 +221,34 @@ void setHeatingSetpoint(Double setpoint) { log.debug "Sending setHeatingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}" + def coolingValue = location.temperatureScale == "C"? convertCtoF(coolingSetpoint) : coolingSetpoint + def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint + def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite" - if (parent.setHold (this, heatingSetpoint, coolingSetpoint, deviceId, sendHoldType)) { + 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() } else { - log.error "Error setHeatingSetpoint(setpoint)" //This error is handled by the connect app + log.error "Error setHeatingSetpoint(setpoint)" } } void setCoolingSetpoint(setpoint) { - setCoolingSetpoint(setpoint.toDouble()) -} - -void setCoolingSetpoint(Double setpoint) { -// def mode = device.currentValue("thermostatMode") + log.debug "***cooling setpoint $setpoint" def heatingSetpoint = device.currentValue("heatingSetpoint").toDouble() - def coolingSetpoint = setpoint + def coolingSetpoint = setpoint.toDouble() def deviceId = device.deviceNetworkId.split(/\./).last() + def maxCoolingSetpoint = device.currentValue("maxCoolingSetpoint").toDouble() + def minCoolingSetpoint = device.currentValue("minCoolingSetpoint").toDouble() - if (coolingSetpoint > 92) { - coolingSetpoint = 92 - } else if (coolingSetpoint < 65) { - coolingSetpoint = 65 + + if (coolingSetpoint > maxCoolingSetpoint) { + coolingSetpoint = maxCoolingSetpoint + } else if (coolingSetpoint < minCoolingSetpoint) { + coolingSetpoint = minCoolingSetpoint } //enforce limits of heatingSetpoint vs coolingSetpoint @@ -236,15 +258,18 @@ void setCoolingSetpoint(Double setpoint) { log.debug "Sending setCoolingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}" + def coolingValue = location.temperatureScale == "C"? convertCtoF(coolingSetpoint) : coolingSetpoint + def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint + def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite" - if (parent.setHold (this, heatingSetpoint, coolingSetpoint, deviceId, sendHoldType)) { + 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() } else { - log.error "Error setCoolingSetpoint(setpoint)" //This error is handled by the connect app + log.error "Error setCoolingSetpoint(setpoint)" } } @@ -448,25 +473,21 @@ def auto() { def fanOn() { log.debug "fanOn" // parent.setFanMode (this,"on") - } def fanAuto() { log.debug "fanAuto" // parent.setFanMode (this,"auto") - } def fanCirculate() { log.debug "fanCirculate" // parent.setFanMode (this,"circulate") - } def fanOff() { log.debug "fanOff" // parent.setFanMode (this,"off") - } def generateSetpointEvent() { @@ -476,20 +497,41 @@ def generateSetpointEvent() { def mode = device.currentValue("thermostatMode") log.debug "Current Mode = ${mode}" - def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() + def heatingSetpoint = device.currentValue("heatingSetpoint") log.debug "Heating Setpoint = ${heatingSetpoint}" - def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() + def coolingSetpoint = device.currentValue("coolingSetpoint") log.debug "Cooling Setpoint = ${coolingSetpoint}" + def maxHeatingSetpoint = device.currentValue("maxHeatingSetpoint") + def maxCoolingSetpoint = device.currentValue("maxCoolingSetpoint") + def minHeatingSetpoint = device.currentValue("minHeatingSetpoint") + def minCoolingSetpoint = device.currentValue("minCoolingSetpoint") + + if(location.temperatureScale == "C") + { + maxHeatingSetpoint = roundC(maxHeatingSetpoint) + maxCoolingSetpoint = roundC(maxCoolingSetpoint) + minHeatingSetpoint = roundC(minHeatingSetpoint) + minCoolingSetpoint = roundC(minCoolingSetpoint) + heatingSetpoint = roundC(heatingSetpoint) + coolingSetpoint = roundC(coolingSetpoint) + } + + sendEvent("name":"maxHeatingSetpoint", "value":maxHeatingSetpoint, "unit":location.temperatureScale) + sendEvent("name":"maxCoolingSetpoint", "value":maxCoolingSetpoint, "unit":location.temperatureScale) + sendEvent("name":"minHeatingSetpoint", "value":minHeatingSetpoint, "unit":location.temperatureScale) + sendEvent("name":"minCoolingSetpoint", "value":minCoolingSetpoint, "unit":location.temperatureScale) + + if (mode == "heat") { - sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()) + sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint ) } else if (mode == "cool") { - sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()) + sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint) } else if (mode == "auto") { @@ -499,9 +541,9 @@ def generateSetpointEvent() { sendEvent("name":"thermostatSetpoint", "value":"Off") - } else if (mode == "emergencyHeat") { + } else if (mode == "auxHeatOnly") { - sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()) + sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint) } @@ -510,26 +552,30 @@ def generateSetpointEvent() { void raiseSetpoint() { def mode = device.currentValue("thermostatMode") def targetvalue + def maxHeatingSetpoint = device.currentValue("maxHeatingSetpoint") + def maxCoolingSetpoint = device.currentValue("maxCoolingSetpoint") + if (mode == "off" || mode == "auto") { log.warn "this mode: $mode does not allow raiseSetpoint" } else { - def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() - def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() - def thermostatSetpoint = device.currentValue("thermostatSetpoint").toInteger() + def heatingSetpoint = device.currentValue("heatingSetpoint") + def coolingSetpoint = device.currentValue("coolingSetpoint") + def thermostatSetpoint = device.currentValue("thermostatSetpoint") log.debug "raiseSetpoint() mode = ${mode}, heatingSetpoint: ${heatingSetpoint}, coolingSetpoint:${coolingSetpoint}, thermostatSetpoint:${thermostatSetpoint}" if (device.latestState('thermostatSetpoint')) { - targetvalue = device.latestState('thermostatSetpoint').value as Integer + targetvalue = device.latestState('thermostatSetpoint').value + targetvalue = location.temperatureScale == "F"? targetvalue.toInteger() : targetvalue.toDouble() } else { targetvalue = 0 } - targetvalue = targetvalue + 1 + targetvalue = location.temperatureScale == "F"? targetvalue + 1 : targetvalue + 0.5 - if (mode == "heat" && targetvalue > 79) { - targetvalue = 79 - } else if (mode == "cool" && targetvalue > 92) { - targetvalue = 92 + if ((mode == "heat" || mode == "auxHeatOnly") && targetvalue > maxHeatingSetpoint) { + targetvalue = maxHeatingSetpoint + } else if (mode == "cool" && targetvalue > maxCoolingSetpoint) { + targetvalue = maxCoolingSetpoint } sendEvent("name":"thermostatSetpoint", "value":targetvalue, displayed: false) @@ -543,25 +589,29 @@ void raiseSetpoint() { void lowerSetpoint() { def mode = device.currentValue("thermostatMode") def targetvalue + def minHeatingSetpoint = device.currentValue("minHeatingSetpoint") + def minCoolingSetpoint = device.currentValue("minCoolingSetpoint") + if (mode == "off" || mode == "auto") { log.warn "this mode: $mode does not allow lowerSetpoint" } else { - def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() - def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() - def thermostatSetpoint = device.currentValue("thermostatSetpoint").toInteger() + def heatingSetpoint = device.currentValue("heatingSetpoint") + def coolingSetpoint = device.currentValue("coolingSetpoint") + def thermostatSetpoint = device.currentValue("thermostatSetpoint") log.debug "lowerSetpoint() mode = ${mode}, heatingSetpoint: ${heatingSetpoint}, coolingSetpoint:${coolingSetpoint}, thermostatSetpoint:${thermostatSetpoint}" if (device.latestState('thermostatSetpoint')) { - targetvalue = device.latestState('thermostatSetpoint').value as Integer + targetvalue = device.latestState('thermostatSetpoint').value + targetvalue = location.temperatureScale == "F"? targetvalue.toInteger() : targetvalue.toDouble() } else { targetvalue = 0 } - targetvalue = targetvalue - 1 + targetvalue = location.temperatureScale == "F"? targetvalue - 1 : targetvalue - 0.5 - if (mode == "heat" && targetvalue.toInteger() < 45) { - targetvalue = 45 - } else if (mode == "cool" && targetvalue.toInteger() < 65) { - targetvalue = 65 + if ((mode == "heat" || mode == "auxHeatOnly") && targetvalue < minHeatingSetpoint) { + targetvalue = minHeatingSetpoint + } else if (mode == "cool" && targetvalue < minCoolingSetpoint) { + targetvalue = minCoolingSetpoint } sendEvent("name":"thermostatSetpoint", "value":targetvalue, displayed: false) @@ -575,15 +625,15 @@ void lowerSetpoint() { void alterSetpoint(temp) { def mode = device.currentValue("thermostatMode") - def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() - def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() + def heatingSetpoint = device.currentValue("heatingSetpoint") + def coolingSetpoint = device.currentValue("coolingSetpoint") def deviceId = device.deviceNetworkId.split(/\./).last() def targetHeatingSetpoint def targetCoolingSetpoint //step1: check thermostatMode, enforce limits before sending request to cloud - if (mode == "heat"){ + if (mode == "heat" || mode == "auxHeatOnly"){ if (temp.value > coolingSetpoint){ targetHeatingSetpoint = temp.value targetCoolingSetpoint = temp.value @@ -602,19 +652,22 @@ void alterSetpoint(temp) { } } - log.debug "alterSetpoint >> in mode ${mode} trying to change heatingSetpoint to ${targetHeatingSetpoint} " + - "coolingSetpoint to ${targetCoolingSetpoint} with holdType : ${holdType}" + log.debug "alterSetpoint >> in mode ${mode} trying to change heatingSetpoint to $targetHeatingSetpoint " + + "coolingSetpoint to $targetCoolingSetpoint with holdType : ${holdType}" def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite" - //step2: call parent.setHold to send http request to 3rd party cloud - if (parent.setHold(this, targetHeatingSetpoint, targetCoolingSetpoint, deviceId, sendHoldType)) { - sendEvent("name": "thermostatSetpoint", "value": temp.value.toString(), displayed: false) + + def coolingValue = location.temperatureScale == "C"? convertCtoF(targetCoolingSetpoint) : targetCoolingSetpoint + def heatingValue = location.temperatureScale == "C"? convertCtoF(targetHeatingSetpoint) : targetHeatingSetpoint + + if (parent.setHold(this, heatingValue, coolingValue, deviceId, sendHoldType)) { + sendEvent("name": "thermostatSetpoint", "value": temp.value, displayed: false) 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()" - if (mode == "heat"){ + if (mode == "heat" || mode == "auxHeatOnly"){ sendEvent("name": "thermostatSetpoint", "value": heatingSetpoint.toString(), displayed: false) } else if (mode == "cool") { sendEvent("name": "thermostatSetpoint", "value": coolingSetpoint.toString(), displayed: false) @@ -626,9 +679,9 @@ void alterSetpoint(temp) { def generateStatusEvent() { def mode = device.currentValue("thermostatMode") - def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() - def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() - def temperature = device.currentValue("temperature").toInteger() + def heatingSetpoint = device.currentValue("heatingSetpoint") + def coolingSetpoint = device.currentValue("coolingSetpoint") + def temperature = device.currentValue("temperature") def statusText @@ -643,14 +696,14 @@ def generateStatusEvent() { if (temperature >= heatingSetpoint) statusText = "Right Now: Idle" else - statusText = "Heating to ${heatingSetpoint}° F" + statusText = "Heating to ${heatingSetpoint} ${location.temperatureScale}" } else if (mode == "cool") { if (temperature <= coolingSetpoint) statusText = "Right Now: Idle" else - statusText = "Cooling to ${coolingSetpoint}° F" + statusText = "Cooling to ${coolingSetpoint} ${location.temperatureScale}" } else if (mode == "auto") { @@ -660,7 +713,7 @@ def generateStatusEvent() { statusText = "Right Now: Off" - } else if (mode == "emergencyHeat") { + } else if (mode == "auxHeatOnly") { statusText = "Emergency Heat" @@ -673,7 +726,18 @@ def generateStatusEvent() { sendEvent("name":"thermostatStatus", "value":statusText, "description":statusText, displayed: true) } -//generate custom mobile activity feeds event def generateActivityFeedsEvent(notificationMessage) { sendEvent(name: "notificationMessage", value: "$device.displayName $notificationMessage", descriptionText: "$device.displayName $notificationMessage", displayed: true) } + +def roundC (tempC) { + return (Math.round(tempC.toDouble() * 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() +} diff --git a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy index 474a4f2..11d6f55 100644 --- a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy +++ b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy @@ -28,7 +28,7 @@ definition( category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee@2x.png", - singleInstance: true + singleInstance: true ) { appSetting "clientId" } @@ -61,7 +61,7 @@ def authPage() { description = "Click to enter Ecobee Credentials" } - def redirectUrl = buildRedirectUrl //"${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}" + def redirectUrl = buildRedirectUrl log.debug "RedirectUrl = ${redirectUrl}" // get rid of next button until the user is actually auth'd if (!oauthTokenProvided) { @@ -103,7 +103,7 @@ def oauthInitUrl() { scope: "smartRead,smartWrite", client_id: smartThingsClientId, state: atomicState.oauthInitState, - redirect_uri: callbackUrl //"https://graph.api.smartthings.com/oauth/callback" + redirect_uri: callbackUrl ] redirect(location: "${apiEndpoint}/authorize?${toQueryString(oauthParams)}") @@ -115,14 +115,13 @@ def callback() { def code = params.code def oauthState = params.state - //verify oauthState == atomicState.oauthInitState, so the callback corresponds to the authentication request if (oauthState == atomicState.oauthInitState){ def tokenParams = [ grant_type: "authorization_code", code : code, client_id : smartThingsClientId, - redirect_uri: callbackUrl //"https://graph.api.smartthings.com/oauth/callback" + redirect_uri: callbackUrl ] def tokenUrl = "https://www.ecobee.com/home/token?${toQueryString(tokenParams)}" @@ -247,32 +246,32 @@ def getEcobeeThermostats() { ] def stats = [:] - try { - httpGet(deviceListParams) { resp -> + try { + httpGet(deviceListParams) { resp -> - if (resp.status == 200) { - resp.data.thermostatList.each { stat -> - atomicState.remoteSensors = stat.remoteSensors - def dni = [app.id, stat.identifier].join('.') - stats[dni] = getThermostatDisplayName(stat) - } - } else { - log.debug "http status: ${resp.status}" - //refresh the auth token - if (resp.status == 500 && resp.data.status.code == 14) { - log.debug "Storing the failed action to try later" - atomicState.action = "getEcobeeThermostats" - log.debug "Refreshing your auth_token!" - refreshAuthToken() - } else { - log.error "Authentication error, invalid authentication method, lack of credentials, etc." - } - } - } - } catch(Exception e) { - log.debug "___exception getEcobeeThermostats(): " + e - refreshAuthToken() - } + if (resp.status == 200) { + resp.data.thermostatList.each { stat -> + atomicState.remoteSensors = stat.remoteSensors + def dni = [app.id, stat.identifier].join('.') + stats[dni] = getThermostatDisplayName(stat) + } + } else { + log.debug "http status: ${resp.status}" + //refresh the auth token + if (resp.data.status.code == 14) { + log.debug "Storing the failed action to try later" + atomicState.action = "getEcobeeThermostats" + log.debug "Refreshing your auth_token!" + refreshAuthToken() + } else { + log.error "Authentication error, invalid authentication method, lack of credentials, etc." + } + } + } + } catch(Exception e) { + log.debug "___exception getEcobeeThermostats(): " + e + refreshAuthToken() + } atomicState.thermostats = stats return stats } @@ -317,7 +316,7 @@ def initialize() { def devices = thermostats.collect { dni -> def d = getChildDevice(dni) if(!d) { - d = addChildDevice(app.namespace, getChildName(), dni, null, ["label":"Ecobee Thermostat:${atomicState.thermostats[dni]}"]) + d = addChildDevice(app.namespace, getChildName(), dni, null, ["label":"${atomicState.thermostats[dni]}" ?: "Ecobee Thermostat"]) log.debug "created ${d.displayName} with id $dni" } else { log.debug "found ${d.displayName} with id $dni already exists" @@ -328,7 +327,7 @@ def initialize() { def sensors = ecobeesensors.collect { dni -> def d = getChildDevice(dni) if(!d) { - d = addChildDevice(app.namespace, getSensorChildName(), dni, null, ["label":"Ecobee Sensor:${atomicState.sensors[dni]}"]) + d = addChildDevice(app.namespace, getSensorChildName(), dni, null, ["label":"${atomicState.sensors[dni]}" ?:"Ecobee Sensor"]) log.debug "created ${d.displayName} with id $dni" } else { log.debug "found ${d.displayName} with id $dni already exists" @@ -354,21 +353,17 @@ def initialize() { 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) - state.timeSendPush = null + //send activity feeds to tell that device is connected + def notificationMessage = "is connected to SmartThings" + sendActivityFeeds(notificationMessage) + atomicState.timeSendPush = null + atomicState.reAttempt = 0 pollHandler() //first time polling data data from thermostat //automatically update devices status every 5 mins runEvery5Minutes("poll") - //since access_token expires every 2 hours - runEvery1Hour("refreshAuthToken") - - atomicState.reAttempt = 0 - } def pollHandler() { @@ -389,18 +384,10 @@ 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 result = false - // // TODO: test this: - // - // def jsonRequestBody = toJson([ - // selection:[ - // selectionType: "thermostats", - // selectionMatch: getChildDeviceIdsString(), - // includeRuntime: true - // ] - // ]) def pollParams = [ uri: apiEndpoint, @@ -411,11 +398,6 @@ def pollChildren(child = null) { try{ httpGet(pollParams) { resp -> - -// if (resp.data) { -// debugEventFromParent(child, "pollChildren(child) >> resp.status = ${resp.status}, resp.data = ${resp.data}") -// } - if(resp.status == 200) { log.debug "poll results returned resp.data ${resp.data}" atomicState.remoteSensors = resp.data.thermostatList.remoteSensors @@ -426,20 +408,41 @@ def pollChildren(child = null) { log.debug "updating dni $dni" - def data = [ + 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, + temperature: (stat.runtime.actualTemperature / 10), heatingSetpoint: stat.runtime.desiredHeat / 10, coolingSetpoint: stat.runtime.desiredCool / 10, - thermostatMode: stat.settings.hvacMode + thermostatMode: stat.settings.hvacMode, + humidity: stat.runtime.actualHumidity ] - data["temperature"] = data["temperature"] ? data["temperature"].toDouble().toInteger() : data["temperature"] - data["heatingSetpoint"] = data["heatingSetpoint"] ? data["heatingSetpoint"].toDouble().toInteger() : data["heatingSetpoint"] - data["coolingSetpoint"] = data["coolingSetpoint"] ? data["coolingSetpoint"].toDouble().toInteger() : data["coolingSetpoint"] -// debugEventFromParent(child, "Event Data = ${data}") + + 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 @@ -450,9 +453,8 @@ def pollChildren(child = null) { log.error "polling children & got http status ${resp.status}" //refresh the auth token - if (resp.status == 500 && resp.data.status.code == 14) { - log.debug "Storing the failed action to try later" - atomicState.action = "pollChildren"; + if (resp.data.status.code == 14) { + atomicState.action = "pollChildren" log.debug "Refreshing your auth_token!" refreshAuthToken() } @@ -463,7 +465,6 @@ def pollChildren(child = null) { } } catch(Exception e) { log.debug "___exception polling children: " + e -// debugEventFromParent(child, "___exception polling children: " + e) refreshAuthToken() } return result @@ -476,18 +477,14 @@ def pollChild(child){ if (!child.device.deviceNetworkId.startsWith("ecobee_sensor")){ if(atomicState.thermostats[child.device.deviceNetworkId] != null) { def tData = atomicState.thermostats[child.device.deviceNetworkId] -// debugEventFromParent(child, "pollChild(child)>> data for ${child.device.deviceNetworkId} : ${tData.data}") //TODO comment log.info "pollChild(child)>> data for ${child.device.deviceNetworkId} : ${tData.data}" child.generateEvent(tData.data) //parse received message from parent -// return tData.data } else if(atomicState.thermostats[child.device.deviceNetworkId] == null) { -// debugEventFromParent(child, "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling") //TODO comment log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId}" return null } } } else { -// debugEventFromParent(child, "ERROR: pollChildren(child) for ${child.device.deviceNetworkId} after polling") //TODO comment log.info "ERROR: pollChildren(child) for ${child.device.deviceNetworkId} after polling" return null } @@ -513,9 +510,6 @@ def availableModes(child) { { log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling" - // TODO: flag device as in error state - // child.errorState = true - return null } @@ -542,8 +536,6 @@ def currentMode(child) { if(!tData) { log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling" - // TODO: flag device as in error state - // child.errorState = true return null } @@ -561,8 +553,12 @@ def updateSensorData() { def occupancy = "" it.capability.each { if (it.type == "temperature") { - temperature = it.value as Double - temperature = (temperature / 10).toInteger() + if (location.temperatureScale == "F") { + temperature = Math.round(it.value.toDouble() / 10) + } else { + temperature = convertFtoC(it.value.toDouble() / 10) + } + } else if (it.type == "occupancy") { if(it.value == "true") occupancy = "active" @@ -575,7 +571,6 @@ def updateSensorData() { if(d) { d.sendEvent(name:"temperature", value: temperature) d.sendEvent(name:"motion", value: occupancy) -// debugEventFromParent(d, "temperature : ${temperature}, motion:${occupancy}") } } } @@ -595,64 +590,63 @@ def toQueryString(Map m) { } private refreshAuthToken() { - log.debug "refreshing auth token" + log.debug "refreshing auth token" - if(!atomicState.refreshToken) { - log.warn "Can not refresh OAuth token since there is no refreshToken stored" - } else { + 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], - ] + def refreshParams = [ + method: 'POST', + uri : apiEndpoint, + path : "/token", + query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId], + ] - log.debug refreshParams + 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 -> + 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!" + if(resp.status == 200) { + log.debug "Token refreshed...calling saved RestAction now!" - debugEvent("Token refreshed ... calling saved RestAction now!") + debugEvent("Token refreshed ... calling saved RestAction now!") - log.debug resp + log.debug resp - jsonMap = resp.data + jsonMap = resp.data - if(resp.data) { + if(resp.data) { - log.debug resp.data - debugEvent("Response = ${resp.data}") + log.debug resp.data + debugEvent("Response = ${resp.data}") - atomicState.refreshToken = resp?.data?.refresh_token - atomicState.authToken = resp?.data?.access_token + atomicState.refreshToken = resp?.data?.refresh_token + atomicState.authToken = resp?.data?.access_token - debugEvent("Refresh Token = ${atomicState.refreshToken}") - debugEvent("OAUTH Token = ${atomicState.authToken}") + debugEvent("Refresh Token = ${atomicState.refreshToken}") + debugEvent("OAUTH Token = ${atomicState.authToken}") - if(atomicState.action && atomicState.action != "") { - log.debug "Executing next action: ${atomicState.action}" + if(atomicState.action && atomicState.action != "") { + log.debug "Executing next action: ${atomicState.action}" - "${atomicState.action}"() + "${atomicState.action}"() - //remove saved action - atomicState.action = "" - } + atomicState.action = "" + } - } - atomicState.action = "" - } else { - log.debug "refresh failed ${resp.status} : ${resp.status.code}" - } - } - } catch(Exception e) { - log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}" + } + atomicState.action = "" + } else { + log.debug "refresh failed ${resp.status} : ${resp.status.code}" + } + } + } catch (groovyx.net.http.HttpResponseException e) { + log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}" def reAttemptPeriod = 300 // in sec if (e.statusCode != 401) { //this issue might comes from exceed 20sec app execution, connectivity issue etc. runIn(reAttemptPeriod, "refreshAuthToken") @@ -665,20 +659,16 @@ private refreshAuthToken() { sendPushAndFeeds(notificationMessage) atomicState.reAttempt = 0 } - } - } - } + } + } + } } def resumeProgram(child, deviceId) { -// def thermostatIdsString = getChildDeviceIdsString() -// log.debug "resumeProgram children: $thermostatIdsString" def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{"type": "resumeProgram"}]}' - //, { "type": "sendMessage", "params": { "text": "Setpoint Updated" } } def result = sendJson(jsonRequestBody) -// debugEventFromParent(child, "resumeProgram(child) with result ${result}") return result } @@ -687,27 +677,16 @@ def setHold(child, heating, cooling, deviceId, sendHoldType) { int h = heating * 10 int c = cooling * 10 -// log.debug "setpoints____________ - h: $heating - $h, c: $cooling - $c" -// def thermostatIdsString = getChildDeviceIdsString() def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{ "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": '+sendHoldType+' } } ]}' -// def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeRuntime":true},"functions": [{"type": "resumeProgram"}, { "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": "indefinite" } } ]}' def result = sendJson(child, jsonRequestBody) -// debugEventFromParent(child, "setHold: heating: ${h}, cooling: ${c} with result ${result}") return result } def setMode(child, mode, deviceId) { -// def thermostatIdsString = getChildDeviceIdsString() -// log.debug "setCoolingSetpoint children: $thermostatIdsString" - def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"thermostat": {"settings":{"hvacMode":"'+"${mode}"+'"}}}' -// log.debug "Mode Request Body = ${jsonRequestBody}" -// debugEvent ("Mode Request Body = ${jsonRequestBody}") - def result = sendJson(jsonRequestBody) -// debugEventFromParent(child, "setMode to ${mode} with result ${result}") return result } @@ -724,8 +703,6 @@ def sendJson(child = null, String jsonBody) { try{ httpPost(cmdParams) { resp -> -// debugEventFromParent(child, "sendJson >> resp.status ${resp.status}, resp.data: ${resp.data}") - if(resp.status == 200) { log.debug "updated ${resp.data}" @@ -741,8 +718,7 @@ def sendJson(child = null, String jsonBody) { debugEvent ("sent Json & got http status ${resp.status} - ${resp.status.code}") //refresh the auth token - if (resp.status == 500 && resp.status.code == 14) { - //log.debug "Storing the failed action to try later" + if (resp.status.code == 14) { log.debug "Refreshing your auth_token!" debugEvent ("Refreshing OAUTH Token") refreshAuthToken() @@ -757,7 +733,7 @@ def sendJson(child = null, String jsonBody) { } catch(Exception e) { log.debug "Exception Sending Json: " + e debugEvent ("Exception Sending JSON: " + e) - refreshAuthToken() + refreshAuthToken() return false } @@ -794,25 +770,37 @@ def debugEventFromParent(child, message) { //send both push notification and mobile activity feeds 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 - sendPush("Your Ecobee thermostat " + notificationMessage) - sendActivityFeeds(notificationMessage) - atomicState.timeSendPush = now() - } - } else { - sendPush("Your Ecobee thermostat " + notificationMessage) - sendActivityFeeds(notificationMessage) - atomicState.timeSendPush = now() - } - atomicState.authToken = null + 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 + sendPush("Your Ecobee thermostat " + notificationMessage) + sendActivityFeeds(notificationMessage) + atomicState.timeSendPush = now() + } + } else { + sendPush("Your Ecobee thermostat " + notificationMessage) + sendActivityFeeds(notificationMessage) + atomicState.timeSendPush = now() + } + atomicState.authToken = null } def sendActivityFeeds(notificationMessage) { - def devices = getChildDevices() - devices.each { child -> - child.generateActivityFeedsEvent(notificationMessage) //parse received message from parent - } + def devices = getChildDevices() + devices.each { child -> + child.generateActivityFeedsEvent(notificationMessage) //parse received message from parent + } +} + +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() }