mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-08 05:31:56 +00:00
Merge pull request #1194 from jimmyjames/revert-latest-ecobee-changes
revert changes for DVCSMP-1980 and SSVD-2534 on staging
This commit is contained in:
@@ -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 = """
|
||||
<p>Your ecobee Account is now connected to SmartThings!</p>
|
||||
<p>Click 'Done' to finish setup.</p>
|
||||
<p>Your ecobee Account is now connected to SmartThings!</p>
|
||||
<p>Click 'Done' to finish setup.</p>
|
||||
"""
|
||||
connectionStatus(message)
|
||||
}
|
||||
@@ -167,63 +171,64 @@ def connectionStatus(message, redirectUrl = null) {
|
||||
}
|
||||
|
||||
def html = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=640">
|
||||
<title>Ecobee & SmartThings connection</title>
|
||||
<style type="text/css">
|
||||
@font-face {
|
||||
font-family: 'Swiss 721 W01 Thin';
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Swiss 721 W01 Light';
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
.container {
|
||||
width: 90%;
|
||||
padding: 4%;
|
||||
text-align: center;
|
||||
}
|
||||
img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
p {
|
||||
font-size: 2.2em;
|
||||
font-family: 'Swiss 721 W01 Thin';
|
||||
text-align: center;
|
||||
color: #666666;
|
||||
padding: 0 40px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
span {
|
||||
font-family: 'Swiss 721 W01 Light';
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=640">
|
||||
<title>Ecobee & SmartThings connection</title>
|
||||
<style type="text/css">
|
||||
@font-face {
|
||||
font-family: 'Swiss 721 W01 Thin';
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Swiss 721 W01 Light';
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
.container {
|
||||
width: 90%;
|
||||
padding: 4%;
|
||||
/*background: #eee;*/
|
||||
text-align: center;
|
||||
}
|
||||
img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
p {
|
||||
font-size: 2.2em;
|
||||
font-family: 'Swiss 721 W01 Thin';
|
||||
text-align: center;
|
||||
color: #666666;
|
||||
padding: 0 40px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
span {
|
||||
font-family: 'Swiss 721 W01 Light';
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/ecobee%402x.png" alt="ecobee icon" />
|
||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
|
||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
|
||||
${message}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user