From 1b9d2fe9ce12250d0d6c8c4e5f9ca2cdc2b8bbd1 Mon Sep 17 00:00:00 2001 From: Warodom Khamphanchai Date: Wed, 28 Oct 2015 12:46:44 -0700 Subject: [PATCH] DVCSMP-1174 FIX - Ecobee - Thermostat isn't responding to routines --- .../ecobee-sensor.src/ecobee-sensor.groovy | 66 + .../ecobee-thermostat.groovy | 812 ++++++------ .../ecobee-connect.src/ecobee-connect.groovy | 1092 +++++++---------- 3 files changed, 947 insertions(+), 1023 deletions(-) create mode 100644 devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy diff --git a/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy b/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy new file mode 100644 index 0000000..b979cf9 --- /dev/null +++ b/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy @@ -0,0 +1,66 @@ +/** + * Ecobee Sensor + * + * Copyright 2015 Juan Risso + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Ecobee Sensor", namespace: "smartthings", author: "SmartThings") { + capability "Sensor" + capability "Temperature Measurement" + capability "Motion Sensor" + capability "Refresh" + 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", + 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("motion", "device.motion") { + state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0") + state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff") + } + + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main (["temperature","motion"]) + details(["temperature","motion","refresh"]) + } +} + +def refresh() { + log.debug "refresh..." + poll() +} + +void poll() { + log.debug "Executing 'poll' using parent SmartApp" + parent.pollChildren(this) +} diff --git a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy index 39434c0..3a3140e 100644 --- a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy +++ b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy @@ -22,31 +22,31 @@ metadata { capability "Polling" capability "Sensor" capability "Refresh" - + command "generateEvent" command "raiseSetpoint" command "lowerSetpoint" command "resumeProgram" command "switchMode" - + attribute "thermostatSetpoint","number" attribute "thermostatStatus","string" } 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") { @@ -54,27 +54,27 @@ metadata { state "heat", action:"switchMode", nextState: "updating", icon: "st.thermostat.heat" state "cool", action:"switchMode", nextState: "updating", icon: "st.thermostat.cool" state "auto", action:"switchMode", nextState: "updating", icon: "st.thermostat.auto" - state "auxHeatOnly", action:"switchMode", icon: "st.thermostat.emergency-heat" - state "updating", label:"Working", icon: "st.secondary.secondary" + state "auxHeatOnly", action:"switchMode", icon: "st.thermostat.emergency-heat" + state "updating", label:"Working", icon: "st.secondary.secondary" } standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") { state "auto", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "on" state "on", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "off" - state "off", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "circulate" + state "off", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "circulate" state "circulate", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "auto" } - standardTile("upButtonControl", "device.thermostatSetpoint", inactiveLabel: false, decoration: "flat") { - state "setpoint", action:"raiseSetpoint", backgroundColor:"#d04e00", icon:"st.thermostat.thermostat-up" + standardTile("upButtonControl", "device.thermostatSetpoint", inactiveLabel: false, decoration: "flat") { + state "setpoint", action:"raiseSetpoint", icon:"st.thermostat.thermostat-up" } - valueTile("thermostatSetpoint", "device.thermostatSetpoint", width: 1, height: 1, decoration: "flat") { - state "thermostatSetpoint", label:'${currentValue}' + valueTile("thermostatSetpoint", "device.thermostatSetpoint", width: 1, height: 1, decoration: "flat") { + state "thermostatSetpoint", label:'${currentValue}°' } valueTile("currentStatus", "device.thermostatStatus", height: 1, width: 2, decoration: "flat") { state "thermostatStatus", label:'${currentValue}', backgroundColor:"#ffffff" - } + } standardTile("downButtonControl", "device.thermostatSetpoint", inactiveLabel: false, decoration: "flat") { - state "setpoint", action:"lowerSetpoint", backgroundColor:"#d04e00", icon:"st.thermostat.thermostat-down" - } + state "setpoint", action:"lowerSetpoint", icon:"st.thermostat.thermostat-down" + } controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) { state "setHeatingSetpoint", action:"thermostat.setHeatingSetpoint", backgroundColor:"#d04e00" } @@ -91,22 +91,23 @@ metadata { state "default", action:"refresh.refresh", icon:"st.secondary.refresh" } standardTile("resumeProgram", "device.resumeProgram", inactiveLabel: false, decoration: "flat") { - state "resume", label:'Resume Program', action:"device.resumeProgram", icon:"st.sonos.play-icon" + state "resume", action:"resumeProgram", nextState: "updating", label:'Resume Schedule', icon:"st.samsung.da.oven_ic_send" + state "updating", label:"Working", icon: "st.secondary.secondary" } main "temperature" - details(["temperature", "upButtonControl", "thermostatSetpoint", "currentStatus", "downButtonControl", "mode", "resumeProgram", "refresh"]) + details(["temperature", "upButtonControl", "thermostatSetpoint", "currentStatus", "downButtonControl", "mode", "resumeProgram", "refresh"]) } } /* - + preferences { input "highTemperature", "number", title: "Auto Mode High Temperature:", defaultValue: 80 input "lowTemperature", "number", title: "Auto Mode Low Temperature:", defaultValue: 70 input name: "holdType", type: "enum", title: "Hold Type", description: "When changing temperature, use Temporary or Permanent hold", required: true, options:["Temporary", "Permanent"] } - + */ @@ -114,195 +115,190 @@ metadata { def parse(String description) { log.debug "Parsing '${description}'" // TODO: handle '' attribute - + } -def refresh() -{ - log.debug "refresh called" - poll() - log.debug "refresh ended" +def refresh() { + log.debug "refresh called" + poll() + log.debug "refresh ended" } - -def go() -{ - log.debug "before:go tile tapped" - poll() - log.debug "after" -} - + void poll() { log.debug "Executing 'poll' using parent SmartApp" - + def results = parent.pollChild(this) - parseEventData(results) - generateStatusEvent() + generateEvent(results) //parse received message from parent } - -def parseEventData(Map results) -{ + +def generateEvent(Map results) { log.debug "parsing data $results" - if(results) - { - results.each { name, value -> - + if(results) { + results.each { name, value -> + def linkText = getLinkText(device) - def isChange = false - def isDisplayed = true - - if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint") { - isChange = isTemperatureStateChange(device, name, value.toString()) - isDisplayed = isChange - - sendEvent( - name: name, - value: value, - unit: "F", - linkText: linkText, - descriptionText: getThermostatDescriptionText(name, value, linkText), - handlerName: name, - isStateChange: isChange, - displayed: isDisplayed) - - } - else { - isChange = isStateChange(device, name, value.toString()) - isDisplayed = isChange - - sendEvent( - name: name, - value: value.toString(), - linkText: linkText, - descriptionText: getThermostatDescriptionText(name, value, linkText), - handlerName: name, - isStateChange: isChange, - displayed: isDisplayed) - - } - } - generateSetpointEvent () - generateStatusEvent () - } -} + def isChange = false + def isDisplayed = true -void generateEvent(Map results) -{ - log.debug "parsing data $results" - if(results) - { - results.each { name, value -> - - def linkText = getLinkText(device) - def isChange = false - def isDisplayed = true - - if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint") { + if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint") { + def sendValue = value? convertTemperatureIfNeeded(value.toDouble(), "F", 1): value //API return temperature value in F isChange = isTemperatureStateChange(device, name, value.toString()) - isDisplayed = isChange - + isDisplayed = isChange + sendEvent( - name: name, - value: value, - unit: "F", - linkText: linkText, - descriptionText: getThermostatDescriptionText(name, value, linkText), - handlerName: name, - isStateChange: isChange, - displayed: isDisplayed) - } - else { - isChange = isStateChange(device, name, value.toString()) - isDisplayed = isChange - - sendEvent( - name: name, - value: value.toString(), - linkText: linkText, - descriptionText: getThermostatDescriptionText(name, value, linkText), - handlerName: name, - isStateChange: isChange, - displayed: isDisplayed) - - } + name: name, + value: sendValue, + unit: location.temperatureScale, + linkText: linkText, + descriptionText: getThermostatDescriptionText(name, value, linkText), + handlerName: name, + isStateChange: isChange, + displayed: isDisplayed) + + } else { + isChange = isStateChange(device, name, value.toString()) + isDisplayed = isChange + + sendEvent( + name: name, + value: value.toString(), + linkText: linkText, + descriptionText: getThermostatDescriptionText(name, value, linkText), + handlerName: name, + isStateChange: isChange, + displayed: isDisplayed) + + } } - generateSetpointEvent () - generateStatusEvent() + generateSetpointEvent () + generateStatusEvent () } } - -private getThermostatDescriptionText(name, value, linkText) -{ - if(name == "temperature") - { - return "$linkText was $value°F" + +//return descriptionText to be shown on mobile activity feed +private getThermostatDescriptionText(name, value, linkText) { + if(name == "temperature") { + return "$linkText temperature is $value°F" + + } else if(name == "heatingSetpoint") { + return "heating setpoint is $value°F" + + } else if(name == "coolingSetpoint"){ + return "cooling setpoint is $value°F" + + } else if (name == "thermostatMode") { + return "thermostat mode is ${value}" + + } else if (name == "thermostatFanMode") { + return "thermostat fan mode is ${value}" + + } else { + return "${name} = ${value}" } - else if(name == "heatingSetpoint") - { - return "latest heating setpoint was $value°F" +} + +void setHeatingSetpoint(setpoint) { + setHeatingSetpoint(setpoint.toDouble()) +} + +void setHeatingSetpoint(Double setpoint) { +// def mode = device.currentValue("thermostatMode") + def heatingSetpoint = setpoint + def coolingSetpoint = device.currentValue("coolingSetpoint").toDouble() + def deviceId = device.deviceNetworkId.split(/\./).last() + + //enforce limits of heatingSetpoint + if (heatingSetpoint > 79) { + heatingSetpoint = 79 + } else if (heatingSetpoint < 45) { + heatingSetpoint = 45 } - else if(name == "coolingSetpoint") - { - return "latest cooling setpoint was $value°F" + + //enforce limits of heatingSetpoint vs coolingSetpoint + if (heatingSetpoint >= coolingSetpoint) { + coolingSetpoint = heatingSetpoint + } + + log.debug "Sending setHeatingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}" + + if (parent.setHold (this, heatingSetpoint, coolingSetpoint, deviceId)) { + 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 } - else if (name == "thermostatMode") - { - return "thermostat mode is ${value}" - } - else - { - return "${name} = ${value}" - } } - -void setHeatingSetpoint(degreesF) { - setHeatingSetpoint(degreesF.toDouble()) +void setCoolingSetpoint(setpoint) { + setCoolingSetpoint(setpoint.toDouble()) } -void setHeatingSetpoint(Double degreesF) { - log.debug "setHeatingSetpoint({$degreesF})" - sendEvent("name":"heatingSetpoint", "value":degreesF) - Double coolingSetpoint = device.currentValue("coolingSetpoint") - log.debug "coolingSetpoint: $coolingSetpoint" - parent.setHold(this, degreesF, coolingSetpoint) +void setCoolingSetpoint(Double setpoint) { +// def mode = device.currentValue("thermostatMode") + def heatingSetpoint = device.currentValue("heatingSetpoint").toDouble() + def coolingSetpoint = setpoint + def deviceId = device.deviceNetworkId.split(/\./).last() + + if (coolingSetpoint > 92) { + coolingSetpoint = 92 + } else if (coolingSetpoint < 65) { + coolingSetpoint = 65 + } + + //enforce limits of heatingSetpoint vs coolingSetpoint + if (heatingSetpoint >= coolingSetpoint) { + heatingSetpoint = coolingSetpoint + } + + log.debug "Sending setCoolingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}" + + if (parent.setHold (this, heatingSetpoint, coolingSetpoint, deviceId)) { + 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 + } } -void setCoolingSetpoint(degreesF) { - setCoolingSetpoint(degreesF.toDouble()) -} +void resumeProgram() { -void setCoolingSetpoint(Double degreesF) { - log.debug "setCoolingSetpoint({$degreesF})" - sendEvent("name":"coolingSetpoint", "value":degreesF) - Double heatingSetpoint = device.currentValue("heatingSetpoint") - parent.setHold(this, heatingSetpoint, degreesF) -} + 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)) { + 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)" + } -def configure() { - -} - -def resumeProgram() { - parent.resumeProgram(this) } def modes() { if (state.modes) { - log.debug "Modes = ${state.modes}" - return state.modes - } - else { - state.modes = parent.availableModes(this) - log.debug "Modes = ${state.modes}" - return state.modes - } + log.debug "Modes = ${state.modes}" + return state.modes + } + else { + state.modes = parent.availableModes(this) + log.debug "Modes = ${state.modes}" + return state.modes + } } def fanModes() { ["off", "on", "auto", "circulate"] } - def switchMode() { log.debug "in switchMode" def currentMode = device.currentState("thermostatMode")?.value @@ -314,7 +310,7 @@ def switchMode() { } def switchToMode(nextMode) { - log.debug "In switchToMode = ${nextMode}" + log.debug "In switchToMode = ${nextMode}" if (nextMode in modes()) { state.lastTriedMode = nextMode "$nextMode"() @@ -376,300 +372,320 @@ def getDataByName(String name) { def setThermostatMode(String value) { log.debug "setThermostatMode({$value})" - + } def setThermostatFanMode(String value) { log.debug "setThermostatFanMode({$value})" - + } def generateModeEvent(mode) { - - sendEvent(name: "thermostatMode", value: mode, descriptionText: "$device.displayName is in ${mode} mode", displayed: true, isStateChange: true) - + sendEvent(name: "thermostatMode", value: mode, descriptionText: "$device.displayName is in ${mode} mode", displayed: true) } def generateFanModeEvent(fanMode) { - - sendEvent(name: "thermostatFanMode", value: fanMode, descriptionText: "$device.displayName fan is in ${mode} mode", displayed: true, isStateChange: true) - + sendEvent(name: "thermostatFanMode", value: fanMode, descriptionText: "$device.displayName fan is in ${mode} mode", displayed: true) } def generateOperatingStateEvent(operatingState) { - - sendEvent(name: "thermostatOperatingState", value: operatingState, descriptionText: "$device.displayName is ${operatingState}", displayed: true, isStateChange: true) - + sendEvent(name: "thermostatOperatingState", value: operatingState, descriptionText: "$device.displayName is ${operatingState}", displayed: true) } def off() { log.debug "off" - generateModeEvent("off") - if (parent.setMode (this,"off")) - generateModeEvent("off") - else { - log.debug "Error setting new mode." - def currentMode = device.currentState("thermostatMode")?.value - generateModeEvent(currentMode) // reset the tile back - } - generateSetpointEvent() - generateStatusEvent() - + def deviceId = device.deviceNetworkId.split(/\./).last() + if (parent.setMode (this,"off", deviceId)) + generateModeEvent("off") + else { + log.debug "Error setting new mode." + def currentMode = device.currentState("thermostatMode")?.value + generateModeEvent(currentMode) // reset the tile back + } + generateSetpointEvent() + generateStatusEvent() } def heat() { log.debug "heat" - generateModeEvent("heat") - if (parent.setMode (this,"heat")) - generateModeEvent("heat") - else { - log.debug "Error setting new mode." - def currentMode = device.currentState("thermostatMode")?.value - generateModeEvent(currentMode) // reset the tile back - } - generateSetpointEvent() - generateStatusEvent() + def deviceId = device.deviceNetworkId.split(/\./).last() + if (parent.setMode (this,"heat", deviceId)) + generateModeEvent("heat") + else { + log.debug "Error setting new mode." + def currentMode = device.currentState("thermostatMode")?.value + generateModeEvent(currentMode) // reset the tile back + } + generateSetpointEvent() + generateStatusEvent() } def auxHeatOnly() { log.debug "auxHeatOnly" - generateModeEvent("auxHeatOnly") - if (parent.setMode (this,"auxHeatOnly")) - generateModeEvent("auxHeatOnly") - else { - log.debug "Error setting new mode." - def currentMode = device.currentState("thermostatMode")?.value - generateModeEvent(currentMode) // reset the tile back - } - generateSetpointEvent() - generateStatusEvent() + def deviceId = device.deviceNetworkId.split(/\./).last() + if (parent.setMode (this,"auxHeatOnly", deviceId)) + generateModeEvent("auxHeatOnly") + else { + log.debug "Error setting new mode." + def currentMode = device.currentState("thermostatMode")?.value + generateModeEvent(currentMode) // reset the tile back + } + generateSetpointEvent() + generateStatusEvent() } def cool() { log.debug "cool" - generateModeEvent("cool") - if (parent.setMode (this,"cool")) - generateModeEvent("cool") - else { - log.debug "Error setting new mode." - def currentMode = device.currentState("thermostatMode")?.value - generateModeEvent(currentMode) // reset the tile back - } - generateSetpointEvent() - generateStatusEvent() + def deviceId = device.deviceNetworkId.split(/\./).last() + if (parent.setMode (this,"cool", deviceId)) + generateModeEvent("cool") + else { + log.debug "Error setting new mode." + def currentMode = device.currentState("thermostatMode")?.value + generateModeEvent(currentMode) // reset the tile back + } + generateSetpointEvent() + generateStatusEvent() } def auto() { log.debug "auto" - generateModeEvent("auto") - if (parent.setMode (this,"auto")) - generateModeEvent("auto") - else { - log.debug "Error setting new mode." - def currentMode = device.currentState("thermostatMode")?.value - generateModeEvent(currentMode) // reset the tile back - } - generateSetpointEvent() - generateStatusEvent() + def deviceId = device.deviceNetworkId.split(/\./).last() + if (parent.setMode (this,"auto", deviceId)) + generateModeEvent("auto") + else { + log.debug "Error setting new mode." + def currentMode = device.currentState("thermostatMode")?.value + generateModeEvent(currentMode) // reset the tile back + } + generateSetpointEvent() + generateStatusEvent() } def fanOn() { log.debug "fanOn" - parent.setFanMode (this,"on") - +// parent.setFanMode (this,"on") + } def fanAuto() { log.debug "fanAuto" - parent.setFanMode (this,"auto") - +// parent.setFanMode (this,"auto") + } def fanCirculate() { log.debug "fanCirculate" - parent.setFanMode (this,"circulate") - +// parent.setFanMode (this,"circulate") + } def fanOff() { log.debug "fanOff" - parent.setFanMode (this,"off") - +// parent.setFanMode (this,"off") + } def generateSetpointEvent() { - log.debug "Generate SetPoint Event" + log.debug "Generate SetPoint Event" def mode = device.currentValue("thermostatMode") - log.debug "Current Mode = ${mode}" + log.debug "Current Mode = ${mode}" - def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() - log.debug "Heating Setpoint = ${heatingSetpoint}" + def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() + log.debug "Heating Setpoint = ${heatingSetpoint}" def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() - log.debug "Cooling Setpoint = ${coolingSetpoint}" - - if (mode == "heat") { - - sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°") - - } - else if (mode == "cool") { - - sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()+"°") + log.debug "Cooling Setpoint = ${coolingSetpoint}" - } else if (mode == "auto") { - - sendEvent("name":"thermostatSetpoint", "value":"Auto") + if (mode == "heat") { - } else if (mode == "off") { - - sendEvent("name":"thermostatSetpoint", "value":"Off") + sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()) - } else if (mode == "emergencyHeat") { - - sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°") + } + else if (mode == "cool") { - } + sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()) + + } else if (mode == "auto") { + + sendEvent("name":"thermostatSetpoint", "value":"Auto") + + } else if (mode == "off") { + + sendEvent("name":"thermostatSetpoint", "value":"Off") + + } else if (mode == "emergencyHeat") { + + sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()) + + } } void raiseSetpoint() { - - log.debug "Raise SetPoint" - def mode = device.currentValue("thermostatMode") - def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() - def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() - - log.debug "Current Mode = ${mode}" - - if (mode == "heat") { - - heatingSetpoint++ - - if (heatingSetpoint > 99) - heatingSetpoint = 99 - - sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°") - sendEvent("name":"heatingSetpoint", "value":heatingSetpoint) - - parent.setHold (this, heatingSetpoint, coolingSetpoint) - - log.debug "New Heating Setpoint = ${heatingSetpoint}" - - } - else if (mode == "cool") { - - coolingSetpoint++ - - if (coolingSetpoint > 99) - coolingSetpoint = 99 - - sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()+"°") - sendEvent("name":"coolingSetpoint", "value":coolingSetpoint) - - parent.setHold (this, heatingSetpoint, coolingSetpoint) - - log.debug "New Cooling Setpoint = ${coolingSetpoint}" - - } - generateStatusEvent() + def targetvalue + 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() + log.debug "raiseSetpoint() mode = ${mode}, heatingSetpoint: ${heatingSetpoint}, coolingSetpoint:${coolingSetpoint}, thermostatSetpoint:${thermostatSetpoint}" + + if (device.latestState('thermostatSetpoint')) { + targetvalue = device.latestState('thermostatSetpoint').value as Integer + } else { + targetvalue = 0 + } + targetvalue = targetvalue + 1 + + if (mode == "heat" && targetvalue > 79) { + targetvalue = 79 + } else if (mode == "cool" && targetvalue > 92) { + targetvalue = 92 + } + + sendEvent("name":"thermostatSetpoint", "value":targetvalue, displayed: false) + log.info "In mode $mode raiseSetpoint() to $targetvalue" + + runIn(3, "alterSetpoint", [data: [value:targetvalue], overwrite: true]) //when user click button this runIn will be overwrite + } } - +//called by tile when user hit raise temperature button on UI void lowerSetpoint() { - log.debug "Lower SetPoint" - def mode = device.currentValue("thermostatMode") - def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() - def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() - - log.debug "Current Mode = ${mode}, Current Heating Setpoint = ${heatingSetpoint}, Current Cooling Setpoint = ${coolingSetpoint}" - - if (mode == "heat" || mode == "emergencyHeat") { - - heatingSetpoint-- - - if (heatingSetpoint < 32) - heatingSetpoint = 32 + def targetvalue - sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°") - sendEvent("name":"heatingSetpoint", "value":heatingSetpoint) - - parent.setHold (this, heatingSetpoint, coolingSetpoint) - - log.debug "New Heating Setpoint = ${heatingSetpoint}" - - } - else if (mode == "cool") { - - coolingSetpoint-- - - if (coolingSetpoint < 32) - coolingSetpoint = 32 - - sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()+"°") - sendEvent("name":"coolingSetpoint", "value":coolingSetpoint) - - parent.setHold (this, heatingSetpoint, coolingSetpoint) - - log.debug "New Cooling Setpoint = ${coolingSetpoint}" - - } - generateStatusEvent() + 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() + log.debug "lowerSetpoint() mode = ${mode}, heatingSetpoint: ${heatingSetpoint}, coolingSetpoint:${coolingSetpoint}, thermostatSetpoint:${thermostatSetpoint}" + if (device.latestState('thermostatSetpoint')) { + targetvalue = device.latestState('thermostatSetpoint').value as Integer + } else { + targetvalue = 0 + } + targetvalue = targetvalue - 1 + + if (mode == "heat" && targetvalue.toInteger() < 45) { + targetvalue = 45 + } else if (mode == "cool" && targetvalue.toInteger() < 65) { + targetvalue = 65 + } + + sendEvent("name":"thermostatSetpoint", "value":targetvalue, displayed: false) + log.info "In mode $mode lowerSetpoint() to $targetvalue" + + runIn(3, "alterSetpoint", [data: [value:targetvalue], overwrite: true]) //when user click button this runIn will be overwrite + } +} + +//called by raiseSetpoint() and lowerSetpoint() +void alterSetpoint(temp) { + + def mode = device.currentValue("thermostatMode") + def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() + def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() + def deviceId = device.deviceNetworkId.split(/\./).last() + + def targetHeatingSetpoint + def targetCoolingSetpoint + + //step1: check thermostatMode, enforce limits before sending request to cloud + if (mode == "heat"){ + if (temp.value > coolingSetpoint){ + targetHeatingSetpoint = temp.value + targetCoolingSetpoint = temp.value + } else { + targetHeatingSetpoint = temp.value + targetCoolingSetpoint = coolingSetpoint + } + } else if (mode == "cool") { + //enforce limits before sending request to cloud + if (temp.value < heatingSetpoint){ + targetHeatingSetpoint = temp.value + targetCoolingSetpoint = temp.value + } else { + targetHeatingSetpoint = heatingSetpoint + targetCoolingSetpoint = temp.value + } + } + + log.debug "alterSetpoint >> in mode ${mode} trying to change heatingSetpoint to ${targetHeatingSetpoint} " + + "coolingSetpoint to ${targetCoolingSetpoint}" + + //step2: call parent.setHold to send http request to 3rd party cloud + if (parent.setHold(this, targetHeatingSetpoint, targetCoolingSetpoint, deviceId)) { + sendEvent("name": "thermostatSetpoint", "value": temp.value.toString(), 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"){ + sendEvent("name": "thermostatSetpoint", "value": heatingSetpoint.toString(), displayed: false) + } else if (mode == "cool") { + sendEvent("name": "thermostatSetpoint", "value": coolingSetpoint.toString(), displayed: false) + } + } + generateStatusEvent() } 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 statusText - - log.debug "Generate Status Event for Mode = ${mode}" - log.debug "Temperature = ${temperature}" - log.debug "Heating set point = ${heatingSetpoint}" - log.debug "Cooling set point = ${coolingSetpoint}" - log.debug "HVAC Mode = ${mode}" - - if (mode == "heat") { - - if (temperature >= heatingSetpoint) - statusText = "Right Now: Idle" - else - statusText = "Heating to ${heatingSetpoint}° F" - - } else if (mode == "cool") { - - if (temperature <= coolingSetpoint) - statusText = "Right Now: Idle" - else - statusText = "Cooling to ${coolingSetpoint}° F" - - } else if (mode == "auto") { - - statusText = "Right Now: Auto" - - } else if (mode == "off") { - - statusText = "Right Now: Off" - - } else if (mode == "emergencyHeat") { - - statusText = "Emergency Heat" - - } else { - - statusText = "?" - - } - log.debug "Generate Status Event = ${statusText}" - sendEvent("name":"thermostatStatus", "value":statusText, "description":statusText, displayed: true, isStateChange: true) -} + def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() + def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() + def temperature = device.currentValue("temperature").toInteger() + def statusText + + log.debug "Generate Status Event for Mode = ${mode}" + log.debug "Temperature = ${temperature}" + log.debug "Heating set point = ${heatingSetpoint}" + log.debug "Cooling set point = ${coolingSetpoint}" + log.debug "HVAC Mode = ${mode}" + + if (mode == "heat") { + + if (temperature >= heatingSetpoint) + statusText = "Right Now: Idle" + else + statusText = "Heating to ${heatingSetpoint}° F" + + } else if (mode == "cool") { + + if (temperature <= coolingSetpoint) + statusText = "Right Now: Idle" + else + statusText = "Cooling to ${coolingSetpoint}° F" + + } else if (mode == "auto") { + + statusText = "Right Now: Auto" + + } else if (mode == "off") { + + statusText = "Right Now: Off" + + } else if (mode == "emergencyHeat") { + + statusText = "Emergency Heat" + + } else { + + statusText = "?" + + } + log.debug "Generate Status Event = ${statusText}" + sendEvent("name":"thermostatStatus", "value":statusText, "description":statusText, displayed: true) +} diff --git a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy index 8b7a02e..a33037b 100644 --- a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy +++ b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy @@ -1,15 +1,4 @@ /** - * Copyright 2015 SmartThings - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License - * for the specific language governing permissions and limitations under the License. - * * Ecobee Service Manager * * Author: scott @@ -18,6 +7,7 @@ * Last Modification: * JLH - 01-23-2014 - Update for Correct SmartApp URL Format * JLH - 02-15-2014 - Fuller use of ecobee API + * 10-28-2015 DVCSMP-604 - accessory sensor, DVCSMP-1174, DVCSMP-1111 - not respond to routines */ definition( name: "Ecobee (Connect)", @@ -30,366 +20,202 @@ definition( singleInstance: true ) { appSetting "clientId" - appSetting "serverUrl" } preferences { - page(name: "auth", title: "ecobee", nextPage:"deviceList", content:"authPage", uninstall: true) - page(name: "deviceList", title: "ecobee", content:"ecobeeDeviceList", install:true) + page(name: "auth", title: "ecobee", nextPage:"", content:"authPage", uninstall: true, install:true) } mappings { - path("/auth") { - action: [ - GET: "auth" - ] - } - path("/swapToken") { - action: [ - GET: "swapToken" - ] - } + path("/oauth/initialize") {action: [GET: "oauthInitUrl"]} + path("/oauth/callback") {action: [GET: "callback"]} } -def auth() { - redirect location: oauthInitUrl() -} - -def authPage() -{ +def authPage() { log.debug "authPage()" - if(!atomicState.accessToken) - { - log.debug "about to create access token" - createAccessToken() - atomicState.accessToken = state.accessToken + if(!atomicState.accessToken) { //this is to access token for 3rd party to make a call to connect app + atomicState.accessToken = createAccessToken() } - - def description = "Required" + def description def uninstallAllowed = false def oauthTokenProvided = false - if(atomicState.authToken) - { - // TODO: Check if it's valid - if(true) - { - description = "You are connected." - uninstallAllowed = true - oauthTokenProvided = true - } - else - { - description = "Required" // Worth differentiating here vs. not having atomicState.authToken? - oauthTokenProvided = false - } + if(atomicState.authToken) { + description = "You are connected." + uninstallAllowed = true + oauthTokenProvided = true + } else { + description = "Click to enter Ecobee Credentials" } - def redirectUrl = buildRedirectUrl("auth") - + def redirectUrl = buildRedirectUrl //"${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}" log.debug "RedirectUrl = ${redirectUrl}" - // get rid of next button until the user is actually auth'd - if (!oauthTokenProvided) { - - return dynamicPage(name: "auth", title: "Login", nextPage:null, uninstall:uninstallAllowed) { + return dynamicPage(name: "auth", title: "Login", nextPage: "", uninstall:uninstallAllowed) { 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 } } - } else { - - return dynamicPage(name: "auth", title: "Log In", nextPage:"deviceList", uninstall:uninstallAllowed) { - section(){ - paragraph "Tap Next to continue to setup your thermostats." - href url:redirectUrl, style:"embedded", state:"complete", title:"ecobee", description:description + def stats = getEcobeeThermostats() + log.debug "thermostat list: $stats" + log.debug "sensor list: ${sensorsDiscovered()}" + return dynamicPage(name: "auth", title: "Select Your Thermostats", uninstall: true) { + 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]) } - } - } - -} - -def ecobeeDeviceList() -{ - log.debug "ecobeeDeviceList()" - - def stats = getEcobeeThermostats() - - log.debug "device list: $stats" - - def p = dynamicPage(name: "deviceList", title: "Select Your Thermostats", uninstall: true) { - 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]) - } - } - - log.debug "list p: $p" - return p -} - -def getEcobeeThermostats() -{ - log.debug "getting device list" - - def requestBody = '{"selection":{"selectionType":"registered","selectionMatch":"","includeRuntime":true}}' - - def deviceListParams = [ - uri: "https://api.ecobee.com", - path: "/1/thermostat", - headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], - query: [format: 'json', body: requestBody] - ] - - log.debug "_______AUTH______ ${atomicState.authToken}" - log.debug "device list params: $deviceListParams" - - def stats = [:] - httpGet(deviceListParams) { resp -> - - if(resp.status == 200) - { - resp.data.thermostatList.each { stat -> - 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." + def options = sensorsDiscovered() ?: [] + def numFound = options.size() ?: 0 + if (numFound > 0) { + 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) + } } } } - - log.debug "thermostats: $stats" - - return stats } -def getThermostatDisplayName(stat) -{ - log.debug "getThermostatDisplayName" - if(stat?.name) - { - return stat.name.toString() - } - - return (getThermostatTypeName(stat) + " (${stat.identifier})").toString() -} - -def getThermostatTypeName(stat) -{ - log.debug "getThermostatTypeName" - return stat.modelNumber == "siSmart" ? "Smart Si" : "Smart" -} - -def installed() { - log.debug "Installed with settings: ${settings}" - - // createAccessToken() - - - initialize() -} - -def updated() { - log.debug "Updated with settings: ${settings}" - - unsubscribe() - initialize() -} - -def initialize() { - // TODO: subscribe to attributes, devices, locations, etc. - log.debug "initialize" - def devices = thermostats.collect { dni -> - - def d = getChildDevice(dni) - - if(!d) - { - d = addChildDevice(getChildNamespace(), getChildName(), dni) - log.debug "created ${d.displayName} with id $dni" - } - else - { - log.debug "found ${d.displayName} with id $dni already exists" - } - - return d - } - - log.debug "created ${devices.size()} thermostats" - - def delete - // Delete any that are no longer in settings - if(!thermostats) - { - log.debug "delete thermostats" - delete = getAllChildDevices() - } - else - { - delete = getChildDevices().findAll { !thermostats.contains(it.deviceNetworkId) } - } - - log.debug "deleting ${delete.size()} thermostats" - delete.each { deleteChildDevice(it.deviceNetworkId) } - - atomicState.thermostatData = [:] - - pollHandler() - - // schedule ("0 0/15 * 1/1 * ? *", pollHandler) -} - - -def oauthInitUrl() -{ - log.debug "oauthInitUrl" - // def oauth_url = "https://api.ecobee.com/authorize?response_type=code&client_id=qqwy6qo0c2lhTZGytelkQ5o8vlHgRsrO&redirect_uri=http://localhost/&scope=smartRead,smartWrite&state=abc123" - def stcid = getSmartThingsClientId(); +def oauthInitUrl() { + log.debug "oauthInitUrl with callback: ${callbackUrl}" atomicState.oauthInitState = UUID.randomUUID().toString() def oauthParams = [ - response_type: "code", - scope: "smartRead,smartWrite", - client_id: stcid, - state: atomicState.oauthInitState, - redirect_uri: buildRedirectUrl() + response_type: "code", + scope: "smartRead,smartWrite", + client_id: smartThingsClientId, + state: atomicState.oauthInitState, + redirect_uri: callbackUrl //"https://graph.api.smartthings.com/oauth/callback" ] - return "https://api.ecobee.com/authorize?" + toQueryString(oauthParams) + redirect(location: "https://api.ecobee.com/authorize?${toQueryString(oauthParams)}") } -def buildRedirectUrl(action = "swapToken") -{ - log.debug "buildRedirectUrl" - // return serverUrl + "/api/smartapps/installations/${app.id}/token/${atomicState.accessToken}" - return serverUrl + "/api/token/${atomicState.accessToken}/smartapps/installations/${app.id}/${action}" -} - -def swapToken() -{ - log.debug "swapping token: $params" - debugEvent ("swapping token: $params") +def callback() { + log.debug "callback()>> params: $params, params.code ${params.code}" def code = params.code def oauthState = params.state - // TODO: verify oauthState == atomicState.oauthInitState + //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" + ] + def tokenUrl = "https://www.ecobee.com/home/token?${toQueryString(tokenParams)}" - // https://www.ecobee.com/home/token?grant_type=authorization_code&code=aliOpagDm3BqbRplugcs1AwdJE0ohxdB&client_id=qqwy6qo0c2lhTZGytelkQ5o8vlHgRsrO&redirect_uri=https://graph.api.smartthings.com/ - def stcid = getSmartThingsClientId() + 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}" + } - def tokenParams = [ - grant_type: "authorization_code", - code: params.code, - client_id: stcid, - redirect_uri: buildRedirectUrl() - ] + if (atomicState.authToken) { + success() + } else { + fail() + } - def tokenUrl = "https://www.ecobee.com/home/token?" + toQueryString(tokenParams) - - log.debug "SCOTT: swapping token $params" - - def jsonMap - httpPost(uri:tokenUrl) { resp -> - jsonMap = resp.data + } else { + log.error "callback() failed oauthState != atomicState.oauthInitState" } - log.debug "SCOTT: swapped token for $jsonMap" - debugEvent ("swapped token for $jsonMap") +} - atomicState.refreshToken = jsonMap.refresh_token - atomicState.authToken = jsonMap.access_token +def success() { + def message = """ +

Your ecobee Account is now connected to SmartThings!

+

Click 'Done' to finish setup.

+ """ + connectionStatus(message) +} + +def fail() { + def message = """ +

The connection could not be established!

+

Click 'Done' to return to the menu.

+ """ + connectionStatus(message) +} + +def connectionStatus(message, redirectUrl = null) { + def redirectHtml = "" + if (redirectUrl) { + redirectHtml = """ + + """ + } def html = """ -Withings Connection +Ecobee & SmartThings connection -
- ecobee icon - connected device icon - SmartThings logo -

Your ecobee Account is now connected to SmartThings!

-

Click 'Done' to finish setup.

-
+
+ ecobee icon + connected device icon + SmartThings logo + ${message} +
""" @@ -397,88 +223,268 @@ def swapToken() render contentType: 'text/html', data: html } -def getPollRateMillis() { return 15 * 60 * 1000 } +def getEcobeeThermostats() { + log.debug "getting device list" -// Poll Child is invoked from the Child Device itself as part of the Poll Capability -def pollChild( child ) -{ - log.debug "poll child" - debugEvent ("poll child") - def now = new Date().time + def requestBody = '{"selection":{"selectionType":"registered","selectionMatch":"","includeRuntime":true,"includeSensors":true}}' - debugEvent ("Last Poll Millis = ${atomicState.lastPollMillis}") - def last = atomicState.lastPollMillis ?: 0 - def next = last + pollRateMillis + def deviceListParams = [ + uri: apiEndpoint, + path: "/1/thermostat", + headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], + query: [format: 'json', body: requestBody] + ] - log.debug "pollChild( ${child.device.deviceNetworkId} ): $now > $next ?? w/ current state: ${atomicState.thermostats}" - debugEvent ("pollChild( ${child.device.deviceNetworkId} ): $now > $next ?? w/ current state: ${atomicState.thermostats}") + def stats = [:] + httpGet(deviceListParams) { resp -> - // if( now > next ) - if( true ) // for now let's always poll/refresh - { - log.debug "polling children because $now > $next" - debugEvent("polling children because $now > $next") - - pollChildren() - - log.debug "polled children and looking for ${child.device.deviceNetworkId} from ${atomicState.thermostats}" - debugEvent ("polled children and looking for ${child.device.deviceNetworkId} from ${atomicState.thermostats}") - - def currentTime = new Date().time - debugEvent ("Current Time = ${currentTime}") - atomicState.lastPollMillis = currentTime - - def tData = atomicState.thermostats[child.device.deviceNetworkId] - - 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 + 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." + } } - - tData.updated = currentTime - - return tData.data } - else if(atomicState.thermostats[child.device.deviceNetworkId] != null) - { - log.debug "not polling children, found child ${child.device.deviceNetworkId} " - - def tData = atomicState.thermostats[child.device.deviceNetworkId] - if(!tData.updated) - { - // we have pulled new data for this thermostat, but it has not asked us for it - // track it and return the data - tData.updated = new Date().time - return tData.data - } - return null - } - else if(atomicState.thermostats[child.device.deviceNetworkId] == null) - { - log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId}" - - // TODO: flag device as in error state - // child.errorState = true - - return null - } - else - { - // it's not time to poll again and this thermostat already has its latest values - } - - return null + atomicState.thermostats = stats + return stats } -def availableModes(child) -{ +Map sensorsDiscovered() { + def map = [:] + atomicState.remoteSensors.each { + if (it.type != "thermostat") { + def value = "${it?.name}" + def key = "ecobee_sensor-"+ it?.id + "-" + it?.code + map["${key}"] = value + } + } + atomicState.sensors = map + return map +} - debugEvent ("atomicState.Thermos = ${atomicState.thermostats}") +def getThermostatDisplayName(stat) { + if(stat?.name) + return stat.name.toString() + return (getThermostatTypeName(stat) + " (${stat.identifier})").toString() +} + +def getThermostatTypeName(stat) { + return stat.modelNumber == "siSmart" ? "Smart Si" : "Smart" +} + +def installed() { + log.debug "Installed with settings: ${settings}" + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + initialize() +} + +def initialize() { + + log.debug "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]}"]) + log.debug "created ${d.displayName} with id $dni" + } else { + log.debug "found ${d.displayName} with id $dni already exists" + } + return d + } + + def sensors = ecobeesensors.collect { dni -> + def d = getChildDevice(dni) + if(!d) { + d = addChildDevice(app.namespace, getSensorChildName(), dni, null, ["label":"Ecobee Sensor:${atomicState.sensors[dni]}"]) + log.debug "created ${d.displayName} with id $dni" + } else { + log.debug "found ${d.displayName} with id $dni already exists" + } + return d + } + log.debug "created ${devices.size()} thermostats and ${sensors.size()} sensors." + + def delete // Delete any that are no longer in settings + if(!thermostats && !ecobeesensors) { + log.debug "delete thermostats ands sensors" + delete = getAllChildDevices() //inherits from SmartApp (data-management) + } else { //delete only thermostat + log.debug "delete individual thermostat and sensor" + if (!ecobeesensors) { + delete = getChildDevices().findAll { !thermostats.contains(it.deviceNetworkId) } + } else { + delete = getChildDevices().findAll { !thermostats.contains(it.deviceNetworkId) && !ecobeesensors.contains(it.deviceNetworkId)} + } + } + 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 + + pollHandler() //first time polling data data from thermostat + + //automatically update devices status every 5 mins + runEvery5Minutes("poll") + +} + +def uninstalled() { + log.info("Uninstalling, removing child devices...") + removeChildDevices(getChildDevices()) +} + +private removeChildDevices(delete) { + delete.each { + deleteChildDevice(it.deviceNetworkId) + } +} + +def pollHandler() { + log.debug "pollHandler()" + pollChildren(null) // Hit the ecobee API for update on all thermostats + + atomicState.thermostats.each {stat -> + def dni = stat.key + log.debug ("DNI = ${dni}") + def d = getChildDevice(dni) + if(d) { + log.debug ("Found Child Device.") + d.generateEvent(atomicState.thermostats[dni].data) + } + } +} + +def pollChildren(child = null) { + def thermostatIdsString = getChildDeviceIdsString() + log.debug "polling children: $thermostatIdsString" + + 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, + path: "/1/thermostat", + headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], + query: [format: 'json', body: jsonRequestBody] + ] + + 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" + 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" + + def data = [ + coolMode: (stat.settings.coolStages > 0), + heatMode: (stat.settings.heatStages > 0), + 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 + ] + data["temperature"] = data["temperature"] ? String.format("%.1f", data["temperature"]) : data["temperature"] +// debugEventFromParent(child, "Event Data = ${data}") + + collector[dni] = [data:data] + return collector + } + result = true + log.debug "updated ${atomicState.thermostats?.size()} stats: ${atomicState.thermostats}" + } else { + 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"; + 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 polling children: " + e +// debugEventFromParent(child, "___exception polling children: " + e) + refreshAuthToken() + } + return result +} + +// Poll Child is invoked from the Child Device itself as part of the Poll Capability +def pollChild(child){ + + if (pollChildren(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 + } + +} + +void poll() { + def devices = getChildDevices() + devices.each {pollChild(it)} +} + +def availableModes(child) { + + debugEvent ("atomicState.thermostats = ${atomicState.thermostats}") debugEvent ("Child DNI = ${child.device.deviceNetworkId}") @@ -507,10 +513,7 @@ def availableModes(child) } - -def currentMode(child) -{ - +def currentMode(child) { debugEvent ("atomicState.Thermos = ${atomicState.thermostats}") debugEvent ("Child DNI = ${child.device.deviceNetworkId}") @@ -519,8 +522,7 @@ def currentMode(child) debugEvent("Data = ${tData}") - if(!tData) - { + if(!tData) { log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling" // TODO: flag device as in error state @@ -532,169 +534,62 @@ def currentMode(child) def mode = tData.data.thermostatMode mode - } - - -def pollChildren() -{ - log.debug "polling children" - def thermostatIdsString = getChildDeviceIdsString() - - log.debug "polling children: $thermostatIdsString" - - - def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeExtendedRuntime":"true","includeSettings":"true","includeRuntime":"true"}}' - - // // TODO: test this: - // - // def jsonRequestBody = toJson([ - // selection:[ - // selectionType: "thermostats", - // selectionMatch: getChildDeviceIdsString(), - // includeRuntime: true - // ] - // ]) - log.debug "json Request: " + jsonRequestBody - - log.debug "State AuthToken: ${atomicState.authToken}" - debugEvent "State AuthToken: ${atomicState.authToken}" - - - def pollParams = [ - uri: "https://api.ecobee.com", - path: "/1/thermostat", - headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], - query: [format: 'json', body: jsonRequestBody] - ] - - debugEvent ("Before HTTPGET to ecobee.") - - try{ - httpGet(pollParams) { resp -> - - if (resp.data) { - debugEvent ("Response from ecobee GET = ${resp.data}") - debugEvent ("Response Status = ${resp.status}") - } - - if(resp.status == 200) { - log.debug "poll results returned" - - atomicState.thermostats = resp.data.thermostatList.inject([:]) { collector, stat -> - - def dni = [ app.id, stat.identifier ].join('.') - - log.debug "updating dni $dni" - - def data = [ - coolMode: (stat.settings.coolStages > 0), - heatMode: (stat.settings.heatStages > 0), - 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 - ] - - debugEvent ("Event Data = ${data}") - - collector[dni] = [data:data] - return collector +def updateSensorData() { + atomicState.remoteSensors.each { + it.each { + if (it.type != "thermostat") { + def temperature = "" + def occupancy = "" + it.capability.each { + if (it.type == "temperature") { + temperature = it.value as Double + temperature = temperature / 10 + } + if (it.type == "occupancy") { + if(it.value == "true") + occupancy = "active" + else + occupancy = "inactive" + } } - - log.debug "updated ${atomicState.thermostats?.size()} stats: ${atomicState.thermostats}" - } - else - { - 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"; - log.debug "Refreshing your auth_token!" - refreshAuthToken() - } - else - { - log.error "Authentication error, invalid authentication method, lack of credentials, etc." + def dni = "ecobee_sensor-"+ it?.id + "-" + it?.code + def d = getChildDevice(dni) + if(d) { + d.sendEvent(name:"temperature", value: temperature) + d.sendEvent(name:"motion", value: occupancy) +// debugEventFromParent(d, "temperature : ${temperature}, motion:${occupancy}") } } } } - catch(Exception e) - { - log.debug "___exception polling children: " + e - debugEvent ("${e}") - - refreshAuthToken() - } - } -def pollHandler() { - - debugEvent ("in Poll() method.") - pollChildren() // Hit the ecobee API for update on all thermostats - - atomicState.thermostats.each {stat -> - - def dni = stat.key - - log.debug ("DNI = ${dni}") - debugEvent ("DNI = ${dni}") - - def d = getChildDevice(dni) - - if(d) - { - log.debug ("Found Child Device.") - debugEvent ("Found Child Device.") - debugEvent("Event Data before generate event call = ${stat}") - - d.generateEvent(atomicState.thermostats[dni].data) - - } - - } - -} - -def getChildDeviceIdsString() -{ +def getChildDeviceIdsString() { return thermostats.collect { it.split(/\./).last() }.join(',') } -def toJson(Map m) -{ +def toJson(Map m) { return new org.json.JSONObject(m).toString() } -def toQueryString(Map m) -{ +def toQueryString(Map m) { return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") } private refreshAuthToken() { log.debug "refreshing auth token" - debugEvent("refreshing OAUTH token") if(!atomicState.refreshToken) { log.warn "Can not refresh OAuth token since there is no refreshToken stored" } else { - def stcid = getSmartThingsClientId() def refreshParams = [ method: 'POST', - uri : "https://api.ecobee.com", + uri : apiEndpoint, path : "/token", - query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: stcid], - - //data?.refreshToken + query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId], ] log.debug refreshParams @@ -727,7 +622,7 @@ private refreshAuthToken() { if(atomicState.action && atomicState.action != "") { log.debug "Executing next action: ${atomicState.action}" - "{atomicState.action}"() + "${atomicState.action}"() //remove saved action atomicState.action = "" @@ -739,123 +634,71 @@ private refreshAuthToken() { log.debug "refresh failed ${resp.status} : ${resp.status.code}" } } - - // atomicState.refreshToken = jsonMap.refresh_token - // atomicState.authToken = jsonMap.access_token - } - catch(Exception e) { + } catch(Exception e) { log.debug "caught exception refreshing auth token: " + e } } } -def resumeProgram(child) -{ +def resumeProgram(child, deviceId) { - def thermostatIdsString = getChildDeviceIdsString() - log.debug "resumeProgram children: $thermostatIdsString" +// def thermostatIdsString = getChildDeviceIdsString() +// log.debug "resumeProgram children: $thermostatIdsString" - def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeRuntime":true},"functions": [{"type": "resumeProgram"}]}' + def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{"type": "resumeProgram"}]}' //, { "type": "sendMessage", "params": { "text": "Setpoint Updated" } } - sendJson(jsonRequestBody) + def result = sendJson(jsonRequestBody) +// debugEventFromParent(child, "resumeProgram(child) with result ${result}") + return result } -def setHold(child, heating, cooling) -{ +def setHold(child, heating, cooling, deviceId) { int h = heating * 10 int c = cooling * 10 - log.debug "setpoints____________ - h: $heating - $h, c: $cooling - $c" - def thermostatIdsString = getChildDeviceIdsString() - log.debug "setCoolingSetpoint children: $thermostatIdsString" - - - - def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeRuntime":true},"functions": [{ "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": "indefinite" } } ]}' +// 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": "indefinite" } } ]}' // def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeRuntime":true},"functions": [{"type": "resumeProgram"}, { "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": "indefinite" } } ]}' - - sendJson(jsonRequestBody) + def result = sendJson(child, jsonRequestBody) +// debugEventFromParent(child, "setHold: heating: ${h}, cooling: ${c} with result ${result}") + return result } -def setMode(child, mode) -{ - log.debug "requested mode = ${mode}" - def thermostatIdsString = getChildDeviceIdsString() - log.debug "setCoolingSetpoint children: $thermostatIdsString" +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}"+'"}}}' - def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeRuntime":true},"thermostat": {"settings":{"hvacMode":"'+"${mode}"+'"}}}' - - log.debug "Mode Request Body = ${jsonRequestBody}" - debugEvent ("Mode Request Body = ${jsonRequestBody}") +// log.debug "Mode Request Body = ${jsonRequestBody}" +// debugEvent ("Mode Request Body = ${jsonRequestBody}") def result = sendJson(jsonRequestBody) - - if (result) { - def tData = atomicState.thermostats[child.device.deviceNetworkId] - tData.data.thermostatMode = mode - } - - return(result) +// debugEventFromParent(child, "setMode to ${mode} with result ${result}") + return result } -def changeSetpoint (child, amount) -{ - def tData = atomicState.thermostats[child.device.deviceNetworkId] - - log.debug "In changeSetpoint." - debugEvent ("In changeSetpoint.") - - if (tData) { - - def thermostat = tData.data - - log.debug "Thermostat=${thermostat}" - debugEvent ("Thermostat=${thermostat}") - - if (thermostat.thermostatMode == "heat") { - thermostat.heatingSetpoint = thermostat.heatingSetpoint + amount - child.setHeatingSetpoint (thermostat.heatingSetpoint) - - log.debug "New Heating Setpoint = ${thermostat.heatingSetpoint}" - debugEvent ("New Heating Setpoint = ${thermostat.heatingSetpoint}") - - } - else if (thermostat.thermostatMode == "cool") { - thermostat.coolingSetpoint = thermostat.coolingSetpoint + amount - child.setCoolingSetpoint (thermostat.coolingSetpoint) - - log.debug "New Cooling Setpoint = ${thermostat.coolingSetpoint}" - debugEvent ("New Cooling Setpoint = ${thermostat.coolingSetpoint}") - } - } -} - - -def sendJson(String jsonBody) -{ - - //log.debug "_____AUTH_____ ${atomicState.authToken}" +def sendJson(child = null, String jsonBody) { + def returnStatus = false def cmdParams = [ - uri: "https://api.ecobee.com", - - 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: jsonBody ] - def returnStatus = -1 - try{ httpPost(cmdParams) { resp -> +// debugEventFromParent(child, "sendJson >> resp.status ${resp.status}, resp.data: ${resp.data}") + if(resp.status == 200) { log.debug "updated ${resp.data}" - debugEvent("updated ${resp.data}") returnStatus = resp.data.status.code if (resp.data.status.code == 0) log.debug "Successful call to ecobee API." @@ -863,34 +706,28 @@ def sendJson(String jsonBody) log.debug "Error return code = ${resp.data.status.code}" debugEvent("Error return code = ${resp.data.status.code}") } - } - else - { + } else { log.error "sent Json & got http status ${resp.status} - ${resp.status.code}" debugEvent ("sent Json & got http status ${resp.status} - ${resp.status.code}") //refresh the auth token - if (resp.status == 500 && resp.status.code == 14) - { + if (resp.status == 500 && resp.status.code == 14) { //log.debug "Storing the failed action to try later" log.debug "Refreshing your auth_token!" debugEvent ("Refreshing OAUTH Token") refreshAuthToken() return false - } - else - { + } else { debugEvent ("Authentication error, invalid authentication method, lack of credentials, etc.") log.error "Authentication error, invalid authentication method, lack of credentials, etc." return false } } } - } - catch(Exception e) - { + } catch(Exception e) { log.debug "Exception Sending Json: " + e debugEvent ("Exception Sending JSON: " + e) +// debugEventFromParent(child, "Exception Sending JSON: " + e) return false } @@ -900,21 +737,26 @@ def sendJson(String jsonBody) return false } - -def getChildNamespace() { "smartthings" } -def getChildName() { "Ecobee Thermostat" } - -def getServerUrl() { return appSettings.serverUrl } +def getChildName() { "Ecobee Thermostat" } +def getSensorChildName() { "Ecobee Sensor" } +def getServerUrl() { getApiServerUrl() } def getSmartThingsClientId() { appSettings.clientId } +def getCallbackUrl() { "https://graph.api.smartthings.com/oauth/callback" } +def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}" } +def getApiEndpoint() { "https://api.ecobee.com" } 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)} +}